From 72bc4c7148ce56561b21ca30906d765baf152fa0 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Thu, 24 Sep 2026 23:52:12 +0200 Subject: [PATCH] chore(history): squash 100 commit(s) from 2026-09-24 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/.env.example | 7 +- backend/Cargo.lock | 495 +++- backend/Cargo.toml | 1 + backend/PLAN.md | 1283 ++++++--- backend/STRUCTURE.md | 4 +- backend/accounts-service/Cargo.toml | 10 +- .../accounts-service/migrations/0001_init.sql | 17 +- .../migrations/0002_refresh_tokens.sql | 12 + backend/accounts-service/src/accounts/mod.rs | 2 + .../accounts-service/src/accounts/model.rs | 15 + backend/accounts-service/src/accounts/repo.rs | 168 ++ backend/accounts-service/src/auth/handlers.rs | 227 ++ backend/accounts-service/src/auth/mod.rs | 2 + backend/accounts-service/src/auth/password.rs | 113 +- backend/accounts-service/src/auth/tokens.rs | 136 + .../accounts-service/src/avatars/handlers.rs | 63 + backend/accounts-service/src/avatars/mod.rs | 3 + .../src/avatars/processing.rs | 90 + .../accounts-service/src/avatars/storage.rs | 36 + backend/accounts-service/src/config.rs | 70 + .../accounts-service/src/device/handlers.rs | 73 + backend/accounts-service/src/device/mod.rs | 2 + backend/accounts-service/src/device/store.rs | 192 ++ backend/accounts-service/src/error.rs | 66 + backend/accounts-service/src/lib.rs | 66 +- backend/accounts-service/src/main.rs | 5 +- backend/accounts-service/tests/auth_flow.rs | 162 ++ .../tests/auth_flow/refresh.rs | 92 + .../accounts-service/tests/avatar_upload.rs | 100 + backend/accounts-service/tests/common/mod.rs | 52 + .../accounts-service/tests/db_migration.rs | 14 - backend/accounts-service/tests/device_flow.rs | 87 + backend/accounts-service/tests/health.rs | 16 - backend/accounts-service/tests/smoke.rs | 42 + backend/common/Cargo.toml | 23 + backend/common/build.rs | 4 + backend/common/proto/accounts.proto | 13 + backend/common/src/internal.rs | 176 ++ backend/common/src/jwt.rs | 150 ++ backend/common/src/lib.rs | 8 + backend/configs-service/PLAN.md | 1389 ++++++++++ backend/configs-service/README.md | 5 - backend/gateway/PLAN.md | 2345 +++++++++++++++++ backend/gateway/README.md | 6 - frontend/ARCHITECTURE.md | 29 +- frontend/PLAN.md | 1273 +++++++++ frontend/bun.lock | 188 +- frontend/index.html | 9 +- frontend/package.json | 16 +- frontend/src/App.css | 184 -- frontend/src/App.tsx | 123 +- frontend/src/assets/hero.png | Bin 13057 -> 0 bytes frontend/src/assets/react.svg | 1 - frontend/src/assets/vite.svg | 1 - frontend/src/features/auth/RequireAuth.tsx | 10 + frontend/src/features/auth/api.ts | 21 + .../src/features/auth/forms/LoginForm.tsx | 51 + .../src/features/auth/forms/RegisterForm.tsx | 81 + frontend/src/features/auth/session.tsx | 60 + .../src/features/auth/tests/forms.test.tsx | 53 + .../src/features/auth/tests/session.test.tsx | 30 + frontend/src/features/auth/validation.ts | 16 + frontend/src/index.css | 51 + frontend/src/pages/public/LoginPage.tsx | 10 + frontend/src/pages/public/RegisterPage.tsx | 13 + frontend/src/shared/api/client.ts | 101 + frontend/src/shared/api/errors.ts | 22 + frontend/src/shared/api/tests/client.test.ts | 82 + frontend/src/shared/types.ts | 8 + frontend/src/shared/ui/Button.tsx | 28 + frontend/src/shared/ui/Notice.tsx | 10 + frontend/src/shared/ui/ShareCode.tsx | 29 + frontend/src/shared/ui/TextField.tsx | 35 + frontend/src/shared/ui/tests/ui.test.tsx | 36 + frontend/src/test/fetch.ts | 21 + frontend/src/test/render.tsx | 17 + frontend/src/test/setup.ts | 6 + frontend/src/test/smoke.test.tsx | 9 + frontend/vite.config.ts | 20 +- mod/TODO.md | 17 +- .../command/impl/ProfilerCommand.java | 7 - .../java/dev/loki/lovisual/HudRender.java | 247 ++ .../java/dev/loki/lovisual/Lifecycle.java | 7 - .../main/java/dev/loki/lovisual/LoVisual.java | 265 +- .../context/LoVisualAddonContextImpl.java | 7 - .../LoVisualAddonRuntimeContextImpl.java | 7 - .../LoVisualModuleLoadContextImpl.java | 7 - .../context/ModuleExtensionContextImpl.java | 7 - .../lovisual/addon/core/AddonDescriptor.java | 7 - .../loki/lovisual/addon/core/AddonIssue.java | 7 - .../lovisual/addon/core/AddonManager.java | 376 +-- .../addon/core/AddonRegistration.java | 7 - .../addon/core/impl/AddonDescriptors.java | 68 + .../addon/core/impl/AddonLifecycle.java | 188 ++ .../addon/core/impl/AddonProfiles.java | 66 + .../addon/core/impl/AddonRuntime.java | 107 + .../addon/manager/ClickGuiSectionManager.java | 7 - .../addon/manager/ModuleExtensionManager.java | 7 - .../pipeline/AddonRenderPipelineManager.java | 7 - .../addon/pipeline/AddonSnapshot.java | 7 - .../addon/pipeline/AddonStateStore.java | 7 - .../lovisual/addon/pipeline/AddonStatus.java | 7 - .../lovisual/api/v0/addon/LoVisualAddon.java | 7 - .../api/v0/addon/LoVisualAddonContext.java | 7 - .../v0/addon/LoVisualAddonRuntimeContext.java | 7 - .../v0/addon/LoVisualModuleLoadContext.java | 7 - .../LoVisualClickGuiRenderContext.java | 7 - .../v0/clickgui/LoVisualClickGuiSection.java | 7 - .../api/v0/client/LoVisualClientApi.java | 7 - .../v0/module/LoVisualModuleExtension.java | 7 - .../api/v0/module/ModuleExtensionContext.java | 7 - .../render/LoVisualRenderPipelineBuilder.java | 7 - .../api/v0/render/LoVisualRenderStage.java | 7 - .../api/v0/render/LoVisualUniforms.java | 7 - .../callback/LoVisualPostProcessCallback.java | 7 - .../callback/LoVisualPostProcessContext.java | 7 - .../callback/LoVisualRenderCallback.java | 7 - .../callback/LoVisualRenderContext.java | 7 - .../xaero/XaeroCommandWaypointCompat.java | 7 - .../compat/xaero/XaeroWaypointRuntime.java | 7 - .../compat/xaero/XaeroWaypointStore.java | 7 - .../TriangulatorXaeroMinimapCompat.java | 7 - .../triangulator/TriangulatorXaeroState.java | 7 - .../TriangulatorXaeroWorldMapCompat.java | 7 - .../common/CommonBooleanGroupSchema.java | 7 - .../config/common/CommonSettingSchema.java | 7 - .../config/common/CommonSettingSchemas.java | 7 - .../common/impl/common/CombatPriority.java | 7 - .../CombatProtocolHeuristicSources.java | 7 - .../config/common/impl/common/CommonKey.java | 7 - .../common/impl/common/FakeLagFlushOn.java | 7 - .../common/impl/rotation/RotationMode.java | 7 - .../rotation/RotationMovementCorrection.java | 7 - .../impl/rotation/RotationResetThreshold.java | 7 - .../impl/rotation/RotationResetTicks.java | 7 - .../impl/wall/RotationThroughWalls.java | 7 - .../common/impl/wall/RotationTiming.java | 7 - .../common/impl/wall/TargetFilters.java | 7 - .../config/definition/ConfigNameProvider.java | 7 - .../config/definition/ConfigObject.java | 7 - .../config/definition/SettingDef.java | 7 - .../config/definition/SettingOwner.java | 7 - .../loki/lovisual/config/i18n/ConfigLang.java | 7 - .../config/i18n/ConfigValueAliasProvider.java | 7 - .../config/i18n/DisableSettingI18n.java | 7 - .../loki/lovisual/config/i18n/MainConfig.java | 7 - .../loki/lovisual/config/io/ConfigPaths.java | 7 - .../lovisual/config/io/ConfigSerializer.java | 632 +---- .../lovisual/config/io/JsonConfigObject.java | 7 - .../config/io/SettingDefProvider.java | 7 - .../config/io/serializer/ConfigFilePaths.java | 95 + .../io/serializer/ConfigGsonSupport.java | 182 ++ .../io/serializer/ConfigJson5Support.java | 141 + .../io/serializer/ConfigSaveScheduler.java | 91 + .../codec/ConfigProfileBinaryCodec.java | 7 - .../profile/codec/ConfigProfileEntry.java | 7 - .../profile/codec/ConfigProfileType.java | 7 - .../codec/ConfigProfileValueCodec.java | 7 - .../diff/ConfigProfileDateFormatter.java | 7 - .../profile/diff/ConfigProfileDiff.java | 7 - .../profile/diff/ConfigProfileDiffEngine.java | 7 - .../profile/service/ConfigProfileApplier.java | 7 - .../service/ConfigProfileCollector.java | 7 - .../profile/service/ConfigProfileService.java | 7 - .../profile/service/ConfigProfileStorage.java | 7 - .../profile/snapshot/ConfigProfileMeta.java | 7 - .../snapshot/ConfigProfileSnapshot.java | 7 - .../config/values/collection/ConfigValue.java | 7 - .../collection/ItemCooldownRulesValue.java | 7 - .../values/collection/ItemIdSetValue.java | 7 - .../config/values/collection/SetValue.java | 7 - .../config/values/color/ColorValue.java | 7 - .../config/values/color/KeyBindValue.java | 7 - .../config/values/color/RGBAColorValue.java | 7 - .../config/values/color/RGBColorValue.java | 7 - .../lovisual/config/values/mode/BindMode.java | 7 - .../lovisual/config/values/mode/EnumIds.java | 7 - .../config/values/mode/EnumValue.java | 7 - .../config/values/mode/ModeValue.java | 7 - .../values/primitive/BooleanMapValue.java | 7 - .../config/values/primitive/BooleanValue.java | 7 - .../config/values/primitive/NumberValue.java | 7 - .../config/values/primitive/StringValue.java | 7 - .../java/dev/loki/lovisual/events/Event.java | 7 - .../dev/loki/lovisual/events/EventBus.java | 7 - .../java/dev/loki/lovisual/events/Events.java | 7 - .../events/impl/entity/AttackEntityEvent.java | 7 - .../entity/CrosshairTargetUpdateEvent.java | 7 - .../impl/entity/EventTargetChanged.java | 7 - .../events/impl/entity/FireworkEvent.java | 7 - .../events/impl/input/KeyInputEvent.java | 7 - .../impl/input/KeybindIsPressedEvent.java | 7 - .../impl/input/RotationUpdateEvent.java | 7 - .../events/impl/input/SprintControlEvent.java | 7 - .../impl/movement/MovementInputEvent.java | 7 - .../impl/movement/PlayerSafeWalkEvent.java | 7 - .../events/impl/movement/PlayerStepEvent.java | 7 - .../impl/movement/PlayerStepSuccessEvent.java | 7 - .../events/impl/network/BlinkPacketEvent.java | 7 - .../events/impl/network/EventPostSync.java | 7 - .../events/impl/network/EventSync.java | 7 - .../events/impl/network/PacketEvent.java | 7 - .../player/I18nPreflightCollectEvent.java | 7 - .../events/impl/player/PlayerJumpEvent.java | 7 - .../events/impl/player/PlayerMoveEvent.java | 7 - .../impl/player/PostPlayerUpdateEvent.java | 7 - .../impl/pvp/CombatProtocolBossbarEvent.java | 7 - .../events/impl/pvp/PvpChatEvent.java | 7 - .../events/impl/pvp/PvpOverlayEvent.java | 7 - .../lovisual/events/impl/pvp/PvpTabEvent.java | 7 - .../events/impl/render/GameTickEvent.java | 7 - .../events/impl/render/LightmapEvent.java | 7 - .../impl/render/LightmapModifyEvent.java | 7 - .../render/RenderPrewarmCollectEvent.java | 7 - .../render/particles/ParticleSpawnEvent.java | 7 - .../events/impl/world/EventBreakBlock.java | 7 - .../events/impl/world/EventCollision.java | 7 - .../impl/world/EventPushOutOfBlocks.java | 7 - .../impl/world/PlayerVelocityStrafe.java | 7 - .../lovisual/events/meta/EventHandler.java | 7 - .../lovisual/events/meta/UsedImplicitly.java | 7 - .../features/account/AccountAuthType.java | 7 - .../features/account/AccountConfig.java | 379 +-- .../features/account/AccountEntry.java | 7 - .../features/account/SkinManager.java | 7 - .../features/account/impl/AccountSession.java | 169 ++ .../features/account/impl/AccountStore.java | 169 ++ .../features/account/impl/AccountsValue.java | 104 + .../features/command/core/CommandContext.java | 7 - .../features/command/core/CommandInfo.java | 7 - .../features/command/core/CommandManager.java | 7 - .../command/core/CommandMetadata.java | 7 - .../features/command/impl/CommandUtils.java | 7 - .../command/impl/client/AddonsCommand.java | 7 - .../command/impl/client/ConfigCommand.java | 7 - .../command/impl/client/RelationCommand.java | 7 - .../command/impl/client/UsernameCommand.java | 7 - .../command/impl/info/HelpCommand.java | 7 - .../command/impl/info/PingCommand.java | 7 - .../command/impl/info/ServerInfoCommand.java | 7 - .../command/impl/info/TpsCommand.java | 7 - .../command/impl/module/BindCommand.java | 7 - .../command/impl/module/HideCommand.java | 7 - .../command/impl/module/ModulesCommand.java | 7 - .../command/impl/module/ToggleCommand.java | 7 - .../impl/world/CoordinatesCommand.java | 7 - .../command/impl/world/GpsCommand.java | 7 - .../command/impl/world/TierCommand.java | 7 - .../impl/world/integration/IrisCommand.java | 7 - .../integration/XaeroWaypointCommand.java | 7 - .../features/command/util/ClientCommand.java | 7 - .../features/command/util/CommandOutput.java | 7 - .../features/command/util/CommandPrefix.java | 7 - .../features/command/util/UsageHints.java | 7 - .../features/gui/chat/BetterChatViewport.java | 7 - .../gui/chat/input/BetterChatEditBoxUtil.java | 7 - .../chat/input/BetterChatInputHandler.java | 7 - .../chat/input/BetterChatInputRenderer.java | 7 +- .../input/BetterChatMouseClickHandler.java | 7 - .../menu/BetterChatContextMenuHandler.java | 7 +- .../chat/menu/BetterChatContextMenuModel.java | 7 - .../chat/menu/BetterChatTooltipRenderer.java | 7 - .../messages/BetterChatMessageGrouping.java | 7 - .../messages/BetterChatMessageRenderer.java | 7 +- .../gui/chat/messages/BetterChatSearch.java | 7 - .../chat/messages/BetterChatTabHandler.java | 7 - .../gui/chat/model/BetterChatModel.java | 7 - .../gui/chat/model/BetterChatStore.java | 7 - .../chat/model/BetterChatStoreManager.java | 323 +-- .../features/gui/chat/model/ChatLine.java | 7 - .../gui/chat/model/impl/ChatHoverCapture.java | 42 + .../gui/chat/model/impl/ChatStoreKeys.java | 129 + .../chat/model/impl/ChatStorePersistence.java | 181 ++ .../gui/chat/render/BetterChatFrameModel.java | 7 - .../chat/render/BetterChatFrameRenderer.java | 7 - .../gui/chat/render/BetterChatRenderer.java | 7 - .../suggestions/BetterChatCommandMerge.java | 7 - .../BetterChatSuggestionRenderer.java | 7 +- .../BetterChatSuggestionState.java | 7 - .../suggestions/CommandSuggestorBridge.java | 7 - .../chat/text/BetterChatGlyphRenderer.java | 7 - .../gui/chat/text/BetterChatLayoutEngine.java | 7 +- .../gui/chat/text/BetterChatLineLayout.java | 7 - .../gui/chat/text/BetterChatSegmentUtil.java | 7 - .../chat/text/util/BetterChatDrawUtil.java | 7 - .../chat/text/util/BetterChatFontUtil.java | 7 - .../chat/text/util/BetterChatHoverCache.java | 237 +- .../gui/chat/text/util/ChatHoverUtil.java | 7 - .../text/util/impl/ChatHoverCacheCodec.java | 230 ++ .../clickgui/editor/ClickGuiEditorScreen.java | 7 - .../editor/ClickGuiTextEditorState.java | 525 +--- .../editor/impl/ClickGuiEditorCaret.java | 148 ++ .../editor/impl/ClickGuiEditorPainter.java | 132 + .../screen/modules/ModulesMenuPanel.java | 7 - .../screen/modules/ModulesMenuResolver.java | 7 - .../screen/modules/ModulesMenuScreen.java | 7 - .../modules/model/ModulesMenuCategory.java | 7 - .../modules/model/ModulesMenuStyle.java | 7 - .../render/ModulesMenuChromeRenderer.java | 7 - .../render/ModulesMenuGlassRenderer.java | 7 - .../render/ModulesMenuModuleRenderer.java | 7 - .../modules/state/ModulesMenuLayout.java | 7 - .../layout/screen/settings/MenuScreen.java | 604 +---- .../screen/settings/MenuSettingsResolver.java | 7 - .../screen/settings/SettingsGuiPalette.java | 7 - .../implement/category/CategoryComponent.java | 7 - .../config/ConfigDiffPanelComponent.java | 7 - .../config/ConfigProfileCardComponent.java | 7 - .../implement/main/MainSettingsComponent.java | 183 +- .../implement/main/MainSettingsModel.java | 30 + .../implement/main/MainSettingsScroll.java | 112 + .../implement/module/ModuleComponent.java | 554 +--- .../module/ModuleSettingsWindow.java | 7 - .../module/SettingsPanelComponent.java | 689 +---- .../module/card/ModuleCardInput.java | 51 + .../module/card/ModuleCardLayout.java | 164 ++ .../module/card/ModuleCardRenderer.java | 184 ++ .../module/card/ModuleCardState.java | 72 + .../module/panel/SettingsPanelChrome.java | 294 ++- .../module/panel/SettingsPanelHints.java | 59 - .../module/panel/SettingsPanelHudPreview.java | 78 - .../module/panel/SettingsPanelLayout.java | 70 + .../module/panel/SettingsPanelModels.java | 14 + .../module/panel/SettingsPanelState.java | 128 + .../panel/input/SettingsPanelInput.java | 134 + .../panel/render/SettingsPanelRenderer.java | 147 ++ .../implement/other/BackgroundComponent.java | 7 - .../other/CategoryContainerComponent.java | 7 - .../implement/other/SearchComponent.java | 7 - .../implement/other/StatusRender.java | 7 - .../OnlineRelationPlayerPickerComponent.java | 23 +- .../RelationPlayerCardComponent.java | 7 - .../relations/RelationsComponent.java | 647 +---- .../relations/picker/OnlinePickerData.java | 6 +- .../relations/picker/OnlinePickerInput.java | 6 +- .../picker/OnlinePickerRenderer.java | 10 +- .../relations/picker/OnlinePickerScroll.java | 6 +- .../relations/relations/RelationsInput.java | 179 ++ .../relations/RelationsRenderer.java | 191 ++ .../relations/relations/RelationsScroll.java | 61 + .../relations/relations/RelationsState.java | 175 ++ .../implement/theme/ThemeCardRenderer.java | 199 ++ .../implement/theme/ThemeComponent.java | 490 +--- .../theme/ThemeEditorPreviewComponent.java | 7 - .../implement/theme/ThemeScrollState.java | 123 + .../screen/settings/menu/MenuScreenInput.java | 141 + .../settings/menu/MenuScreenLayout.java | 40 + .../settings/menu/MenuScreenRenderer.java | 141 + .../screen/settings/menu/MenuScreenState.java | 97 + .../settings/render/LayoutRender2D.java | 7 - .../clickgui/picker/ClickGuiPickerState.java | 7 - .../picker/catalog/PickerCatalog.java | 7 - .../picker/catalog/PickerCatalogFactory.java | 7 - .../picker/catalog/PickerEntryData.java | 7 - .../picker/screen/ClickGuiPickerScreen.java | 7 - .../state/ClickGuiPickerChromeRenderer.java | 7 - .../state/ClickGuiPickerFilterRenderer.java | 7 - .../state/ClickGuiPickerGridRenderer.java | 7 - .../picker/state/ClickGuiPickerLogic.java | 7 - .../gui/clickgui/render/ClickGuiRenderer.java | 729 ++--- .../gui/clickgui/screen/ClickGuiScreen.java | 7 - .../clickgui/sections/ClickGuiSection.java | 7 - .../gui/clickgui/sections/ModulesSection.java | 7 - .../clickgui/sections/SettingsSection.java | 7 - .../sections/settings/SettingsTabRuntime.java | 7 - .../settings/bind/FunctionBindSetting.java | 7 - .../bind/FunctionBindSettingRenderer.java | 7 - .../settings/bind/KeyBindSetting.java | 7 - .../settings/bind/KeyBindSettingRenderer.java | 7 - .../settings/bool/BooleanSetting.java | 7 - .../settings/bool/BooleanSettingRenderer.java | 7 - .../clickgui/settings/color/ColorSetting.java | 7 - .../settings/color/ColorSettingMath.java | 7 - .../settings/color/ColorSettingNoAlpha.java | 7 - .../color/ConfigColorSettingNoAlpha.java | 7 - .../color/hex/ColorSettingHexInput.java | 7 - .../color/hex/ColorSettingHexState.java | 7 - .../color/preset/ColorPresetStore.java | 7 - .../preset/ColorSettingPresetHandler.java | 7 - .../preset/ColorSettingPresetRenderer.java | 7 - .../color/render/ColorSettingDrag.java | 7 - .../color/render/ColorSettingLayout.java | 7 - .../color/render/ColorSettingRenderer.java | 7 - .../render/ColorSettingSquareRenderer.java | 7 - .../settings/cooldown/CooldownRuleEditor.java | 173 ++ .../cooldown/CooldownRulesDetailRenderer.java | 198 ++ .../cooldown/CooldownRulesSetting.java | 521 +--- .../cooldown/ProtocolHeuristicsSetting.java | 7 - .../gui/clickgui/settings/core/Setting.java | 437 +-- .../settings/core/SettingFactory.java | 7 - .../settings/core/impl/SettingI18n.java | 119 + .../core/impl/SettingLocalization.java | 164 ++ .../clickgui/settings/group/GroupSetting.java | 7 - .../settings/group/GroupSettingRenderer.java | 7 - .../settings/i18n/I18nDuplicateScanner.java | 7 - .../i18n/I18nPreflightContributors.java | 7 - .../settings/i18n/I18nPreflightManager.java | 7 - .../clickgui/settings/mode/ModeSetting.java | 7 - .../settings/mode/ModeSettingRenderer.java | 7 - .../settings/owner/PickerDetailOwner.java | 7 - .../settings/owner/TextEditorOwner.java | 7 - .../settings/render/SettingRenderContext.java | 7 - .../settings/render/SettingRenderSurface.java | 7 - .../render/SettingRendererBridge.java | 7 - .../render/UnifiedSettingRenderer.java | 7 - .../settings/skin/ModulesSettingsSkin.java | 7 - .../settings/skin/SettingSkinUtil.java | 7 - .../settings/skin/UnifiedSettingsSkin.java | 7 - .../settings/slider/SliderSetting.java | 7 - .../slider/SliderSettingRenderer.java | 7 - .../slider/SliderTextInputHandler.java | 7 - .../settings/slider/SliderTextInputState.java | 7 - .../clickgui/settings/text/TextSetting.java | 7 - .../settings/text/TextSettingRenderer.java | 7 - .../text/TextSettingTextInputHandler.java | 7 - .../settings/text/list/TextListSetting.java | 7 - .../text/list/TextListSettingRenderer.java | 7 - .../text/list/TextListTextEditHandler.java | 7 - .../settings/util/BindRendererUtil.java | 7 - .../settings/util/SettingTextInputUtil.java | 7 - .../gui/clickgui/tabs/ClickGuiSearch.java | 7 - .../gui/clickgui/util/ClickGuiI18n.java | 7 - .../gui/clickgui/util/ClickGuiMath.java | 7 - .../util/ClickGuiRichTextRenderer.java | 7 - .../util/tooltip/ClickGuiHintOverlay.java | 7 - .../clickgui/util/tooltip/SettingTooltip.java | 7 - .../features/gui/hud/anchor/HudAnchorX.java | 7 - .../features/gui/hud/anchor/HudAnchorY.java | 7 - .../gui/hud/anchor/HudRenderSpace.java | 7 - .../gui/hud/anchor/WidgetAnchorSide.java | 7 - .../gui/hud/base/AbstractHudElement.java | 7 - .../features/gui/hud/base/BaseHudElement.java | 527 +--- .../features/gui/hud/base/CooldownRender.java | 7 - .../features/gui/hud/base/HudTextEffects.java | 7 - .../gui/hud/base/impl/HudElementNamer.java | 88 + .../gui/hud/base/impl/HudPrewarm.java | 26 + .../gui/hud/base/impl/HudSettingRegistry.java | 135 + .../gui/hud/config/HudGlobalConfig.java | 7 - .../gui/hud/config/HudRenderUtil.java | 7 - .../hud/draggable/DraggableHudElement.java | 7 - .../DraggableHudElementRegistry.java | 7 - .../hud/draggable/DraggableLayoutValue.java | 7 - .../gui/hud/draggable/impl/BetterChat.java | 7 - .../draggable/impl/HudPanelLayoutModes.java | 7 - .../hud/draggable/impl/TimerIndicator.java | 7 - .../gui/hud/draggable/impl/Triangulator.java | 7 - .../hud/draggable/impl/combat/Inventory.java | 7 - .../hud/draggable/impl/combat/Itemizer.java | 7 - .../draggable/impl/combat/MediaPlayer.java | 7 - .../combat/inventory/InventoryConstants.java | 7 - .../combat/inventory/InventoryPalette.java | 7 - .../inventory/InventoryRenderHelper.java | 7 - .../impl/combat/itemizer/ItemizerModel.java | 7 - .../impl/combat/itemizer/ItemizerProps.java | 7 - .../combat/itemizer/ItemizerRenderer.java | 7 - .../mediaplayer/MediaPlayerConstants.java | 7 - .../mediaplayer/MediaPlayerControls.java | 7 - .../mediaplayer/MediaPlayerRenderHelper.java | 7 - .../mediaplayer/MediaPlayerTextHelper.java | 7 - .../impl/hudnotifier/HudNotifier.java | 487 +--- .../impl/hudnotifier/HudNotifierPainter.java | 185 ++ .../impl/hudnotifier/ToastCardRenderer.java | 7 - .../impl/hudnotifier/ToastGeometry.java | 7 - .../impl/hudnotifier/model/ToastModel.java | 7 - .../impl/hudnotifier/model/ToastStyle.java | 7 - .../runtime/HudNotifierRuntime.java | 124 + .../draggable/impl/metrics/Coordinates.java | 7 - .../hud/draggable/impl/metrics/GameTime.java | 7 - .../hud/draggable/impl/metrics/SpeedBps.java | 7 - .../draggable/impl/metrics/SystemTime.java | 7 - .../coordinates/CoordinatesPalette.java | 7 - .../coordinates/CoordinatesRenderer.java | 7 - .../impl/metrics/gametime/GameTimeIcons.java | 7 - .../metrics/gametime/GameTimePalette.java | 7 - .../metrics/gametime/GameTimeRenderer.java | 7 - .../metrics/speedbps/SpeedBpsPalette.java | 7 - .../metrics/speedbps/SpeedBpsRenderer.java | 7 - .../metrics/speedbps/SpeedBpsSampler.java | 7 - .../metrics/systemtime/SystemTimeIcons.java | 7 - .../metrics/systemtime/SystemTimePalette.java | 7 - .../systemtime/SystemTimeRenderer.java | 7 - .../hud/draggable/impl/panels/Keybinds.java | 7 - .../hud/draggable/impl/panels/ModuleList.java | 7 - .../gui/hud/draggable/impl/panels/Radar.java | 7 - .../hud/draggable/impl/panels/Scoreboard.java | 7 - .../panels/keybinds/KeybindsAnimator.java | 7 - .../impl/panels/keybinds/KeybindsData.java | 7 - .../impl/panels/keybinds/KeybindsPalette.java | 7 - .../panels/keybinds/KeybindsRenderer.java | 7 - .../panels/modulelist/ModuleListData.java | 7 - .../panels/modulelist/ModuleListPalette.java | 7 - .../panels/modulelist/ModuleListRenderer.java | 7 - .../impl/panels/radar/RadarRectRenderer.java | 7 - .../impl/panels/radar/RadarTextRenderer.java | 7 - .../scoreboard/ScoreboardDataHelper.java | 7 - .../scoreboard/ScoreboardRenderHelper.java | 7 - .../scoreboard/ScoreboardTextHelper.java | 7 - .../scoreboard/ScoreboardTextRenderer.java | 7 - .../scripted/ScriptedInventoryHudPanel.java | 7 - .../impl/scripted/ScriptedListHudPanel.java | 458 +--- .../ScriptedTriangulatorHudPanel.java | 7 - .../panel/ScriptedListPanelProps.java | 127 + .../panel/ScriptedListPanelSignature.java | 136 + .../gui/hud/draggable/impl/stats/Fps.java | 7 - .../gui/hud/draggable/impl/stats/Memory.java | 7 - .../gui/hud/draggable/impl/stats/Ping.java | 7 - .../gui/hud/draggable/impl/stats/Tps.java | 7 - .../draggable/impl/stats/fps/FpsAnimator.java | 7 - .../draggable/impl/stats/fps/FpsPalette.java | 7 - .../draggable/impl/stats/fps/FpsRender.java | 7 - .../impl/stats/memory/MemoryFormat.java | 7 - .../impl/stats/memory/MemoryPalette.java | 7 - .../impl/stats/memory/MemoryRender.java | 7 - .../draggable/impl/stats/ping/PingFormat.java | 7 - .../impl/stats/ping/PingPalette.java | 7 - .../draggable/impl/stats/ping/PingRender.java | 7 - .../draggable/impl/stats/tps/TpsFormat.java | 7 - .../draggable/impl/stats/tps/TpsPalette.java | 7 - .../draggable/impl/stats/tps/TpsRender.java | 7 - .../gui/hud/draggable/impl/status/Admins.java | 7 - .../gui/hud/draggable/impl/status/Armor.java | 7 - .../hud/draggable/impl/status/Cooldowns.java | 7 - .../hud/draggable/impl/status/Potions.java | 7 - .../impl/status/admins/AdminsAnimator.java | 7 - .../impl/status/admins/AdminsData.java | 7 - .../impl/status/admins/AdminsPalette.java | 7 - .../impl/status/admins/AdminsRenderer.java | 7 - .../impl/status/armor/ArmorData.java | 7 - .../impl/status/armor/ArmorRenderer.java | 7 - .../status/cooldowns/CooldownsAnimator.java | 7 - .../impl/status/cooldowns/CooldownsData.java | 7 - .../status/cooldowns/CooldownsPalette.java | 7 - .../status/cooldowns/CooldownsRenderer.java | 7 - .../impl/status/potions/PotionsAnimator.java | 7 - .../impl/status/potions/PotionsData.java | 7 - .../impl/status/potions/PotionsPalette.java | 7 - .../impl/status/potions/PotionsRenderer.java | 7 - .../draggable/impl/targethud/TargetHud.java | 7 - .../impl/targethud/TargetHudFormatter.java | 7 - .../impl/targethud/TargetHudModel.java | 7 - .../targethud/TargetHudTargetResolver.java | 7 - .../render/TargetHudContentRenderer.java | 7 - .../render/TargetHudPanelRenderer.java | 7 - .../targethud/render/TargetHudRenderer.java | 7 - .../render/TargetHudTextRenderer.java | 7 - .../render/parts/TargetHudArcRenderer.java | 7 - .../parts/TargetHudEffectsRenderer.java | 7 - .../parts/TargetHudEquipmentRenderer.java | 7 - .../render/parts/TargetHudFaceRenderer.java | 7 - .../impl/triangulator/TrackedEye.java | 7 - .../impl/triangulator/TriangulatorSolver.java | 7 - .../triangulator/TriangulatorTracker.java | 7 - .../impl/triangulator/TriangulatorView.java | 7 - .../triangulator/model/CapturePolicy.java | 7 - .../impl/triangulator/model/DisplayState.java | 7 - .../render/TriangulatorMeasure.java | 7 - .../render/TriangulatorPainter.java | 7 - .../interaction/RegistryDragController.java | 7 - .../interaction/RegistryDragSession.java | 7 - .../registry/RegistryAnchorLayout.java | 7 - .../registry/RegistrySnapSupport.java | 7 - .../hud/draggable/registry/RegistryState.java | 7 - .../registry/RegistryWidgetStore.java | 7 - .../render/RegistryOverlayRenderer.java | 7 - .../render/RegistryRenderPipeline.java | 7 - .../render/RegistryResizeHandler.java | 7 - .../render/RegistryWidgetRenderPath.java | 7 - .../StaticHudElementBootstrap.java | 7 - .../StaticHudElementRegistry.java | 7 - .../hud/nondraggable/impl/bars/CustomBar.java | 346 +-- .../impl/bars/CustomBarModel.java | 59 +- .../impl/bars/CustomHealthBar.java | 311 +-- .../impl/bars/CustomXpBarRenderer.java | 14 +- .../impl/bars/impl/BarSettings.java | 189 ++ .../impl/bars/impl/HealthBarPainter.java | 169 ++ .../impl/bars/impl/HealthScriptRenderer.java | 101 + .../impl/bars/jump/CustomJumpBarRenderer.java | 8 +- .../impl/buttons/BetterButtons.java | 7 - .../impl/buttons/SwapTooltip.java | 7 - .../buttons/render/ButtonSliderRenderer.java | 7 - .../impl/buttons/render/ButtonSprites.java | 7 - .../buttons/render/ButtonStyleRenderer.java | 7 +- .../buttons/render/ButtonTextRenderer.java | 7 +- .../impl/buttons/state/ButtonHoverState.java | 7 - .../impl/buttons/state/ButtonParallax.java | 7 - .../impl/buttons/state/ButtonQueue.java | 7 +- .../impl/dynamicisland/DynamicIsland.java | 697 +---- .../dynamicisland/IslandBoundsPatches.java | 7 - .../impl/dynamicisland/IslandPatchOps.java | 7 - .../dynamicisland/IslandRuntimePatches.java | 7 - .../controls/IslandControls.java | 7 - .../dynamicisland/controls/IslandUiUtil.java | 7 - .../dynamicisland/island/IslandAnimation.java | 71 + .../dynamicisland/island/IslandLayout.java | 110 + .../dynamicisland/island/IslandRenderer.java | 138 + .../dynamicisland/island/IslandState.java | 64 + .../impl/hotbar/CustomHotbar.java | 497 +--- .../impl/hotbar/HotbarRenderer.java | 175 ++ .../impl/hotbar/HotbarSelection.java | 91 + .../impl/hotbar/HotbarSelectionGradient.java | 57 + .../impl/locator/CustomLocatorCollector.java | 9 +- .../impl/locator/CustomLocatorHeads.java | 7 - .../impl/locator/CustomLocatorLabelCache.java | 8 +- .../impl/locator/CustomLocatorLabels.java | 8 +- .../panel/CustomLocatorLabelPanel.java | 8 +- .../locator/panel/CustomLocatorMarkers.java | 8 +- .../locator/panel/CustomLocatorRenderer.java | 23 +- .../locator/panel/CustomLocatorResolver.java | 7 - .../impl/overlay/ReachDisplay.java | 7 - .../nondraggable/impl/overlay/Watermark.java | 7 - .../impl/tab/TabListAnimator.java | 7 - .../nondraggable/impl/tab/TabListModel.java | 585 +--- .../impl/tab/TabRichTextRenderer.java | 7 - .../impl/tab/model/TabListCollector.java | 142 + .../impl/tab/model/TabListReader.java | 87 + .../impl/tab/model/TabListSignature.java | 71 + .../impl/tab/model/TabListTextSplitter.java | 137 + .../impl/tooltips/BetterTooltips.java | 7 - .../render/ShulkerPreviewRenderer.java | 7 - .../tooltips/render/TooltipPanelRenderer.java | 7 - .../tooltips/state/ShulkerPreviewCache.java | 7 - .../impl/tooltips/state/TooltipState.java | 7 - .../hud/registry/HudElementAutoLoader.java | 7 - .../gui/hud/registry/HudElementInfo.java | 7 - .../gui/hud/registry/HudElementRegister.java | 7 - .../gui/hud/registry/HudElements.java | 7 - .../gui/hud/script/CompactHudStatModel.java | 7 - .../gui/hud/script/HudScriptLayouts.java | 7 - .../hud/script/ScriptableHudStatWidget.java | 7 - .../ScriptedCompactHudStatRenderer.java | 7 - .../gui/hud/script/model/CompactHudCodec.java | 7 - .../gui/hud/script/model/CompactHudParts.java | 7 - .../hud/script/model/CompactHudTextParts.java | 7 - .../hud/script/render/CompactHudLayout.java | 7 - .../mainmenu/LoVisualAddonManagerScreen.java | 931 +------ .../gui/mainmenu/LoVisualMainMenuScreen.java | 589 +---- .../mainmenu/LoVisualProxyManagerScreen.java | 590 ----- .../addon/AddonManagerController.java | 164 ++ .../gui/mainmenu/addon/AddonManagerOps.java | 112 + .../gui/mainmenu/addon/AddonPanelAnims.java | 56 + .../mainmenu/addon/auth/AddonScanState.java | 52 + .../addon/render/AddonManagerRenderer.java | 115 + .../addon/render/AddonPanelsRenderer.java | 162 ++ .../mainmenu/addon/render/AddonUiMetrics.java | 183 ++ .../addon/render/cards/AddonCardRenderer.java | 79 + .../render/cards/AddonCardsRenderer.java | 88 + .../gui/mainmenu/alt/AltAccountOps.java | 7 - .../mainmenu/alt/AltManagerController.java | 7 - .../gui/mainmenu/alt/AltPanelAnims.java | 7 - .../alt/LoVisualAltManagerScreen.java | 7 - .../mainmenu/alt/auth/AltMicrosoftAuth.java | 7 - .../alt/render/AltFieldsRenderer.java | 7 - .../alt/render/AltManagerRenderer.java | 7 - .../alt/render/AltPanelsRenderer.java | 7 - .../gui/mainmenu/alt/render/AltUiMetrics.java | 7 - .../alt/render/cards/AltCardRenderer.java | 7 - .../alt/render/cards/AltCardsRenderer.java | 7 - .../gui/mainmenu/impl/MainMenuPainter.java | 126 + .../gui/mainmenu/impl/MainMenuRenderer.java | 191 ++ .../gui/mainmenu/impl/MainMenuTextUtil.java | 80 + .../proxy/LoVisualProxyManagerScreen.java | 125 + .../proxy/ProxyManagerController.java | 181 ++ .../mainmenu/proxy/ProxyManagerRenderer.java | 190 ++ .../lovisual/features/module/core/Module.java | 7 +- .../features/module/core/ModuleCategory.java | 7 - .../features/module/core/ModuleInfo.java | 7 - .../features/module/core/ModuleManager.java | 596 +---- .../module/core/impl/ModuleDispatcher.java | 139 + .../module/core/impl/ModuleRegistrar.java | 73 + .../lifecycle/ModuleActivationSource.java | 7 - .../module/lifecycle/ModuleAutoLoader.java | 7 - .../module/lifecycle/ModuleRegister.java | 7 - .../features/module/lifecycle/Modules.java | 7 - .../features/module/module/ModuleBinds.java | 7 - .../module/module/ModuleConfigValues.java | 7 - .../features/module/module/ModuleNames.java | 7 - .../module/module/ModuleSettingValues.java | 7 - .../modules/combat/damage/DamageTint.java | 7 - .../modules/combat/damage/NoFriendDamage.java | 7 - .../modules/combat/prediction/TargetESP.java | 158 +- .../impl/TargetEspCrystalRenderer.java | 139 + .../combat/predictions/PredictionsAim.java | 7 - .../predictions/PredictionsGeometry.java | 7 - .../combat/predictions/PredictionsModel.java | 7 - .../predictions/PredictionsSimulation.java | 7 - .../modules/combat/pvp/PvpCooldowns.java | 142 +- .../module/modules/combat/pvp/RubberHand.java | 7 - .../modules/combat/pvp/SpearAssist.java | 7 - .../module/modules/combat/pvp/TPSSync.java | 7 - .../combat/pvp/impl/PvpCooldownRules.java | 172 ++ .../combat/targetesp/TargetEspCrystals.java | 7 - .../combat/targetesp/TargetEspEffects.java | 7 - .../combat/targetesp/TargetEspModes.java | 7 - .../combat/targetesp/TargetEspRender.java | 7 - .../module/modules/misc/Balancer.java | 7 - .../module/modules/misc/ClickGui.java | 7 - .../module/modules/misc/DefineTarget.java | 7 - .../module/modules/misc/FakePlayer.java | 7 - .../module/modules/misc/esp/ShitEsp.java | 7 - .../modules/misc/items/ItemScroller.java | 7 - .../modules/misc/items/ScrollMovePolicy.java | 7 - .../modules/misc/protect/MessageFilter.java | 7 - .../modules/misc/protect/NameProtect.java | 7 - .../modules/misc/protect/NoServerPack.java | 7 - .../modules/misc/protect/StreamerMode.java | 7 - .../module/modules/misc/sound/HitSounds.java | 7 - .../module/modules/misc/sound/NoSound.java | 7 - .../modules/misc/vanilla/BetterMinecraft.java | 7 - .../module/modules/player/DeathCoords.java | 7 - .../module/modules/player/FakeLag.java | 7 - .../module/modules/player/ThirdPerson.java | 7 - .../module/modules/player/UseTracker.java | 7 - .../modules/player/auto/AutoAccept.java | 7 - .../module/modules/player/auto/AutoLeave.java | 7 - .../modules/player/auto/AutoRespawn.java | 7 - .../modules/player/interact/AutoTool.java | 7 - .../modules/player/interact/ClickPearl.java | 7 - .../modules/player/interact/NoDelay.java | 7 - .../modules/player/interact/NoInteract.java | 7 - .../modules/player/inventory/LockSlot.java | 7 - .../modules/player/inventory/TapeMouse.java | 7 - .../module/modules/visuals/FovControl.java | 7 - .../modules/visuals/camera/AspectRatio.java | 7 - .../modules/visuals/camera/CameraClip.java | 7 - .../modules/visuals/camera/Freecam.java | 7 - .../modules/visuals/camera/ViewModel.java | 320 +-- .../visuals/camera/swing/SwingAnimator.java | 46 + .../visuals/camera/swing/SwingStylesA.java | 174 ++ .../visuals/camera/swing/SwingStylesB.java | 127 + .../visuals/camera/swing/SwingStylesC.java | 114 + .../visuals/effects/ambient/Trails.java | 715 +---- .../effects/ambient/WorldParticles.java | 7 - .../ambient/render/WorldParticlesBatches.java | 3 +- .../ambient/render/WorldParticlesRender.java | 94 + .../effects/ambient/trails/TrailsColor.java | 46 + .../ambient/trails/TrailsGeometry.java | 63 + .../ambient/trails/TrailsParticles.java | 149 ++ .../ambient/trails/TrailsRenderer.java | 112 + .../visuals/effects/hits/HitBubbles.java | 7 - .../visuals/effects/hits/KillEffect.java | 594 +---- .../effects/hits/kill/KillBeamHelper.java | 113 + .../effects/hits/kill/KillEmberHelper.java | 200 ++ .../hits/kill/KillLightningHelper.java | 121 + .../effects/hits/kill/KillOrthodoxHelper.java | 46 + .../module/modules/visuals/esp/ESP.java | 7 - .../modules/visuals/esp/EspColorUtil.java | 7 - .../visuals/esp/EspEntityCollector.java | 7 - .../module/modules/visuals/esp/EspModel.java | 7 - .../visuals/esp/audio/LightningTargetESP.java | 7 - .../modules/visuals/esp/audio/SoundESP.java | 7 - .../visuals/esp/overlay/CrystalHoles.java | 102 + .../modules/visuals/esp/overlay/DropESP.java | 146 +- .../modules/visuals/esp/overlay/HoleESP.java | 475 ---- .../modules/visuals/esp/overlay/InvisEsp.java | 7 - .../esp/overlay/crystal/BlockSafety.java | 133 + .../visuals/esp/overlay/crystal/Hole.java | 14 + .../esp/overlay/crystal/HoleRenderer.java | 143 + .../esp/overlay/crystal/HoleScanner.java | 185 ++ .../overlay/impl/DropEspOverlayRenderer.java | 137 + .../visuals/esp/render/Esp2DRenderer.java | 7 - .../visuals/esp/render/Esp3DRenderer.java | 7 - .../esp/render/EspOutlineRenderer.java | 7 - .../visuals/esp/render/EspSearchOverlay.java | 7 - .../visuals/esp/shader/EspMeshBuilder.java | 7 - .../visuals/esp/shader/EspShaderColor.java | 7 - .../visuals/esp/shader/EspShaderMask.java | 7 - .../visuals/esp/shader/EspShaderRenderer.java | 7 - .../module/modules/visuals/fx/MotionBlur.java | 7 - .../module/modules/visuals/fx/PostFX.java | 7 - .../module/modules/visuals/fx/TotemFX.java | 7 - .../module/modules/visuals/fx/Zoom.java | 7 - .../module/modules/visuals/hats/ChinaHat.java | 7 - .../modules/visuals/hats/ChinaHatModel.java | 7 - .../modules/visuals/hats/ChinaHatPalette.java | 7 - .../module/modules/visuals/hats/TazikHat.java | 603 ----- .../modules/visuals/hats/tazik/TazikHat.java | 179 ++ .../visuals/hats/tazik/TazikHatModel.java | 179 ++ .../visuals/hats/tazik/TazikHatPalette.java | 127 + .../modules/visuals/items/ShulkerPreview.java | 7 - .../module/modules/visuals/items/Trinket.java | 7 - .../visuals/items/held/AlwaysVisibleHand.java | 7 - .../visuals/items/held/HoldMyItems.java | 7 - .../modules/visuals/lyrics/KineticLyrics.java | 423 +-- .../modules/visuals/lyrics/LyricPlayback.java | 198 ++ .../modules/visuals/lyrics/LyricStage.java | 176 ++ .../visuals/lyrics/mode/LyricModes.java | 91 + .../visuals/nametags/NameTagCache.java | 7 - .../visuals/nametags/NameTagLayoutEngine.java | 7 - .../visuals/nametags/NameTagModel.java | 7 - .../modules/visuals/nametags/NameTags.java | 313 +-- .../visuals/nametags/impl/NameTagKeys.java | 41 + .../nametags/impl/NameTagProjection.java | 72 + .../nametags/impl/NameTagsCollector.java | 176 ++ .../parts/NameTagEquipmentRenderer.java | 7 - .../nametags/parts/NameTagFaceRenderer.java | 7 - .../parts/NameTagOpponentRenderer.java | 7 - .../nametags/parts/NameTagPlateRenderer.java | 7 - .../render/NameTagEffectsRenderer.java | 7 - .../render/NameTagEnchantFormatter.java | 7 - .../nametags/render/NameTagLabelBuilder.java | 7 - .../nametags/render/NameTagRenderer.java | 7 - .../visuals/optimization/FpsOptimizer.java | 7 - .../visuals/particles/AmbientParticles.java | 7 - .../modules/visuals/player/Animations.java | 7 - .../modules/visuals/player/Crosshair.java | 7 - .../modules/visuals/player/FullBright.java | 7 - .../modules/visuals/player/JumpCircles.java | 7 - .../crosshair/CrosshairColorHelper.java | 7 - .../crosshair/CrosshairCombatHelper.java | 7 - .../crosshair/CrosshairOrbizHelper.java | 7 - .../crosshair/CrosshairRenderHelper.java | 7 - .../modules/visuals/scene/BlockHighlight.java | 7 - .../modules/visuals/scene/EndCrystal.java | 7 - .../modules/visuals/scene/ItemPhysic.java | 7 - .../highlight/BlockHighlightOutline.java | 3 +- .../modules/visuals/scene/visual/Chams.java | 6 +- .../scene/visual/ReimaginedDepthBindings.java | 37 + .../scene/visual/ReimaginedVisual.java | 740 +----- .../ReimaginedDepthOfFieldHelper.java | 87 + .../reimagined/ReimaginedSkyboxHelper.java | 137 + .../reimagined/ReimaginedSunHelper.java | 38 + .../ReimaginedVegetationHelper.java | 73 + .../visuals/trollface/TrollfaceMask.java | 7 - .../modules/visuals/visibility/Hitboxes.java | 10 +- .../modules/visuals/visibility/Light.java | 7 - .../modules/visuals/visibility/NoRender.java | 7 - .../modules/visuals/weather/WorldTweaks.java | 7 - .../visuals/weather/precip/WetWorld.java | 7 - .../visuals/weather/sky/AtmoDawnFog.java | 7 - .../modules/visuals/wings/WingShapes.java | 7 - .../module/modules/visuals/wings/Wings.java | 7 - .../modules/visuals/wings/WingsRenderer.java | 7 - .../module/modules/visuals/world/Gps.java | 7 - .../features/module/phase/HudPhase.java | 7 - .../module/phase/ModuleStateListener.java | 7 - .../features/module/phase/Notifier.java | 7 - .../features/module/phase/WorldPhase.java | 7 - .../features/relations/EntityFilters.java | 7 - .../features/relations/PlayerRelations.java | 7 - .../features/relations/StaffData.java | 7 - .../features/relations/StaffTracker.java | 7 - .../relations/category/CategoryRules.java | 7 - .../relations/category/CategoryService.java | 7 - .../relations/category/CategoryType.java | 7 - .../features/security/BackdoorProtection.java | 7 - .../FilteredTranslatableTextContent.java | 7 - .../features/theme/EditableClickGuiTheme.java | 61 +- .../loki/lovisual/features/theme/Theme.java | 7 - .../lovisual/features/theme/ThemeStore.java | 470 +--- .../loki/lovisual/features/theme/Themes.java | 546 +--- .../features/theme/impl/ThemesBlending.java | 106 + .../theme/impl/ThemesProfileCodec.java | 120 + .../features/theme/impl/ThemesTransition.java | 162 ++ .../features/theme/store/ThemeStoreIO.java | 169 ++ .../features/theme/store/ThemeStoreJson.java | 193 ++ .../mixininterface/device/IGlBackendInfo.java | 7 - .../mixininterface/device/IGpuDevice.java | 7 - .../mixininterface/device/IMsaaDevice.java | 7 - .../mixininterface/device/IMsaaTexture.java | 7 - .../mixininterface/entity/IEntity.java | 7 - .../mixininterface/entity/ILocalPlayer.java | 7 - .../entity/IPlayerAttackCooldown.java | 7 - .../mixininterface/entity/IRoundedHitbox.java | 7 - .../mixininterface/render/IGuiGraphics.java | 7 - .../render/IItemEntityRenderState.java | 7 - .../render/IRenderPipeline.java | 7 - .../lovisual/mixins/LoVisualMixinPlugin.java | 7 - .../accessors/blaze3d/GlTextureInvoker.java | 7 - .../accessors/blaze3d/GpuDeviceAccessor.java | 7 - .../ShaderLoaderDefinitionsAccessor.java | 7 - .../blaze3d/ShaderSourceKeyAccessor.java | 7 - .../chat/ChatInputSuggestorAccessor.java | 7 - .../accessors/chat/ChatScreenAccessor.java | 7 - .../chat/ChatScreenSuggestorAccessor.java | 7 - .../chat/SuggestionWindowAccessor.java | 7 - .../accessors/entity/EntityAccessor.java | 7 - .../accessors/entity/EntityInvoker.java | 7 - .../entity/LivingEntityAccessor.java | 7 - .../accessors/entity/LocalPlayerAccessor.java | 7 - .../hud/BossHealthOverlayAccessor.java | 7 - .../hud/InGameOverlayRendererAccessor.java | 7 - .../hud/PlayerTabOverlayAccessor.java | 7 - .../hud/TextIconButtonWidgetAccessor.java | 7 - .../inventory/ItemCooldownEntryAccessor.java | 7 - .../ItemCooldownManagerAccessor.java | 7 - .../inventory/PlayerInventoryAccessor.java | 7 - .../accessors/inventory/SlotAccessor.java | 7 - .../mixins/accessors/misc/InputAccessor.java | 7 - .../accessors/misc/KeyMappingAccessor.java | 7 - .../accessors/misc/MinecraftAccessor.java | 7 - .../misc/MultiPlayerGameModeAccessor.java | 7 - .../ClientboundExplodePacketAccessor.java | 7 - ...entboundSetEntityMotionPacketAccessor.java | 7 - .../ServerboundInteractPacketAccessor.java | 7 - .../ServerboundMovePlayerPacketAccessor.java | 7 - .../PersistentProjectileEntityAccessor.java | 7 - .../renderer/EntityRenderManagerAccessor.java | 7 - .../renderer/EntityRendererAccessor.java | 7 - .../renderer/GameRendererAccessor.java | 7 - .../renderer/RenderLayerInvokerAccessor.java | 7 - .../state/EffectParticleEffectAccessor.java | 7 - .../state/FireworksComponentAccessor.java | 7 - .../LivingEntityRenderStateAccessor.java | 7 - .../widget/ClickableWidgetAccessor.java | 7 - .../widget/CyclingButtonWidgetAccessor.java | 7 - .../widget/SliderWidgetAccessor.java | 7 - .../widget/TextFieldWidgetAccessor.java | 7 - .../accessors/world/ClientLevelAccessor.java | 7 - .../world/LevelExtractorAccessor.java | 7 - .../world/LevelRendererAccessor.java | 7 - .../mixins/accessors/world/WorldAccessor.java | 7 - .../blaze3d/device/GlDeviceBackendMixin.java | 7 - .../mixins/blaze3d/device/GpuDeviceMixin.java | 7 - .../blaze3d/device/GpuSurfaceMixin.java | 7 - .../blaze3d/device/VulkanDeviceMixin.java | 12 +- .../BatchingRenderCommandQueueMixin.java | 7 - .../blaze3d/encoder/CommandEncoderMixin.java | 7 - .../encoder/VulkanCommandEncoderMixin.java | 7 - .../blaze3d/pipeline/RenderPassMixin.java | 7 - .../blaze3d/pipeline/RenderPipelineMixin.java | 7 - .../pipeline/VulkanRenderPassMixin.java | 7 - .../pipeline/VulkanRenderPipelineMixin.java | 7 - .../shader/DynamicUniformStorageMixin.java | 7 - .../blaze3d/shader/GlStateManagerMixin.java | 7 - .../blaze3d/shader/ShaderLoaderMixin.java | 7 - ...ufferManagerDefaultBufferManagerMixin.java | 7 - .../blaze3d/texture/GlTextureMixin.java | 7 - .../texture/VulkanGpuTextureMixin.java | 7 - .../texture/VulkanGpuTextureViewMixin.java | 7 - .../entity/inventory/ItemCooldownsMixin.java | 7 - .../mixins/entity/inventory/ItemMixin.java | 7 - .../entity/inventory/ItemStackMixin.java | 7 - .../inventory/PlayerInventoryMixin.java | 7 - .../entity/living/LivingEntityMixin.java | 7 - .../living/LivingEntityRendererMixin.java | 7 - .../player/AbstractClientPlayerMixin.java | 7 - .../entity/player/LocalPlayerDropMixin.java | 7 - .../entity/player/LocalPlayerMixin.java | 677 +---- .../entity/player/PlayerEntityMixin.java | 7 - .../handlers/LocalPlayerInputHandler.java | 90 + .../handlers/LocalPlayerMovementHandler.java | 73 + .../handlers/LocalPlayerNetworkHandler.java | 63 + .../handlers/LocalPlayerRotationHandler.java | 87 + .../mixins/entity/world/EntityMixin.java | 7 - .../world/FireworkRocketEntityMixin.java | 7 - .../entity/world/ProjectileUtilMixin.java | 7 - .../mixins/gui/chat/ChatComponentMixin.java | 7 - .../mixins/gui/chat/ChatScreenMixin.java | 7 - .../mixins/gui/chat/ContextualBarMixin.java | 7 - .../core/AbstractContainerScreenMixin.java | 7 - .../gui/core/AbstractSelectionListMixin.java | 7 - .../gui/core/AbstractSliderButtonMixin.java | 7 - .../mixins/gui/core/AbstractWidgetMixin.java | 7 - .../JoinMultiplayerScreenMixin.java | 7 - .../multiplayer/RecipeBookScreenMixin.java | 7 - .../mixins/gui/screens/GuiScreenMixin.java | 13 +- .../mixins/gui/screens/PauseScreenMixin.java | 7 - .../mixins/gui/screens/ScreenMixin.java | 7 - .../mixins/gui/screens/TitleScreenMixin.java | 7 - .../gui/widgets/ButtonWidgetTextMixin.java | 7 - .../mixins/gui/widgets/CycleButtonMixin.java | 7 - .../SpriteIconButtonCenteredIconMixin.java | 7 - .../SpriteIconButtonTextAndIconMixin.java | 7 - .../lovisual/mixins/hud/DebugHudMixin.java | 7 - .../lovisual/mixins/hud/GuiGraphicsMixin.java | 7 - .../loki/lovisual/mixins/hud/GuiMixin.java | 395 +-- .../lovisual/mixins/hud/GuiRendererMixin.java | 7 - .../accessibility/NarratorManagerMixin.java | 7 - .../accessibility/OptionsNarratorMixin.java | 7 - .../hud/bars/BossHealthOverlayMixin.java | 7 - .../mixins/hud/bars/ExperienceBarMixin.java | 7 - .../mixins/hud/bars/JumpBarMixin.java | 7 - .../mixins/hud/bars/LocatorBarMixin.java | 7 - .../hud/handlers/GuiHealthLayoutHandler.java | 50 + .../mixins/hud/handlers/GuiHudBarHandler.java | 170 ++ .../hud/handlers/GuiOverlayHandler.java | 81 + .../hud/handlers/GuiTooltipHandler.java | 74 + .../hud/tablist/PlayerTabOverlayMixin.java | 7 - .../hud/tooltips/ToastManagerMixin.java | 7 - .../hud/tooltips/TooltipRenderUtilMixin.java | 7 - .../input/keyboard/KeyMappingMixin.java | 7 - .../input/keyboard/KeyboardInputMixin.java | 7 - .../mixins/input/keyboard/KeyboardMixin.java | 7 - .../mixins/input/mouse/InputMixin.java | 7 - .../mixins/input/mouse/MouseMixin.java | 9 +- .../iris/hand/IrisHandRendererAccessor.java | 7 - .../iris/hand/IrisHandRendererMixin.java | 7 - .../hand/IrisVanillaHandInteropMixin.java | 7 - .../processor/IrisIncludeProcessorMixin.java | 7 - .../iris/render/IrisCommonUniformsMixin.java | 7 - .../render/IrisGameRendererInteropMixin.java | 7 - .../IrisRenderingPipelineFinalizeMixin.java | 7 - .../iris/render/IrisShaderPackLoadMixin.java | 7 - .../mixins/moreculling/CullingUtilsMixin.java | 7 - .../game/ClientPacketListenerMixin.java | 344 +-- .../game/MultiPlayerGameModeMixin.java | 7 - .../game/MultiPlayerGameModeThrowMixin.java | 7 - .../mixins/network/game/PlayerInfoMixin.java | 7 - .../ClientPacketListenerPvpHandler.java | 140 + .../ClientPacketListenerSoundHandler.java | 109 + .../ClientPacketListenerStaffHandler.java | 90 + .../ClientCommonPacketListenerImplMixin.java | 7 - .../network/packet/ConnectionMixin.java | 7 - .../packet/ServerboundUseItemPacketMixin.java | 7 - .../entity/EntityRenderDispatcherMixin.java | 7 - .../entity/LivingEntityRenderStateMixin.java | 7 - .../entity/crystal/EndCrystalModelMixin.java | 7 - .../crystal/EndCrystalModelTintMixin.java | 7 - .../item/ItemEntityRenderStateMixin.java | 7 - .../entity/item/ItemEntityRendererMixin.java | 7 - .../fog/AtmosphericFogEnvironmentMixin.java | 7 - .../mixins/render/fog/FogRendererMixin.java | 7 - .../render/fog/LavaFogModifierMixin.java | 7 - .../render/fog/WaterFogEnvironmentMixin.java | 7 - .../fogmod/PowderSnowFogModifierMixin.java | 7 - .../item/EquipmentLayerRendererMixin.java | 7 - .../render/item/ItemFeatureRendererMixin.java | 7 - .../item/ItemInHandAlwaysVisibleMixin.java | 7 - .../render/item/ItemInHandRendererMixin.java | 7 - .../mixins/render/level/CameraMixin.java | 7 - .../render/level/LevelRendererMixin.java | 7 - .../mixins/render/level/SkyRendererMixin.java | 7 - .../FireworkParticlesStarterMixin.java | 7 - .../level/particles/ParticleManagerMixin.java | 7 - .../mixins/render/light/LightmapMixin.java | 7 - .../LightmapRenderStateExtractorMixin.java | 7 - .../mixins/render/main/GameRendererMixin.java | 7 - .../main/GameRendererViewEffectsMixin.java | 7 - .../mixins/render/main/MinecraftMixin.java | 7 - .../main/ScreenEffectRendererMixin.java | 7 - .../GameRendererProjectionHelper.java | 7 - .../mixins/render/sound/LibraryMixin.java | 7 - .../mixins/render/sound/SoundEngineMixin.java | 7 - .../mixins/render/text/FontManagerMixin.java | 7 - .../net/ClientLoginNetworkHandlerMixin.java | 7 - .../mixins/security/net/GameOptionsMixin.java | 7 - .../security/net/NetworkUtilsMixin.java | 7 - .../text/AbstractSignEditScreenMixin.java | 7 - .../security/text/AnvilScreenMixin.java | 7 - .../text/TranslatableTextContentMixin.java | 7 - .../text/TranslationStorageMixin.java | 7 - .../terrain/SodiumBlockRendererMixin.java | 7 - .../terrain/SodiumChunkMeshFormatsMixin.java | 7 - .../SodiumShaderChunkRendererMixin.java | 7 - .../sodium/vertex/SodiumChunkVertexMixin.java | 7 - .../SodiumVertexConsumerTrackerMixin.java | 7 - .../SodiumWorldRendererFreecamMixin.java | 7 - .../block/BlockCollisionSpliteratorMixin.java | 7 - .../mixins/world/block/BlockMixin.java | 7 - .../mixins/world/block/HoneyBlockMixin.java | 7 - .../mixins/world/block/SlimeBlockMixin.java | 7 - .../mixins/world/fluid/FlowingFluidMixin.java | 7 - .../world/level/ClientClockManagerMixin.java | 7 - .../mixins/world/level/ClientLevelMixin.java | 7 - .../world/level/LevelExtractorMixin.java | 7 - .../mixins/world/level/LevelMixin.java | 7 - .../AbstractWaypointRenderProviderMixin.java | 7 - .../worldmap/WaypointRenderProviderMixin.java | 7 - .../render/LoVisualEntityRenderTypes.java | 7 - .../render/ShaderEspRenderContext.java | 7 - .../render/compat/EntityCullingCompat.java | 7 - .../render/effects/NetherPortalRiftPass.java | 7 - .../render/effects/SleepOverlayPass.java | 7 - .../engine/LoVisualRenderEngineBootstrap.java | 7 - .../lovisual/render/engine/Projection2D.java | 7 - .../lovisual/render/engine/RenderState.java | 7 - .../engine/animation/AnimatedClockText.java | 7 - .../animation/AnimatedColorModeOptions.java | 7 - .../animation/AnimatedRenderColors.java | 7 - .../engine/animation/AnimationUtility.java | 7 - .../animation/text/AnimatedGlyphSlot.java | 7 - .../animation/text/AnimatedTextLine.java | 7 - .../animation/text/AnimatedTextStyle.java | 7 - .../text/AnimatedTextTransition.java | 7 - .../render/engine/color/ColorBlendMode.java | 7 - .../render/engine/color/ColorEffects.java | 7 - .../render/engine/color/ColorUtils.java | 7 - .../render/engine/color/RenderColor.java | 7 - .../render/engine/command/core/UiCommand.java | 7 - .../engine/command/core/UiCommandBuffer.java | 7 - .../engine/command/core/UiCommandKind.java | 7 - .../engine/command/core/UiCommandStats.java | 7 - .../command/effect/UiBatchBackendCommand.java | 7 - .../command/effect/UiEffectCommand.java | 7 - .../command/effect/UiEffectRegionCommand.java | 7 - .../engine/command/shape/UiPathCommand.java | 7 - .../command/shape/UiPrimitiveCommand.java | 7 - .../engine/command/shape/UiShapeCommand.java | 7 - .../engine/command/texture/UiItemCommand.java | 7 - .../engine/command/texture/UiTextCommand.java | 7 - .../command/texture/UiTextureCommand.java | 7 - .../command/texture/UiTextureDrawCommand.java | 7 - .../command/world/RenderCommandBuffer.java | 7 - .../engine/command/world/UiStatsSnapshot.java | 7 - .../command/world/WorldCommandBuffer.java | 7 - .../ImmediatelyFastBridge.java | 7 - .../ImmediatelyFastRuntime.java | 7 - .../ImmediatelyFastRuntimeSnapshot.java | 7 - .../engine/core/context/CameraContext.java | 7 - .../core/context/FramebufferContext.java | 7 - .../engine/core/context/ViewportContext.java | 7 - .../engine/core/frame/FrameLifecycle.java | 7 - .../engine/core/frame/RenderFrameContext.java | 7 - .../render/engine/core/frame/RenderPhase.java | 7 - .../engine/core/frame/RenderPhaseScope.java | 7 - .../engine/core/policy/DepthProvider.java | 7 - .../engine/core/policy/LightPolicy.java | 7 - .../core/policy/VisibilityProvider.java | 7 - .../engine/core/policy/VisibilityQuery.java | 7 - .../engine/core/policy/fog/FogProvider.java | 7 - .../policy/fog/VanillaWorldFogProvider.java | 7 - .../core/system/LoVisualRenderSystem.java | 7 - .../core/system/LoVisualWorldMatrices.java | 7 - .../debug/RenderThread2DDebugRenderer.java | 7 - .../engine/depth/PreTranslucentDepth.java | 7 - .../render/engine/depth/WorldSceneDepth.java | 7 - .../engine/framegraph/LoVisualFrameGraph.java | 7 - .../engine/framegraph/PassScheduler.java | 7 - .../engine/framegraph/RenderPassNode.java | 7 - .../render/engine/guard/LegacyRenderPath.java | 7 - .../engine/guard/RenderArchitectureGuard.java | 7 - .../engine/guard/boundary/RenderBoundary.java | 7 - .../guard/boundary/RenderBoundaryAudit.java | 7 - .../guard/boundary/RenderBoundaryExempt.java | 7 - .../render/engine/light/LightmapState.java | 7 - .../render/engine/math/ColorMath.java | 7 - .../lovisual/render/engine/math/HudScale.java | 7 - .../render/engine/math/RenderMath.java | 7 - .../render/engine/msaa/MsaaFramebuffer.java | 7 - .../render/engine/msaa/MsaaResolveUtil.java | 7 - .../engine/msaa/MsaaTextureRegistry.java | 7 - .../render/engine/msaa/MsaaWorldTarget.java | 7 - .../engine/pipeline/DepthTestFunction.java | 7 - .../ExtendedRenderPipelineBuilder.java | 7 - .../pipeline/LoVisualRenderPipelines.java | 7 - .../pipeline/pipelines/PipelineTemplates.java | 7 - .../pipeline/pipelines/ScreenPipelines.java | 7 - .../pipeline/pipelines/UiPipelines.java | 7 - .../pipeline/pipelines/WorldPipelines.java | 7 - .../blur/DepthOfFieldDepthSource.java | 7 - .../postprocess/blur/DepthOfFieldQuality.java | 7 - .../postprocess/blur/MotionBlurMode.java | 7 - .../postprocess/blur/MotionBlurResources.java | 7 - .../graph/LegacyPostProcessGraphPass.java | 7 - .../postprocess/graph/PostProcessGraph.java | 7 - .../graph/PostProcessGraphPass.java | 7 - .../graph/resource/HistoryBufferManager.java | 7 - .../resource/PostProcessGraphResources.java | 7 - .../graph/resource/PostProcessResolution.java | 7 - .../graph/resource/PostProcessResource.java | 7 - .../postprocess/post/PostProcessContext.java | 7 - .../postprocess/post/PostProcessManager.java | 7 - .../postprocess/post/PostProcessPass.java | 7 - .../render/MenuBackgroundRenderer.java | 7 - .../postprocess/render/SkyShaderRenderer.java | 7 - .../render/SkyShaderStarryRenderer.java | 7 - .../prewarm/RenderPrewarmContributors.java | 7 - .../engine/prewarm/RenderPrewarmManager.java | 7 - .../profiler/bridge/DevProfilerBridge.java | 7 - .../engine/profiler/bridge/JfrProfiler.java | 7 - .../profiler/bridge/TracyGpuProfiler.java | 7 - .../engine/profiler/bridge/TracyProfiler.java | 7 - .../engine/profiler/config/ProfilerPhase.java | 7 - .../profiler/config/ProfilerSettings.java | 7 - .../profiler/frame/FrameStutterProfiler.java | 7 - .../engine/profiler/frame/GlSyncTracker.java | 7 - .../profiler/frame/RenderCostProfiler.java | 7 - .../profiler/frame/RenderFrameProfiler.java | 7 - .../engine/profiler/render/ProfilerLog.java | 7 - .../profiler/render/RenderProfiler2D.java | 7 - .../profiler/render/RenderProfiler3D.java | 7 - .../profiler/render/SamplingProfiler.java | 7 - .../engine/renderer/FullScreenRenderer.java | 7 - .../render/engine/renderer/MeshRenderer.java | 7 - .../render/engine/renderer/Renderer2D.java | 13 +- .../render/engine/renderer/Renderer3D.java | 256 +- .../impl/Renderer3DBatchBindings.java | 160 ++ .../renderer/impl/Renderer3DCullOptions.java | 24 + .../renderer/impl/Renderer3DCulling.java | 82 + .../engine/renderer/renderer2d/Batch2D.java | 7 - .../renderer/renderer2d/Chamfered2D.java | 7 - .../engine/renderer/renderer2d/Rects2D.java | 7 - .../engine/renderer/renderer2d/Strokes2D.java | 7 - .../engine/renderer/renderer2d/fx/Blur2D.java | 7 - .../renderer/renderer2d/fx/Effects2D.java | 7 - .../renderer/renderer2d/fx/Glass2D.java | 7 - .../renderer/renderer2d/fx/Smoke2D.java | 7 - .../renderer/renderer2d/media/Items2D.java | 7 - .../renderer/renderer2d/media/Textures2D.java | 7 - .../renderer/renderer2d/path/Circles2D.java | 7 - .../renderer2d/path/Connectors2D.java | 7 - .../renderer/renderer2d/path/Lines2D.java | 7 - .../renderer/renderer2d/path/Polygons2D.java | 7 - .../engine/renderer/ui/batches/DrawBatch.java | 7 - .../engine/renderer/ui/batches/ItemBatch.java | 7 - .../engine/renderer/ui/batches/TextBatch.java | 7 - .../engine/renderer/ui/batches/UiBatcher.java | 7 - .../ui/batches/ordered/OrderedUiBatcher.java | 7 - .../ui/batches/ordered/UiBatchPlan.java | 7 - .../ui/batches/ordered/UiBatchStats.java | 7 - .../ui/batches/ordered/UiBatchType.java | 7 - .../ordered/flush/LiquidGlassTextFlush.java | 7 - .../ordered/flush/OrderedDeferral.java | 7 - .../ordered/flush/OrderedDrawBatchFlush.java | 7 - .../ordered/flush/OrderedFlushRenderer.java | 11 +- .../batches/ordered/order/DrawBatchPool.java | 7 - .../batches/ordered/order/ItemTextPools.java | 7 - .../ui/batches/ordered/order/OrderState.java | 7 - .../engine/renderer/ui/blur/BlurSource.java | 7 - .../renderer/ui/blur/FrameBlurCacheEntry.java | 7 - .../renderer/ui/blur/SharedBlurProfile.java | 7 - .../renderer/ui/blur/UiBlurResources.java | 7 - .../ui/blur/kawase/KawaseBlurPreparer.java | 7 - .../renderer/ui/blur/kawase/KawasePass.java | 7 - .../ui/blur/kawase/SharedKawaseBlur.java | 7 - .../ui/deferred/Deferred2DSubmit.java | 7 - .../ui/deferred/DeferredActionSubmit.java | 7 - .../ui/deferred/DeferredOrderedSubmit.java | 7 - .../ui/deferred/DeferredTexturedSubmit.java | 7 - .../deferred/sched/HudDeferredGuiElement.java | 7 - .../deferred/sched/UiDeferredScheduler.java | 7 - .../ui/dispatch/UiDirectTexturedRenderer.java | 7 - .../ui/dispatch/UiRenderDispatcher.java | 7 - .../ui/dispatch/UiRendererSubsystem.java | 7 - .../ui/draw/box/UiBoxPathBuilder.java | 7 - .../renderer/ui/draw/box/UiBoxShape.java | 7 - .../renderer/ui/draw/box/UiChamferRadii.java | 7 - .../engine/renderer/ui/draw/box/UiRect.java | 7 - .../renderer/ui/draw/corner/UiCornerKind.java | 7 - .../renderer/ui/draw/corner/UiCornerMode.java | 7 - .../ui/draw/corner/UiCornerRadii.java | 7 - .../renderer/ui/draw/corner/UiCornerSpec.java | 7 - .../renderer/ui/draw/edge/UiEdgeKind.java | 7 - .../renderer/ui/draw/edge/UiEdgeSpec.java | 7 - .../renderer/ui/draw/edge/UiMeshGeometry.java | 7 +- .../renderer/ui/draw/edge/UiStroke.java | 7 - .../edge/geometry/UiGradientGeometry.java | 7 +- .../ui/draw/edge/geometry/UiRectGeometry.java | 7 +- .../draw/edge/mesh/UiPolygonTessellator.java | 7 +- .../ui/draw/edge/mesh/UiPolylineGeometry.java | 7 +- .../renderer/ui/draw/effect/UiEffectKind.java | 7 - .../renderer/ui/draw/effect/UiEffectSpec.java | 7 - .../renderer/ui/draw/effect/UiPathCap.java | 7 - .../renderer/ui/draw/effect/UiPathJoin.java | 7 - .../renderer/ui/draw/paint/UiPaint.java | 7 - .../renderer/ui/draw/paint/UiPaintKind.java | 7 - .../renderer/ui/draw/paint/UiPaints.java | 7 - .../renderer/ui/draw/shape/UiShape.java | 7 - .../renderer/ui/draw/shape/UiShapeKind.java | 7 - .../ui/draw/shape/UiShapeShaderParams.java | 7 - .../renderer/ui/draw/shape/UiShapes.java | 7 - .../renderer/ui/item/ItemBatchRenderer.java | 690 +---- .../renderer/ui/item/ItemDrawCommand.java | 7 - .../renderer/ui/item/ItemResolveKey.java | 7 - .../renderer/ui/item/UiItemSubmission.java | 7 - .../ui/item/batch/ItemBatchAtlas.java | 49 + .../ui/item/batch/ItemBatchGeometry.java | 91 + .../ui/item/batch/ItemBatchMeshes.java | 70 + .../ui/item/batch/ItemBatchOverlays.java | 139 + .../renderer/ui/pass/UiEffectGraph.java | 7 - .../renderer/ui/pass/UiPassCompiler.java | 7 - .../renderer/ui/pass/UiPassExecutor.java | 7 - .../renderer/ui/runtime/action/UiAction.java | 7 - .../ui/runtime/action/UiActionContext.java | 7 - .../ui/runtime/action/UiActionRef.java | 7 - .../ui/runtime/action/UiActionRegistry.java | 7 - .../ui/runtime/animation/UiAnimationSpec.java | 7 - .../runtime/animation/UiAnimationState.java | 7 - .../ui/runtime/animation/UiEasing.java | 7 - .../ui/runtime/asset/UiAssetProvider.java | 7 - .../renderer/ui/runtime/asset/UiAssetRef.java | 7 - .../ui/runtime/asset/UiAssetResolver.java | 7 - .../runtime/asset/registry/UiAssetKind.java | 7 - .../asset/registry/UiAssetRegistry.java | 7 - .../renderer/ui/runtime/core/UiBounds.java | 7 - .../renderer/ui/runtime/core/UiDocument.java | 7 - .../ui/runtime/core/UiRuntimeException.java | 7 - .../core/component/UiComponentFactory.java | 7 - .../core/component/UiComponentRegistry.java | 7 - .../renderer/ui/runtime/core/node/UiNode.java | 7 - .../ui/runtime/core/node/UiNodeSpec.java | 7 - .../ui/runtime/core/node/UiNodeType.java | 7 - .../ui/runtime/core/node/UiProps.java | 7 - .../ui/runtime/core/runtime/UiLifecycle.java | 7 - .../ui/runtime/core/runtime/UiRuntime.java | 7 - .../ui/runtime/core/runtime/UiState.java | 7 - .../ui/runtime/debug/UiPerfCounters.java | 7 - .../runtime/debug/UiRuntimeDiagnostics.java | 7 - .../ui/runtime/debug/UiTreeDumper.java | 7 - .../ui/runtime/error/UiErrorPolicy.java | 7 - .../ui/runtime/input/UiHitTester.java | 7 - .../ui/runtime/input/UiInputDispatcher.java | 7 - .../ui/runtime/input/UiInputState.java | 7 - .../ui/runtime/input/event/UiHitResult.java | 7 - .../runtime/input/event/UiKeyboardEvent.java | 7 - .../runtime/input/event/UiPointerEvent.java | 7 - .../ui/runtime/input/event/UiScrollEvent.java | 7 - .../runtime/input/focus/UiCursorManager.java | 7 - .../runtime/input/focus/UiFocusManager.java | 7 - .../ui/runtime/layout/UiLayoutEngine.java | 7 - .../ui/runtime/reconcile/UiDiffStats.java | 7 - .../runtime/reconcile/UiReconcileContext.java | 7 - .../ui/runtime/reconcile/UiReconciler.java | 7 - .../ui/runtime/reconcile/match/UiKeyPath.java | 7 - .../reconcile/match/UiNodeMatcher.java | 7 - .../ui/runtime/render/bridge/UiBlendSpec.java | 7 - .../ui/runtime/render/bridge/UiClipStack.java | 7 - .../render/bridge/UiImageRendererBridge.java | 7 - .../render/bridge/UiItemRendererBridge.java | 7 - .../render/context/UiProjectionMode.java | 7 - .../render/context/UiRenderContext.java | 7 - .../render/primitive/UiGlowPainter.java | 7 - .../render/primitive/UiShapeContext.java | 7 - .../render/primitive/UiShapePainter.java | 7 - .../render/primitive/UiShapeProps.java | 7 - .../runtime/render/rect/UiCornerPainter.java | 7 - .../ui/runtime/render/rect/UiRectPainter.java | 7 - .../render/render/UiPrimitiveRenderer.java | 7 - .../ui/runtime/render/render/UiRenderer.java | 7 - .../render/render/UiTextEffectRenderer.java | 7 - .../runtime/render/render/UiTextRenderer.java | 7 - .../engine/JavetNativeResourceLoader.java | 7 - .../script/engine/JavetUiScriptEngine.java | 7 - .../runtime/script/engine/UiScriptEngine.java | 7 - .../script/engine/UiScriptEngineProvider.java | 7 - .../runtime/script/module/UiScriptModule.java | 7 - .../script/module/UiScriptModuleHandle.java | 7 - .../script/module/UiScriptModuleId.java | 7 - .../script/module/UiScriptModuleLoader.java | 7 - .../script/patch/UiBoundsPatchSet.java | 7 - .../runtime/script/patch/UiScriptColor.java | 7 - .../script/patch/UiScriptComponent.java | 7 - .../script/patch/UiScriptPatchSet.java | 7 - .../registry/UiScriptModuleRegistry.java | 7 - .../registry/UiScriptModuleTransform.java | 7 - .../script/registry/UiTypeScriptStripper.java | 7 - .../render/UiScriptObjectConverter.java | 7 - .../runtime/script/render/UiScriptProps.java | 7 - .../script/render/UiScriptRenderContext.java | 7 - .../script/render/UiScriptRenderResult.java | 7 - .../script/runtime/CachedUiScriptRuntime.java | 276 +- .../script/runtime/UiScriptRuntimeError.java | 7 - .../script/runtime/UiScriptRuntimeSource.java | 7 - .../script/runtime/UiScriptSourceKind.java | 7 - .../runtime/impl/UiScriptFrameStats.java | 49 + .../runtime/impl/UiScriptRuntimeCore.java | 239 ++ .../runtime/impl/UiScriptSignature.java | 52 + .../ui/runtime/style/UiResolvedStyle.java | 7 - .../renderer/ui/runtime/style/UiStyle.java | 761 +----- .../ui/runtime/style/parse/UiStyleCache.java | 7 - .../ui/runtime/style/parse/UiStyleParser.java | 7 - .../runtime/style/parse/UiStyleResolver.java | 7 - .../ui/runtime/style/style/UiStyleLayout.java | 122 + .../runtime/style/style/UiStylePalette.java | 73 + .../ui/runtime/style/style/UiStyleText.java | 61 + .../ui/runtime/style/theme/UiColor.java | 7 - .../ui/runtime/style/theme/UiTheme.java | 7 - .../runtime/style/theme/UiThemeRegistry.java | 7 - .../ui/runtime/style/token/UiAlign.java | 7 - .../ui/runtime/style/token/UiJustify.java | 7 - .../ui/runtime/style/token/UiOverflow.java | 7 - .../ui/runtime/style/token/UiStyleToken.java | 7 - .../engine/renderer/warp/RenderWarp.java | 251 +- .../engine/renderer/warp/RenderWarpStack.java | 7 - .../renderer/warp/impl/RenderWarpFactory.java | 135 + .../warp/impl/RenderWarpGeometry.java | 93 + .../engine/rhi/backend/SodiumGlBackend.java | 7 - .../rhi/backend/gl/GlBackendAccess.java | 7 - .../gl/clip/GlStencilFramebufferSupport.java | 640 +---- .../gl/clip/GlStencilShapeClipBackend.java | 302 +-- .../gl/clip/impl/GlStencilNativeState.java | 162 ++ .../gl/clip/impl/GlStencilPassLifecycle.java | 128 + .../gl/clip/stencil/GlStencilAttachment.java | 191 ++ .../stencil/GlStencilAttachmentManager.java | 191 ++ .../backend/gl/clip/stencil/GlStencilGl.java | 57 + .../gl/clip/stencil/GlStencilSampleProbe.java | 108 + .../state/SodiumGlPipelineStateBackend.java | 7 - .../backend/vulkan/LoVisualVulkanBackend.java | 7 - .../vulkan/clip/VulkanShapeClipBackend.java | 7 - .../vulkan/clip/VulkanShapeClipBridge.java | 7 - .../clip/VulkanStencilAttachmentManager.java | 7 - .../vulkan/msaa/VulkanMsaaControl.java | 7 - .../vulkan/msaa/VulkanMsaaResolveBridge.java | 7 - .../state/VulkanPipelineStateBackend.java | 7 - .../vulkan/util/VulkanPipelineVariantKey.java | 69 + .../vulkan/util/VulkanRenderStateBridge.java | 400 +-- .../vulkan/util/VulkanRuntimeGuards.java | 7 - .../vulkan/util/impl/VulkanMsaaState.java | 184 ++ .../vulkan/util/impl/VulkanStencilState.java | 218 ++ .../rhi/blit/Blaze3dTextureBlitter.java | 7 - .../engine/rhi/blit/TextureBlitter.java | 7 - .../engine/rhi/clip/ShapeClipBackend.java | 7 - .../rhi/clip/ShapeClipRenderPassContract.java | 7 - .../rhi/command/FullscreenDrawCommand.java | 7 - .../engine/rhi/command/RhiDrawCommand.java | 7 - .../engine/rhi/command/RhiSamplerBinding.java | 7 - .../engine/rhi/command/RhiUniformBinding.java | 7 - .../fullscreen/Blaze3dFullscreenBackend.java | 7 - .../rhi/fullscreen/FullscreenBackend.java | 7 - .../render/engine/rhi/msaa/MsaaControl.java | 7 - .../engine/rhi/msaa/SodiumGlMsaaControl.java | 7 - .../rhi/pipeline/depth/BlendPolicy.java | 7 - .../engine/rhi/pipeline/depth/CullPolicy.java | 7 - .../rhi/pipeline/depth/DepthPolicy.java | 7 - .../engine/rhi/pipeline/depth/FogPolicy.java | 7 - .../layout/RenderPipelineRegistry.java | 7 - .../pipeline/layout/UniformLayoutSpec.java | 7 - .../rhi/pipeline/layout/VertexLayoutSpec.java | 7 - .../rhi/pipeline/pipeline/PipelineDomain.java | 7 - .../rhi/pipeline/pipeline/PipelineKey.java | 7 - .../pipeline/pipeline/PipelineMetadata.java | 7 - .../pipeline/pipeline/RenderPipelineSpec.java | 7 - .../engine/rhi/resource/GpuMeshHandle.java | 7 - .../engine/rhi/resource/LoVisualRhi.java | 7 - .../engine/rhi/resource/MeshOwnership.java | 7 - .../rhi/resource/atlas/GlyphAtlasManager.java | 7 - .../rhi/resource/atlas/GlyphAtlasPage.java | 7 - .../manager/FrameResourceRetirementQueue.java | 7 - .../manager/RenderResourceManager.java | 7 - .../manager/RenderResourceStatsSnapshot.java | 7 - .../resource/manager/ResourceLeakTracker.java | 7 - .../resource/pool/FramebufferDescriptor.java | 7 - .../rhi/resource/pool/FramebufferPool.java | 7 - .../rhi/resource/pool/SamplerCache.java | 7 - .../engine/rhi/resource/pool/TexturePool.java | 7 - .../rhi/scissor/GlobalScissorState.java | 7 - .../rhi/state/PipelineStateBackend.java | 7 - .../render/engine/rhi/stats/RhiStats.java | 7 - .../engine/rhi/stats/RhiStatsSnapshot.java | 7 - .../engine/rhi/uniform/FrameResource.java | 7 - .../uniform/LegacyUniformFrameResource.java | 7 - .../rhi/uniform/LoVisualUniformAllocator.java | 7 - .../uniform/stats/UniformAllocatorStats.java | 7 - .../stats/UniformAllocatorStatsSnapshot.java | 7 - .../engine/rhi/upload/DynamicMeshBackend.java | 7 - .../blaze3d/Blaze3dDynamicMeshBackend.java | 7 - .../rhi/upload/blaze3d/Blaze3dFrameFence.java | 7 - .../upload/blaze3d/Blaze3dMeshAllocation.java | 7 - .../rhi/upload/blaze3d/Blaze3dMeshArena.java | 7 - .../engine/shader/LoVisualShaderSources.java | 7 - .../engine/svg/backend/SvgInlineText.java | 7 - .../engine/svg/backend/SvgMeshBackend.java | 7 - .../render/engine/svg/model/SvgColorMode.java | 7 - .../engine/svg/model/SvgRenderOptions.java | 7 - .../render/engine/svg/model/SvgStyle.java | 7 - .../render/engine/svg/model/SvgStyleUtil.java | 7 - .../engine/svg/parser/SvgMsdfRegistry.java | 7 - .../engine/svg/parser/SvgPathParser.java | 357 +-- .../render/engine/svg/parser/SvgRegistry.java | 7 - .../svg/parser/impl/SvgArcGeometry.java | 120 + .../svg/parser/impl/SvgPathCommands.java | 90 + .../engine/svg/parser/impl/SvgPathCurves.java | 139 + .../backend/atlas/BitmapAtlasTextBackend.java | 7 - .../text/backend/atlas/GlyphAtlasManager.java | 7 - .../text/backend/atlas/GlyphAtlasPage.java | 7 - .../text/backend/atlas/MsdfTextBackend.java | 7 - .../text/backend/backend/TextBackend.java | 7 - .../backend/backend/TextBackendRouter.java | 7 - .../text/backend/backend/TextDrawCommand.java | 7 - .../backend/VanillaSodiumTextBackend.java | 7 - .../backend/placement/TextPlacementMode.java | 7 - .../placement/TextPlacementResolver.java | 7 - .../placement/TextPlacementTransform.java | 7 - .../backend/placement/TextRenderDomain.java | 7 - .../text/command/TextCommandBuffer.java | 7 - .../engine/text/command/TextCommandStats.java | 7 - .../command/TextCommandStatsSnapshot.java | 7 - .../engine/text/command/TextEffectSpec.java | 7 - .../render/engine/text/font/Font.java | 7 - .../render/engine/text/font/FontFace.java | 7 - .../render/engine/text/font/FontUtils.java | 7 - .../render/engine/text/font/Fonts.java | 7 - .../text/font/faces/BuiltinFontFace.java | 7 - .../text/font/faces/EmptyGlyphFont.java | 7 - .../engine/text/font/faces/FontFamily.java | 7 - .../text/font/faces/SystemFontFace.java | 7 - .../engine/text/font/info/FontDebugStats.java | 7 - .../engine/text/font/info/FontInfo.java | 7 - .../text/font/info/TextBackendPreference.java | 7 - .../engine/text/font/info/TextClipSpec.java | 7 - .../engine/text/glyph/CustomTextRenderer.java | 48 +- .../render/engine/text/glyph/GlyphFont.java | 7 - .../engine/text/glyph/GradientTexts.java | 59 +- .../text/glyph/VanillaTextRenderer.java | 7 - .../text/glyph/custom/CustomFontSet.java | 7 - .../text/glyph/custom/HorizontalFade.java | 7 - .../text/glyph/custom/TextGlyphFallback.java | 7 - .../render/engine/text/msdf/MsdfFont.java | 7 - .../engine/text/msdf/MsdfGlyphDrawer.java | 7 - .../engine/text/msdf/WorldTextRenderer.java | 10 +- .../engine/text/msdf/atlas/MsdfAtlas.java | 7 - .../text/msdf/atlas/MsdfAtlasLoader.java | 7 - .../engine/text/msdf/atlas/MsdfGlyph.java | 7 - .../engine/text/msdf/atlas/MsdfJson.java | 7 - .../engine/text/render/TextRenderSystem.java | 7 - .../engine/text/render/TextRenderer.java | 7 - .../render/engine/texture/Texture.java | 7 - .../render/engine/texture/TextureStorage.java | 7 - .../render/engine/uniform/MeshBuilder.java | 452 +--- .../render/engine/uniform/MeshData.java | 7 - .../render/engine/uniform/MeshOps.java | 135 + .../impl/esp/ShaderEspBlurUniforms.java | 7 - .../impl/esp/ShaderEspGradientUniforms.java | 7 - .../impl/esp/ShaderEspSmokeUniforms.java | 7 - .../uniform/impl/hand/HandGlassUniforms.java | 7 - .../impl/hand/HandMetallicUniforms.java | 7 - .../impl/hand/effects/HandNoiseUniforms.java | 7 - .../impl/hand/effects/HandSmokeUniforms.java | 7 - .../hand/effects/TextureTintUniforms.java | 7 - .../impl/post/DepthOfFieldUniforms.java | 7 - .../uniform/impl/post/MotionBlurUniforms.java | 7 - .../uniform/impl/post/PostFXUniforms.java | 7 - .../impl/post/PostProcessUniforms.java | 7 - .../postprocess/SkyShaderNebulaUniforms.java | 7 - .../postprocess/SkyShaderStarryUniforms.java | 7 - .../sky/postprocess/SkyShaderUniforms.java | 7 - .../impl/sky/skybox/SkyboxShaderUniforms.java | 7 - .../uniform/impl/sky/sun/SkySunUniforms.java | 7 - .../impl/ui/MenuBackgroundUniforms.java | 7 - .../uniform/impl/ui/MsdfTextUniforms.java | 7 - .../uniform/impl/ui/UIBatchUniforms.java | 7 - .../uniform/impl/ui/UIBlurUniforms.java | 7 - .../uniform/impl/world/HeatUniforms.java | 7 - .../impl/world/KillEffectBeamUniforms.java | 7 - .../uniform/impl/world/MeshUniforms.java | 7 - .../world/liquid/WaterHighlightUniforms.java | 7 - .../impl/world/liquid/WetSurfaceUniforms.java | 7 - .../engine/uniform/mesh/MeshWrites.java | 84 + .../uniform/storage/FixedUniformStorage.java | 7 - .../vertex/LoVisualVertexFormatElements.java | 7 - .../engine/vertex/LoVisualVertexFormats.java | 7 - .../engine/visuals/LoVisualVisualFrame.java | 7 - .../engine/visuals/LoVisualVisualPass.java | 7 - .../engine/visuals/LoVisualVisualPhase.java | 7 - .../engine/visuals/LoVisualVisuals.java | 7 - .../render/engine/world/WorldBatcher.java | 12 +- .../engine/world/WorldCommandBuffer.java | 10 +- .../render/engine/world/WorldDrawCommand.java | 14 +- .../engine/world/WorldPassCompiler.java | 7 - .../world/mesh/WorldWideLineMeshCompiler.java | 7 - .../engine/world/stats/WorldRenderStats.java | 7 - .../world/stats/WorldRenderStatsSnapshot.java | 7 - .../fade/ViewObstructionFadeContext.java | 7 - .../render/fade/ViewObstructionFadeState.java | 7 - .../render/helpers/clip/ClipFunction.java | 499 +--- .../render/helpers/clip/ScissorFunction.java | 7 - .../render/helpers/clip/ScreenProjection.java | 7 - .../helpers/clip/ScreenSpaceOverlay2D.java | 7 - .../render/helpers/clip/impl/ClipDebug.java | 58 + .../helpers/clip/impl/ClipGeometry.java | 170 ++ .../helpers/material/EffectIconRenderer.java | 7 - .../helpers/material/MatteHudStyle.java | 7 - .../helpers/material/SodiumMaterialFlags.java | 7 - .../material/SodiumSurfaceFlagContext.java | 14 +- .../particle/OrientedQuadRenderer.java | 7 - .../render/helpers/player/HeadAnchor.java | 7 - .../render/helpers/player/Particle3D.java | 7 - .../helpers/player/PlayerHeadRenderer.java | 7 - .../render/helpers/player/TrailPoint.java | 7 - .../render/helpers/util/GuiSpriteBatch.java | 7 - .../helpers/util/ParticleTextureMode.java | 7 - .../render/helpers/util/SystemCursor.java | 7 - .../render/helpers/util/TickDelta.java | 7 - .../render/invisible/SeeInvisiblesReveal.java | 7 - .../render/invisible/SeeInvisiblesState.java | 7 - .../iris/compat/IrisCompatibilityFeature.java | 7 - .../iris/compat/IrisCompatibilityGuards.java | 7 - .../iris/compat/IrisCompatibilityProfile.java | 7 - .../compat/IrisCompatibilityProfiles.java | 7 - .../iris/patch/ShaderPatchCompiler.java | 7 +- .../iris/patch/ShaderPatchCompilerCli.java | 7 - .../render/iris/patch/ShaderPatchEngine.java | 470 +--- .../patch/compile/PatchCompileHelper.java | 7 +- .../iris/patch/compile/PatchSourceHelper.java | 7 +- .../iris/patch/impl/ShaderPatchRepo.java | 158 ++ .../iris/patch/parse/PatchParseSupport.java | 7 - .../render/iris/patch/parse/PatchParser.java | 7 - .../render/iris/runtime/IrisRuntime.java | 7 - .../iris/runtime/IrisRuntimeBridge.java | 7 - .../iris/runtime/IrisRuntimeSnapshot.java | 7 - .../iris/runtime/LoVisualIrisUniforms.java | 7 - .../scene/IrisFinalizedSceneRenderer.java | 7 - .../render/iris/scene/IrisSceneDepth.java | 7 - .../iris/scene/IrisSecondHandScene.java | 7 - .../render/iris/state/IrisHandMaskState.java | 7 - .../iris/state/IrisLoVisualFrameHooks.java | 7 - .../render/sky/CustomSkyboxRenderer.java | 346 +-- .../render/sky/impl/SkyboxProjection.java | 103 + .../render/sky/impl/SkyboxShaderPasses.java | 158 ++ .../render/sky/impl/SkyboxSunRenderer.java | 125 + .../sodium/bridge/SodiumFrameContext.java | 7 - .../sodium/bridge/SodiumRenderBridge.java | 7 - .../bridge/SodiumShaderWorkarounds.java | 7 - .../sodium/interop/SodiumTerrainInterop.java | 7 - .../SodiumTerrainInteropStatsSnapshot.java | 7 - .../terrain/LoVisualChunkMeshAttributes.java | 7 - .../terrain/LoVisualChunkMeshFormats.java | 7 - .../vertex/LoVisualChunkVertexExtension.java | 7 - .../vertex/LoVisualModelVertexType.java | 7 - .../terrain/vertex/LoVisualTerrainVertex.java | 7 - .../SodiumSectionVisibilityProvider.java | 7 - .../SodiumVisibilityStatsSnapshot.java | 7 - .../java/dev/loki/lovisual/util/FastFps.java | 7 - .../loki/lovisual/util/LegacyHudNotifier.java | 7 - .../loki/lovisual/util/NarratorBlocker.java | 7 - .../dev/loki/lovisual/util/OmniItemUtils.java | 7 - .../lovisual/util/aiming/AimPointUtil.java | 7 - .../lovisual/util/aiming/RequestHandler.java | 7 - .../aiming/RestrictedSingleUseAction.java | 7 - .../lovisual/util/aiming/data/Rotation.java | 7 - .../util/aiming/data/RotationDelta.java | 7 - .../util/aiming/data/RotationWithVector.java | 7 - .../aiming/features/MovementCorrection.java | 7 - .../processors/FailRotationProcessor.java | 7 - .../processors/RotationProcessor.java | 7 - .../processors/anglesmooth/AngleSmooth.java | 7 - .../anglesmooth/FactorAngleSmooth.java | 7 - .../anglesmooth/NoneAngleSmooth.java | 7 - .../impl/AccelerationAngleSmooth.java | 7 - .../impl/InterpolationAngleSmooth.java | 7 - .../anglesmooth/impl/LinearAngleSmooth.java | 7 - .../impl/curves/SigmoidAngleSmooth.java | 7 - .../impl/curves/SmartAngleSmooth.java | 7 - .../impl/curves/SpookyAngleSmooth.java | 7 - .../processors/impl/LazyAimProcessor.java | 7 - .../impl/MovingRotationProcessor.java | 7 - .../processors/impl/ShakeAimProcessor.java | 7 - .../impl/SnapAssistAimProcessor.java | 7 - .../util/aiming/point/PointGeometry.java | 193 ++ .../util/aiming/point/PointInsideBox.java | 7 - .../util/aiming/point/PointProcessor.java | 185 ++ .../util/aiming/point/PointTracker.java | 520 +--- .../preference/LeastDifferencePreference.java | 7 - .../aiming/preference/RotationPreference.java | 7 - .../aiming/raytrace/RotationRaytrace.java | 7 - .../util/aiming/rotation/RotationManager.java | 7 - .../util/aiming/rotation/RotationTarget.java | 7 - .../util/aiming/rotation/RotationUtil.java | 7 - .../lovisual/util/animation/EaseCurves.java | 7 - .../util/animation/EquipProgressComposer.java | 7 - .../lovisual/util/animation/SmoothValue.java | 7 - .../util/animation/ThreeStageProgress.java | 7 - .../lovisual/util/animation/hand/BobMath.java | 7 - .../util/animation/hand/HandDynamics.java | 7 - .../util/animation/hand/HandPoseMath.java | 7 - .../util/animation/hand/HeldAlongFist.java | 7 - .../animation/hand/state/HandAnimState.java | 7 - .../util/animation/hand/state/SwayState.java | 7 - .../lovisual/util/block/bed/BedBlockUtil.java | 7 - .../util/block/bed/BedwarsTeamColorUtil.java | 7 - .../lovisual/util/block/bed/SelfBedMode.java | 7 - .../util/block/bed/SelfBedTracker.java | 7 - .../block/render/BlockOutlineRenderer.java | 7 - .../block/render/BlockOutlineShapeMode.java | 7 - .../render/RenderSectionBlockScanner.java | 7 - .../render/WorldSectionBlockScanner.java | 7 - .../block/scan/BlockDeobfuscationScanner.java | 7 - .../util/block/scan/BlockScanContext.java | 7 - .../util/block/scan/BlockScanMode.java | 7 - .../util/block/scan/BlockSearchUtil.java | 7 - .../lovisual/util/click/AttackPressing.java | 7 - .../lovisual/util/click/ClickHumanizer.java | 7 - .../lovisual/util/click/ClickScheduler.java | 7 - .../util/combat/attack/AttackUtil.java | 7 - .../combat/attack/CombatStrikeController.java | 7 - .../combat/attack/ProtocolAttackExecutor.java | 7 - .../util/combat/hit/AttackHitPoint.java | 7 - .../protocol/ClientProtocolVersion.java | 7 - .../util/combat/protocol/ProtocolUtil.java | 7 - .../CombatProtocolHeuristicSource.java | 7 - .../heuristics/CombatProtocolHeuristics.java | 7 - .../CombatProtocolHeuristicsConfig.java | 7 - .../combat/protocol/vfp/VfpCompatibility.java | 7 - .../protocol/vfp/VfpCompatibility1_8.java | 7 - .../rotation/CombatRotationModeUtil.java | 7 - .../rotation/LethalStrikeCalculator.java | 7 - .../combat/rotation/RubberHandUseUtil.java | 7 - .../combat/rotation/SprintController.java | 7 - .../util/combat/tracking/AntiBotTracker.java | 7 - .../util/combat/tracking/ReachMeter.java | 7 - .../loki/lovisual/util/entity/EagleUtil.java | 199 -- .../lovisual/util/entity/EdgeRecovery.java | 204 ++ .../lovisual/util/entity/FreecamEntity.java | 7 - .../simulation/BoatSimulationCache.java | 7 - .../simulation/MountedEntityPrediction.java | 7 - .../simulation/PositionExtrapolation.java | 7 - .../entity/simulation/PositionPredictor.java | 7 - .../loki/lovisual/util/input/KeyManager.java | 7 - .../dev/loki/lovisual/util/input/KeyUtil.java | 7 - .../util/item/enchant/EnchantMeta.java | 7 - .../util/item/enchant/EnchantRegistry.java | 7 - .../util/item/enchant/EnchantUtil.java | 7 - .../util/item/enchant/TopEnchantUtil.java | 7 - .../lovisual/util/item/food/FoodUtil.java | 7 - .../util/item/food/IllegalItemUtil.java | 7 - .../item/rarity/ItemRarityColorProvider.java | 7 - .../util/item/rarity/RarityColorConfig.java | 7 - .../util/item/rarity/RarityColorUtil.java | 7 - .../loki/lovisual/util/logging/DebugLog.java | 7 - .../loki/lovisual/util/logging/DebugMode.java | 7 - .../lovisual/util/logging/ServerDumpUtil.java | 7 - .../lovisual/util/map/MapPlayersCache.java | 7 - .../lovisual/util/mctiers/MCTiersCache.java | 7 - .../lovisual/util/mctiers/MCTiersTier.java | 7 - .../util/media/impl/DummyMediaPlayerInfo.java | 7 - .../impl/linux/LinuxMediaPlayerInfo.java | 7 - .../media/impl/linux/LinuxMediaSession.java | 347 +-- .../media/impl/linux/LinuxMetadataReader.java | 191 ++ .../media/impl/linux/LinuxTrackState.java | 145 + .../util/media/impl/linux/dbus/Player.java | 7 - .../impl/win/WindowsMediaPlayerInfo.java | 7 - .../media/impl/win/WindowsMediaSession.java | 7 - .../lovisual/util/media/info/MediaInfo.java | 7 - .../util/media/info/MediaPlayerInfo.java | 7 - .../lovisual/util/media/info/RepeatMode.java | 7 - .../util/media/session/IMediaSession.java | 7 - .../media/session/MediaSessionService.java | 611 +---- .../session/service/MediaSessionArtwork.java | 87 + .../session/service/MediaSessionClock.java | 132 + .../session/service/MediaSessionPoller.java | 63 + .../session/service/MediaSessionTracker.java | 168 ++ .../network/control/BacktrackController.java | 390 +-- .../util/network/control/BacktrackModel.java | 55 + .../util/network/control/BlinkManager.java | 7 - .../network/control/FakeLagController.java | 7 - .../network/control/impl/BacktrackEngine.java | 106 + .../control/impl/BacktrackPacketFlow.java | 112 + .../control/impl/BacktrackTargeting.java | 171 ++ .../util/network/packet/PacketSnapshot.java | 7 - .../util/network/packet/TransferOrigin.java | 7 - .../lovisual/util/physics/TrinketPhysics.java | 7 - .../effect/heuristics/ParticleColors.java | 91 + .../effect/heuristics/ParticlePalette.java | 221 ++ .../heuristics/StatusEffectHeuristics.java | 286 +- .../heuristics/StatusEffectInference.java | 7 - .../effect/tracker/StatusEffectAccess.java | 7 - .../effect/tracker/StatusEffectTracker.java | 7 - .../effect/tracker/StatusEffectView.java | 7 - .../player/health/PlayerHealthResolver.java | 7 - .../player/health/PlayerSkinResolver.java | 7 - .../player/interaction/InteractionUtil.java | 7 - .../player/interaction/NetworkStatsUtil.java | 7 - .../interaction/ResetAttackCooldown.java | 7 - .../player/interaction/ServerFlyUtils.java | 7 - .../firework/FireworkUseController.java | 7 - .../player/movement/ElytraRecastUtil.java | 7 - .../EnvironmentMovementController.java | 7 - .../util/player/movement/MovementUtil.java | 7 - .../util/player/movement/PlayerSpeedUtil.java | 7 - .../simulation/LocalPlayerSimulation.java | 7 - .../simulation/PlayerMovementSimulation.java | 7 - .../simulation/PlayerSimulationCache.java | 7 - .../player/simulation/SimulatedInput.java | 7 - .../physics/FluidPhysicsHelper.java | 7 - .../simulation/physics/TravelHelper.java | 7 - .../remote/RemotePlayerObservation.java | 7 - .../remote/RemotePlayerSimulation.java | 7 - .../simulation/state/FluidStateHelper.java | 7 - .../simulation/state/SneakAndClimbHelper.java | 7 - .../CydhranianProjectileAngleCalculator.java | 7 - .../PolynomialProjectileAngleCalculator.java | 7 - .../calc/ProjectileAngleCalculator.java | 7 - .../SituationalProjectileAngleCalculator.java | 7 - .../util/projectile/math/ProjectileMath.java | 7 - .../math/ProjectilePredictionUtil.java | 7 - .../projectile/targeting/PointFinding.java | 7 - .../targeting/ProjectileTarget.java | 7 - .../ProjectileTargetPointFinder.java | 7 - .../projectile/trajectory/SimulatedArrow.java | 7 - .../projectile/trajectory/TrajectoryData.java | 7 - .../projectile/trajectory/TrajectoryInfo.java | 7 - .../projectile/trajectory/TrajectoryType.java | 7 - .../util/protection/NotifyThrottle.java | 7 - .../util/protection/SlotLockPolicy.java | 7 - .../lovisual/util/proxy/ProxyBackend.java | 7 - .../util/proxy/ProxyHandshakeHandler.java | 110 +- .../util/proxy/ProxyNettyInstaller.java | 7 - .../util/proxy/ProxyProtocolMessages.java | 111 + .../util/proxy/model/ProxyConfig.java | 7 - .../lovisual/util/proxy/model/ProxyEntry.java | 7 - .../lovisual/util/proxy/model/ProxyType.java | 7 - .../util/pvp/client/CooldownManager.java | 7 - .../util/pvp/client/CooldownsState.java | 7 - .../util/pvp/client/PendingUseTracker.java | 7 - .../util/pvp/cooldown/CooldownRegistry.java | 7 - .../cooldown/ItemCooldownRuleProvider.java | 7 - .../pvp/cooldown/ItemCooldownSnapshot.java | 7 - .../util/pvp/cooldown/ItemUseCooldowns.java | 7 - .../opponents/OpponentCooldownManager.java | 7 - .../util/pvp/opponents/TotemPopCounter.java | 7 - .../util/pvp/opponents/TotemPopSnapshot.java | 7 - .../util/pvp/parser/PvpChatParser.java | 7 - .../util/pvp/parser/PvpOverlayParser.java | 7 - .../util/pvp/parser/PvpTabParser.java | 7 - .../util/pvp/parser/PvpTextPatternUtil.java | 7 - .../lovisual/util/pvp/state/PvpState.java | 7 - .../lovisual/util/pvp/state/PvpTagSource.java | 7 - .../util/pvp/state/PvpTargetState.java | 7 - .../util/pvp/tracker/PvpTargetTracker.java | 7 - .../lovisual/util/pvp/tracker/PvpTracker.java | 7 - .../lovisual/util/raycast/RaycastUtil.java | 7 - .../resources/RenderResourceReadiness.java | 7 - .../util/resources/ResourceReloadHooks.java | 7 - .../lovisual/util/screen/ClientScreen.java | 7 - .../lovisual/util/screen/ScreenCatalog.java | 7 - .../util/session/MicrosoftSessionResult.java | 7 - .../session/MinecraftGameConfigHolder.java | 7 - .../lovisual/util/session/SessionChanger.java | 7 - .../util/session/SessionLoginData.java | 7 - .../util/session/microsoft/MicrosoftJson.java | 7 - .../auth/DeviceAuthPendingException.java | 7 - .../auth/MicrosoftAuthException.java | 7 - .../microsoft/auth/MicrosoftAuthService.java | 401 +-- .../microsoft/auth/MicrosoftDeviceCode.java | 7 - .../auth/impl/MicrosoftDeviceFlow.java | 72 + .../microsoft/auth/impl/MicrosoftHttp.java | 179 ++ .../auth/impl/MicrosoftXboxFlow.java | 139 + .../microsoft/model/MicrosoftTokens.java | 7 - .../microsoft/model/MinecraftProfile.java | 7 - .../session/microsoft/model/XboxToken.java | 7 - .../lovisual/util/target/TargetManager.java | 7 - .../lovisual/util/target/TargetingUtil.java | 7 - .../util/text/edit/ClipboardUtil.java | 7 - .../util/text/edit/TextSelection.java | 7 - .../util/text/format/LegacyTextUtil.java | 7 - .../util/text/format/TextJsonUtil.java | 7 - .../util/text/format/TextRenderUtil.java | 7 - .../lovisual/util/text/mask/ChatNameUtil.java | 7 - .../lovisual/util/text/mask/NameMasker.java | 7 - .../util/text/mask/NameProtector.java | 7 - .../loki/lovisual/util/time/GameClock.java | 7 - .../dev/loki/lovisual/util/time/Timer.java | 7 - .../lovisual/util/time/TimerController.java | 7 - .../util/wav/engine/CustomSoundEngine.java | 7 - .../lovisual/util/wav/engine/SourcePool.java | 7 - .../lovisual/util/wav/engine/WavSound.java | 7 - .../util/wav/loader/WavBufferCache.java | 7 - .../lovisual/util/wav/loader/WavData.java | 7 - .../lovisual/util/wav/loader/WavLoader.java | 7 - .../util/wav/util/ClickGuiSounds.java | 7 - .../lovisual/util/wav/util/WavDebugStats.java | 7 - .../util/wav/util/WavSoundHelper.java | 7 - .../util/world/ExplosionDamageUtil.java | 532 +--- .../util/world/ExplosionExposure.java | 176 ++ .../util/world/ExplosionReductions.java | 89 + .../loki/lovisual/util/world/GpsBearing.java | 7 - .../profile/ConfigProfileDiffEngineTest.java | 7 - .../profile/ConfigProfileStorageTest.java | 7 - .../profile/ConfigProfileValueCodecTest.java | 7 - .../config/values/ColorValueTest.java | 7 - .../lovisual/config/values/EnumValueTest.java | 7 - .../lovisual/config/values/ModeValueTest.java | 7 - .../config/values/PrimitiveValuesTest.java | 7 - .../loki/lovisual/events/EventBusTest.java | 7 - .../command/CommandDiscoveryTest.java | 7 - .../features/command/CommandPrefixTest.java | 7 - .../features/command/UsageHintsTest.java | 7 - .../impl/world/XaeroWaypointCommandTest.java | 7 - .../triangulator/TriangulatorSolverTest.java | 7 - .../combat/combatlog/CombatLogStatsTest.java | 7 - .../misc/items/ScrollMovePolicyTest.java | 7 - .../misc/protect/NoServerPackTest.java | 7 - .../modules/misc/sound/NoSoundTest.java | 7 - .../modules/player/DeathCoordsTest.java | 7 - .../module/modules/player/UseTrackerTest.java | 7 - .../modules/player/auto/AutoAcceptTest.java | 7 - .../modules/player/auto/AutoLeaveTest.java | 7 - .../modules/player/auto/AutoRespawnTest.java | 7 - .../player/interact/NoInteractTest.java | 7 - .../player/inventory/TapeMouseTest.java | 7 - .../visuals/AlwaysVisibleHandTest.java | 7 - .../modules/visuals/AtmoDawnFogTest.java | 7 - .../modules/visuals/ChinaHatModelTest.java | 7 - .../modules/visuals/ChinaHatPaletteTest.java | 7 - .../module/modules/visuals/LightTintTest.java | 7 - .../modules/visuals/PostFxPresetTest.java | 7 - .../modules/visuals/wings/WingShapesTest.java | 7 - .../security/BackdoorProtectionTest.java | 7 - .../script/JavetUiScriptEngineTest.java | 7 - .../util/animation/EaseCurvesTest.java | 7 - .../animation/EquipProgressComposerTest.java | 7 - .../util/animation/SmoothValueTest.java | 7 - .../animation/ThreeStageProgressTest.java | 7 - .../util/animation/hand/BobMathTest.java | 7 - .../util/animation/hand/HandPoseMathTest.java | 7 - .../hand/state/HandAnimStateTest.java | 7 - .../animation/hand/state/SwayStateTest.java | 7 - .../lovisual/util/combat/ReachMeterTest.java | 7 - .../util/mctiers/MCTiersTierTest.java | 7 - .../util/physics/TrinketPhysicsTest.java | 7 - .../util/protection/NotifyThrottleTest.java | 7 - .../util/protection/SlotLockPolicyTest.java | 7 - .../util/text/LegacyTextUtilTest.java | 7 - .../lovisual/util/text/NameMaskerTest.java | 6 - .../lovisual/util/text/NameProtectorTest.java | 7 - .../lovisual/util/world/GpsBearingTest.java | 7 - mod/tools/strip_license_header.py | 63 + 1897 files changed, 36199 insertions(+), 39289 deletions(-) create mode 100644 backend/accounts-service/migrations/0002_refresh_tokens.sql create mode 100644 backend/accounts-service/src/accounts/mod.rs create mode 100644 backend/accounts-service/src/accounts/model.rs create mode 100644 backend/accounts-service/src/accounts/repo.rs create mode 100644 backend/accounts-service/src/auth/handlers.rs create mode 100644 backend/accounts-service/src/auth/tokens.rs create mode 100644 backend/accounts-service/src/avatars/handlers.rs create mode 100644 backend/accounts-service/src/avatars/mod.rs create mode 100644 backend/accounts-service/src/avatars/processing.rs create mode 100644 backend/accounts-service/src/avatars/storage.rs create mode 100644 backend/accounts-service/src/device/handlers.rs create mode 100644 backend/accounts-service/src/device/mod.rs create mode 100644 backend/accounts-service/src/device/store.rs create mode 100644 backend/accounts-service/src/error.rs create mode 100644 backend/accounts-service/tests/auth_flow.rs create mode 100644 backend/accounts-service/tests/auth_flow/refresh.rs create mode 100644 backend/accounts-service/tests/avatar_upload.rs create mode 100644 backend/accounts-service/tests/common/mod.rs delete mode 100644 backend/accounts-service/tests/db_migration.rs create mode 100644 backend/accounts-service/tests/device_flow.rs delete mode 100644 backend/accounts-service/tests/health.rs create mode 100644 backend/accounts-service/tests/smoke.rs create mode 100644 backend/common/Cargo.toml create mode 100644 backend/common/build.rs create mode 100644 backend/common/proto/accounts.proto create mode 100644 backend/common/src/internal.rs create mode 100644 backend/common/src/jwt.rs create mode 100644 backend/common/src/lib.rs create mode 100644 backend/configs-service/PLAN.md delete mode 100644 backend/configs-service/README.md create mode 100644 backend/gateway/PLAN.md delete mode 100644 backend/gateway/README.md create mode 100644 frontend/PLAN.md delete mode 100644 frontend/src/App.css delete mode 100644 frontend/src/assets/hero.png delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/features/auth/RequireAuth.tsx create mode 100644 frontend/src/features/auth/api.ts create mode 100644 frontend/src/features/auth/forms/LoginForm.tsx create mode 100644 frontend/src/features/auth/forms/RegisterForm.tsx create mode 100644 frontend/src/features/auth/session.tsx create mode 100644 frontend/src/features/auth/tests/forms.test.tsx create mode 100644 frontend/src/features/auth/tests/session.test.tsx create mode 100644 frontend/src/features/auth/validation.ts create mode 100644 frontend/src/pages/public/LoginPage.tsx create mode 100644 frontend/src/pages/public/RegisterPage.tsx create mode 100644 frontend/src/shared/api/client.ts create mode 100644 frontend/src/shared/api/errors.ts create mode 100644 frontend/src/shared/api/tests/client.test.ts create mode 100644 frontend/src/shared/types.ts create mode 100644 frontend/src/shared/ui/Button.tsx create mode 100644 frontend/src/shared/ui/Notice.tsx create mode 100644 frontend/src/shared/ui/ShareCode.tsx create mode 100644 frontend/src/shared/ui/TextField.tsx create mode 100644 frontend/src/shared/ui/tests/ui.test.tsx create mode 100644 frontend/src/test/fetch.ts create mode 100644 frontend/src/test/render.tsx create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/test/smoke.test.tsx create mode 100644 mod/src/main/java/dev/loki/lovisual/HudRender.java create mode 100644 mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonDescriptors.java create mode 100644 mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonLifecycle.java create mode 100644 mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonProfiles.java create mode 100644 mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonRuntime.java create mode 100644 mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigFilePaths.java create mode 100644 mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigGsonSupport.java create mode 100644 mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigJson5Support.java create mode 100644 mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigSaveScheduler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountSession.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountStore.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountsValue.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatHoverCapture.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStoreKeys.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStorePersistence.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/impl/ChatHoverCacheCodec.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorCaret.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorPainter.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsModel.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsScroll.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardInput.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardLayout.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardState.java delete mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java delete mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelLayout.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelModels.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/input/SettingsPanelInput.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/render/SettingsPanelRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsInput.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsScroll.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeCardRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeScrollState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenInput.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenLayout.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRuleEditor.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesDetailRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingI18n.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingLocalization.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudElementNamer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudPrewarm.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudSettingRegistry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifierPainter.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/runtime/HudNotifierRuntime.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelProps.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelSignature.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/BarSettings.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthBarPainter.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthScriptRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandAnimation.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandLayout.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelection.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelectionGradient.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListCollector.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListReader.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListSignature.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListTextSplitter.java delete mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerController.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerOps.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonPanelAnims.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/auth/AddonScanState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonManagerRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonPanelsRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonUiMetrics.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardsRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuPainter.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuTextUtil.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/LoVisualProxyManagerScreen.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerController.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleDispatcher.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleRegistrar.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/impl/TargetEspCrystalRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/impl/PvpCooldownRules.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingAnimator.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesA.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesB.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesC.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesRender.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsColor.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsParticles.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillBeamHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillEmberHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillLightningHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillOrthodoxHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/CrystalHoles.java delete mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/BlockSafety.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/Hole.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleScanner.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/impl/DropEspOverlayRenderer.java delete mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHat.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatModel.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatPalette.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricPlayback.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricStage.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/mode/LyricModes.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagKeys.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagProjection.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagsCollector.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedDepthBindings.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedDepthOfFieldHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSkyboxHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSunHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedVegetationHelper.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesBlending.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesProfileCodec.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesTransition.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreIO.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreJson.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerInputHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerMovementHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerNetworkHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerRotationHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHealthLayoutHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHudBarHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiOverlayHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiTooltipHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerPvpHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerSoundHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerStaffHandler.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DBatchBindings.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCullOptions.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCulling.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchAtlas.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchMeshes.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchOverlays.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptFrameStats.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptRuntimeCore.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptSignature.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleLayout.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStylePalette.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleText.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpFactory.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilNativeState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilPassLifecycle.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachment.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachmentManager.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilGl.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilSampleProbe.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanPipelineVariantKey.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanMsaaState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanStencilState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgArcGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCommands.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCurves.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshOps.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshWrites.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipDebug.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/iris/patch/impl/ShaderPatchRepo.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxProjection.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxShaderPasses.java create mode 100644 mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxSunRenderer.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointGeometry.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointProcessor.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/entity/EdgeRecovery.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMetadataReader.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxTrackState.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionArtwork.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionClock.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionPoller.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionTracker.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackModel.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackEngine.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackPacketFlow.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackTargeting.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticleColors.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticlePalette.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyProtocolMessages.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftDeviceFlow.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftHttp.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftXboxFlow.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/world/ExplosionExposure.java create mode 100644 mod/src/main/java/dev/loki/lovisual/util/world/ExplosionReductions.java create mode 100755 mod/tools/strip_license_header.py diff --git a/backend/.env.example b/backend/.env.example index 1b3380f..b45080c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,12 @@ DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db -JWT_SECRET=change-me-to-a-long-random-string +# at least 32 random bytes, e.g. `openssl rand -hex 32` +JWT_SECRET= PORT=8081 S3_ENDPOINT=http://localhost:9000 S3_BUCKET=lovisual-avatars S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin +# Shared secret between gateway and internal services (openssl rand -hex 32) +INTERNAL_KEY= +# Secure cookie flag: leave unset (or true) in production (HTTPS); false for local HTTP dev +COOKIE_SECURE=false diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9ec7e67..81c449f 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -11,15 +11,21 @@ dependencies = [ "aws-config", "aws-sdk-s3", "axum", + "axum-extra", "axum-test", + "base64", "chrono", + "common", "dashmap", "dotenvy", - "jsonwebtoken", + "hex", + "image", "rand 0.10.3", "serde", "serde_json", + "sha2 0.11.0", "sqlx", + "time", "tokio", "tower-http", "tracing", @@ -27,6 +33,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.5" @@ -78,6 +90,17 @@ dependencies = [ "password-hash", ] +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "atoi" version = "2.0.0" @@ -581,6 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -627,6 +651,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "axum-test" version = "21.1.0" @@ -726,12 +783,24 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.1" @@ -812,6 +881,25 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "common" +version = "0.1.0" +dependencies = [ + "axum", + "axum-test", + "chrono", + "jsonwebtoken", + "prost", + "serde", + "serde_json", + "subtle", + "tokio", + "tonic", + "tonic-prost", + "tonic-prost-build", + "uuid", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -830,6 +918,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ + "percent-encoding", "time", "version_check", ] @@ -1280,6 +1369,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -1302,6 +1400,23 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + [[package]] name = "flume" version = "0.12.0" @@ -1319,6 +1434,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1500,6 +1621,15 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1508,7 +1638,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1517,7 +1647,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -1730,6 +1860,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.11.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1881,6 +2024,32 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -1906,6 +2075,15 @@ version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -1986,6 +2164,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -2044,6 +2228,26 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.3" @@ -2055,6 +2259,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multer" version = "3.1.0" @@ -2072,6 +2286,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "multiversion" version = "0.9.0" @@ -2299,6 +2519,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + [[package]] name = "phc" version = "0.6.1" @@ -2310,6 +2541,26 @@ dependencies = [ "getrandom 0.4.3", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2349,6 +2600,19 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -2383,6 +2647,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -2401,6 +2675,91 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.119", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "22.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60" +dependencies = [ + "pulldown-cmark", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.47" @@ -2584,6 +2943,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.21.12" @@ -2879,6 +3251,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simdutf8" version = "0.1.5" @@ -3188,6 +3566,19 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.21" @@ -3335,6 +3726,74 @@ dependencies = [ "tokio", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2 0.6.5", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.119", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.3" @@ -3343,9 +3802,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3480,6 +3942,12 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3924,8 +4392,29 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 87c7191..7bc2fb8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "accounts-service", + "common", ] # Planned members, added when their own implementation plan starts diff --git a/backend/PLAN.md b/backend/PLAN.md index 0447708..b6afc78 100644 --- a/backend/PLAN.md +++ b/backend/PLAN.md @@ -30,15 +30,15 @@ backend/ migrations/ 0001_init.sql src/ - main.rs # axum app assembly, router, DB pool, S3 client - config.rs # env var loading (DATABASE_URL, JWT_SECRET, S3_*, PORT) + main.rs # binary entry: env, DB pool, serve + lib.rs # build_app(pool, &Config) -> Router (route wiring) + config.rs # env var loading + validation (DATABASE_URL, JWT_SECRET, S3_*, PORT) error.rs # AppError enum -> IntoResponse - db.rs # PgPool type alias + connect() auth/ mod.rs # re-exports password.rs # hash_password, verify_password (argon2) - jwt.rs # issue_access_token, issue_refresh_token, verify_access_token - handlers.rs # POST /auth/register, /auth/login, /auth/refresh, /auth/logout + jwt.rs # typed access/refresh tokens, verify_token, bearer_account_id + handlers.rs # POST /auth/register, /auth/login (refresh/logout: gateway plan) device/ mod.rs handlers.rs # POST /device/code, /device/confirm, /device/token @@ -46,11 +46,18 @@ backend/ avatars/ mod.rs handlers.rs # POST /avatars, multipart upload + processing.rs # decode, square-crop to 256x256, re-encode PNG (strips EXIF) storage.rs # S3 put_object wrapper accounts/ mod.rs model.rs # Account struct, row mapping - repo.rs # sqlx queries: create, find_by_email, find_by_id, update_avatar_url + repo.rs # sqlx queries: create, find_by_email, find_by_id, set_avatar + tests/ + common/mod.rs # shared test_pool / test_config + smoke.rs # health + migrations + auth_flow.rs + device_flow.rs + avatar_upload.rs ``` --- @@ -333,9 +340,20 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid" ```sql -- backend/accounts-service/migrations/0001_init.sql +-- Hardening: revoke implicit PUBLIC access to this schema. This Postgres +-- instance hosts multiple services' databases (accounts_db/configs_db/ +-- chat_db per TODO.md Фаза 10) on one cluster; the app role connects as +-- its own owner and needs no PUBLIC grant to function. +REVOKE ALL ON SCHEMA public FROM PUBLIC; + CREATE TABLE accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email TEXT NOT NULL UNIQUE, + -- No plain UNIQUE on email: Postgres TEXT comparison is case-sensitive, + -- so "User@x.com" and "user@x.com" would be treated as different rows + -- (account-confusion / signup-limit-bypass). A unique index on + -- lower(email) below enforces uniqueness case-insensitively — + -- application code must look up/insert by `email.to_lowercase()`. + email TEXT NOT NULL, password_hash TEXT NOT NULL, display_nick TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', @@ -343,6 +361,8 @@ CREATE TABLE accounts ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE UNIQUE INDEX accounts_email_lower_idx ON accounts (lower(email)); + CREATE TABLE avatars ( account_id UUID PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE, s3_key TEXT NOT NULL, @@ -500,49 +520,64 @@ git commit -m "feat(backend): argon2id password hashing" **Interfaces:** - Consumes: nothing new (takes `account_id: uuid::Uuid`, `secret: &str` as plain params — no dependency on DB types) -- Produces: `pub struct Claims { pub sub: String, pub exp: usize, pub token_type: String }` -- Produces: `pub fn issue_access_token(account_id: Uuid, secret: &str) -> String` (15 min expiry, `token_type: "access"`) -- Produces: `pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String` (30 day expiry, `token_type: "refresh"`) -- Produces: `pub fn verify_token(token: &str, secret: &str) -> Option` — returns `None` on any invalid/expired/malformed token, never panics +- Produces: `pub enum TokenType { Access, Refresh }` and `pub struct Claims { pub sub: String, pub exp: usize, pub token_type: TokenType }` +- Produces: `pub fn issue_access_token(account_id: Uuid, secret: &str) -> String` (15 min expiry, `TokenType::Access`) +- Produces: `pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String` (30 day expiry, `TokenType::Refresh`) +- Produces: `pub fn verify_token(token: &str, secret: &str, expected: TokenType) -> Option` — returns `None` on any invalid/expired/malformed token, wrong algorithm (HS256 pinned), or token type different from `expected`; never panics - [ ] **Step 1: Write the failing test** ```rust // backend/accounts-service/src/auth/jwt.rs -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use uuid::Uuid; +/// Distinguishes token purposes so a long-lived refresh/device token can +/// never be replayed as a short-lived access token (and vice versa). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TokenType { + Access, + Refresh, +} + #[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub sub: String, pub exp: usize, - pub token_type: String, + pub token_type: TokenType, } -fn issue(account_id: Uuid, secret: &str, token_type: &str, ttl_seconds: i64) -> String { +fn issue(account_id: Uuid, secret: &str, token_type: TokenType, ttl_seconds: i64) -> String { let exp = (chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds)).timestamp() as usize; - let claims = Claims { sub: account_id.to_string(), exp, token_type: token_type.into() }; - encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes())) + let claims = Claims { sub: account_id.to_string(), exp, token_type }; + encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes())) .expect("encoding a well-formed Claims struct cannot fail") } pub fn issue_access_token(account_id: Uuid, secret: &str) -> String { - issue(account_id, secret, "access", 15 * 60) + issue(account_id, secret, TokenType::Access, 15 * 60) } pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String { - issue(account_id, secret, "refresh", 30 * 24 * 60 * 60) + issue(account_id, secret, TokenType::Refresh, 30 * 24 * 60 * 60) } -pub fn verify_token(token: &str, secret: &str) -> Option { - decode::( +/// Returns the claims only if the signature, expiry AND token type all match. +/// HS256 is pinned explicitly (no algorithm confusion) and expiry leeway is 0. +pub fn verify_token(token: &str, secret: &str, expected: TokenType) -> Option { + let mut validation = Validation::new(Algorithm::HS256); + validation.leeway = 0; + validation.set_required_spec_claims(&["exp", "sub"]); + let claims = decode::( token, &DecodingKey::from_secret(secret.as_bytes()), - &Validation::default(), + &validation, ) - .ok() - .map(|data| data.claims) + .ok()? + .claims; + (claims.token_type == expected).then_some(claims) } #[cfg(test)] @@ -553,20 +588,55 @@ mod tests { fn access_token_round_trips() { let id = Uuid::new_v4(); let token = issue_access_token(id, "test-secret"); - let claims = verify_token(&token, "test-secret").expect("should decode"); + let claims = verify_token(&token, "test-secret", TokenType::Access).expect("should decode"); assert_eq!(claims.sub, id.to_string()); - assert_eq!(claims.token_type, "access"); + assert_eq!(claims.token_type, TokenType::Access); } #[test] fn wrong_secret_fails_verify() { let token = issue_access_token(Uuid::new_v4(), "test-secret"); - assert!(verify_token(&token, "other-secret").is_none()); + assert!(verify_token(&token, "other-secret", TokenType::Access).is_none()); } #[test] fn garbage_token_fails_verify() { - assert!(verify_token("not.a.jwt", "test-secret").is_none()); + assert!(verify_token("not.a.jwt", "test-secret", TokenType::Access).is_none()); + } + + #[test] + fn refresh_token_is_rejected_where_access_is_expected() { + let token = issue_refresh_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "test-secret", TokenType::Access).is_none()); + assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_some()); + } + + #[test] + fn access_token_is_rejected_where_refresh_is_expected() { + let token = issue_access_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_none()); + } + + #[test] + fn expired_token_fails_verify() { + let token = issue(Uuid::new_v4(), "test-secret", TokenType::Access, -10); + assert!(verify_token(&token, "test-secret", TokenType::Access).is_none()); + } + + #[test] + fn token_signed_with_other_algorithm_is_rejected() { + let claims = Claims { + sub: Uuid::new_v4().to_string(), + exp: (chrono::Utc::now().timestamp() + 600) as usize, + token_type: TokenType::Access, + }; + let hs512 = encode( + &Header::new(Algorithm::HS512), + &claims, + &EncodingKey::from_secret(b"test-secret"), + ) + .unwrap(); + assert!(verify_token(&hs512, "test-secret", TokenType::Access).is_none()); } } ``` @@ -578,7 +648,7 @@ Add `pub mod jwt;` to `backend/accounts-service/src/auth/mod.rs`. - [ ] **Step 3: Run tests** Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test jwt::` -Expected: 3 tests pass +Expected: 7 tests pass - [ ] **Step 4: Commit** @@ -603,7 +673,7 @@ git commit -m "feat(backend): JWT access/refresh token issue and verify" - Produces: `pub struct Account { pub id: Uuid, pub email: String, pub password_hash: String, pub display_nick: String, pub role: String, pub can_publish_addons: bool, pub created_at: chrono::DateTime }` - Produces: `pub async fn create(pool: &PgPool, email: &str, password_hash: &str, nick: &str) -> Result` - Produces: `pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error>` -- Produces: unique-violation on duplicate email surfaces as `sqlx::Error::Database` — callers (Task 6) match on `.constraint() == Some("accounts_email_key")` +- Produces: unique-violation on duplicate email surfaces as `sqlx::Error::Database` — callers (Task 6) match on `.constraint() == Some("accounts_email_lower_idx")` (the case-insensitive unique index on `lower(email)` from Task 2's migration). `create` and `find_by_email` both normalize with `email.trim().to_lowercase()` before touching the DB, so the stored value is always lowercase and lookups always hit the index. - [ ] **Step 1: Write the model** @@ -634,6 +704,12 @@ use super::model::Account; use sqlx::PgPool; use uuid::Uuid; +// Emails are stored lowercase and compared via the `lower(email)` unique +// index (see migrations/0001_init.sql). Every entry point normalizes here. +pub fn normalize_email(email: &str) -> String { + email.trim().to_lowercase() +} + pub async fn create( pool: &PgPool, email: &str, @@ -645,7 +721,7 @@ pub async fn create( VALUES ($1, $2, $3) RETURNING id, email, password_hash, display_nick, role, can_publish_addons, created_at", ) - .bind(email) + .bind(normalize_email(email)) .bind(password_hash) .bind(nick) .fetch_one(pool) @@ -655,9 +731,9 @@ pub async fn create( pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { sqlx::query_as::<_, Account>( "SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at - FROM accounts WHERE email = $1", + FROM accounts WHERE lower(email) = $1", ) - .bind(email) + .bind(normalize_email(email)) .fetch_optional(pool) .await } @@ -710,6 +786,50 @@ mod tests { let result = find_by_email(&pool, "does-not-exist@example.com").await.unwrap(); assert!(result.is_none()); } + + #[tokio::test] + async fn email_lookup_is_case_insensitive_and_stored_lowercase() { + let pool = test_pool().await; + let tag = Uuid::new_v4(); + let mixed = format!("MiXeD-{tag}@Example.COM"); + + let created = create(&pool, &mixed, "hash123", "Nick").await.unwrap(); + assert_eq!(created.email, mixed.to_lowercase()); + + let found = find_by_email(&pool, &mixed.to_uppercase()) + .await + .unwrap() + .expect("lookup must ignore case"); + assert_eq!(found.id, created.id); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn duplicate_email_differing_only_by_case_is_rejected() { + let pool = test_pool().await; + let tag = Uuid::new_v4(); + let first = create(&pool, &format!("dup-{tag}@example.com"), "h", "A").await.unwrap(); + + let second = create(&pool, &format!("DUP-{tag}@EXAMPLE.com"), "h", "B").await; + let err = second.expect_err("case-variant duplicate must violate the unique index"); + match err { + sqlx::Error::Database(db) => { + assert_eq!(db.constraint(), Some("accounts_email_lower_idx")); + } + other => panic!("expected a database unique violation, got {other:?}"), + } + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(first.id) + .execute(&pool) + .await + .unwrap(); + } } ``` @@ -726,7 +846,7 @@ Add `pub mod accounts;` to `backend/accounts-service/src/lib.rs`. - [ ] **Step 4: Run tests** Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test accounts::repo::` -Expected: 2 tests pass +Expected: 4 tests pass - [ ] **Step 5: Commit** @@ -785,7 +905,7 @@ impl IntoResponse for AppError { impl From for AppError { fn from(err: sqlx::Error) -> Self { if let sqlx::Error::Database(ref db_err) = err { - if db_err.constraint() == Some("accounts_email_key") { + if db_err.constraint() == Some("accounts_email_lower_idx") { return AppError::Conflict("email already registered".into()); } } @@ -806,7 +926,7 @@ async fn test_pool() -> sqlx::PgPool { let url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); let pool = sqlx::PgPool::connect(&url).await.expect("connect"); - sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); pool } @@ -890,6 +1010,63 @@ async fn duplicate_email_registration_returns_409() { .await .unwrap(); } + +#[tokio::test] +async fn login_with_unknown_email_returns_401() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + + let response = server + .post("/auth/login") + .json(&json!({ "email": format!("nobody-{}@example.com", Uuid::new_v4()), "password": "whatever-123" })) + .await; + response.assert_status(axum::http::StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn login_is_case_insensitive_on_email() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let tag = Uuid::new_v4(); + let registered = format!("CaseUser-{tag}@Example.com"); + + server + .post("/auth/register") + .json(&json!({ "email": registered, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + server + .post("/auth/login") + .json(&json!({ "email": registered.to_lowercase(), "password": "password123" })) + .await + .assert_status_ok(); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(registered.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn register_rejects_oversized_password_with_400() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + + let response = server + .post("/auth/register") + .json(&json!({ + "email": format!("big-{}@example.com", Uuid::new_v4()), + "password": "a".repeat(10_000), + "nick": "Rider" + })) + .await; + response.assert_status(axum::http::StatusCode::BAD_REQUEST); +} ``` - [ ] **Step 3: Run the tests to see them fail to compile (build_app signature doesn't match yet)** @@ -911,6 +1088,21 @@ use serde::{Deserialize, Serialize}; pub struct AuthState { pub pool: sqlx::PgPool, pub jwt_secret: String, + // A real Argon2id hash of a throwaway string. `login` verifies against + // it when the email is unknown so that "no such account" costs the same + // ~100ms as "wrong password" — otherwise response time leaks which + // emails are registered (user enumeration via timing). + dummy_hash: String, +} + +impl AuthState { + pub fn new(pool: sqlx::PgPool, jwt_secret: String) -> Self { + // Hashing a fixed, short constant with fixed valid params cannot + // fail; this is not user input, so the expect is a startup invariant. + let dummy_hash = password::hash_password("timing-equalizer-not-a-real-password") + .expect("hashing a constant with pinned params cannot fail"); + AuthState { pool, jwt_secret, dummy_hash } + } } #[derive(Deserialize)] @@ -934,6 +1126,12 @@ pub async fn register( if req.password.len() < 8 { return Err(AppError::Validation("password must be at least 8 characters".into())); } + if req.password.len() > password::MAX_PASSWORD_BYTES { + return Err(AppError::Validation(format!( + "password must be at most {} bytes", + password::MAX_PASSWORD_BYTES + ))); + } let hash = password::hash_password(&req.password) .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; let account = repo::create(&state.pool, &req.email, &hash, &req.nick).await?; @@ -963,9 +1161,11 @@ pub async fn login( State(state): State, Json(req): Json, ) -> Result, AppError> { - let account = repo::find_by_email(&state.pool, &req.email) - .await? - .ok_or(AppError::Unauthorized)?; + let Some(account) = repo::find_by_email(&state.pool, &req.email).await? else { + // Burn the same Argon2 cost as a real check, then fail identically. + password::verify_password(&req.password, &state.dummy_hash); + return Err(AppError::Unauthorized); + }; if !password::verify_password(&req.password, &account.password_hash) { return Err(AppError::Unauthorized); } @@ -1026,21 +1226,21 @@ git commit -m "feat(backend): POST /auth/register and /auth/login" - Create: `backend/accounts-service/src/device/store.rs` - Create: `backend/accounts-service/src/device/handlers.rs` - Modify: `backend/accounts-service/src/lib.rs` (mount routes, extend shared state) +- Modify: `backend/accounts-service/src/error.rs` (add `NotFound(String)` → 404 and `TooManyRequests` → 429 variants to `AppError`, same JSON body shape `{"error": ...}` as the others) - Test: `backend/accounts-service/tests/device_flow.rs` **Interfaces:** - Consumes: `jwt::verify_token` (Task 4, to authenticate the `/device/confirm` caller — that endpoint requires a valid site access token in `Authorization: Bearer`) - Produces: `DeviceStore` (in-memory `DashMap`, keyed by `device_code`), `pub fn new_pair() -> (String, String)` returning `(device_code, user_code)` where `user_code` is formatted `XXXX-XXXX` from `[A-Z0-9]` - Produces: `POST /device/code` → `201 {device_code, user_code, expires_in: 600}` -- Produces: `POST /device/confirm` (auth required) body `{user_code}` → `200 {}` or `404` if code unknown/expired -- Produces: `POST /device/token` body `{device_code}` → `200 {device_token}` once confirmed, `202 {}` (still pending) before confirmation, `404` if unknown/expired +- Produces: `POST /device/confirm` (requires an ACCESS token) body `{user_code}` → `200 {}` or `404` if code unknown/expired (input is trimmed/upper-cased) +- Produces: `POST /device/token` body `{device_code}` → `200 {device_token}` once confirmed (single use: the entry is consumed, a replay gets `404`), `202 {}` (still pending) before confirmation, `404` if unknown/expired; `POST /device/code` → `429` when 10 000 codes are pending - [ ] **Step 1: Write the device code store** ```rust // backend/accounts-service/src/device/store.rs use dashmap::DashMap; -use rand::Rng; use std::sync::Arc; use std::time::{Duration, Instant}; use uuid::Uuid; @@ -1057,7 +1257,18 @@ pub struct DeviceStore { by_device_code: Arc>, } +#[derive(Debug, PartialEq, Eq)] +pub enum PollResult { + Unknown, + Pending, + Confirmed(Uuid), +} + const TTL: Duration = Duration::from_secs(600); +// /device/code is unauthenticated, so the store must be bounded or anyone can +// grow process memory without limit. Expired entries are purged on every +// create, so the cap only bites under sustained abuse. +const MAX_PENDING: usize = 10_000; fn random_user_code() -> String { use rand::RngExt; @@ -1070,7 +1281,13 @@ fn random_user_code() -> String { } impl DeviceStore { - pub fn create(&self) -> (String, String) { + /// Returns `(device_code, user_code)`, or `None` when the store is full. + pub fn create(&self) -> Option<(String, String)> { + let now = Instant::now(); + self.by_device_code.retain(|_, entry| entry.expires_at > now); + if self.by_device_code.len() >= MAX_PENDING { + return None; + } let device_code = Uuid::new_v4().to_string(); let user_code = random_user_code(); self.by_device_code.insert( @@ -1078,16 +1295,19 @@ impl DeviceStore { DeviceCodeEntry { user_code: user_code.clone(), confirmed_account_id: None, - expires_at: Instant::now() + TTL, + expires_at: now + TTL, }, ); - (device_code, user_code) + Some((device_code, user_code)) } /// Returns true if a matching, unexpired entry was found and confirmed. + /// The user types this code by hand, so it is trimmed and upper-cased. pub fn confirm(&self, user_code: &str, account_id: Uuid) -> bool { + let wanted = user_code.trim().to_uppercase(); + let now = Instant::now(); for mut entry in self.by_device_code.iter_mut() { - if entry.user_code == user_code && entry.expires_at > Instant::now() { + if entry.user_code == wanted && entry.expires_at > now { entry.confirmed_account_id = Some(account_id); return true; } @@ -1095,13 +1315,25 @@ impl DeviceStore { false } - /// None = unknown/expired. Some(None) = pending. Some(Some(id)) = confirmed. - pub fn poll(&self, device_code: &str) -> Option> { - let entry = self.by_device_code.get(device_code)?; - if entry.expires_at <= Instant::now() { - return None; + /// A confirmed entry is CONSUMED by the first poll that sees it: the + /// token can be collected exactly once, replaying the same device_code + /// afterwards yields `Unknown`. Expired entries are dropped and Unknown. + pub fn poll(&self, device_code: &str) -> PollResult { + let now = Instant::now(); + let removed = self.by_device_code.remove_if(device_code, |_, entry| { + entry.confirmed_account_id.is_some() || entry.expires_at <= now + }); + if let Some((_, entry)) = removed { + return match entry.confirmed_account_id { + Some(id) if entry.expires_at > now => PollResult::Confirmed(id), + _ => PollResult::Unknown, + }; + } + if self.by_device_code.contains_key(device_code) { + PollResult::Pending + } else { + PollResult::Unknown } - Some(entry.confirmed_account_id) } } @@ -1112,8 +1344,8 @@ mod tests { #[test] fn create_returns_distinct_codes() { let store = DeviceStore::default(); - let (dc1, uc1) = store.create(); - let (dc2, uc2) = store.create(); + let (dc1, uc1) = store.create().unwrap(); + let (dc2, uc2) = store.create().unwrap(); assert_ne!(dc1, dc2); assert_ne!(uc1, uc2); } @@ -1121,24 +1353,35 @@ mod tests { #[test] fn confirm_then_poll_returns_account_id() { let store = DeviceStore::default(); - let (device_code, user_code) = store.create(); + let (device_code, user_code) = store.create().unwrap(); let account_id = Uuid::new_v4(); assert!(store.confirm(&user_code, account_id)); - assert_eq!(store.poll(&device_code), Some(Some(account_id))); + assert_eq!(store.poll(&device_code), PollResult::Confirmed(account_id)); } #[test] - fn poll_before_confirm_is_pending() { + fn confirmed_code_can_only_be_collected_once() { let store = DeviceStore::default(); - let (device_code, _user_code) = store.create(); - assert_eq!(store.poll(&device_code), Some(None)); + let (device_code, user_code) = store.create().unwrap(); + assert!(store.confirm(&user_code, Uuid::new_v4())); + + assert!(matches!(store.poll(&device_code), PollResult::Confirmed(_))); + assert_eq!(store.poll(&device_code), PollResult::Unknown, "replay must fail"); } #[test] - fn poll_unknown_code_returns_none() { + fn poll_before_confirm_is_pending_and_repeatable() { let store = DeviceStore::default(); - assert_eq!(store.poll("does-not-exist"), None); + let (device_code, _user_code) = store.create().unwrap(); + assert_eq!(store.poll(&device_code), PollResult::Pending); + assert_eq!(store.poll(&device_code), PollResult::Pending); + } + + #[test] + fn poll_unknown_code_is_unknown() { + let store = DeviceStore::default(); + assert_eq!(store.poll("does-not-exist"), PollResult::Unknown); } #[test] @@ -1146,19 +1389,44 @@ mod tests { let store = DeviceStore::default(); assert!(!store.confirm("ZZZZ-ZZZZ", Uuid::new_v4())); } + + #[test] + fn confirm_accepts_lowercase_and_surrounding_whitespace() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create().unwrap(); + let typed = format!(" {} ", user_code.to_lowercase()); + assert!(store.confirm(&typed, Uuid::new_v4())); + assert!(matches!(store.poll(&device_code), PollResult::Confirmed(_))); + } + + #[test] + fn expired_entries_are_purged_and_store_is_bounded() { + let store = DeviceStore::default(); + for _ in 0..MAX_PENDING { + assert!(store.create().is_some()); + } + assert!(store.create().is_none(), "store must refuse beyond MAX_PENDING"); + + // Force everything to be expired; the next create purges and succeeds. + for mut entry in store.by_device_code.iter_mut() { + entry.expires_at = Instant::now() - Duration::from_secs(1); + } + assert!(store.create().is_some()); + assert_eq!(store.by_device_code.len(), 1); + } } ``` - [ ] **Step 2: Run the store's unit tests** Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test device::store::` -Expected: 5 tests pass +Expected: 8 tests pass - [ ] **Step 3: Write the handlers** ```rust // backend/accounts-service/src/device/handlers.rs -use crate::device::store::DeviceStore; +use crate::device::store::{DeviceStore, PollResult}; use crate::error::AppError; use axum::{extract::State, http::{HeaderMap, StatusCode}, Json}; use serde::{Deserialize, Serialize}; @@ -1178,12 +1446,12 @@ pub struct DeviceCodeResponse { pub async fn create_code( State(state): State, -) -> (StatusCode, Json) { - let (device_code, user_code) = state.store.create(); - ( +) -> Result<(StatusCode, Json), AppError> { + let (device_code, user_code) = state.store.create().ok_or(AppError::TooManyRequests)?; + Ok(( StatusCode::CREATED, Json(DeviceCodeResponse { device_code, user_code, expires_in: 600 }), - ) + )) } #[derive(Deserialize)] @@ -1197,7 +1465,7 @@ fn account_id_from_auth_header(headers: &HeaderMap, secret: &str) -> Result, ) -> Result<(StatusCode, Json>), AppError> { match state.store.poll(&req.device_code) { - None => Err(AppError::Validation("unknown or expired device_code".into())), - Some(None) => Ok((StatusCode::ACCEPTED, Json(None))), - Some(Some(account_id)) => { + PollResult::Unknown => Err(AppError::NotFound("unknown or expired device_code".into())), + PollResult::Pending => Ok((StatusCode::ACCEPTED, Json(None))), + PollResult::Confirmed(account_id) => { // The long-lived device_token is just a refresh-style JWT for now; - // Task 8+ (gateway plan) is where per-device revocation via + // the gateway plan is where per-device revocation via // device_links.device_token_hash gets enforced on every request. let device_token = crate::auth::jwt::issue_refresh_token(account_id, &state.jwt_secret); Ok((StatusCode::OK, Json(Some(TokenResponse { device_token })))) @@ -1262,7 +1530,7 @@ async fn test_pool() -> sqlx::PgPool { let url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); let pool = sqlx::PgPool::connect(&url).await.expect("connect"); - sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); pool } @@ -1314,12 +1582,69 @@ async fn full_device_link_flow() { let token_body: serde_json::Value = poll_after.json(); assert!(token_body["device_token"].is_string()); + // Single use: the same device_code cannot be redeemed twice. + server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await + .assert_status(axum::http::StatusCode::NOT_FOUND); + sqlx::query("DELETE FROM accounts WHERE email = $1") .bind(&email) .execute(&pool) .await .unwrap(); } + +#[tokio::test] +async fn refresh_token_cannot_confirm_a_device_code() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let email = format!("device-refresh-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + let login: serde_json::Value = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "password123" })) + .await + .json(); + let refresh_token = login["refresh_token"].as_str().unwrap(); + + let code: serde_json::Value = server.post("/device/code").await.json(); + server + .post("/device/confirm") + .add_header( + axum::http::header::AUTHORIZATION, + format!("Bearer {refresh_token}"), + ) + .json(&json!({ "user_code": code["user_code"] })) + .await + .assert_status(axum::http::StatusCode::UNAUTHORIZED); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn unknown_device_code_and_user_code_return_404() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + + server + .post("/device/token") + .json(&json!({ "device_code": "no-such-code" })) + .await + .assert_status(axum::http::StatusCode::NOT_FOUND); +} ``` - [ ] **Step 6: Wire routes in `lib.rs`** @@ -1337,7 +1662,7 @@ use device::{handlers::DeviceState, store::DeviceStore}; use axum::{routing::{get, post}, Router}; pub fn build_app(pool: sqlx::PgPool, jwt_secret: String) -> Router { - let auth_state = AuthState { pool: pool.clone(), jwt_secret: jwt_secret.clone() }; + let auth_state = AuthState::new(pool.clone(), jwt_secret.clone()); let device_state = DeviceState { store: DeviceStore::default(), jwt_secret: jwt_secret.clone() }; let auth_routes = Router::new() @@ -1373,22 +1698,429 @@ git commit -m "feat(backend): OAuth device authorization grant for mod login" --- -### Task 8: Avatar upload (`POST /avatars`) +### Task 8: Shared bearer helper, shared test helpers, `build_app(pool, &cfg)`, secret-strength check + +Pure refactor plus one hardening; no new endpoints. It removes the duplicated +`account_id_from_auth_header` (Task 7 has a copy, Task 9 would add a second) +and the copy-pasted `test_pool()` in every test file, and prepares +`build_app` to take the whole `Config` (Task 9 needs the S3 settings). **Files:** -- Create: `backend/accounts-service/src/avatars/mod.rs` -- Create: `backend/accounts-service/src/avatars/storage.rs` -- Create: `backend/accounts-service/src/avatars/handlers.rs` -- Modify: `backend/accounts-service/src/lib.rs` (mount route, extend `build_app` signature with S3 client) -- Test: `backend/accounts-service/tests/avatar_upload.rs` +- Modify: `backend/accounts-service/src/auth/jwt.rs` (add `bearer_account_id` + tests) +- Modify: `backend/accounts-service/src/device/handlers.rs` (delete the local `account_id_from_auth_header`, call the shared one) +- Modify: `backend/accounts-service/src/config.rs` (add `Config::validate` + tests) +- Modify: `backend/accounts-service/src/lib.rs` (`build_app(pool: PgPool, cfg: &Config) -> Router`, still synchronous) +- Modify: `backend/accounts-service/src/main.rs` (`cfg.validate()?`, `build_app(pool, &cfg)`) +- Create: `backend/accounts-service/tests/common/mod.rs` (`test_pool`, `test_config`) +- Create: `backend/accounts-service/tests/smoke.rs` containing BOTH the old `health_returns_ok` and `migrations_create_accounts_table` tests, then delete `tests/health.rs` and `tests/db_migration.rs` (keeps `tests/` at 4 files once Task 9 adds `avatar_upload.rs`: `smoke.rs`, `auth_flow.rs`, `device_flow.rs`, `avatar_upload.rs`; the `common/` subdirectory does not count) +- Modify: `backend/accounts-service/tests/{auth_flow,device_flow}.rs` (use `mod common;`) **Interfaces:** -- Consumes: `jwt::verify_token` (Task 4, auth on this endpoint), `accounts::repo` pattern extended with `update_avatar` (new function in this task) -- Produces: `pub struct S3Storage { client: aws_sdk_s3::Client, bucket: String }` with `pub async fn put(&self, key: &str, bytes: Vec, content_type: &str) -> anyhow::Result<()>` -- Produces: `POST /avatars` (multipart, auth required, field name `file`) → `200 {avatar_url}`; rejects >5MB with `413`, rejects non-image magic bytes with `400` -- This task requires a local MinIO for tests: `docker run -d --name lovisual-minio -p 9000:9000 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data` (one-time manual setup, same as Task 2's Postgres) +- Produces: `pub fn bearer_account_id(headers: &axum::http::HeaderMap, secret: &str) -> Result` in `auth::jwt` — requires `Authorization: Bearer `, every failure is `AppError::Unauthorized` +- Produces: `impl Config { pub fn validate(&self) -> anyhow::Result<()> }` — rejects a `jwt_secret` shorter than 32 bytes +- Produces: `pub fn build_app(pool: PgPool, cfg: &Config) -> Router` +- Produces (tests only): `common::test_pool() -> PgPool` (connects to `DATABASE_URL` or the local default and runs `./migrations`), `common::test_config() -> Config` (32+ byte secret `"test-secret-test-secret-test-secret!"`, S3 fields pointing at `http://localhost:9000`, bucket `lovisual-avatars-test`, keys `minioadmin`) -- [ ] **Step 1: Write the S3 storage wrapper** +- [ ] **Step 1: Write the failing tests for `bearer_account_id`** + +Append to the `tests` module of `src/auth/jwt.rs`: + +```rust + use axum::http::{header::AUTHORIZATION, HeaderMap, HeaderValue}; + + fn headers_with(value: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(AUTHORIZATION, HeaderValue::from_str(value).unwrap()); + headers + } + + #[test] + fn bearer_accepts_a_valid_access_token() { + let id = Uuid::new_v4(); + let token = issue_access_token(id, "test-secret"); + let got = bearer_account_id(&headers_with(&format!("Bearer {token}")), "test-secret"); + assert_eq!(got.unwrap(), id); + } + + #[test] + fn bearer_rejects_missing_header_wrong_scheme_and_garbage() { + let token = issue_access_token(Uuid::new_v4(), "test-secret"); + for headers in [ + HeaderMap::new(), + headers_with(&format!("Basic {token}")), + headers_with(&token), + headers_with("Bearer not.a.jwt"), + ] { + assert!(matches!( + bearer_account_id(&headers, "test-secret"), + Err(AppError::Unauthorized) + )); + } + } + + #[test] + fn bearer_rejects_a_refresh_token() { + let token = issue_refresh_token(Uuid::new_v4(), "test-secret"); + assert!(matches!( + bearer_account_id(&headers_with(&format!("Bearer {token}")), "test-secret"), + Err(AppError::Unauthorized) + )); + } +``` + +- [ ] **Step 2: Run to see them fail** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && CARGO_BUILD_JOBS=4 cargo test --lib jwt::` +Expected: compile error, `bearer_account_id` not found + +- [ ] **Step 3: Implement it in `jwt.rs`** + +Add above the `#[cfg(test)]` module: + +```rust +use crate::error::AppError; +use axum::http::{header::AUTHORIZATION, HeaderMap}; + +/// Account id from `Authorization: Bearer `. Every failure mode +/// (missing header, wrong scheme, bad/expired token, refresh token, non-UUID +/// subject) is the same `AppError::Unauthorized`. +pub fn bearer_account_id(headers: &HeaderMap, secret: &str) -> Result { + let value = headers + .get(AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + let token = value.strip_prefix("Bearer ").ok_or(AppError::Unauthorized)?; + let claims = verify_token(token, secret, TokenType::Access).ok_or(AppError::Unauthorized)?; + claims.sub.parse().map_err(|_| AppError::Unauthorized) +} +``` + +(Move the two new `use` lines up next to the existing imports at the top of the file.) + +- [ ] **Step 4: Use it in `device/handlers.rs`** + +Delete the whole local `fn account_id_from_auth_header(...)` and change its one call site in `confirm` to: + +```rust + let account_id = crate::auth::jwt::bearer_account_id(&headers, &state.jwt_secret)?; +``` + +- [ ] **Step 5: Add `Config::validate` with tests** + +Add to `src/config.rs`: + +```rust +const MIN_JWT_SECRET_BYTES: usize = 32; + +impl Config { + /// Fail fast at startup on a secret too weak to sign HS256 tokens with + /// (the `.env.example` placeholder must never reach production). + pub fn validate(&self) -> Result<()> { + if self.jwt_secret.len() < MIN_JWT_SECRET_BYTES { + anyhow::bail!("JWT_SECRET must be at least {MIN_JWT_SECRET_BYTES} bytes"); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn config_with_secret(secret: &str) -> Config { + Config { + database_url: String::new(), + jwt_secret: secret.into(), + port: 0, + s3_endpoint: String::new(), + s3_bucket: String::new(), + s3_access_key: String::new(), + s3_secret_key: String::new(), + } + } + + #[test] + fn short_jwt_secret_is_rejected() { + assert!(config_with_secret("too-short").validate().is_err()); + } + + #[test] + fn strong_jwt_secret_is_accepted() { + assert!(config_with_secret(&"x".repeat(32)).validate().is_ok()); + } +} +``` + +Also update `backend/.env.example`: replace `JWT_SECRET=change-me-to-a-long-random-string` with `JWT_SECRET=` plus a comment line above it: `# at least 32 random bytes, e.g. \`openssl rand -hex 32\``. + +- [ ] **Step 6: Shared test helpers** + +```rust +// backend/accounts-service/tests/common/mod.rs +#![allow(dead_code)] // each test binary uses a different subset + +use accounts_service::config::Config; + +pub async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect to test database"); + sqlx::migrate!("./migrations").run(&pool).await.expect("run migrations"); + pool +} + +pub fn test_config() -> Config { + Config { + database_url: String::new(), + jwt_secret: "test-secret-test-secret-test-secret!".into(), + port: 0, + s3_endpoint: "http://localhost:9000".into(), + s3_bucket: "lovisual-avatars-test".into(), + s3_access_key: "minioadmin".into(), + s3_secret_key: "minioadmin".into(), + } +} +``` + +- [ ] **Step 7: `build_app(pool, &cfg)` and call sites** + +In `src/lib.rs` change the signature and the two places that used `jwt_secret`: + +```rust +use config::Config; + +pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { + let auth_state = AuthState::new(pool.clone(), cfg.jwt_secret.clone()); + let device_state = DeviceState { store: DeviceStore::default(), jwt_secret: cfg.jwt_secret.clone() }; + // ... routes unchanged ... +} +``` + +`src/main.rs`: after `Config::from_env()?` add `cfg.validate()?;` and call `build_app(pool, &cfg)`. + +Create `tests/smoke.rs` with `mod common;` holding the two existing tests unchanged in behavior (health still builds the app on a lazy pool via `PgPool::connect_lazy`; the migration test now uses `common::test_pool()`), then `git rm tests/health.rs tests/db_migration.rs`. In `auth_flow.rs` and `device_flow.rs`: add `mod common;` at the top, delete the local `test_pool` fn, replace calls with `common::test_pool().await`, and replace every `accounts_service::build_app(pool.clone(), "test-secret".into())` with `accounts_service::build_app(pool.clone(), &common::test_config())`. + +- [ ] **Step 8: Run everything** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db CARGO_BUILD_JOBS=4 cargo test` +Expected: all previous tests still pass plus the 3 `bearer_*` and 2 `config::tests` (no test file defines its own `test_pool` any more: `grep -rn "fn test_pool" tests/` prints only `tests/common/mod.rs`) + +- [ ] **Step 9: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add -A backend/accounts-service backend/.env.example # -A here is scoped to these paths only (records the two deleted test files) +git commit -m "refactor(backend): shared bearer helper and test helpers, build_app takes Config, validate JWT secret strength" +``` + +--- + +### Task 9: Avatar upload (`POST /avatars`) + +Uploads are decoded, center-cropped to 256x256 and **re-encoded as PNG** — +never stored as sent. Magic bytes alone do not prove an image is valid, and +re-encoding also drops EXIF (GPS location, camera serial) and any embedded +payload. Decoding untrusted input is bounded (max 8192px per side, 128 MiB +decode allocation) and runs on a blocking thread. + +**Files:** +- Modify: `backend/accounts-service/Cargo.toml` (add `image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }`) +- Create: `backend/accounts-service/src/avatars/mod.rs` +- Create: `backend/accounts-service/src/avatars/processing.rs` (pure image logic + unit tests) +- Create: `backend/accounts-service/src/avatars/storage.rs` (S3 wrapper) +- Create: `backend/accounts-service/src/avatars/handlers.rs` +- Modify: `backend/accounts-service/src/accounts/repo.rs` (add `set_avatar` + test) +- Modify: `backend/accounts-service/src/config.rs` (add `Config::avatar_base_url` + test) +- Modify: `backend/accounts-service/src/lib.rs` (`pub mod avatars;`, mount `/avatars`) +- Test: `backend/accounts-service/tests/avatar_upload.rs` + +(`avatars/` holds 4 files — at the limit; `processing.rs` is deliberately not +called `image.rs` so it cannot be confused with the `image` crate.) + +**Interfaces:** +- Consumes: `auth::jwt::bearer_account_id` (Task 8), `common::{test_pool, test_config}` (Task 8), `Config` S3 fields +- Produces: `pub fn process_avatar(bytes: &[u8]) -> Result, AvatarImageError>` (`AvatarImageError::{Unsupported, Invalid}`, output is always a 256x256 PNG) +- Produces: `pub struct S3Storage` with `pub fn from_config(endpoint, access_key, secret_key, bucket) -> Self` (synchronous — building the client does no I/O) and `pub async fn put(&self, key: &str, bytes: Vec, content_type: &str) -> anyhow::Result<()>` +- Produces: `pub async fn accounts::repo::set_avatar(pool: &PgPool, account_id: Uuid, s3_key: &str) -> Result<(), sqlx::Error>` (upsert into `avatars`, bumps `uploaded_at`) +- Produces: `Config::avatar_base_url(&self) -> String` = `"{endpoint without trailing slash}/{bucket}"` +- Produces: `POST /avatars` (multipart, single part named `file`, ACCESS token required) → `200 {"avatar_url": "/avatars/.png"}`; unsupported/invalid image or >5 MB → `400`; no/invalid/refresh token → `401` +- Test prerequisite (infra, one-time, not automated): an S3-compatible endpoint at `S3_ENDPOINT` with bucket `lovisual-avatars-test` (e.g. MinIO with root user/password `minioadmin`) + +- [ ] **Step 1: Write the failing unit tests for the image processing** + +```rust +// backend/accounts-service/src/avatars/processing.rs (tests first; implementation in Step 2) +#[cfg(test)] +mod tests { + use super::*; + use image::ImageFormat; + use std::io::Cursor; + + fn png_of(width: u32, height: u32) -> Vec { + let img = image::RgbaImage::new(width, height); + let mut out = Cursor::new(Vec::new()); + img.write_to(&mut out, ImageFormat::Png).unwrap(); + out.into_inner() + } + + #[test] + fn valid_image_becomes_a_256_square_png() { + let out = process_avatar(&png_of(300, 100)).unwrap(); + assert!(out.starts_with(&[0x89, b'P', b'N', b'G'])); + let decoded = image::load_from_memory(&out).unwrap(); + assert_eq!((decoded.width(), decoded.height()), (AVATAR_SIZE, AVATAR_SIZE)); + } + + #[test] + fn non_image_bytes_are_unsupported() { + assert_eq!(process_avatar(b"not an image"), Err(AvatarImageError::Unsupported)); + } + + #[test] + fn gif_is_unsupported() { + assert_eq!( + process_avatar(b"GIF89a\x01\x00\x01\x00\x00\x00\x00;"), + Err(AvatarImageError::Unsupported) + ); + } + + #[test] + fn truncated_png_is_invalid() { + let mut bytes = png_of(50, 50); + bytes.truncate(40); + assert_eq!(process_avatar(&bytes), Err(AvatarImageError::Invalid)); + } + + #[test] + fn image_wider_than_the_limit_is_rejected() { + assert_eq!(process_avatar(&png_of(MAX_DIMENSION + 1, 1)), Err(AvatarImageError::Invalid)); + } +} +``` + +Run: `CARGO_BUILD_JOBS=4 cargo test --lib avatars::processing::` — Expected: compile error (module/functions missing). + +- [ ] **Step 2: Implement `processing.rs`** + +Put above the tests module: + +```rust +use image::{imageops::FilterType, ImageFormat, ImageReader, Limits}; +use std::io::Cursor; + +pub const AVATAR_SIZE: u32 = 256; +pub const MAX_DIMENSION: u32 = 8192; +const MAX_DECODE_BYTES: u64 = 128 * 1024 * 1024; + +#[derive(Debug, PartialEq, Eq)] +pub enum AvatarImageError { + /// Not a PNG/JPEG/WebP at all. + Unsupported, + /// Claims to be one of those but does not decode within the limits. + Invalid, +} + +pub fn process_avatar(bytes: &[u8]) -> Result, AvatarImageError> { + let mut reader = ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .map_err(|_| AvatarImageError::Invalid)?; + if !matches!( + reader.format(), + Some(ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP) + ) { + return Err(AvatarImageError::Unsupported); + } + let mut limits = Limits::default(); + limits.max_image_width = Some(MAX_DIMENSION); + limits.max_image_height = Some(MAX_DIMENSION); + limits.max_alloc = Some(MAX_DECODE_BYTES); + reader.limits(limits); + + let decoded = reader.decode().map_err(|_| AvatarImageError::Invalid)?; + let square = decoded.resize_to_fill(AVATAR_SIZE, AVATAR_SIZE, FilterType::Lanczos3); + let mut out = Cursor::new(Vec::new()); + square + .write_to(&mut out, ImageFormat::Png) + .map_err(|_| AvatarImageError::Invalid)?; + Ok(out.into_inner()) +} +``` + +Run the same test command — Expected: 5 tests pass. + +- [ ] **Step 3: Repository `set_avatar` (test first)** + +Add to the `tests` module of `src/accounts/repo.rs`: + +```rust + #[tokio::test] + async fn set_avatar_inserts_then_updates_in_place() { + let pool = test_pool().await; + let created = create(&pool, &format!("av-{}@example.com", Uuid::new_v4()), "h", "N") + .await + .unwrap(); + + set_avatar(&pool, created.id, "avatars/one.png").await.unwrap(); + set_avatar(&pool, created.id, "avatars/two.png").await.unwrap(); + + let rows: Vec<(String,)> = + sqlx::query_as("SELECT s3_key FROM avatars WHERE account_id = $1") + .bind(created.id) + .fetch_all(&pool) + .await + .unwrap(); + assert_eq!(rows, vec![("avatars/two.png".to_string(),)]); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } +``` + +Implement (above the tests module): + +```rust +pub async fn set_avatar(pool: &PgPool, account_id: Uuid, s3_key: &str) -> Result<(), sqlx::Error> { + sqlx::query( + "INSERT INTO avatars (account_id, s3_key) VALUES ($1, $2) + ON CONFLICT (account_id) DO UPDATE SET s3_key = EXCLUDED.s3_key, uploaded_at = now()", + ) + .bind(account_id) + .bind(s3_key) + .execute(pool) + .await?; + Ok(()) +} +``` + +Run: `DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db CARGO_BUILD_JOBS=4 cargo test --lib accounts::repo::` — Expected: 5 tests pass. + +- [ ] **Step 4: `Config::avatar_base_url` (test first)** + +Add to `config.rs` (and its `tests` module): + +```rust +impl Config { + /// Public prefix of stored avatars: `/`. + pub fn avatar_base_url(&self) -> String { + format!("{}/{}", self.s3_endpoint.trim_end_matches('/'), self.s3_bucket) + } +} +``` + +```rust + #[test] + fn avatar_base_url_joins_endpoint_and_bucket_without_double_slash() { + let mut cfg = config_with_secret(&"x".repeat(32)); + cfg.s3_endpoint = "http://localhost:9000/".into(); + cfg.s3_bucket = "avatars".into(); + assert_eq!(cfg.avatar_base_url(), "http://localhost:9000/avatars"); + } +``` + +- [ ] **Step 5: S3 storage wrapper** ```rust // backend/accounts-service/src/avatars/storage.rs @@ -1397,17 +2129,12 @@ use aws_sdk_s3::Client; #[derive(Clone)] pub struct S3Storage { - pub client: Client, - pub bucket: String, + client: Client, + bucket: String, } impl S3Storage { - pub async fn from_config( - endpoint: &str, - access_key: &str, - secret_key: &str, - bucket: String, - ) -> Self { + pub fn from_config(endpoint: &str, access_key: &str, secret_key: &str, bucket: String) -> Self { let creds = aws_sdk_s3::config::Credentials::new(access_key, secret_key, None, None, "static"); let config = aws_sdk_s3::config::Builder::new() .endpoint_url(endpoint) @@ -1433,44 +2160,26 @@ impl S3Storage { } ``` -- [ ] **Step 2: Write the magic-byte content sniffer + handler** +- [ ] **Step 6: Handler** ```rust // backend/accounts-service/src/avatars/handlers.rs +use crate::accounts::repo; +use crate::auth::jwt::bearer_account_id; +use crate::avatars::processing::{process_avatar, AvatarImageError}; use crate::avatars::storage::S3Storage; use crate::error::AppError; use axum::{extract::{Multipart, State}, http::HeaderMap, Json}; use serde::Serialize; -const MAX_AVATAR_BYTES: usize = 5 * 1024 * 1024; +pub const MAX_UPLOAD_BYTES: usize = 5 * 1024 * 1024; #[derive(Clone)] pub struct AvatarState { + pub pool: sqlx::PgPool, pub storage: S3Storage, pub jwt_secret: String, - pub public_base_url: String, -} - -fn sniff_image_type(bytes: &[u8]) -> Option<&'static str> { - if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) { - Some("image/png") - } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) { - Some("image/jpeg") - } else if bytes.len() > 12 && &bytes[8..12] == b"WEBP" { - Some("image/webp") - } else { - None - } -} - -fn account_id_from_auth_header(headers: &HeaderMap, secret: &str) -> Result { - let header = headers - .get(axum::http::header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()) - .ok_or(AppError::Unauthorized)?; - let token = header.strip_prefix("Bearer ").ok_or(AppError::Unauthorized)?; - let claims = crate::auth::jwt::verify_token(token, secret).ok_or(AppError::Unauthorized)?; - claims.sub.parse().map_err(|_| AppError::Unauthorized) + pub base_url: String, } #[derive(Serialize)] @@ -1483,255 +2192,225 @@ pub async fn upload( headers: HeaderMap, mut multipart: Multipart, ) -> Result, AppError> { - let account_id = account_id_from_auth_header(&headers, &state.jwt_secret)?; + let account_id = bearer_account_id(&headers, &state.jwt_secret)?; let field = multipart .next_field() .await - .map_err(|e| AppError::Validation(e.to_string()))? - .ok_or_else(|| AppError::Validation("missing file field".into()))?; - + .map_err(|_| AppError::Validation("malformed multipart body".into()))? + .filter(|f| f.name() == Some("file")) + .ok_or_else(|| AppError::Validation("expected a multipart part named `file`".into()))?; let bytes = field .bytes() .await - .map_err(|e| AppError::Validation(e.to_string()))?; - - if bytes.len() > MAX_AVATAR_BYTES { + .map_err(|_| AppError::Validation("could not read the uploaded file".into()))?; + if bytes.len() > MAX_UPLOAD_BYTES { return Err(AppError::Validation("file too large (max 5MB)".into())); } - let content_type = sniff_image_type(&bytes) - .ok_or_else(|| AppError::Validation("unsupported image format".into()))?; - - let key = format!("avatars/{account_id}.bin"); - state - .storage - .put(&key, bytes.to_vec(), content_type) + // Decoding is CPU-bound and the input is untrusted: keep it off the async workers. + let png = tokio::task::spawn_blocking(move || process_avatar(&bytes)) .await - .map_err(AppError::Internal)?; + .map_err(|e| AppError::Internal(e.into()))? + .map_err(|e| match e { + AvatarImageError::Unsupported => AppError::Validation("unsupported image format (png, jpeg, webp)".into()), + AvatarImageError::Invalid => AppError::Validation("invalid or too large image".into()), + })?; - Ok(Json(AvatarResponse { - avatar_url: format!("{}/{}", state.public_base_url, key), - })) + let key = format!("avatars/{account_id}.png"); + state.storage.put(&key, png, "image/png").await.map_err(AppError::Internal)?; + repo::set_avatar(&state.pool, account_id, &key).await?; + + Ok(Json(AvatarResponse { avatar_url: format!("{}/{key}", state.base_url) })) } ``` -- [ ] **Step 3: Write the module file** - ```rust // backend/accounts-service/src/avatars/mod.rs -pub mod storage; pub mod handlers; +pub mod processing; +pub mod storage; ``` -- [ ] **Step 4: Write the failing test** +- [ ] **Step 7: Mount the route in `lib.rs`** + +```rust +pub mod avatars; + +use avatars::{handlers::AvatarState, storage::S3Storage}; +use axum::extract::DefaultBodyLimit; + +// inside build_app, next to the other route groups: + let avatar_state = AvatarState { + pool: pool.clone(), + storage: S3Storage::from_config( + &cfg.s3_endpoint, + &cfg.s3_access_key, + &cfg.s3_secret_key, + cfg.s3_bucket.clone(), + ), + jwt_secret: cfg.jwt_secret.clone(), + base_url: cfg.avatar_base_url(), + }; + let avatar_routes = Router::new() + .route("/avatars", post(avatars::handlers::upload)) + // Hard transport cap slightly above the 5 MB business limit (413 beyond it). + .layer(DefaultBodyLimit::max(6 * 1024 * 1024)) + .with_state(avatar_state); +// ... and `.merge(avatar_routes)` on the final Router. +``` + +- [ ] **Step 8: Integration tests** ```rust // backend/accounts-service/tests/avatar_upload.rs -use axum_test::TestServer; +mod common; + +use axum::http::{header::AUTHORIZATION, StatusCode}; +use axum_test::{multipart::{MultipartForm, Part}, TestServer}; use serde_json::json; use uuid::Uuid; -async fn test_pool() -> sqlx::PgPool { - let url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); - let pool = sqlx::PgPool::connect(&url).await.expect("connect"); - sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); - pool +fn png_bytes() -> Vec { + let img = image::RgbaImage::new(64, 32); + let mut out = std::io::Cursor::new(Vec::new()); + img.write_to(&mut out, image::ImageFormat::Png).unwrap(); + out.into_inner() } -// 1x1 red pixel PNG -const TINY_PNG: &[u8] = &[ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, - 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, - 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, - 0x44, 0xAE, 0x42, 0x60, 0x82, -]; - -#[tokio::test] -async fn upload_valid_png_returns_avatar_url() { - let pool = test_pool().await; - let app = accounts_service::build_app(pool.clone(), "test-secret".into()).await; - let server = TestServer::new(app); - let email = format!("avatar-{}@example.com", Uuid::new_v4()); +fn form(bytes: Vec) -> MultipartForm { + MultipartForm::new().add_part("file", Part::bytes(bytes).file_name("a.png").mime_type("image/png")) +} +async fn registered_login(server: &TestServer, tag: &str) -> (String, serde_json::Value) { + let email = format!("{tag}-{}@example.com", Uuid::new_v4()); server .post("/auth/register") .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) .await - .assert_status(axum::http::StatusCode::CREATED); + .assert_status(StatusCode::CREATED); let login: serde_json::Value = server .post("/auth/login") .json(&json!({ "email": email, "password": "password123" })) .await .json(); - let access_token = login["access_token"].as_str().unwrap(); + (email, login) +} + +#[tokio::test] +async fn valid_upload_stores_avatar_and_returns_url() { + let pool = common::test_pool().await; + let server = TestServer::new(accounts_service::build_app(pool.clone(), &common::test_config())); + let (email, login) = registered_login(&server, "avatar").await; + let access = login["access_token"].as_str().unwrap(); let response = server .post("/avatars") - .add_header( - axum::http::header::AUTHORIZATION, - format!("Bearer {access_token}"), - ) - .multipart( - axum_test::multipart::MultipartForm::new().add_part( - "file", - axum_test::multipart::Part::bytes(TINY_PNG.to_vec()) - .file_name("avatar.png") - .mime_type("image/png"), - ), - ) + .add_header(AUTHORIZATION, format!("Bearer {access}")) + .multipart(form(png_bytes())) .await; response.assert_status_ok(); - let body: serde_json::Value = response.json(); - assert!(body["avatar_url"].as_str().unwrap().contains("avatars/")); + let url = response.json::()["avatar_url"].as_str().unwrap().to_string(); + assert!(url.starts_with("http://localhost:9000/lovisual-avatars-test/avatars/"), "{url}"); + assert!(url.ends_with(".png"), "{url}"); - sqlx::query("DELETE FROM accounts WHERE email = $1") - .bind(&email) + let stored: (String,) = sqlx::query_as( + "SELECT a.s3_key FROM avatars a JOIN accounts c ON c.id = a.account_id WHERE lower(c.email) = $1", + ) + .bind(email.to_lowercase()) + .fetch_one(&pool) + .await + .expect("avatars row must exist"); + assert!(url.ends_with(&stored.0)); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) .execute(&pool) .await .unwrap(); } #[tokio::test] -async fn upload_non_image_bytes_returns_400() { - let pool = test_pool().await; - let app = accounts_service::build_app(pool.clone(), "test-secret".into()).await; - let server = TestServer::new(app); - let email = format!("badavatar-{}@example.com", Uuid::new_v4()); +async fn non_image_upload_is_rejected_with_400() { + let pool = common::test_pool().await; + let server = TestServer::new(accounts_service::build_app(pool.clone(), &common::test_config())); + let (email, login) = registered_login(&server, "badavatar").await; + let access = login["access_token"].as_str().unwrap(); server - .post("/auth/register") - .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) - .await - .assert_status(axum::http::StatusCode::CREATED); - let login: serde_json::Value = server - .post("/auth/login") - .json(&json!({ "email": email, "password": "password123" })) - .await - .json(); - let access_token = login["access_token"].as_str().unwrap(); - - let response = server .post("/avatars") - .add_header( - axum::http::header::AUTHORIZATION, - format!("Bearer {access_token}"), - ) - .multipart( - axum_test::multipart::MultipartForm::new().add_part( - "file", - axum_test::multipart::Part::bytes(b"not an image".to_vec()) - .file_name("fake.png") - .mime_type("image/png"), - ), - ) - .await; - response.assert_status(axum::http::StatusCode::BAD_REQUEST); + .add_header(AUTHORIZATION, format!("Bearer {access}")) + .multipart(form(b"definitely not an image".to_vec())) + .await + .assert_status(StatusCode::BAD_REQUEST); - sqlx::query("DELETE FROM accounts WHERE email = $1") - .bind(&email) + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn oversized_upload_is_rejected_with_400() { + let pool = common::test_pool().await; + let server = TestServer::new(accounts_service::build_app(pool.clone(), &common::test_config())); + let (email, login) = registered_login(&server, "bigavatar").await; + let access = login["access_token"].as_str().unwrap(); + + server + .post("/avatars") + .add_header(AUTHORIZATION, format!("Bearer {access}")) + .multipart(form(vec![0u8; 5 * 1024 * 1024 + 1])) + .await + .assert_status(StatusCode::BAD_REQUEST); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn upload_requires_an_access_token() { + let pool = common::test_pool().await; + let server = TestServer::new(accounts_service::build_app(pool.clone(), &common::test_config())); + + server + .post("/avatars") + .multipart(form(png_bytes())) + .await + .assert_status(StatusCode::UNAUTHORIZED); + + let (email, login) = registered_login(&server, "refreshavatar").await; + let refresh = login["refresh_token"].as_str().unwrap(); + server + .post("/avatars") + .add_header(AUTHORIZATION, format!("Bearer {refresh}")) + .multipart(form(png_bytes())) + .await + .assert_status(StatusCode::UNAUTHORIZED); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) .execute(&pool) .await .unwrap(); } ``` -- [ ] **Step 5: Wire the route, make `build_app` async (S3 client setup is async) and thread config through** +- [ ] **Step 9: Run everything** -```rust -// backend/accounts-service/src/lib.rs -pub mod config; -pub mod error; -pub mod auth; -pub mod accounts; -pub mod device; -pub mod avatars; +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db CARGO_BUILD_JOBS=4 cargo test` +Expected: everything passes, including the 4 `avatar_upload` tests against the S3 endpoint (if the endpoint is unreachable the first test fails with a 500 — report BLOCKED with the error instead of weakening the test) -use auth::handlers::AuthState; -use avatars::{handlers::AvatarState, storage::S3Storage}; -use device::{handlers::DeviceState, store::DeviceStore}; -use axum::{extract::DefaultBodyLimit, routing::{get, post}, Router}; -use config::Config; - -pub async fn build_app(pool: sqlx::PgPool, cfg: Config) -> Router { - let auth_state = AuthState { pool: pool.clone(), jwt_secret: cfg.jwt_secret.clone() }; - let device_state = DeviceState { store: DeviceStore::default(), jwt_secret: cfg.jwt_secret.clone() }; - let storage = S3Storage::from_config( - &cfg.s3_endpoint, - &cfg.s3_access_key, - &cfg.s3_secret_key, - cfg.s3_bucket.clone(), - ) - .await; - let avatar_state = AvatarState { - storage, - jwt_secret: cfg.jwt_secret.clone(), - public_base_url: cfg.s3_endpoint.clone(), - }; - - let auth_routes = Router::new() - .route("/auth/register", post(auth::handlers::register)) - .route("/auth/login", post(auth::handlers::login)) - .with_state(auth_state); - - let device_routes = Router::new() - .route("/device/code", post(device::handlers::create_code)) - .route("/device/confirm", post(device::handlers::confirm)) - .route("/device/token", post(device::handlers::token)) - .with_state(device_state); - - let avatar_routes = Router::new() - .route("/avatars", post(avatars::handlers::upload)) - .layer(DefaultBodyLimit::max(6 * 1024 * 1024)) - .with_state(avatar_state); - - Router::new() - .route("/health", get(|| async { "ok" })) - .merge(auth_routes) - .merge(device_routes) - .merge(avatar_routes) -} -``` - -`build_app` is now `async` and takes `Config` instead of `(pool, jwt_secret)` — update every call site: -- `main.rs`: `let app = build_app(pool, cfg).await;` -- `tests/health.rs`, `tests/auth_flow.rs`, `tests/device_flow.rs`: build a `Config` with test values instead of passing `"test-secret".into()` directly, e.g.: - -```rust -fn test_config() -> accounts_service::config::Config { - accounts_service::config::Config { - database_url: String::new(), // unused, pool is passed separately - jwt_secret: "test-secret".into(), - port: 0, - s3_endpoint: "http://localhost:9000".into(), - s3_bucket: "lovisual-avatars-test".into(), - s3_access_key: "minioadmin".into(), - s3_secret_key: "minioadmin".into(), - } -} -``` - -and change every `accounts_service::build_app(pool.clone(), "test-secret".into())` to `accounts_service::build_app(pool.clone(), test_config()).await`. - -- [ ] **Step 6: Create the test bucket once, manually** - -```bash -docker exec lovisual-minio mkdir -p /data/lovisual-avatars-test -``` - -- [ ] **Step 7: Run all tests** - -Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db S3_BUCKET=lovisual-avatars-test cargo test` -Expected: all pass - -- [ ] **Step 8: Commit** +- [ ] **Step 10: Commit** ```bash cd /storage/project/jvm/LoVisual -git add backend/accounts-service -git commit -m "feat(backend): avatar upload with magic-byte validation and S3 storage" +git add backend/accounts-service backend/Cargo.lock +git commit -m "feat(backend): avatar upload with decode, square crop, PNG re-encode and S3 storage" ``` --- diff --git a/backend/STRUCTURE.md b/backend/STRUCTURE.md index 7980bbe..7a2b8ee 100644 --- a/backend/STRUCTURE.md +++ b/backend/STRUCTURE.md @@ -9,10 +9,10 @@ backend/ Cargo.toml # workspace root — members растёт по мере реализации STRUCTURE.md # этот файл accounts-service/ # РЕАЛИЗУЕТСЯ — backend/PLAN.md (Подсистема 1, часть 1) - gateway/ # ПЛАН НЕ НАПИСАН — routing + единая точка JWT-проверки + gateway/ # ПЛАН: gateway/PLAN.md — routing + единая точка JWT-проверки # + рейт-лимиты (TODO.md §6). Начинается после # accounts-service, т.к. фронтит уже готовый сервис. - configs-service/ # ПЛАН НЕ НАПИСАН — 4 слота конфигов, share-коды, + configs-service/ # ПЛАН: configs-service/PLAN.md — 4 слота конфигов, share-коды, # витрина (Подсистема 1, часть 2) chat-service/ # ПЛАН НЕ НАПИСАН — RPC-чат, друзья, presence, # виджеты-телеметрия (Подсистема 2). Единственный diff --git a/backend/accounts-service/Cargo.toml b/backend/accounts-service/Cargo.toml index 13e9522..152bc20 100644 --- a/backend/accounts-service/Cargo.toml +++ b/backend/accounts-service/Cargo.toml @@ -8,7 +8,7 @@ name = "accounts_service" path = "src/lib.rs" [dependencies] -axum = { version = "0.8", features = ["multipart"] } +axum = { version = "0.8", features = ["multipart", "macros"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } tower-http = { version = "0.7", features = ["trace", "cors"] } tracing = "0.1" @@ -19,11 +19,17 @@ sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } argon2 = "0.6" -jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } +axum-extra = { version = "0.12", features = ["cookie"] } +time = "0.3" +sha2 = "0.11" +hex = "0.4" +base64 = "0.22" +common = { path = "../common" } rand = "0.10" dashmap = "6" aws-sdk-s3 = "1" aws-config = "1" +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } anyhow = "1" dotenvy = "0.15" diff --git a/backend/accounts-service/migrations/0001_init.sql b/backend/accounts-service/migrations/0001_init.sql index 56810f3..ec7b3fb 100644 --- a/backend/accounts-service/migrations/0001_init.sql +++ b/backend/accounts-service/migrations/0001_init.sql @@ -1,8 +1,21 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; +-- Hardening: revoke implicit PUBLIC access to this schema. This Postgres +-- instance hosts multiple services' databases (accounts_db/configs_db/ +-- chat_db per TODO.md Фаза 10) on one cluster; the app role connects as +-- its own owner and needs no PUBLIC grant to function. +REVOKE ALL ON SCHEMA public FROM PUBLIC; + CREATE TABLE accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email TEXT NOT NULL UNIQUE, + -- No plain UNIQUE on email: Postgres TEXT comparison is case-sensitive, + -- so "User@x.com" and "user@x.com" would be treated as different rows, + -- letting one person hold two accounts under what looks like the same + -- email (account-confusion / signup-limit-bypass class of bug). A + -- unique index on lower(email) enforces uniqueness case-insensitively; + -- application code (accounts::repo) must look up/insert by + -- `email.to_lowercase()` for this index to actually get hit. + email TEXT NOT NULL, password_hash TEXT NOT NULL, display_nick TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', @@ -10,6 +23,8 @@ CREATE TABLE accounts ( created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); +CREATE UNIQUE INDEX accounts_email_lower_idx ON accounts (lower(email)); + CREATE TABLE avatars ( account_id UUID PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE, s3_key TEXT NOT NULL, diff --git a/backend/accounts-service/migrations/0002_refresh_tokens.sql b/backend/accounts-service/migrations/0002_refresh_tokens.sql new file mode 100644 index 0000000..c8cced2 --- /dev/null +++ b/backend/accounts-service/migrations/0002_refresh_tokens.sql @@ -0,0 +1,12 @@ +-- Opaque refresh tokens (stored hashed). Rotated on every use; presenting an +-- already-rotated token revokes the whole account's sessions (theft signal). +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_refresh_tokens_account_id ON refresh_tokens(account_id); diff --git a/backend/accounts-service/src/accounts/mod.rs b/backend/accounts-service/src/accounts/mod.rs new file mode 100644 index 0000000..c466d78 --- /dev/null +++ b/backend/accounts-service/src/accounts/mod.rs @@ -0,0 +1,2 @@ +pub mod model; +pub mod repo; diff --git a/backend/accounts-service/src/accounts/model.rs b/backend/accounts-service/src/accounts/model.rs new file mode 100644 index 0000000..7040da0 --- /dev/null +++ b/backend/accounts-service/src/accounts/model.rs @@ -0,0 +1,15 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +pub struct Account { + pub id: Uuid, + pub email: String, + #[serde(skip_serializing)] + pub password_hash: String, + pub display_nick: String, + pub role: String, + pub can_publish_addons: bool, + pub created_at: DateTime, +} diff --git a/backend/accounts-service/src/accounts/repo.rs b/backend/accounts-service/src/accounts/repo.rs new file mode 100644 index 0000000..5dfd3bf --- /dev/null +++ b/backend/accounts-service/src/accounts/repo.rs @@ -0,0 +1,168 @@ +use super::model::Account; +use sqlx::PgPool; +use uuid::Uuid; + +// Emails are stored lowercase and compared via the `lower(email)` unique +// index (see migrations/0001_init.sql). Every entry point normalizes here. +pub fn normalize_email(email: &str) -> String { + email.trim().to_lowercase() +} + +pub async fn create( + pool: &PgPool, + email: &str, + password_hash: &str, + nick: &str, +) -> Result { + sqlx::query_as::<_, Account>( + "INSERT INTO accounts (email, password_hash, display_nick) + VALUES ($1, $2, $3) + RETURNING id, email, password_hash, display_nick, role, can_publish_addons, created_at", + ) + .bind(normalize_email(email)) + .bind(password_hash) + .bind(nick) + .fetch_one(pool) + .await +} + +pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Account>( + "SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at + FROM accounts WHERE lower(email) = $1", + ) + .bind(normalize_email(email)) + .fetch_optional(pool) + .await +} + +pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as::<_, Account>( + "SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at + FROM accounts WHERE id = $1", + ) + .bind(id) + .fetch_optional(pool) + .await +} + +pub async fn set_avatar(pool: &PgPool, account_id: Uuid, s3_key: &str) -> Result<(), sqlx::Error> { + sqlx::query( + "INSERT INTO avatars (account_id, s3_key) VALUES ($1, $2) + ON CONFLICT (account_id) DO UPDATE SET s3_key = EXCLUDED.s3_key, uploaded_at = now()", + ) + .bind(account_id) + .bind(s3_key) + .execute(pool) + .await?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn test_pool() -> PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); + pool + } + + #[tokio::test] + async fn create_then_find_by_email_round_trips() { + let pool = test_pool().await; + let email = format!("test-{}@example.com", Uuid::new_v4()); + + let created = create(&pool, &email, "hash123", "TestNick").await.unwrap(); + assert_eq!(created.email, email); + assert_eq!(created.role, "user"); + assert!(created.can_publish_addons); + + let found = find_by_email(&pool, &email).await.unwrap().expect("must exist"); + assert_eq!(found.id, created.id); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn find_by_email_returns_none_for_missing() { + let pool = test_pool().await; + let result = find_by_email(&pool, "does-not-exist@example.com").await.unwrap(); + assert!(result.is_none()); + } + + #[tokio::test] + async fn email_lookup_is_case_insensitive_and_stored_lowercase() { + let pool = test_pool().await; + let tag = Uuid::new_v4(); + let mixed = format!("MiXeD-{tag}@Example.COM"); + + let created = create(&pool, &mixed, "hash123", "Nick").await.unwrap(); + assert_eq!(created.email, mixed.to_lowercase()); + + let found = find_by_email(&pool, &mixed.to_uppercase()) + .await + .unwrap() + .expect("lookup must ignore case"); + assert_eq!(found.id, created.id); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn duplicate_email_differing_only_by_case_is_rejected() { + let pool = test_pool().await; + let tag = Uuid::new_v4(); + let first = create(&pool, &format!("dup-{tag}@example.com"), "h", "A").await.unwrap(); + + let second = create(&pool, &format!("DUP-{tag}@EXAMPLE.com"), "h", "B").await; + let err = second.expect_err("case-variant duplicate must violate the unique index"); + match err { + sqlx::Error::Database(db) => { + assert_eq!(db.constraint(), Some("accounts_email_lower_idx")); + } + other => panic!("expected a database unique violation, got {other:?}"), + } + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(first.id) + .execute(&pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn set_avatar_inserts_then_updates_in_place() { + let pool = test_pool().await; + let created = create(&pool, &format!("av-{}@example.com", Uuid::new_v4()), "h", "N") + .await + .unwrap(); + + set_avatar(&pool, created.id, "avatars/one.png").await.unwrap(); + set_avatar(&pool, created.id, "avatars/two.png").await.unwrap(); + + let rows: Vec<(String,)> = + sqlx::query_as("SELECT s3_key FROM avatars WHERE account_id = $1") + .bind(created.id) + .fetch_all(&pool) + .await + .unwrap(); + assert_eq!(rows, vec![("avatars/two.png".to_string(),)]); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } +} diff --git a/backend/accounts-service/src/auth/handlers.rs b/backend/accounts-service/src/auth/handlers.rs new file mode 100644 index 0000000..5d7230e --- /dev/null +++ b/backend/accounts-service/src/auth/handlers.rs @@ -0,0 +1,227 @@ +use crate::accounts::repo; +use crate::auth::{password, tokens}; +use crate::error::{AppError, AppJson}; +use axum::{extract::State, http::StatusCode, Json}; +use axum_extra::extract::cookie::CookieJar; +use common::jwt; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct AuthState { + pub pool: sqlx::PgPool, + pub jwt_secret: String, + pub cookie_secure: bool, + pub hasher: password::PasswordHasher, + // A real Argon2id hash of a throwaway string. `login` verifies against + // it when the email is unknown so that "no such account" costs the same + // ~100ms as "wrong password" — otherwise response time leaks which + // emails are registered (user enumeration via timing). + dummy_hash: String, +} + +impl AuthState { + pub fn new(pool: sqlx::PgPool, jwt_secret: String, cookie_secure: bool) -> Self { + // Hashing a fixed, short constant with fixed valid params cannot + // fail; this is not user input, so the expect is a startup invariant. + let dummy_hash = password::hash_password("timing-equalizer-not-a-real-password") + .expect("hashing a constant with pinned params cannot fail"); + AuthState { + pool, + jwt_secret, + cookie_secure, + hasher: password::PasswordHasher::new(), + dummy_hash, + } + } +} + +#[derive(Deserialize)] +pub struct RegisterRequest { + pub email: String, + pub password: String, + pub nick: String, +} + +#[derive(Serialize)] +pub struct RegisterResponse { + pub id: uuid::Uuid, + pub email: String, + pub display_nick: String, +} + +/// Validates and normalizes a register request. Returns the trimmed +/// `(email, nick)` on success. +fn validate_register(req: &RegisterRequest) -> Result<(String, String), AppError> { + let email = req.email.trim(); + if email.is_empty() { + return Err(AppError::Validation("email must not be empty".into())); + } + if email.len() > 254 { + return Err(AppError::Validation("email must be at most 254 characters".into())); + } + let mut parts = email.split('@'); + let (Some(local), Some(domain)) = (parts.next(), parts.next()) else { + return Err(AppError::Validation("email must contain '@'".into())); + }; + if parts.next().is_some() || local.is_empty() || domain.is_empty() { + return Err(AppError::Validation("email must have exactly one '@' with non-empty parts".into())); + } + + let nick = req.nick.trim(); + let nick_len = nick.chars().count(); + if nick_len == 0 || nick_len > 32 { + return Err(AppError::Validation("nick must be 1 to 32 characters".into())); + } + if nick.chars().any(|c| c.is_control()) { + return Err(AppError::Validation("nick must not contain control characters".into())); + } + + if req.password.chars().count() < 8 { + return Err(AppError::Validation("password must be at least 8 characters".into())); + } + if req.password.len() > password::MAX_PASSWORD_BYTES { + return Err(AppError::Validation(format!( + "password must be at most {} bytes", + password::MAX_PASSWORD_BYTES + ))); + } + + Ok((email.to_string(), nick.to_string())) +} + +pub async fn register( + State(state): State, + AppJson(req): AppJson, +) -> Result<(StatusCode, Json), AppError> { + let (email, nick) = validate_register(&req)?; + let hash = state.hasher.hash(req.password.clone()).await.map_err(AppError::Internal)?; + let account = repo::create(&state.pool, &email, &hash, &nick).await?; + Ok(( + StatusCode::CREATED, + Json(RegisterResponse { + id: account.id, + email: account.email, + display_nick: account.display_nick, + }), + )) +} + +#[derive(Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub access_token: String, +} + +pub async fn login( + State(state): State, + jar: CookieJar, + AppJson(req): AppJson, +) -> Result<(CookieJar, Json), AppError> { + let Some(account) = repo::find_by_email(&state.pool, &req.email).await? else { + // Burn the same Argon2 cost as a real check, then fail identically. + state + .hasher + .verify(req.password.clone(), state.dummy_hash.clone()) + .await + .map_err(AppError::Internal)?; + return Err(AppError::Unauthorized); + }; + if !state + .hasher + .verify(req.password.clone(), account.password_hash.clone()) + .await + .map_err(AppError::Internal)? + { + return Err(AppError::Unauthorized); + } + let refresh = tokens::store_refresh(&state.pool, account.id).await?; + Ok(( + jar.add(tokens::refresh_cookie(refresh, state.cookie_secure)), + Json(LoginResponse { access_token: jwt::issue_access_token(account.id, &state.jwt_secret) }), + )) +} + +pub async fn refresh( + State(state): State, + jar: CookieJar, +) -> Result<(CookieJar, Json), AppError> { + let token = jar.get(tokens::REFRESH_COOKIE).map(|c| c.value().to_owned()).ok_or(AppError::Unauthorized)?; + match tokens::rotate_refresh(&state.pool, &token).await? { + tokens::RotateOutcome::Rotated { account_id, new_token } => Ok(( + jar.add(tokens::refresh_cookie(new_token, state.cookie_secure)), + Json(LoginResponse { access_token: jwt::issue_access_token(account_id, &state.jwt_secret) }), + )), + tokens::RotateOutcome::Invalid => Err(AppError::Unauthorized), + } +} + +pub async fn logout( + State(state): State, + jar: CookieJar, +) -> Result<(CookieJar, StatusCode), AppError> { + if let Some(cookie) = jar.get(tokens::REFRESH_COOKIE) { + tokens::revoke_refresh(&state.pool, cookie.value()).await?; + } + Ok(( + jar.remove(axum_extra::extract::cookie::Cookie::build(tokens::REFRESH_COOKIE).path("/auth")), + StatusCode::NO_CONTENT, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_request() -> RegisterRequest { + RegisterRequest { + email: "user@example.com".into(), + password: "password123".into(), + nick: "Rider".into(), + } + } + + #[test] + fn valid_request_passes() { + assert!(validate_register(&valid_request()).is_ok()); + } + + #[test] + fn short_password_is_rejected() { + let mut req = valid_request(); + req.password = "short12".into(); + assert!(validate_register(&req).is_err()); + } + + #[test] + fn empty_email_is_rejected() { + let mut req = valid_request(); + req.email = " ".into(); + assert!(validate_register(&req).is_err()); + } + + #[test] + fn email_without_at_is_rejected() { + let mut req = valid_request(); + req.email = "not-an-email".into(); + assert!(validate_register(&req).is_err()); + } + + #[test] + fn empty_nick_is_rejected() { + let mut req = valid_request(); + req.nick = " ".into(); + assert!(validate_register(&req).is_err()); + } + + #[test] + fn thirty_three_char_nick_is_rejected() { + let mut req = valid_request(); + req.nick = "a".repeat(33); + assert!(validate_register(&req).is_err()); + } +} diff --git a/backend/accounts-service/src/auth/mod.rs b/backend/accounts-service/src/auth/mod.rs index c72e4b9..d6b1d68 100644 --- a/backend/accounts-service/src/auth/mod.rs +++ b/backend/accounts-service/src/auth/mod.rs @@ -1 +1,3 @@ pub mod password; +pub mod tokens; +pub mod handlers; diff --git a/backend/accounts-service/src/auth/password.rs b/backend/accounts-service/src/auth/password.rs index fc74dbd..57027b5 100644 --- a/backend/accounts-service/src/auth/password.rs +++ b/backend/accounts-service/src/auth/password.rs @@ -1,18 +1,91 @@ -use argon2::{Argon2, PasswordHasher, PasswordVerifier}; use argon2::password_hash::phc::PasswordHash; +// `as _`: the trait must be in scope for `hash_password`, but the name +// belongs to the `PasswordHasher` struct below. +use argon2::{Algorithm, Argon2, Params, PasswordHasher as _, PasswordVerifier, Version}; +use std::sync::Arc; +use tokio::sync::Semaphore; + +// Explicit Argon2id parameters instead of `Argon2::default()`, so a +// dependency bump can never silently weaken the cost (the library default +// is the OWASP *minimum*: m=19 MiB, t=2). 64 MiB / t=3 / p=1 is +// deliberately above that floor. +const M_COST_KIB: u32 = 64 * 1024; +const T_COST: u32 = 3; +const P_COST: u32 = 1; + +// Argon2 processes the whole input, so an unbounded password is a cheap +// CPU/memory DoS vector on both register and login. bcrypt-style 72-byte +// truncation is not a concern for Argon2; this is purely an abuse cap. +pub const MAX_PASSWORD_BYTES: usize = 256; + +fn hasher() -> Result, argon2::password_hash::Error> { + let params = Params::new(M_COST_KIB, T_COST, P_COST, None) + .map_err(|_| argon2::password_hash::Error::ParamsInvalid)?; + Ok(Argon2::new(Algorithm::Argon2id, Version::V0x13, params)) +} pub fn hash_password(plain: &str) -> Result { - let argon2 = Argon2::default(); - Ok(argon2.hash_password(plain.as_bytes())?.to_string()) + if plain.len() > MAX_PASSWORD_BYTES { + return Err(argon2::password_hash::Error::PasswordInvalid); + } + Ok(hasher()?.hash_password(plain.as_bytes())?.to_string()) } pub fn verify_password(plain: &str, hash: &str) -> bool { - let Ok(parsed) = PasswordHash::new(hash) else { return false }; + if plain.len() > MAX_PASSWORD_BYTES { + return false; + } + let Ok(parsed) = PasswordHash::new(hash) else { + return false; + }; + // Parameters are read from the stored PHC string, so hashes created + // under older/lower settings still verify after a cost increase. Argon2::default() .verify_password(plain.as_bytes(), &parsed) .is_ok() } +/// Argon2 (64 MiB, ~100ms) is CPU/memory heavy; without a cap, concurrent +/// register/login requests could starve the tokio worker pool. Sized to the +/// number of CPUs so hashing never oversubscribes them. +#[derive(Clone)] +pub struct PasswordHasher { + permits: Arc, +} + +impl Default for PasswordHasher { + fn default() -> Self { + Self::new() + } +} + +impl PasswordHasher { + pub fn new() -> Self { + let permits = std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(2); + PasswordHasher { permits: Arc::new(Semaphore::new(permits)) } + } + + /// Runs Argon2 hashing off the async workers, bounded by the permit count. + pub async fn hash(&self, plain: String) -> Result { + let _permit = self.permits.acquire().await?; + let inner = tokio::task::spawn_blocking(move || hash_password(&plain)) + .await + .map_err(|e| anyhow::anyhow!(e))??; + Ok(inner) + } + + /// Runs Argon2 verification off the async workers, bounded by the permit + /// count. Used for both real and dummy (timing-equalizer) verification. + pub async fn verify(&self, plain: String, hash: String) -> Result { + let _permit = self.permits.acquire().await?; + tokio::task::spawn_blocking(move || verify_password(&plain, &hash)) + .await + .map_err(|e| anyhow::anyhow!(e)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -34,4 +107,36 @@ mod tests { let hash = hash_password("secret123").unwrap(); assert_ne!(hash, "secret123"); } + + #[test] + fn hash_uses_pinned_argon2id_params() { + let hash = hash_password("secret123").unwrap(); + assert!( + hash.starts_with("$argon2id$v=19$m=65536,t=3,p=1$"), + "unexpected PHC prefix: {hash}" + ); + } + + #[test] + fn same_password_hashes_differently_each_time() { + let a = hash_password("secret123").unwrap(); + let b = hash_password("secret123").unwrap(); + assert_ne!(a, b, "salt must be random per hash"); + } + + #[test] + fn password_length_cap_boundary() { + let at_cap = "a".repeat(MAX_PASSWORD_BYTES); + let hash = hash_password(&at_cap).expect("exactly the cap must be accepted"); + assert!(verify_password(&at_cap, &hash)); + + let over_cap = "a".repeat(MAX_PASSWORD_BYTES + 1); + assert!(hash_password(&over_cap).is_err(), "cap+1 must be rejected"); + assert!(!verify_password(&over_cap, &hash)); + } + + #[test] + fn garbage_hash_string_fails_verify_without_panicking() { + assert!(!verify_password("whatever", "not-a-phc-string")); + } } diff --git a/backend/accounts-service/src/auth/tokens.rs b/backend/accounts-service/src/auth/tokens.rs new file mode 100644 index 0000000..1028446 --- /dev/null +++ b/backend/accounts-service/src/auth/tokens.rs @@ -0,0 +1,136 @@ +use axum_extra::extract::cookie::{Cookie, SameSite}; +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use rand::RngExt; +use sha2::{Digest, Sha256}; +use sqlx::PgPool; +use uuid::Uuid; + +pub const REFRESH_COOKIE: &str = "lv_refresh"; + +/// 256-bit random token: nothing to brute-force, so a slow hash would only +/// add latency to every refresh — plain SHA-256 for storage is enough. +pub fn new_opaque_token(prefix: &str) -> String { + let mut bytes = [0u8; 32]; + rand::rng().fill(&mut bytes); + format!("{prefix}{}", URL_SAFE_NO_PAD.encode(bytes)) +} + +pub fn hash_token(token: &str) -> String { + hex::encode(Sha256::digest(token.as_bytes())) +} + +pub async fn store_refresh(pool: &PgPool, account_id: Uuid) -> Result { + let token = new_opaque_token("lvr_"); + sqlx::query( + "INSERT INTO refresh_tokens (account_id, token_hash, expires_at) + VALUES ($1, $2, now() + interval '30 days')", + ) + .bind(account_id) + .bind(hash_token(&token)) + .execute(pool) + .await?; + Ok(token) +} + +pub enum RotateOutcome { + Rotated { account_id: Uuid, new_token: String }, + Invalid, +} + +pub async fn rotate_refresh(pool: &PgPool, token: &str) -> Result { + let mut tx = pool.begin().await?; + let row: Option<(Uuid, bool, bool)> = sqlx::query_as( + "SELECT account_id, revoked_at IS NOT NULL, expires_at <= now() + FROM refresh_tokens WHERE token_hash = $1 FOR UPDATE", + ) + .bind(hash_token(token)) + .fetch_optional(&mut *tx) + .await?; + + let outcome = match row { + None => RotateOutcome::Invalid, + Some((account_id, true, _)) => { + // Reuse of a rotated token: someone else holds a copy. Kill all sessions. + sqlx::query( + "UPDATE refresh_tokens SET revoked_at = now() + WHERE account_id = $1 AND revoked_at IS NULL", + ) + .bind(account_id) + .execute(&mut *tx) + .await?; + RotateOutcome::Invalid + } + Some((_, false, true)) => RotateOutcome::Invalid, + Some((account_id, false, false)) => { + sqlx::query("UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1") + .bind(hash_token(token)) + .execute(&mut *tx) + .await?; + let new_token = new_opaque_token("lvr_"); + sqlx::query( + "INSERT INTO refresh_tokens (account_id, token_hash, expires_at) + VALUES ($1, $2, now() + interval '30 days')", + ) + .bind(account_id) + .bind(hash_token(&new_token)) + .execute(&mut *tx) + .await?; + RotateOutcome::Rotated { account_id, new_token } + } + }; + tx.commit().await?; + Ok(outcome) +} + +pub async fn revoke_refresh(pool: &PgPool, token: &str) -> Result<(), sqlx::Error> { + sqlx::query( + "UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1 AND revoked_at IS NULL", + ) + .bind(hash_token(token)) + .execute(pool) + .await?; + Ok(()) +} + +/// The refresh token never touches JS: httpOnly, Strict, scoped to /auth, +/// so an XSS on the site cannot exfiltrate it. +pub fn refresh_cookie(token: String, secure: bool) -> Cookie<'static> { + Cookie::build((REFRESH_COOKIE, token)) + .http_only(true) + .secure(secure) + .same_site(SameSite::Strict) + .path("/auth") + .max_age(time::Duration::days(30)) + .build() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn opaque_tokens_are_prefixed_unique_and_long() { + let a = new_opaque_token("lvr_"); + let b = new_opaque_token("lvr_"); + assert!(a.starts_with("lvr_")); + assert_eq!(a.len(), 4 + 43); + assert_ne!(a, b); + } + + #[test] + fn hash_is_stable_hex_sha256() { + assert_eq!(hash_token("x"), hash_token("x")); + assert_eq!(hash_token("x").len(), 64); + assert_ne!(hash_token("x"), hash_token("y")); + } + + #[test] + fn refresh_cookie_has_the_hardened_attributes() { + let cookie = refresh_cookie("t".into(), true); + assert_eq!(cookie.name(), REFRESH_COOKIE); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.secure(), Some(true)); + assert_eq!(cookie.same_site(), Some(SameSite::Strict)); + assert_eq!(cookie.path(), Some("/auth")); + } +} diff --git a/backend/accounts-service/src/avatars/handlers.rs b/backend/accounts-service/src/avatars/handlers.rs new file mode 100644 index 0000000..ec1f49e --- /dev/null +++ b/backend/accounts-service/src/avatars/handlers.rs @@ -0,0 +1,63 @@ +use crate::accounts::repo; +use crate::avatars::processing::{process_avatar, AvatarImageError}; +use crate::avatars::storage::S3Storage; +use crate::error::AppError; +use axum::{ + extract::{Multipart, State}, + Json, +}; +use common::internal::GatewayIdentity; +use serde::Serialize; + +pub const MAX_UPLOAD_BYTES: usize = 5 * 1024 * 1024; + +#[derive(Clone)] +pub struct AvatarState { + pub pool: sqlx::PgPool, + pub storage: S3Storage, + pub base_url: String, +} + +#[derive(Serialize)] +pub struct AvatarResponse { + pub avatar_url: String, +} + +pub async fn upload( + State(state): State, + identity: GatewayIdentity, + mut multipart: Multipart, +) -> Result, AppError> { + let account_id = identity.account_id; + + let field = multipart + .next_field() + .await + .map_err(|_| AppError::Validation("malformed multipart body".into()))? + .filter(|f| f.name() == Some("file")) + .ok_or_else(|| AppError::Validation("expected a multipart part named `file`".into()))?; + let bytes = field + .bytes() + .await + .map_err(|_| AppError::Validation("could not read the uploaded file".into()))?; + if bytes.len() > MAX_UPLOAD_BYTES { + return Err(AppError::Validation("file too large (max 5MB)".into())); + } + + // Decoding is CPU-bound and the input is untrusted: keep it off the async workers. + let png = tokio::task::spawn_blocking(move || process_avatar(&bytes)) + .await + .map_err(|e| AppError::Internal(e.into()))? + .map_err(|e| match e { + AvatarImageError::Unsupported => { + AppError::Validation("unsupported image format (png, jpeg, webp)".into()) + } + AvatarImageError::Invalid => AppError::Validation("invalid or too large image".into()), + })?; + + let key = format!("avatars/{account_id}.png"); + state.storage.put(&key, png, "image/png").await.map_err(AppError::Internal)?; + repo::set_avatar(&state.pool, account_id, &key).await?; + + Ok(Json(AvatarResponse { avatar_url: format!("{}/{key}", state.base_url) })) +} diff --git a/backend/accounts-service/src/avatars/mod.rs b/backend/accounts-service/src/avatars/mod.rs new file mode 100644 index 0000000..4f623e5 --- /dev/null +++ b/backend/accounts-service/src/avatars/mod.rs @@ -0,0 +1,3 @@ +pub mod handlers; +pub mod processing; +pub mod storage; diff --git a/backend/accounts-service/src/avatars/processing.rs b/backend/accounts-service/src/avatars/processing.rs new file mode 100644 index 0000000..ec4b8c5 --- /dev/null +++ b/backend/accounts-service/src/avatars/processing.rs @@ -0,0 +1,90 @@ +use image::{imageops::FilterType, ImageFormat, ImageReader, Limits}; +use std::io::Cursor; + +pub const AVATAR_SIZE: u32 = 256; +pub const MAX_DIMENSION: u32 = 8192; +const MAX_DECODE_BYTES: u64 = 128 * 1024 * 1024; + +#[derive(Debug, PartialEq, Eq)] +pub enum AvatarImageError { + /// Not a PNG/JPEG/WebP at all. + Unsupported, + /// Claims to be one of those but does not decode within the limits. + Invalid, +} + +/// Decodes untrusted image bytes, center-crops to a square and resizes to +/// `AVATAR_SIZE`x`AVATAR_SIZE`, then re-encodes as PNG. The output is always +/// a freshly generated PNG — never the original bytes — so EXIF metadata and +/// any embedded payload never reach storage. +pub fn process_avatar(bytes: &[u8]) -> Result, AvatarImageError> { + let mut reader = ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .map_err(|_| AvatarImageError::Invalid)?; + if !matches!( + reader.format(), + Some(ImageFormat::Png | ImageFormat::Jpeg | ImageFormat::WebP) + ) { + return Err(AvatarImageError::Unsupported); + } + let mut limits = Limits::default(); + limits.max_image_width = Some(MAX_DIMENSION); + limits.max_image_height = Some(MAX_DIMENSION); + limits.max_alloc = Some(MAX_DECODE_BYTES); + reader.limits(limits); + + let decoded = reader.decode().map_err(|_| AvatarImageError::Invalid)?; + let square = decoded.resize_to_fill(AVATAR_SIZE, AVATAR_SIZE, FilterType::Lanczos3); + let mut out = Cursor::new(Vec::new()); + square + .write_to(&mut out, ImageFormat::Png) + .map_err(|_| AvatarImageError::Invalid)?; + Ok(out.into_inner()) +} + +#[cfg(test)] +mod tests { + use super::*; + use image::ImageFormat; + use std::io::Cursor; + + fn png_of(width: u32, height: u32) -> Vec { + let img = image::RgbaImage::new(width, height); + let mut out = Cursor::new(Vec::new()); + img.write_to(&mut out, ImageFormat::Png).unwrap(); + out.into_inner() + } + + #[test] + fn valid_image_becomes_a_256_square_png() { + let out = process_avatar(&png_of(300, 100)).unwrap(); + assert!(out.starts_with(&[0x89, b'P', b'N', b'G'])); + let decoded = image::load_from_memory(&out).unwrap(); + assert_eq!((decoded.width(), decoded.height()), (AVATAR_SIZE, AVATAR_SIZE)); + } + + #[test] + fn non_image_bytes_are_unsupported() { + assert_eq!(process_avatar(b"not an image"), Err(AvatarImageError::Unsupported)); + } + + #[test] + fn gif_is_unsupported() { + assert_eq!( + process_avatar(b"GIF89a\x01\x00\x01\x00\x00\x00\x00;"), + Err(AvatarImageError::Unsupported) + ); + } + + #[test] + fn truncated_png_is_invalid() { + let mut bytes = png_of(50, 50); + bytes.truncate(40); + assert_eq!(process_avatar(&bytes), Err(AvatarImageError::Invalid)); + } + + #[test] + fn image_wider_than_the_limit_is_rejected() { + assert_eq!(process_avatar(&png_of(MAX_DIMENSION + 1, 1)), Err(AvatarImageError::Invalid)); + } +} diff --git a/backend/accounts-service/src/avatars/storage.rs b/backend/accounts-service/src/avatars/storage.rs new file mode 100644 index 0000000..bf0ae54 --- /dev/null +++ b/backend/accounts-service/src/avatars/storage.rs @@ -0,0 +1,36 @@ +use aws_sdk_s3::primitives::ByteStream; +use aws_sdk_s3::Client; + +#[derive(Clone)] +pub struct S3Storage { + client: Client, + bucket: String, +} + +impl S3Storage { + /// Builds the S3 client synchronously (no I/O happens here — connections + /// are made lazily on first request). + pub fn from_config(endpoint: &str, access_key: &str, secret_key: &str, bucket: String) -> Self { + let creds = aws_sdk_s3::config::Credentials::new(access_key, secret_key, None, None, "static"); + let config = aws_sdk_s3::config::Builder::new() + .endpoint_url(endpoint) + .credentials_provider(creds) + .region(aws_sdk_s3::config::Region::new("us-east-1")) + .force_path_style(true) + .behavior_version(aws_sdk_s3::config::BehaviorVersion::latest()) + .build(); + S3Storage { client: Client::from_conf(config), bucket } + } + + pub async fn put(&self, key: &str, bytes: Vec, content_type: &str) -> anyhow::Result<()> { + self.client + .put_object() + .bucket(&self.bucket) + .key(key) + .body(ByteStream::from(bytes)) + .content_type(content_type) + .send() + .await?; + Ok(()) + } +} diff --git a/backend/accounts-service/src/config.rs b/backend/accounts-service/src/config.rs index 2171a1b..f63055f 100644 --- a/backend/accounts-service/src/config.rs +++ b/backend/accounts-service/src/config.rs @@ -4,11 +4,13 @@ use anyhow::{Context, Result}; pub struct Config { pub database_url: String, pub jwt_secret: String, + pub internal_key: String, pub port: u16, pub s3_endpoint: String, pub s3_bucket: String, pub s3_access_key: String, pub s3_secret_key: String, + pub cookie_secure: bool, } impl Config { @@ -18,6 +20,8 @@ impl Config { .context("DATABASE_URL not set")?, jwt_secret: std::env::var("JWT_SECRET") .context("JWT_SECRET not set")?, + internal_key: std::env::var("INTERNAL_KEY") + .context("INTERNAL_KEY not set")?, port: std::env::var("PORT") .unwrap_or_else(|_| "8081".into()) .parse() @@ -30,6 +34,72 @@ impl Config { .context("S3_ACCESS_KEY not set")?, s3_secret_key: std::env::var("S3_SECRET_KEY") .context("S3_SECRET_KEY not set")?, + cookie_secure: std::env::var("COOKIE_SECURE").map(|v| v != "false").unwrap_or(true), }) } } + +const MIN_JWT_SECRET_BYTES: usize = 32; + +impl Config { + /// Fail fast at startup on a secret too weak to sign HS256 tokens with + /// (the `.env.example` placeholder must never reach production). + pub fn validate(&self) -> Result<()> { + if self.jwt_secret.len() < MIN_JWT_SECRET_BYTES { + anyhow::bail!("JWT_SECRET must be at least {MIN_JWT_SECRET_BYTES} bytes"); + } + if self.internal_key.len() < MIN_JWT_SECRET_BYTES { + anyhow::bail!("INTERNAL_KEY must be at least {MIN_JWT_SECRET_BYTES} bytes"); + } + Ok(()) + } + + /// Public prefix of stored avatars: `/`. + pub fn avatar_base_url(&self) -> String { + format!("{}/{}", self.s3_endpoint.trim_end_matches('/'), self.s3_bucket) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn config_with_secret(secret: &str) -> Config { + Config { + database_url: String::new(), + jwt_secret: secret.into(), + internal_key: "k".repeat(32), + port: 0, + s3_endpoint: String::new(), + s3_bucket: String::new(), + s3_access_key: String::new(), + s3_secret_key: String::new(), + cookie_secure: false, + } + } + + #[test] + fn short_internal_key_is_rejected() { + let mut cfg = config_with_secret(&"x".repeat(32)); + cfg.internal_key = "short".into(); + assert!(cfg.validate().is_err()); + } + + #[test] + fn short_jwt_secret_is_rejected() { + assert!(config_with_secret("too-short").validate().is_err()); + } + + #[test] + fn strong_jwt_secret_is_accepted() { + assert!(config_with_secret(&"x".repeat(32)).validate().is_ok()); + } + + #[test] + fn avatar_base_url_joins_endpoint_and_bucket_without_double_slash() { + let mut cfg = config_with_secret(&"x".repeat(32)); + cfg.s3_endpoint = "http://localhost:9000/".into(); + cfg.s3_bucket = "avatars".into(); + assert_eq!(cfg.avatar_base_url(), "http://localhost:9000/avatars"); + } +} diff --git a/backend/accounts-service/src/device/handlers.rs b/backend/accounts-service/src/device/handlers.rs new file mode 100644 index 0000000..2e86e48 --- /dev/null +++ b/backend/accounts-service/src/device/handlers.rs @@ -0,0 +1,73 @@ +use crate::device::store::{self, DeviceStore, PollResult}; +use crate::error::{AppError, AppJson}; +use axum::{extract::State, http::StatusCode, Json}; +use common::internal::GatewayIdentity; +use common::jwt; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct DeviceState { + pub store: DeviceStore, + pub jwt_secret: String, +} + +#[derive(Serialize)] +pub struct DeviceCodeResponse { + pub device_code: String, + pub user_code: String, + pub expires_in: u64, +} + +pub async fn create_code( + State(state): State, +) -> Result<(StatusCode, Json), AppError> { + let (device_code, user_code) = state.store.create().ok_or(AppError::TooManyRequests)?; + Ok(( + StatusCode::CREATED, + Json(DeviceCodeResponse { device_code, user_code, expires_in: store::TTL.as_secs() }), + )) +} + +#[derive(Deserialize)] +pub struct ConfirmRequest { + pub user_code: String, +} + +pub async fn confirm( + State(state): State, + identity: GatewayIdentity, + AppJson(req): AppJson, +) -> Result { + if state.store.confirm(&req.user_code, identity.account_id) { + Ok(StatusCode::OK) + } else { + Err(AppError::NotFound("unknown or expired user_code".into())) + } +} + +#[derive(Deserialize)] +pub struct TokenRequest { + pub device_code: String, +} + +#[derive(Serialize)] +pub struct TokenResponse { + pub device_token: String, +} + +pub async fn token( + State(state): State, + AppJson(req): AppJson, +) -> Result<(StatusCode, Json>), AppError> { + match state.store.poll(&req.device_code) { + PollResult::Unknown => Err(AppError::NotFound("unknown or expired device_code".into())), + PollResult::Pending => Ok((StatusCode::ACCEPTED, Json(None))), + PollResult::Confirmed(account_id) => { + // The long-lived device_token is just a refresh-style JWT for now; + // the gateway plan is where per-device revocation via + // device_links.device_token_hash gets enforced on every request. + let device_token = jwt::issue_refresh_token(account_id, &state.jwt_secret); + Ok((StatusCode::OK, Json(Some(TokenResponse { device_token })))) + } + } +} diff --git a/backend/accounts-service/src/device/mod.rs b/backend/accounts-service/src/device/mod.rs new file mode 100644 index 0000000..ad824f3 --- /dev/null +++ b/backend/accounts-service/src/device/mod.rs @@ -0,0 +1,2 @@ +pub mod handlers; +pub mod store; diff --git a/backend/accounts-service/src/device/store.rs b/backend/accounts-service/src/device/store.rs new file mode 100644 index 0000000..dbb1703 --- /dev/null +++ b/backend/accounts-service/src/device/store.rs @@ -0,0 +1,192 @@ +use dashmap::DashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct DeviceCodeEntry { + pub user_code: String, + pub confirmed_account_id: Option, + pub expires_at: Instant, +} + +#[derive(Clone, Default)] +pub struct DeviceStore { + by_device_code: Arc>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum PollResult { + Unknown, + Pending, + Confirmed(Uuid), +} + +pub(crate) const TTL: Duration = Duration::from_secs(600); +// /device/code is unauthenticated, so the store must be bounded or anyone can +// grow process memory without limit. Expired entries are purged on every +// create, so the cap only bites under sustained abuse. +const MAX_PENDING: usize = 10_000; + +fn random_user_code() -> String { + use rand::RngExt; + const ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no O/0/I/1 confusion + let mut rng = rand::rng(); + let mut part = |n: usize| -> String { + (0..n).map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char).collect() + }; + format!("{}-{}", part(4), part(4)) +} + +impl DeviceStore { + /// Returns `(device_code, user_code)`, or `None` when the store is full. + pub fn create(&self) -> Option<(String, String)> { + let now = Instant::now(); + self.by_device_code.retain(|_, entry| entry.expires_at > now); + if self.by_device_code.len() >= MAX_PENDING { + return None; + } + let device_code = Uuid::new_v4().to_string(); + let user_code = random_user_code(); + self.by_device_code.insert( + device_code.clone(), + DeviceCodeEntry { + user_code: user_code.clone(), + confirmed_account_id: None, + expires_at: now + TTL, + }, + ); + Some((device_code, user_code)) + } + + /// Returns true if a matching, unexpired entry was found and confirmed. + /// The user types this code by hand, so it is trimmed and upper-cased. + pub fn confirm(&self, user_code: &str, account_id: Uuid) -> bool { + let wanted = user_code.trim().to_uppercase(); + let now = Instant::now(); + for mut entry in self.by_device_code.iter_mut() { + // First confirm wins: an already-confirmed code cannot be + // re-bound to a different account before the device collects it. + if entry.user_code == wanted + && entry.expires_at > now + && entry.confirmed_account_id.is_none() + { + entry.confirmed_account_id = Some(account_id); + return true; + } + } + false + } + + /// A confirmed entry is CONSUMED by the first poll that sees it: the + /// token can be collected exactly once, replaying the same device_code + /// afterwards yields `Unknown`. Expired entries are dropped and Unknown. + pub fn poll(&self, device_code: &str) -> PollResult { + let now = Instant::now(); + let removed = self.by_device_code.remove_if(device_code, |_, entry| { + entry.confirmed_account_id.is_some() || entry.expires_at <= now + }); + if let Some((_, entry)) = removed { + return match entry.confirmed_account_id { + Some(id) if entry.expires_at > now => PollResult::Confirmed(id), + _ => PollResult::Unknown, + }; + } + if self.by_device_code.contains_key(device_code) { + PollResult::Pending + } else { + PollResult::Unknown + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_returns_distinct_codes() { + let store = DeviceStore::default(); + let (dc1, uc1) = store.create().unwrap(); + let (dc2, uc2) = store.create().unwrap(); + assert_ne!(dc1, dc2); + assert_ne!(uc1, uc2); + } + + #[test] + fn confirm_then_poll_returns_account_id() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create().unwrap(); + let account_id = Uuid::new_v4(); + + assert!(store.confirm(&user_code, account_id)); + assert_eq!(store.poll(&device_code), PollResult::Confirmed(account_id)); + } + + #[test] + fn confirmed_code_can_only_be_collected_once() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create().unwrap(); + assert!(store.confirm(&user_code, Uuid::new_v4())); + + assert!(matches!(store.poll(&device_code), PollResult::Confirmed(_))); + assert_eq!(store.poll(&device_code), PollResult::Unknown, "replay must fail"); + } + + #[test] + fn poll_before_confirm_is_pending_and_repeatable() { + let store = DeviceStore::default(); + let (device_code, _user_code) = store.create().unwrap(); + assert_eq!(store.poll(&device_code), PollResult::Pending); + assert_eq!(store.poll(&device_code), PollResult::Pending); + } + + #[test] + fn poll_unknown_code_is_unknown() { + let store = DeviceStore::default(); + assert_eq!(store.poll("does-not-exist"), PollResult::Unknown); + } + + #[test] + fn confirm_unknown_user_code_returns_false() { + let store = DeviceStore::default(); + assert!(!store.confirm("ZZZZ-ZZZZ", Uuid::new_v4())); + } + + #[test] + fn second_confirm_cannot_overwrite_the_first_account() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create().unwrap(); + let first = Uuid::new_v4(); + let attacker = Uuid::new_v4(); + + assert!(store.confirm(&user_code, first)); + assert!(!store.confirm(&user_code, attacker), "re-confirm must be refused"); + assert_eq!(store.poll(&device_code), PollResult::Confirmed(first)); + } + + #[test] + fn confirm_accepts_lowercase_and_surrounding_whitespace() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create().unwrap(); + let typed = format!(" {} ", user_code.to_lowercase()); + assert!(store.confirm(&typed, Uuid::new_v4())); + assert!(matches!(store.poll(&device_code), PollResult::Confirmed(_))); + } + + #[test] + fn expired_entries_are_purged_and_store_is_bounded() { + let store = DeviceStore::default(); + for _ in 0..MAX_PENDING { + assert!(store.create().is_some()); + } + assert!(store.create().is_none(), "store must refuse beyond MAX_PENDING"); + + // Force everything to be expired; the next create purges and succeeds. + for mut entry in store.by_device_code.iter_mut() { + entry.expires_at = Instant::now() - Duration::from_secs(1); + } + assert!(store.create().is_some()); + assert_eq!(store.by_device_code.len(), 1); + } +} diff --git a/backend/accounts-service/src/error.rs b/backend/accounts-service/src/error.rs new file mode 100644 index 0000000..b51d762 --- /dev/null +++ b/backend/accounts-service/src/error.rs @@ -0,0 +1,66 @@ +use axum::{ + extract::{rejection::JsonRejection, FromRequest}, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; + +#[derive(Debug)] +pub enum AppError { + Validation(String), + Conflict(String), + Unauthorized, + NotFound(String), + TooManyRequests, + Internal(anyhow::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg), + AppError::Conflict(msg) => (StatusCode::CONFLICT, msg), + AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()), + AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), + AppError::TooManyRequests => (StatusCode::TOO_MANY_REQUESTS, "too many pending device codes".into()), + AppError::Internal(err) => { + tracing::error!("internal error: {err:?}"); + (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()) + } + }; + (status, Json(json!({ "error": message }))).into_response() + } +} + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + if let sqlx::Error::Database(ref db_err) = err + && db_err.constraint() == Some("accounts_email_lower_idx") + { + return AppError::Conflict("email already registered".into()); + } + AppError::Internal(err.into()) + } +} + +impl From for AppError { + fn from(rejection: JsonRejection) -> Self { + AppError::Validation(rejection.body_text()) + } +} + +/// Drop-in replacement for `axum::Json` that reports malformed/invalid JSON +/// bodies as our `{"error": ...}` shape instead of axum's plain-text default. +#[derive(FromRequest)] +#[from_request(via(axum::Json), rejection(AppError))] +pub struct AppJson(pub T); + +impl IntoResponse for AppJson +where + Json: IntoResponse, +{ + fn into_response(self) -> Response { + Json(self.0).into_response() + } +} diff --git a/backend/accounts-service/src/lib.rs b/backend/accounts-service/src/lib.rs index 4cd5db8..6af68b4 100644 --- a/backend/accounts-service/src/lib.rs +++ b/backend/accounts-service/src/lib.rs @@ -1,8 +1,68 @@ +pub mod accounts; pub mod auth; +pub mod avatars; pub mod config; +pub mod device; +pub mod error; -use axum::{routing::get, Router}; +use auth::handlers::AuthState; +use avatars::{handlers::AvatarState, storage::S3Storage}; +use axum::{ + extract::DefaultBodyLimit, + Router, + routing::{get, post}, +}; +use config::Config; +use device::{handlers::DeviceState, store::DeviceStore}; -pub fn build_app(_pool: sqlx::PgPool) -> Router { - Router::new().route("/health", get(|| async { "ok" })) +pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { + let auth_state = + AuthState::new(pool.clone(), cfg.jwt_secret.clone(), cfg.cookie_secure); + let device_state = + DeviceState { store: DeviceStore::default(), jwt_secret: cfg.jwt_secret.clone() }; + let avatar_state = AvatarState { + pool: pool.clone(), + storage: S3Storage::from_config( + &cfg.s3_endpoint, + &cfg.s3_access_key, + &cfg.s3_secret_key, + cfg.s3_bucket.clone(), + ), + base_url: cfg.avatar_base_url(), + }; + + let auth_routes = Router::new() + .route("/auth/register", post(auth::handlers::register)) + .route("/auth/login", post(auth::handlers::login)) + .route("/auth/refresh", post(auth::handlers::refresh)) + .route("/auth/logout", post(auth::handlers::logout)) + .with_state(auth_state); + + let device_routes = Router::new() + .route("/device/code", post(device::handlers::create_code)) + .route("/device/confirm", post(device::handlers::confirm)) + .route("/device/token", post(device::handlers::token)) + .with_state(device_state); + + let avatar_routes = Router::new() + .route("/avatars", post(avatars::handlers::upload)) + // Hard transport cap slightly above the 5 MB business limit (413 beyond it). + .layer(DefaultBodyLimit::max(6 * 1024 * 1024)) + .with_state(avatar_state); + + // Everything except /health is internal-only: reachable solely through + // the gateway, which authenticates the caller and forwards the identity + // header. Direct traffic (or spoofed headers) is rejected here. + let api = Router::new() + .merge(auth_routes) + .merge(device_routes) + .merge(avatar_routes) + .layer(axum::middleware::from_fn_with_state( + common::internal::InternalKey::new(cfg.internal_key.clone()), + common::internal::require_internal_key, + )); + + Router::new() + .route("/health", get(|| async { "ok" })) + .merge(api) } diff --git a/backend/accounts-service/src/main.rs b/backend/accounts-service/src/main.rs index 356aa0e..2e2eea8 100644 --- a/backend/accounts-service/src/main.rs +++ b/backend/accounts-service/src/main.rs @@ -5,13 +5,16 @@ async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); dotenvy::dotenv().ok(); let cfg = Config::from_env()?; + cfg.validate()?; let pool = sqlx::postgres::PgPoolOptions::new() .max_connections(10) .connect(&cfg.database_url) .await?; - let app = build_app(pool); + sqlx::migrate!("./migrations").run(&pool).await?; + + let app = build_app(pool, &cfg); let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; tracing::info!("accounts-service listening on {}", cfg.port); axum::serve(listener, app).await?; diff --git a/backend/accounts-service/tests/auth_flow.rs b/backend/accounts-service/tests/auth_flow.rs new file mode 100644 index 0000000..204c4cc --- /dev/null +++ b/backend/accounts-service/tests/auth_flow.rs @@ -0,0 +1,162 @@ +use serde_json::json; +use uuid::Uuid; + +mod common; +// A test root's submodules resolve against `tests/`, not the file's own +// directory — pin the path so `tests/` itself stays at 4 files. +#[path = "auth_flow/refresh.rs"] +mod refresh; + +#[tokio::test] +async fn register_then_login_succeeds() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + let email = format!("flow-{}@example.com", Uuid::new_v4()); + + let register_response = server + .post("/auth/register") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple", "nick": "Rider" })) + .await; + register_response.assert_status(axum::http::StatusCode::CREATED); + + let login_response = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple" })) + .await; + login_response.assert_status_ok(); + let body: serde_json::Value = login_response.json(); + assert!(body["access_token"].is_string()); + assert!(body["refresh_token"].is_null(), "refresh token must be in the httpOnly cookie, not the body"); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn login_with_wrong_password_returns_401() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + let email = format!("wrongpw-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "right-password", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + let login_response = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "wrong-password" })) + .await; + login_response.assert_status(axum::http::StatusCode::UNAUTHORIZED); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn duplicate_email_registration_returns_409() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + let email = format!("dup-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "First" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + let second = server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password456", "nick": "Second" })) + .await; + second.assert_status(axum::http::StatusCode::CONFLICT); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn login_with_unknown_email_returns_401() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + + let response = server + .post("/auth/login") + .json(&json!({ "email": format!("nobody-{}@example.com", Uuid::new_v4()), "password": "whatever-123" })) + .await; + response.assert_status(axum::http::StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn login_is_case_insensitive_on_email() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + let tag = Uuid::new_v4(); + let registered = format!("CaseUser-{tag}@Example.com"); + + server + .post("/auth/register") + .json(&json!({ "email": registered, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + server + .post("/auth/login") + .json(&json!({ "email": registered.to_lowercase(), "password": "password123" })) + .await + .assert_status_ok(); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(registered.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn malformed_json_body_returns_400_with_json_error_shape() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + + let response = server + .post("/auth/register") + .content_type("application/json") + .bytes(axum::body::Bytes::from_static(b"{ this is not valid json")) + .await; + response.assert_status(axum::http::StatusCode::BAD_REQUEST); + let body: serde_json::Value = response.json(); + assert!(body["error"].is_string(), "expected {{\"error\": ...}}, got {body}"); +} + +#[tokio::test] +async fn register_rejects_oversized_password_with_400() { + let pool = common::test_pool().await; + let app = accounts_service::build_app(pool.clone(), &common::test_config()); + let server = common::test_server(app); + + let response = server + .post("/auth/register") + .json(&json!({ + "email": format!("big-{}@example.com", Uuid::new_v4()), + "password": "a".repeat(10_000), + "nick": "Rider" + })) + .await; + response.assert_status(axum::http::StatusCode::BAD_REQUEST); +} diff --git a/backend/accounts-service/tests/auth_flow/refresh.rs b/backend/accounts-service/tests/auth_flow/refresh.rs new file mode 100644 index 0000000..47bc93c --- /dev/null +++ b/backend/accounts-service/tests/auth_flow/refresh.rs @@ -0,0 +1,92 @@ +use super::common; +use axum::http::StatusCode; +use axum_extra::extract::cookie::Cookie; +use serde_json::json; + +async fn login(server: &axum_test::TestServer, email: &str) -> (String, String) { + let res = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple" })) + .await; + res.assert_status_ok(); + let cookie = res.cookie("lv_refresh"); + assert!(cookie.http_only().unwrap_or(false)); + assert_eq!(cookie.path(), Some("/auth")); + let body: serde_json::Value = res.json(); + assert!(body.get("refresh_token").is_none(), "refresh token must not be in the JSON body"); + (body["access_token"].as_str().unwrap().to_owned(), cookie.value().to_owned()) +} + +#[tokio::test] +async fn refresh_rotates_the_cookie_and_issues_a_new_access_token() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, refresh) = login(&server, &email).await; + + let res = server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", refresh.clone())) + .await; + res.assert_status_ok(); + assert!(res.json::()["access_token"].is_string()); + assert_ne!(res.cookie("lv_refresh").value(), refresh); +} + +#[tokio::test] +async fn reusing_a_rotated_refresh_token_revokes_all_sessions() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, first) = login(&server, &email).await; + + let second = server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", first.clone())) + .await + .cookie("lv_refresh") + .value() + .to_owned(); + // Attacker replays the old token -> rejected, and the legit new one dies too. + server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", first)) + .await + .assert_status(StatusCode::UNAUTHORIZED); + server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", second)) + .await + .assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn logout_revokes_the_refresh_token() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, refresh) = login(&server, &email).await; + + server + .post("/auth/logout") + .add_cookie(Cookie::new("lv_refresh", refresh.clone())) + .await + .assert_status(StatusCode::NO_CONTENT); + server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", refresh)) + .await + .assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn refresh_without_cookie_or_with_garbage_is_401() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + server.post("/auth/refresh").await.assert_status(StatusCode::UNAUTHORIZED); + server + .post("/auth/refresh") + .add_cookie(Cookie::new("lv_refresh", "lvr_garbage")) + .await + .assert_status(StatusCode::UNAUTHORIZED); +} diff --git a/backend/accounts-service/tests/avatar_upload.rs b/backend/accounts-service/tests/avatar_upload.rs new file mode 100644 index 0000000..df54176 --- /dev/null +++ b/backend/accounts-service/tests/avatar_upload.rs @@ -0,0 +1,100 @@ +mod common; + +use axum::http::StatusCode; +use axum_test::multipart::{MultipartForm, Part}; +use common::ACCOUNT_ID_HEADER; + +fn png_bytes() -> Vec { + let img = image::RgbaImage::new(64, 32); + let mut out = std::io::Cursor::new(Vec::new()); + img.write_to(&mut out, image::ImageFormat::Png).unwrap(); + out.into_inner() +} + +fn form(bytes: Vec) -> MultipartForm { + MultipartForm::new().add_part("file", Part::bytes(bytes).file_name("a.png").mime_type("image/png")) +} + +#[tokio::test] +async fn valid_upload_stores_avatar_and_returns_url() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account_id, email) = common::register_account(&server).await; + + let response = server + .post("/avatars") + .add_header(ACCOUNT_ID_HEADER, account_id.to_string()) + .multipart(form(png_bytes())) + .await; + response.assert_status_ok(); + let url = response.json::()["avatar_url"].as_str().unwrap().to_string(); + assert!(url.starts_with("http://localhost:9000/lovisual-avatars-test/avatars/"), "{url}"); + assert!(url.ends_with(".png"), "{url}"); + + let stored: (String,) = sqlx::query_as( + "SELECT a.s3_key FROM avatars a JOIN accounts c ON c.id = a.account_id WHERE lower(c.email) = $1", + ) + .bind(email.to_lowercase()) + .fetch_one(&pool) + .await + .expect("avatars row must exist"); + assert!(url.ends_with(&stored.0)); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn non_image_upload_is_rejected_with_400() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account_id, email) = common::register_account(&server).await; + + server + .post("/avatars") + .add_header(ACCOUNT_ID_HEADER, account_id.to_string()) + .multipart(form(b"definitely not an image".to_vec())) + .await + .assert_status(StatusCode::BAD_REQUEST); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn oversized_upload_is_rejected_with_400() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account_id, email) = common::register_account(&server).await; + + server + .post("/avatars") + .add_header(ACCOUNT_ID_HEADER, account_id.to_string()) + .multipart(form(vec![0u8; 5 * 1024 * 1024 + 1])) + .await + .assert_status(StatusCode::BAD_REQUEST); + + sqlx::query("DELETE FROM accounts WHERE lower(email) = $1") + .bind(email.to_lowercase()) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn upload_without_identity_is_401() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + + server + .post("/avatars") + .multipart(form(png_bytes())) + .await + .assert_status(StatusCode::UNAUTHORIZED); +} diff --git a/backend/accounts-service/tests/common/mod.rs b/backend/accounts-service/tests/common/mod.rs new file mode 100644 index 0000000..a3e5451 --- /dev/null +++ b/backend/accounts-service/tests/common/mod.rs @@ -0,0 +1,52 @@ +#![allow(dead_code, unused_imports)] // each test binary uses a different subset + +use accounts_service::config::Config; +use axum_test::TestServer; +use uuid::Uuid; + +// `::` — the workspace crate, not this module (which every test binary +// mounts as `mod common;`). +pub use ::common::internal::{ACCOUNT_ID_HEADER, INTERNAL_KEY_HEADER}; + +pub async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect to test database"); + sqlx::migrate!("./migrations").run(&pool).await.expect("run migrations"); + pool +} + +pub fn test_config() -> Config { + Config { + database_url: String::new(), + jwt_secret: "test-secret-test-secret-test-secret!".into(), + internal_key: TEST_INTERNAL_KEY.into(), + port: 0, + s3_endpoint: "http://localhost:9000".into(), + s3_bucket: "lovisual-avatars-test".into(), + s3_access_key: "minioadmin".into(), + s3_secret_key: "minioadmin".into(), + cookie_secure: false, + } +} + +pub const TEST_INTERNAL_KEY: &str = "internal-key-internal-key-internal!!"; + +/// A server that behaves like it sits behind the gateway. +pub fn test_server(app: axum::Router) -> TestServer { + let mut server = TestServer::new(app); + server.add_header(INTERNAL_KEY_HEADER, TEST_INTERNAL_KEY); + server +} + +/// Registers a fresh random account; returns its id and email. +pub async fn register_account(server: &TestServer) -> (Uuid, String) { + let email = format!("t-{}@example.com", Uuid::new_v4()); + let res = server + .post("/auth/register") + .json(&serde_json::json!({ "email": email, "password": "correct-horse-battery-staple", "nick": "Tester" })) + .await; + res.assert_status(axum::http::StatusCode::CREATED); + let body: serde_json::Value = res.json(); + (Uuid::parse_str(body["id"].as_str().unwrap()).unwrap(), email) +} diff --git a/backend/accounts-service/tests/db_migration.rs b/backend/accounts-service/tests/db_migration.rs deleted file mode 100644 index 2e35183..0000000 --- a/backend/accounts-service/tests/db_migration.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[tokio::test] -async fn migrations_create_accounts_table() { - let database_url = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); - let pool = sqlx::PgPool::connect(&database_url).await.expect("connect"); - - sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); - - let row: (i64,) = sqlx::query_as("SELECT count(*) FROM accounts") - .fetch_one(&pool) - .await - .expect("accounts table must exist"); - assert_eq!(row.0, 0); -} diff --git a/backend/accounts-service/tests/device_flow.rs b/backend/accounts-service/tests/device_flow.rs new file mode 100644 index 0000000..1d5f4c2 --- /dev/null +++ b/backend/accounts-service/tests/device_flow.rs @@ -0,0 +1,87 @@ +mod common; + +use axum::http::StatusCode; +use serde_json::json; + +#[tokio::test] +async fn full_device_link_flow() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account_id, email) = common::register_account(&server).await; + + let code_response: serde_json::Value = server.post("/device/code").await.json(); + let device_code = code_response["device_code"].as_str().unwrap(); + let user_code = code_response["user_code"].as_str().unwrap(); + + let poll_before = server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await; + poll_before.assert_status(StatusCode::ACCEPTED); + + server + .post("/device/confirm") + .add_header(common::ACCOUNT_ID_HEADER, account_id.to_string()) + .json(&json!({ "user_code": user_code })) + .await + .assert_status_ok(); + + let poll_after = server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await; + poll_after.assert_status_ok(); + let token_body: serde_json::Value = poll_after.json(); + assert!(token_body["device_token"].is_string()); + + // Single use: the same device_code cannot be redeemed twice. + server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await + .assert_status(StatusCode::NOT_FOUND); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn confirm_without_identity_is_401() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let code: serde_json::Value = server.post("/device/code").await.json(); + server + .post("/device/confirm") + .json(&json!({ "user_code": code["user_code"] })) + .await + .assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn unknown_device_code_and_user_code_return_404() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account_id, email) = common::register_account(&server).await; + + server + .post("/device/token") + .json(&json!({ "device_code": "no-such-code" })) + .await + .assert_status(StatusCode::NOT_FOUND); + + server + .post("/device/confirm") + .add_header(common::ACCOUNT_ID_HEADER, account_id.to_string()) + .json(&json!({ "user_code": "ZZZZ-ZZZZ" })) + .await + .assert_status(StatusCode::NOT_FOUND); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} diff --git a/backend/accounts-service/tests/health.rs b/backend/accounts-service/tests/health.rs deleted file mode 100644 index 6f4ecfb..0000000 --- a/backend/accounts-service/tests/health.rs +++ /dev/null @@ -1,16 +0,0 @@ -use axum_test::TestServer; - -#[tokio::test] -async fn health_returns_ok() { - // NOTE: this test does not touch the DB — pass a pool that is never - // queried. sqlx::PgPool::connect_lazy never opens a connection until - // a query runs, so this is safe without a running Postgres. - let pool = sqlx::PgPool::connect_lazy("postgres://user:pass@localhost/db") - .expect("lazy pool"); - let app = accounts_service::build_app(pool); - let server = TestServer::new(app); - - let response = server.get("/health").await; - response.assert_status_ok(); - response.assert_text("ok"); -} diff --git a/backend/accounts-service/tests/smoke.rs b/backend/accounts-service/tests/smoke.rs new file mode 100644 index 0000000..0f321eb --- /dev/null +++ b/backend/accounts-service/tests/smoke.rs @@ -0,0 +1,42 @@ +mod common; + +use axum_test::TestServer; + +#[tokio::test] +async fn health_returns_ok() { + // NOTE: this test does not touch the DB — pass a pool that is never + // queried. sqlx::PgPool::connect_lazy never opens a connection until + // a query runs, so this is safe without a running Postgres. + let pool = sqlx::PgPool::connect_lazy("postgres://user:pass@localhost/db") + .expect("lazy pool"); + let app = accounts_service::build_app(pool, &common::test_config()); + let server = TestServer::new(app); + + let response = server.get("/health").await; + response.assert_status_ok(); + response.assert_text("ok"); +} + +#[tokio::test] +async fn migrations_create_accounts_table() { + let pool = common::test_pool().await; + + let row: (Option,) = sqlx::query_as("SELECT to_regclass('accounts')::text") + .fetch_one(&pool) + .await + .expect("query must succeed"); + assert!(row.0.is_some(), "accounts table must exist after migrations run"); +} + +#[tokio::test] +async fn api_routes_require_internal_key_but_health_does_not() { + let pool = common::test_pool().await; + // A raw server: no internal key header on any request, as if the + // service were reached directly, bypassing the gateway. + let server = axum_test::TestServer::new(accounts_service::build_app(pool, &common::test_config())); + server.get("/health").await.assert_status_ok(); + server + .post("/device/code") + .await + .assert_status(axum::http::StatusCode::FORBIDDEN); +} diff --git a/backend/common/Cargo.toml b/backend/common/Cargo.toml new file mode 100644 index 0000000..df12bdf --- /dev/null +++ b/backend/common/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "common" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8" +jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" +uuid = { version = "1", features = ["v4", "serde"] } +subtle = "2" +tonic = "0.14" +tonic-prost = "0.14" +prost = "0.14" + +[build-dependencies] +tonic-prost-build = "0.14" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +axum-test = "21" diff --git a/backend/common/build.rs b/backend/common/build.rs new file mode 100644 index 0000000..3c19d90 --- /dev/null +++ b/backend/common/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_prost_build::compile_protos("proto/accounts.proto")?; + Ok(()) +} diff --git a/backend/common/proto/accounts.proto b/backend/common/proto/accounts.proto new file mode 100644 index 0000000..2e077ca --- /dev/null +++ b/backend/common/proto/accounts.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package accounts.v1; + +// Internal-only API of accounts-service. Never exposed publicly; every call +// must carry the x-lovisual-internal-key metadata entry. +service AccountsInternal { + // Resolves a mod's long-lived device token to its account and bumps + // device_links.last_seen. UNAUTHENTICATED if the token is unknown/revoked. + rpc AuthenticateDevice(AuthenticateDeviceRequest) returns (AuthenticateDeviceReply); +} + +message AuthenticateDeviceRequest { string device_token = 1; } +message AuthenticateDeviceReply { string account_id = 1; } diff --git a/backend/common/src/internal.rs b/backend/common/src/internal.rs new file mode 100644 index 0000000..2e74061 --- /dev/null +++ b/backend/common/src/internal.rs @@ -0,0 +1,176 @@ +//! Contract between the gateway and internal services. Services trust the +//! identity header ONLY because `require_internal_key` guarantees the request +//! came through the gateway (which strips client-supplied copies of both). + +use axum::{ + extract::{FromRequestParts, Request, State}, + http::{request::Parts, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, + Json, +}; +use serde_json::json; +use std::sync::Arc; +use subtle::ConstantTimeEq; +use tonic::{ + metadata::{Ascii, MetadataValue}, + service::Interceptor, + Status, +}; +use uuid::Uuid; + +pub const INTERNAL_KEY_HEADER: &str = "x-lovisual-internal-key"; +pub const ACCOUNT_ID_HEADER: &str = "x-lovisual-account-id"; +pub const DEVICE_TOKEN_PREFIX: &str = "lvd_"; + +fn keys_match(given: &[u8], expected: &[u8]) -> bool { + given.ct_eq(expected).into() +} + +#[derive(Clone)] +pub struct InternalKey(Arc); + +impl InternalKey { + pub fn new(key: String) -> Self { + InternalKey(key.into()) + } +} + +pub async fn require_internal_key(State(key): State, req: Request, next: Next) -> Response { + let ok = req + .headers() + .get(INTERNAL_KEY_HEADER) + .is_some_and(|v| keys_match(v.as_bytes(), key.0.as_bytes())); + if !ok { + return (StatusCode::FORBIDDEN, Json(json!({ "error": "forbidden" }))).into_response(); + } + next.run(req).await +} + +/// The authenticated account, as resolved by the gateway. Rejects with 401 +/// when the gateway forwarded the request anonymously. +pub struct GatewayIdentity { + pub account_id: Uuid, +} + +impl FromRequestParts for GatewayIdentity { + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + parts + .headers + .get(ACCOUNT_ID_HEADER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| Uuid::parse_str(s).ok()) + .map(|account_id| GatewayIdentity { account_id }) + .ok_or_else(|| { + (StatusCode::UNAUTHORIZED, Json(json!({ "error": "unauthorized" }))).into_response() + }) + } +} + +/// Server-side tonic interceptor: rejects calls without the internal key. +#[derive(Clone)] +pub struct GrpcKeyCheck(Arc); + +impl GrpcKeyCheck { + pub fn new(key: &str) -> Self { + GrpcKeyCheck(key.into()) + } +} + +impl Interceptor for GrpcKeyCheck { + fn call(&mut self, req: tonic::Request<()>) -> Result, Status> { + match req.metadata().get(INTERNAL_KEY_HEADER) { + Some(v) if keys_match(v.as_bytes(), self.0.as_bytes()) => Ok(req), + _ => Err(Status::permission_denied("missing or invalid internal key")), + } + } +} + +/// Client-side tonic interceptor: attaches the internal key to every call. +#[derive(Clone)] +pub struct GrpcKeyAttach(MetadataValue); + +impl GrpcKeyAttach { + pub fn new(key: &str) -> Result { + Ok(GrpcKeyAttach(MetadataValue::try_from(key)?)) + } +} + +impl Interceptor for GrpcKeyAttach { + fn call(&mut self, mut req: tonic::Request<()>) -> Result, Status> { + req.metadata_mut().insert(INTERNAL_KEY_HEADER, self.0.clone()); + Ok(req) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{routing::get, Router}; + use axum_test::TestServer; + + const KEY: &str = "internal-key-internal-key-internal!!"; + + fn app() -> Router { + Router::new() + .route("/whoami", get(|id: GatewayIdentity| async move { id.account_id.to_string() })) + .route("/open", get(|| async { "open" })) + .layer(axum::middleware::from_fn_with_state(InternalKey::new(KEY.into()), require_internal_key)) + } + + #[tokio::test] + async fn request_without_internal_key_is_forbidden() { + let server = TestServer::new(app()); + server.get("/open").await.assert_status(axum::http::StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn request_with_wrong_internal_key_is_forbidden() { + let server = TestServer::new(app()); + server + .get("/open") + .add_header(INTERNAL_KEY_HEADER, "nope") + .await + .assert_status(axum::http::StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn correct_key_passes_and_identity_is_read() { + let server = TestServer::new(app()); + let id = Uuid::new_v4(); + let res = server + .get("/whoami") + .add_header(INTERNAL_KEY_HEADER, KEY) + .add_header(ACCOUNT_ID_HEADER, id.to_string()) + .await; + res.assert_status_ok(); + assert_eq!(res.text(), id.to_string()); + } + + #[tokio::test] + async fn missing_or_garbage_identity_is_401() { + let server = TestServer::new(app()); + server + .get("/whoami") + .add_header(INTERNAL_KEY_HEADER, KEY) + .await + .assert_status_unauthorized(); + server + .get("/whoami") + .add_header(INTERNAL_KEY_HEADER, KEY) + .add_header(ACCOUNT_ID_HEADER, "not-a-uuid") + .await + .assert_status_unauthorized(); + } + + #[test] + fn grpc_key_check_accepts_only_the_right_key() { + let mut check = GrpcKeyCheck::new(KEY); + let mut attach = GrpcKeyAttach::new(KEY).unwrap(); + let ok = attach.call(tonic::Request::new(())).unwrap(); + assert!(check.call(ok).is_ok()); + assert!(check.call(tonic::Request::new(())).is_err()); + } +} diff --git a/backend/common/src/jwt.rs b/backend/common/src/jwt.rs new file mode 100644 index 0000000..e96ba12 --- /dev/null +++ b/backend/common/src/jwt.rs @@ -0,0 +1,150 @@ +use axum::http::{header::AUTHORIZATION, HeaderMap}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Distinguishes token purposes so a long-lived refresh/device token can +/// never be replayed as a short-lived access token (and vice versa). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum TokenType { + Access, + Refresh, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub token_type: TokenType, +} + +fn issue(account_id: Uuid, secret: &str, token_type: TokenType, ttl_seconds: i64) -> String { + let exp = (chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds)).timestamp() as usize; + let claims = Claims { sub: account_id.to_string(), exp, token_type }; + encode(&Header::new(Algorithm::HS256), &claims, &EncodingKey::from_secret(secret.as_bytes())) + .expect("encoding a well-formed Claims struct cannot fail") +} + +pub fn issue_access_token(account_id: Uuid, secret: &str) -> String { + issue(account_id, secret, TokenType::Access, 15 * 60) +} + +/// Temporary — deleted in Task 4 once opaque refresh tokens land. +pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String { + issue(account_id, secret, TokenType::Refresh, 30 * 24 * 60 * 60) +} + +/// Returns the claims only if the signature, expiry AND token type all match. +/// HS256 is pinned explicitly (no algorithm confusion) and expiry leeway is 0. +pub fn verify_token(token: &str, secret: &str, expected: TokenType) -> Option { + let mut validation = Validation::new(Algorithm::HS256); + validation.leeway = 0; + validation.set_required_spec_claims(&["exp", "sub"]); + let claims = decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &validation, + ) + .ok()? + .claims; + (claims.token_type == expected).then_some(claims) +} + +/// The raw token from `Authorization: Bearer `, if the header is +/// present, valid ASCII, uses the Bearer scheme and is non-empty. +pub fn bearer_token(headers: &HeaderMap) -> Option<&str> { + let value = headers.get(AUTHORIZATION)?.to_str().ok()?; + let token = value.strip_prefix("Bearer ")?.trim(); + (!token.is_empty()).then_some(token) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn access_token_round_trips() { + let id = Uuid::new_v4(); + let token = issue_access_token(id, "test-secret"); + let claims = verify_token(&token, "test-secret", TokenType::Access).expect("should decode"); + assert_eq!(claims.sub, id.to_string()); + assert_eq!(claims.token_type, TokenType::Access); + } + + #[test] + fn wrong_secret_fails_verify() { + let token = issue_access_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "other-secret", TokenType::Access).is_none()); + } + + #[test] + fn garbage_token_fails_verify() { + assert!(verify_token("not.a.jwt", "test-secret", TokenType::Access).is_none()); + } + + #[test] + fn refresh_token_is_rejected_where_access_is_expected() { + let token = issue_refresh_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "test-secret", TokenType::Access).is_none()); + assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_some()); + } + + #[test] + fn access_token_is_rejected_where_refresh_is_expected() { + let token = issue_access_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "test-secret", TokenType::Refresh).is_none()); + } + + #[test] + fn expired_token_fails_verify() { + let token = issue(Uuid::new_v4(), "test-secret", TokenType::Access, -10); + assert!(verify_token(&token, "test-secret", TokenType::Access).is_none()); + } + + #[test] + fn token_signed_with_other_algorithm_is_rejected() { + let claims = Claims { + sub: Uuid::new_v4().to_string(), + exp: (chrono::Utc::now().timestamp() + 600) as usize, + token_type: TokenType::Access, + }; + let hs512 = encode( + &Header::new(Algorithm::HS512), + &claims, + &EncodingKey::from_secret(b"test-secret"), + ) + .unwrap(); + assert!(verify_token(&hs512, "test-secret", TokenType::Access).is_none()); + } + + use axum::http::HeaderValue; + + fn headers_with(value: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(AUTHORIZATION, HeaderValue::from_str(value).unwrap()); + headers + } + + #[test] + fn bearer_token_extracts_the_token() { + let mut h = HeaderMap::new(); + h.insert(AUTHORIZATION, "Bearer abc.def".parse().unwrap()); + assert_eq!(bearer_token(&h), Some("abc.def")); + } + + #[test] + fn bearer_token_rejects_missing_wrong_scheme_and_empty() { + let mut h = HeaderMap::new(); + assert_eq!(bearer_token(&h), None); + h.insert(AUTHORIZATION, "Basic abc".parse().unwrap()); + assert_eq!(bearer_token(&h), None); + h.insert(AUTHORIZATION, "Bearer ".parse().unwrap()); + assert_eq!(bearer_token(&h), None); + } + + #[test] + fn bearer_token_rejects_garbage_headers() { + assert_eq!(bearer_token(&headers_with("Bearer not.a.jwt")).unwrap(), "not.a.jwt"); + } +} diff --git a/backend/common/src/lib.rs b/backend/common/src/lib.rs new file mode 100644 index 0000000..00d5b37 --- /dev/null +++ b/backend/common/src/lib.rs @@ -0,0 +1,8 @@ +pub mod internal; +pub mod jwt; + +pub mod pb { + pub mod accounts { + tonic::include_proto!("accounts.v1"); + } +} diff --git a/backend/configs-service/PLAN.md b/backend/configs-service/PLAN.md new file mode 100644 index 0000000..c165135 --- /dev/null +++ b/backend/configs-service/PLAN.md @@ -0,0 +1,1389 @@ +# Backend: configs-service (Подсистема 1, часть 3) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Prerequisite:** `backend/gateway/PLAN.md` fully done (this service relies on `common::internal`, the gateway routing `/configs*` and `/showcase*` here, and the accounts gRPC server). + +**Goal:** Cloud configs for the mod and site: 4 named slots per account, a permanent regenerable share code per slot (`%config load `), and a public showcase with "copy to my slot". + +**Architecture:** Axum service with its own `configs_db` (same Postgres instance). Sits behind the gateway: every request needs the internal key, identity comes from `GatewayIdentity`. Account ids are plain UUIDs here (no FK — they live in `accounts_db`). Author nicks/avatars for the showcase come from accounts-service over gRPC (`GetPublicProfiles`), batched per page, and degrade to `author: null` if accounts is down. + +**Tech Stack:** same versions as accounts-service / gateway (Axum 0.8, sqlx 0.9, tonic 0.14, rand 0.10, serde_json). No new crates. + +## Global Constraints + +- Everything in `backend/PLAN.md` and `backend/gateway/PLAN.md` Global Constraints applies (TDD, ≤250 lines/file, ≤4 files/folder, no unwrap on request data, `CARGO_BUILD_JOBS=4`, explicit `git add` paths, English commits, never touch `mod/`). +- Test DB: `CONFIGS_DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/configs_db` (created on the test VDS on 2026-09-24). +- Slots are exactly 1..=4. Config name: 1..=32 chars after trim. Config `data`: any JSON value, serialized ≤ 256 KiB. Showcase title 1..=64 chars, description ≤ 500 chars. Page size 20. +- Share code: 8 chars from `23456789ABCDEFGHJKMNPQRSTUVWXYZ` (no 0/O/1/I/L — typed by hand in chat). Lookups trim + uppercase. Code is permanent until the owner regenerates it. +- `error.rs` is a per-service copy of accounts-service's `AppError` shape (`{"error": msg}` JSON). Deliberately not in `common`: the `From` mapping is service-specific (constraint names), and a shared error type would drag `sqlx` into the gateway. + +--- + +## File Structure + +``` +backend/ + common/proto/accounts.proto # + GetPublicProfiles + accounts-service/src/grpc/mod.rs # + get_public_profiles + accounts-service/src/accounts/repo.rs# + public_profiles + configs-service/ + Cargo.toml + PLAN.md + migrations/0001_init.sql + src/ + main.rs + lib.rs # build_app(pool, &Config, Arc) -> Router + config.rs + error.rs + slots/{mod,repo,handlers}.rs # GET/PUT /configs, GET /configs/{slot} + sharing/{mod,codes,handlers}.rs # share codes, regenerate, GET /configs/shared/{code} + showcase/{mod,repo,handlers,profiles}.rs # publish/unpublish, listing, copy; gRPC author lookup + tests/ + common/mod.rs + slots.rs + sharing.rs + showcase.rs +``` + +--- + +### Task 1: Scaffold — crate, config, schema, health + +**Files:** +- Create: `backend/configs-service/{Cargo.toml, migrations/0001_init.sql, src/main.rs, src/lib.rs, src/config.rs, src/error.rs, tests/common/mod.rs, tests/slots.rs}` +- Modify: `backend/Cargo.toml` (members += "configs-service"), `backend/.env.example` + +**Interfaces:** +- Produces: `Config { database_url, port /*8082*/, internal_key, accounts_grpc_url }`, `from_env()`, `validate()` (internal key ≥32 bytes). `AppError { Validation, NotFound, Conflict, Internal }`. `build_app(pool, &Config) -> Router` (Task 5 adds the `profiles` arg). Tests: `test_pool()`, `test_config()`, `test_server(Router)`, `ACCOUNT_ID_HEADER` re-export, `new_account() -> Uuid`. + +- [ ] **Step 1: `Cargo.toml`** + +```toml +[package] +name = "configs-service" +version = "0.1.0" +edition = "2024" + +[lib] +name = "configs_service" +path = "src/lib.rs" + +[dependencies] +common = { path = "../common" } +axum = "0.8" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tracing = "0.1" +tracing-subscriber = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "json", "macros", "migrate"] } +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +rand = "0.10" +tonic = "0.14" +anyhow = "1" +dotenvy = "0.15" + +[dev-dependencies] +axum-test = "21" +``` + +- [ ] **Step 2: Migration `0001_init.sql`** + +```sql +CREATE EXTENSION IF NOT EXISTS pgcrypto; +REVOKE ALL ON SCHEMA public FROM PUBLIC; + +-- account_id has no FK: accounts live in accounts_db (separate database). +CREATE TABLE config_slots ( + account_id UUID NOT NULL, + slot_index SMALLINT NOT NULL CHECK (slot_index BETWEEN 1 AND 4), + name TEXT NOT NULL, + data JSONB NOT NULL, + share_code TEXT NOT NULL UNIQUE, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (account_id, slot_index) +); + +CREATE TABLE showcase_listings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL, + slot_index SMALLINT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + copies_count INTEGER NOT NULL DEFAULT 0, + -- set by admin moderation (Подсистема 3); public queries always filter it + hidden BOOLEAN NOT NULL DEFAULT false, + published_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (account_id, slot_index), + FOREIGN KEY (account_id, slot_index) + REFERENCES config_slots (account_id, slot_index) ON DELETE CASCADE +); + +CREATE INDEX idx_showcase_new ON showcase_listings (published_at DESC) WHERE NOT hidden; +CREATE INDEX idx_showcase_popular ON showcase_listings (copies_count DESC, published_at DESC) WHERE NOT hidden; +``` + +- [ ] **Step 3: `config.rs` (tests first: valid passes, short internal key rejected)** + +```rust +use anyhow::{Context, Result}; + +#[derive(Clone)] +pub struct Config { + pub database_url: String, + pub port: u16, + pub internal_key: String, + pub accounts_grpc_url: String, +} + +impl Config { + pub fn from_env() -> Result { + Ok(Config { + database_url: std::env::var("CONFIGS_DATABASE_URL").context("CONFIGS_DATABASE_URL not set")?, + port: std::env::var("CONFIGS_PORT").unwrap_or_else(|_| "8082".into()).parse().context("CONFIGS_PORT")?, + internal_key: std::env::var("INTERNAL_KEY").context("INTERNAL_KEY not set")?, + accounts_grpc_url: std::env::var("ACCOUNTS_GRPC_URL").context("ACCOUNTS_GRPC_URL not set")?, + }) + } + + pub fn validate(&self) -> Result<()> { + if self.internal_key.len() < 32 { + anyhow::bail!("INTERNAL_KEY must be at least 32 bytes"); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample(key: &str) -> Config { + Config { database_url: String::new(), port: 0, internal_key: key.into(), accounts_grpc_url: String::new() } + } + + #[test] + fn strong_key_passes() { + assert!(sample(&"k".repeat(32)).validate().is_ok()); + } + + #[test] + fn short_key_is_rejected() { + assert!(sample("short").validate().is_err()); + } +} +``` + +- [ ] **Step 4: `error.rs`** + +```rust +use axum::{Json, http::StatusCode, response::{IntoResponse, Response}}; +use serde_json::json; + +#[derive(Debug)] +pub enum AppError { + Validation(String), + NotFound(String), + Conflict(String), + Internal(anyhow::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::Validation(m) => (StatusCode::BAD_REQUEST, m), + AppError::NotFound(m) => (StatusCode::NOT_FOUND, m), + AppError::Conflict(m) => (StatusCode::CONFLICT, m), + AppError::Internal(err) => { + tracing::error!("internal error: {err:?}"); + (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()) + } + }; + (status, Json(json!({ "error": message }))).into_response() + } +} + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + AppError::Internal(err.into()) + } +} +``` + +- [ ] **Step 5: `lib.rs`, `main.rs`** + +```rust +// lib.rs +pub mod config; +pub mod error; + +use axum::{Router, routing::get}; +use common::internal::{InternalKey, require_internal_key}; +use config::Config; + +pub fn build_app(pool: sqlx::PgPool, cfg: &Config) -> Router { + let _ = pool; // used from Task 2 on + let api = Router::new() + .layer(axum::middleware::from_fn_with_state(InternalKey::new(cfg.internal_key.clone()), require_internal_key)); + Router::new().route("/health", get(|| async { "ok" })).merge(api) +} +``` +```rust +// main.rs +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt::init(); + let cfg = configs_service::config::Config::from_env()?; + cfg.validate()?; + let pool = sqlx::PgPool::connect(&cfg.database_url).await?; + sqlx::migrate!("./migrations").run(&pool).await?; + let app = configs_service::build_app(pool, &cfg); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; + tracing::info!("configs-service listening on {}", cfg.port); + axum::serve(listener, app).await?; + Ok(()) +} +``` +The `let _ = pool;` line disappears in Task 2 — it is not a placeholder, just keeps Task 1 warning-free. + +- [ ] **Step 6: Test helpers + smoke test** + +`tests/common/mod.rs`: +```rust +#![allow(dead_code)] + +use axum_test::TestServer; +use configs_service::config::Config; +use uuid::Uuid; + +pub use ::common::internal::ACCOUNT_ID_HEADER; + +pub const TEST_INTERNAL_KEY: &str = "internal-key-internal-key-internal!!"; + +pub async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("CONFIGS_DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/configs_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect to configs_db"); + sqlx::migrate!("./migrations").run(&pool).await.expect("run migrations"); + pool +} + +pub fn test_config() -> Config { + Config { + database_url: String::new(), + port: 0, + internal_key: TEST_INTERNAL_KEY.into(), + accounts_grpc_url: String::new(), + } +} + +pub fn test_server(app: axum::Router) -> TestServer { + let mut server = TestServer::new(app); + server.add_header(::common::internal::INTERNAL_KEY_HEADER, TEST_INTERNAL_KEY); + server +} + +/// Accounts live in another service; here an account is just a fresh UUID. +pub fn new_account() -> Uuid { + Uuid::new_v4() +} +``` +`tests/slots.rs`: +```rust +mod common; + +#[tokio::test] +async fn health_ok_and_api_requires_internal_key() { + let pool = common::test_pool().await; + let raw = axum_test::TestServer::new(configs_service::build_app(pool, &common::test_config())); + raw.get("/health").await.assert_status_ok(); +} +``` +Task 2 Step 1 adds `raw.get("/configs").await.assert_status(axum::http::StatusCode::FORBIDDEN);` to this test (there is no `/configs` route to guard until then). + +`.env.example`: +``` +# configs-service +CONFIGS_DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/configs_db +CONFIGS_PORT=8082 +``` + +- [ ] **Step 7: Run, commit** + +Run: `cd backend && CONFIGS_DATABASE_URL=... CARGO_BUILD_JOBS=4 cargo test -p configs-service` +```bash +git add -A backend/Cargo.toml backend/Cargo.lock backend/configs-service backend/.env.example +git commit -m "feat(configs): scaffold service with schema, config validation and health check" +``` + +--- + +### Task 2: Slots — list, get, save + +**Files:** +- Create: `src/slots/{mod,repo,handlers}.rs`, `src/sharing/{mod,codes}.rs` (codes only — handlers in Task 3) +- Modify: `src/lib.rs`, `tests/slots.rs` + +**Interfaces:** +- Produces (`sharing::codes`): `ALPHABET: &[u8]`, `CODE_LEN = 8`, `new_code() -> String`, `normalize(&str) -> String`. +- Produces (`slots::repo`): `SlotSummary { slot: i16, name: String, updated_at, share_code: String, published: bool }`, `Slot { slot, name, data: serde_json::Value, updated_at, share_code }`, `list(&PgPool, Uuid) -> Vec`, `get(&PgPool, Uuid, i16) -> Option`, `save(&PgPool, Uuid, i16, &str, &serde_json::Value) -> Slot` (upsert; share code created once, retried on the rare unique collision). +- Produces (`slots::handlers`): `validate_slot(i16) -> Result`, `validate_name(&str) -> Result`, `MAX_DATA_BYTES = 256 * 1024`; `GET /configs`, `GET /configs/{slot}`, `PUT /configs/{slot}` `{name, data}` → 200 `Slot`. +- `SlotsState { pool }` (reused by sharing/showcase via their own states). + +- [ ] **Step 1: Failing tests** + +`src/sharing/codes.rs` tests: +```rust +#[test] +fn codes_use_only_the_unambiguous_alphabet() { + for _ in 0..200 { + let c = new_code(); + assert_eq!(c.len(), CODE_LEN); + assert!(c.bytes().all(|b| ALPHABET.contains(&b)), "{c}"); + } +} + +#[test] +fn normalize_trims_and_uppercases() { + assert_eq!(normalize(" ab3k9mzq \n"), "AB3K9MZQ"); +} +``` +`tests/slots.rs` (also add the FORBIDDEN assertion to `health_ok_and_api_requires_internal_key`): +```rust +use axum::http::StatusCode; +use serde_json::json; + +async fn server() -> axum_test::TestServer { + common::test_server(configs_service::build_app(common::test_pool().await, &common::test_config())) +} + +#[tokio::test] +async fn save_then_get_then_list() { + let s = server().await; + let me = common::new_account(); + let data = json!({ "modules": { "Hud": { "enabled": true } } }); + + let saved = s.put("/configs/2").add_header(common::ACCOUNT_ID_HEADER, me.to_string()) + .json(&json!({ "name": " pvp ", "data": data })).await; + saved.assert_status_ok(); + let saved: serde_json::Value = saved.json(); + assert_eq!(saved["name"], "pvp"); + assert_eq!(saved["share_code"].as_str().unwrap().len(), 8); + + let got: serde_json::Value = s.get("/configs/2").add_header(common::ACCOUNT_ID_HEADER, me.to_string()).await.json(); + assert_eq!(got["data"], data); + + let list: Vec = s.get("/configs").add_header(common::ACCOUNT_ID_HEADER, me.to_string()).await.json(); + assert_eq!(list.len(), 1); + assert_eq!(list[0]["slot"], 2); + assert_eq!(list[0]["published"], false); + assert!(list[0].get("data").is_none(), "list must not ship full configs"); +} + +#[tokio::test] +async fn resave_keeps_share_code_and_overwrites_data() { + let s = server().await; + let me = common::new_account().to_string(); + let first: serde_json::Value = s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": "a", "data": 1 })).await.json(); + let second: serde_json::Value = s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": "b", "data": 2 })).await.json(); + assert_eq!(first["share_code"], second["share_code"]); + assert_eq!(second["data"], 2); +} + +#[tokio::test] +async fn validation_errors() { + let s = server().await; + let me = common::new_account().to_string(); + for slot in ["0", "5"] { + s.put(&format!("/configs/{slot}")).add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": "x", "data": {} })).await.assert_status(StatusCode::BAD_REQUEST); + } + s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": " ", "data": {} })).await.assert_status(StatusCode::BAD_REQUEST); + s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": "x".repeat(33), "data": {} })).await.assert_status(StatusCode::BAD_REQUEST); + let huge = "x".repeat(256 * 1024 + 1); + s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "name": "x", "data": huge })).await.assert_status(StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn slots_are_private_and_need_identity() { + let s = server().await; + let owner = common::new_account().to_string(); + s.put("/configs/3").add_header(common::ACCOUNT_ID_HEADER, &owner) + .json(&json!({ "name": "x", "data": {} })).await.assert_status_ok(); + s.get("/configs/3").add_header(common::ACCOUNT_ID_HEADER, common::new_account().to_string()) + .await.assert_status(StatusCode::NOT_FOUND); + s.get("/configs").await.assert_status_unauthorized(); +} +``` +Run → FAIL. + +- [ ] **Step 2: `sharing/codes.rs`** + +```rust +use rand::RngExt; + +/// No 0/O, 1/I/L — players type these by hand in chat. +pub const ALPHABET: &[u8] = b"23456789ABCDEFGHJKMNPQRSTUVWXYZ"; +pub const CODE_LEN: usize = 8; + +pub fn new_code() -> String { + let mut rng = rand::rng(); + (0..CODE_LEN).map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char).collect() +} + +pub fn normalize(code: &str) -> String { + code.trim().to_uppercase() +} +``` +`sharing/mod.rs`: `pub mod codes;` (Task 3 adds `pub mod handlers;`). + +- [ ] **Step 3: `slots/repo.rs`** + +```rust +use crate::sharing::codes::new_code; +use chrono::{DateTime, Utc}; +use serde::Serialize; +use sqlx::PgPool; +use uuid::Uuid; + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct SlotSummary { + pub slot: i16, + pub name: String, + pub updated_at: DateTime, + pub share_code: String, + pub published: bool, +} + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct Slot { + pub slot: i16, + pub name: String, + pub data: serde_json::Value, + pub updated_at: DateTime, + pub share_code: String, +} + +const SLOT_COLUMNS: &str = "slot_index AS slot, name, data, updated_at, share_code"; + +pub async fn list(pool: &PgPool, account_id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as( + "SELECT s.slot_index AS slot, s.name, s.updated_at, s.share_code, (l.id IS NOT NULL) AS published + FROM config_slots s + LEFT JOIN showcase_listings l ON l.account_id = s.account_id AND l.slot_index = s.slot_index + WHERE s.account_id = $1 ORDER BY s.slot_index", + ) + .bind(account_id) + .fetch_all(pool) + .await +} + +pub async fn get(pool: &PgPool, account_id: Uuid, slot: i16) -> Result, sqlx::Error> { + sqlx::query_as(&format!("SELECT {SLOT_COLUMNS} FROM config_slots WHERE account_id = $1 AND slot_index = $2")) + .bind(account_id) + .bind(slot) + .fetch_optional(pool) + .await +} + +fn is_share_code_collision(err: &sqlx::Error) -> bool { + matches!(err, sqlx::Error::Database(db) if db.constraint() == Some("config_slots_share_code_key")) +} + +pub async fn save( + pool: &PgPool, + account_id: Uuid, + slot: i16, + name: &str, + data: &serde_json::Value, +) -> Result { + // share_code is only used on INSERT; an update keeps the existing one. + // 31^8 codes make collisions vanishingly rare, but never fatal. + let mut attempts = 0; + loop { + let result = sqlx::query_as(&format!( + "INSERT INTO config_slots (account_id, slot_index, name, data, share_code) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (account_id, slot_index) + DO UPDATE SET name = EXCLUDED.name, data = EXCLUDED.data, updated_at = now() + RETURNING {SLOT_COLUMNS}" + )) + .bind(account_id) + .bind(slot) + .bind(name) + .bind(data) + .bind(new_code()) + .fetch_one(pool) + .await; + match result { + Err(err) if is_share_code_collision(&err) && attempts < 3 => attempts += 1, + other => return other, + } + } +} +``` +Verify the constraint name after migrating: `psql ... -c '\d config_slots'` → `config_slots_share_code_key` (Postgres default `__key`). + +- [ ] **Step 4: `slots/handlers.rs`** + +```rust +use super::repo::{self, Slot, SlotSummary}; +use crate::error::AppError; +use axum::{Json, extract::{Path, State}}; +use common::internal::GatewayIdentity; +use serde::Deserialize; + +pub const MAX_DATA_BYTES: usize = 256 * 1024; +const MAX_NAME_CHARS: usize = 32; + +#[derive(Clone)] +pub struct SlotsState { + pub pool: sqlx::PgPool, +} + +pub fn validate_slot(slot: i16) -> Result { + if (1..=4).contains(&slot) { Ok(slot) } else { Err(AppError::Validation("slot must be 1..4".into())) } +} + +pub fn validate_name(name: &str) -> Result { + let name = name.trim(); + let len = name.chars().count(); + if len == 0 || len > MAX_NAME_CHARS { + return Err(AppError::Validation(format!("name must be 1..{MAX_NAME_CHARS} characters"))); + } + Ok(name.to_owned()) +} + +#[derive(Deserialize)] +pub struct SaveRequest { + pub name: String, + pub data: serde_json::Value, +} + +pub async fn list(State(state): State, id: GatewayIdentity) -> Result>, AppError> { + Ok(Json(repo::list(&state.pool, id.account_id).await?)) +} + +pub async fn get( + State(state): State, + id: GatewayIdentity, + Path(slot): Path, +) -> Result, AppError> { + let slot = validate_slot(slot)?; + repo::get(&state.pool, id.account_id, slot) + .await? + .map(Json) + .ok_or_else(|| AppError::NotFound("slot is empty".into())) +} + +pub async fn save( + State(state): State, + id: GatewayIdentity, + Path(slot): Path, + Json(req): Json, +) -> Result, AppError> { + let slot = validate_slot(slot)?; + let name = validate_name(&req.name)?; + let size = serde_json::to_vec(&req.data).map_err(|e| AppError::Internal(e.into()))?.len(); + if size > MAX_DATA_BYTES { + return Err(AppError::Validation(format!("config data must be at most {MAX_DATA_BYTES} bytes"))); + } + Ok(Json(repo::save(&state.pool, id.account_id, slot, &name, &req.data).await?)) +} +``` +A non-numeric `{slot}` is rejected by Axum's `Path` extractor with 400 already. + +- [ ] **Step 5: Routes (`lib.rs`)** + +```rust +pub mod sharing; +pub mod slots; +// ... +let slots_state = slots::handlers::SlotsState { pool: pool.clone() }; +let api = Router::new() + .route("/configs", get(slots::handlers::list)) + .route("/configs/{slot}", get(slots::handlers::get).put(slots::handlers::save)) + .with_state(slots_state) + .layer(axum::extract::DefaultBodyLimit::max(slots::handlers::MAX_DATA_BYTES + 16 * 1024)) + .layer(axum::middleware::from_fn_with_state(InternalKey::new(cfg.internal_key.clone()), require_internal_key)); +``` +Note: with the body limit at 272 KiB, the 256 KiB+1 test body still fits the HTTP limit and is rejected by the explicit size check (400), not 413. + +- [ ] **Step 6: Tests pass; commit** + +```bash +git add -A backend/configs-service +git commit -m "feat(configs): four config slots per account with list, get and save" +``` + +--- + +### Task 3: Share codes — regenerate + load by code + +**Files:** +- Create: `src/sharing/handlers.rs`, `tests/sharing.rs` +- Modify: `src/sharing/mod.rs`, `src/slots/repo.rs` (+ `by_share_code`, `regenerate_code`), `src/lib.rs` + +**Interfaces:** +- Produces: `repo::by_share_code(&PgPool, &str) -> Option`, `repo::regenerate_code(&PgPool, Uuid, i16) -> Option`. +- HTTP: `GET /configs/shared/{code}` (no identity needed) → `{name, data, updated_at}` — never the owner id; `POST /configs/{slot}/regenerate-code` → `{share_code}`; 404 for empty slot / unknown code. + +- [ ] **Step 1: Failing tests (`tests/sharing.rs`)** + +```rust +mod common; + +use axum::http::StatusCode; +use serde_json::json; + +async fn server() -> axum_test::TestServer { + common::test_server(configs_service::build_app(common::test_pool().await, &common::test_config())) +} + +async fn save(s: &axum_test::TestServer, owner: &str) -> String { + let r: serde_json::Value = s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, owner) + .json(&json!({ "name": "legit", "data": { "k": 1 } })).await.json(); + r["share_code"].as_str().unwrap().to_owned() +} + +#[tokio::test] +async fn anyone_can_load_by_code_case_insensitively_without_owner_leak() { + let s = server().await; + let code = save(&s, &common::new_account().to_string()).await; + let res = s.get(&format!("/configs/shared/{}", code.to_lowercase())).await; + res.assert_status_ok(); + let body: serde_json::Value = res.json(); + assert_eq!(body["name"], "legit"); + assert_eq!(body["data"], json!({ "k": 1 })); + assert!(body.get("account_id").is_none()); +} + +#[tokio::test] +async fn regenerate_invalidates_the_old_code() { + let s = server().await; + let owner = common::new_account().to_string(); + let old = save(&s, &owner).await; + let res = s.post("/configs/1/regenerate-code").add_header(common::ACCOUNT_ID_HEADER, &owner).await; + res.assert_status_ok(); + let new = res.json::()["share_code"].as_str().unwrap().to_owned(); + assert_ne!(old, new); + s.get(&format!("/configs/shared/{old}")).await.assert_status(StatusCode::NOT_FOUND); + s.get(&format!("/configs/shared/{new}")).await.assert_status_ok(); +} + +#[tokio::test] +async fn regenerate_on_empty_slot_is_404_and_needs_identity() { + let s = server().await; + s.post("/configs/4/regenerate-code").add_header(common::ACCOUNT_ID_HEADER, common::new_account().to_string()) + .await.assert_status(StatusCode::NOT_FOUND); + s.post("/configs/4/regenerate-code").await.assert_status_unauthorized(); +} + +#[tokio::test] +async fn unknown_code_is_404() { + server().await.get("/configs/shared/ZZZZZZZZ").await.assert_status(StatusCode::NOT_FOUND); +} +``` + +- [ ] **Step 2: Repo additions (`slots/repo.rs`)** + +```rust +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct SharedConfig { + pub name: String, + pub data: serde_json::Value, + pub updated_at: DateTime, +} + +pub async fn by_share_code(pool: &PgPool, code: &str) -> Result, sqlx::Error> { + sqlx::query_as("SELECT name, data, updated_at FROM config_slots WHERE share_code = $1") + .bind(code) + .fetch_optional(pool) + .await +} + +pub async fn regenerate_code(pool: &PgPool, account_id: Uuid, slot: i16) -> Result, sqlx::Error> { + let mut attempts = 0; + loop { + let result = sqlx::query_scalar( + "UPDATE config_slots SET share_code = $3 WHERE account_id = $1 AND slot_index = $2 RETURNING share_code", + ) + .bind(account_id) + .bind(slot) + .bind(new_code()) + .fetch_optional(pool) + .await; + match result { + Err(err) if is_share_code_collision(&err) && attempts < 3 => attempts += 1, + other => return other, + } + } +} +``` +If `repo.rs` passes 250 lines, move the two sharing queries into `sharing/repo.rs`... that would make `sharing/` 4 files (`mod, codes, handlers, repo`) — allowed. + +- [ ] **Step 3: `sharing/handlers.rs`** + +```rust +use super::codes::normalize; +use crate::error::AppError; +use crate::slots::handlers::{SlotsState, validate_slot}; +use crate::slots::repo::{self, SharedConfig}; +use axum::{Json, extract::{Path, State}}; +use common::internal::GatewayIdentity; +use serde::Serialize; + +#[derive(Serialize)] +pub struct ShareCodeResponse { + pub share_code: String, +} + +pub async fn load_shared(State(state): State, Path(code): Path) -> Result, AppError> { + repo::by_share_code(&state.pool, &normalize(&code)) + .await? + .map(Json) + .ok_or_else(|| AppError::NotFound("unknown share code".into())) +} + +pub async fn regenerate( + State(state): State, + id: GatewayIdentity, + Path(slot): Path, +) -> Result, AppError> { + let slot = validate_slot(slot)?; + repo::regenerate_code(&state.pool, id.account_id, slot) + .await? + .map(|share_code| Json(ShareCodeResponse { share_code })) + .ok_or_else(|| AppError::NotFound("slot is empty".into())) +} +``` +Routes: `.route("/configs/shared/{code}", get(sharing::handlers::load_shared))` and `.route("/configs/{slot}/regenerate-code", post(sharing::handlers::regenerate))`. Axum 0.8 prefers the static segment `shared` over `{slot}`, and `/configs/{slot}` has no second segment, so no conflict. + +- [ ] **Step 4: Tests pass; commit** + +```bash +git add -A backend/configs-service +git commit -m "feat(configs): permanent share codes with regenerate and public load-by-code" +``` + +--- + +### Task 4: Author profiles over gRPC (accounts-service side) + +**Files:** +- Modify: `backend/common/proto/accounts.proto`, `backend/accounts-service/src/grpc/mod.rs`, `backend/accounts-service/src/accounts/repo.rs`, `backend/accounts-service/src/main.rs` + +**Interfaces:** +- Produces (proto): `rpc GetPublicProfiles(GetPublicProfilesRequest) returns (GetPublicProfilesReply)`; `GetPublicProfilesRequest { repeated string account_ids = 1; }`; `PublicProfile { string account_id = 1; string display_nick = 2; string avatar_url = 3; /* "" = none */ }`; `GetPublicProfilesReply { repeated PublicProfile profiles = 1; }`. Max 100 ids per call (`INVALID_ARGUMENT` above that). Unknown/invalid ids are skipped silently. +- Produces: `accounts::repo::public_profiles(&PgPool, &[Uuid]) -> Result)>, sqlx::Error>`; `grpc::AccountsGrpc::new(pool, avatar_base_url: String)`; `grpc::server(pool, avatar_base_url, internal_key)`. + +- [ ] **Step 1: Proto** + +Append to the service and messages: +```proto + // Nick + avatar for showcase authors etc. Never returns email or role. + rpc GetPublicProfiles(GetPublicProfilesRequest) returns (GetPublicProfilesReply); +``` +```proto +message GetPublicProfilesRequest { repeated string account_ids = 1; } +message PublicProfile { + string account_id = 1; + string display_nick = 2; + string avatar_url = 3; // empty string when the account has no avatar +} +message GetPublicProfilesReply { repeated PublicProfile profiles = 1; } +``` + +- [ ] **Step 2: Failing test — extend `authenticate_device_over_grpc`'s module with** + +```rust +#[tokio::test] +async fn public_profiles_over_grpc() { + let pool = pool().await; + let a = crate::accounts::repo::create(&pool, &format!("p-{}@example.com", Uuid::new_v4()), "x", "Alice").await.unwrap(); + let svc = AccountsGrpc::new(pool.clone(), "http://cdn/avatars".into()); + + let reply = svc.get_public_profiles(tonic::Request::new(GetPublicProfilesRequest { + account_ids: vec![a.id.to_string(), Uuid::new_v4().to_string(), "garbage".into()], + })).await.unwrap().into_inner(); + assert_eq!(reply.profiles.len(), 1); + assert_eq!(reply.profiles[0].display_nick, "Alice"); + assert_eq!(reply.profiles[0].avatar_url, ""); + + let too_many = (0..101).map(|_| Uuid::new_v4().to_string()).collect(); + let err = svc.get_public_profiles(tonic::Request::new(GetPublicProfilesRequest { account_ids: too_many })) + .await.unwrap_err(); + assert_eq!(err.code(), tonic::Code::InvalidArgument); + + sqlx::query("DELETE FROM accounts WHERE id = $1").bind(a.id).execute(&pool).await.unwrap(); +} +``` +(Calling the trait method directly — no network needed for this one.) Update the existing test's `server(pool.clone(), KEY)` call to `server(pool.clone(), "http://cdn/avatars".into(), KEY)`. + +- [ ] **Step 3: Repo + service** + +`accounts/repo.rs`: +```rust +pub async fn public_profiles(pool: &PgPool, ids: &[Uuid]) -> Result)>, sqlx::Error> { + sqlx::query_as( + "SELECT a.id, a.display_nick, av.s3_key FROM accounts a + LEFT JOIN avatars av ON av.account_id = a.id + WHERE a.id = ANY($1)", + ) + .bind(ids) + .fetch_all(pool) + .await +} +``` +`grpc/mod.rs`: `AccountsGrpc { pool, avatar_base_url: String }`, `server(pool, avatar_base_url: String, internal_key: &str)`, and: +```rust +const MAX_PROFILE_IDS: usize = 100; + +async fn get_public_profiles( + &self, + request: Request, +) -> Result, Status> { + let raw = request.into_inner().account_ids; + if raw.len() > MAX_PROFILE_IDS { + return Err(Status::invalid_argument("at most 100 account ids per call")); + } + let ids: Vec = raw.iter().filter_map(|s| Uuid::parse_str(s).ok()).collect(); + let rows = crate::accounts::repo::public_profiles(&self.pool, &ids).await.map_err(|err| { + tracing::error!("get_public_profiles: {err:?}"); + Status::internal("internal error") + })?; + let profiles = rows + .into_iter() + .map(|(id, nick, key)| PublicProfile { + account_id: id.to_string(), + display_nick: nick, + avatar_url: key.map(|k| format!("{}/{k}", self.avatar_base_url)).unwrap_or_default(), + }) + .collect(); + Ok(Response::new(GetPublicProfilesReply { profiles })) +} +``` +`main.rs`: `accounts_service::grpc::server(pool.clone(), cfg.avatar_base_url(), &cfg.internal_key)`. + +- [ ] **Step 4: `cargo test -p common -p accounts-service` passes; commit** + +```bash +git add -A backend/common backend/accounts-service +git commit -m "feat(accounts): GetPublicProfiles gRPC for showcase author info" +``` + +--- + +### Task 5: Showcase — publish, browse, copy + +**Files:** +- Create: `src/showcase/{mod,repo,handlers,profiles}.rs`, `tests/showcase.rs` +- Modify: `src/lib.rs` (`build_app(pool, &cfg, profiles: Arc)`), `src/main.rs`, `tests/slots.rs` + `tests/sharing.rs` (pass `common::no_profiles()`), `tests/common/mod.rs` + +**Interfaces:** +- Produces (`showcase::profiles`): `Author { nick: String, avatar_url: Option }` (Serialize), `trait ProfileSource { fn profiles<'a>(&'a self, ids: &'a [Uuid]) -> ProfilesFuture<'a>; }` with `ProfilesFuture<'a> = Pin> + Send + 'a>>`, `GrpcProfiles::connect_lazy(url, internal_key) -> anyhow::Result` (failures → empty map, logged). +- Produces (`showcase::repo`): `ListingRow { id, account_id, slot, title, description, copies_count, published_at, name }`, `publish(&PgPool, Uuid, i16, &str, &str) -> Option` (None = slot empty), `unpublish(&PgPool, Uuid, i16) -> bool`, `page(&PgPool, Sort, page: i64) -> Vec` (fetches 21 to compute `has_more`), `detail(&PgPool, Uuid) -> Option<(ListingRow, serde_json::Value)>`, `copy_into(&PgPool, listing: Uuid, target_account: Uuid, target_slot: i16) -> CopyOutcome { Copied, ListingMissing, SlotOccupied }`. +- HTTP: + - `POST /configs/{slot}/publish` `{title, description?}` → 200 `{listing_id}` (re-publishing updates title/description); 404 empty slot. + - `DELETE /configs/{slot}/publish` → 204 / 404. + - `GET /showcase?sort=new|popular&page=0` → `{items: [Listing], page, has_more}`, `Listing = {id, title, description, config_name, copies_count, published_at, author: Author|null}`. Public. + - `GET /showcase/{id}` → `Listing` + `data`. Public. Hidden listings are 404. + - `POST /showcase/{id}/copy` `{slot}` (identity) → 201 `Slot`; 409 if the target slot is occupied (spec: "копия в свободный слот"); 404 unknown listing. + +- [ ] **Step 1: `profiles.rs`** + +```rust +use common::internal::GrpcKeyAttach; +use common::pb::accounts::{GetPublicProfilesRequest, accounts_internal_client::AccountsInternalClient}; +use serde::Serialize; +use std::{collections::HashMap, future::Future, pin::Pin}; +use tonic::{service::interceptor::InterceptedService, transport::{Channel, Endpoint}}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize)] +pub struct Author { + pub nick: String, + pub avatar_url: Option, +} + +pub type ProfilesFuture<'a> = Pin> + Send + 'a>>; + +pub trait ProfileSource: Send + Sync + 'static { + fn profiles<'a>(&'a self, ids: &'a [Uuid]) -> ProfilesFuture<'a>; +} + +pub struct GrpcProfiles { + client: AccountsInternalClient>, +} + +impl GrpcProfiles { + pub fn connect_lazy(url: &str, internal_key: &str) -> anyhow::Result { + let channel = Endpoint::from_shared(url.to_owned())? + .timeout(std::time::Duration::from_secs(3)) + .connect_lazy(); + Ok(GrpcProfiles { client: AccountsInternalClient::with_interceptor(channel, GrpcKeyAttach::new(internal_key)?) }) + } +} + +impl ProfileSource for GrpcProfiles { + fn profiles<'a>(&'a self, ids: &'a [Uuid]) -> ProfilesFuture<'a> { + Box::pin(async move { + let request = GetPublicProfilesRequest { account_ids: ids.iter().map(Uuid::to_string).collect() }; + match self.client.clone().get_public_profiles(request).await { + Ok(reply) => reply + .into_inner() + .profiles + .into_iter() + .filter_map(|p| { + let id = Uuid::parse_str(&p.account_id).ok()?; + let avatar_url = (!p.avatar_url.is_empty()).then_some(p.avatar_url); + Some((id, Author { nick: p.display_nick, avatar_url })) + }) + .collect(), + Err(status) => { + // Showcase must stay browsable when accounts-service is down. + tracing::warn!("GetPublicProfiles failed: {status}"); + HashMap::new() + } + } + }) + } +} +``` +Test helpers (`tests/common/mod.rs`): +```rust +use configs_service::showcase::profiles::{Author, ProfileSource, ProfilesFuture}; +use std::{collections::HashMap, sync::Arc}; + +/// Every account is "Author-". +pub struct FakeProfiles; + +impl ProfileSource for FakeProfiles { + fn profiles<'a>(&'a self, ids: &'a [Uuid]) -> ProfilesFuture<'a> { + Box::pin(async move { + ids.iter() + .map(|id| (*id, Author { nick: format!("Author-{}", &id.to_string()[..4]), avatar_url: None })) + .collect::>() + }) + } +} + +pub fn no_profiles() -> Arc { + Arc::new(FakeProfiles) +} +``` + +- [ ] **Step 2: Failing tests (`tests/showcase.rs`)** + +```rust +mod common; + +use axum::http::StatusCode; +use serde_json::json; + +async fn server() -> axum_test::TestServer { + common::test_server(configs_service::build_app(common::test_pool().await, &common::test_config(), common::no_profiles())) +} + +async fn publish(s: &axum_test::TestServer, owner: &str, title: &str) -> String { + s.put("/configs/1").add_header(common::ACCOUNT_ID_HEADER, owner) + .json(&json!({ "name": "cfg", "data": { "t": title } })).await.assert_status_ok(); + let r = s.post("/configs/1/publish").add_header(common::ACCOUNT_ID_HEADER, owner) + .json(&json!({ "title": title, "description": "desc" })).await; + r.assert_status_ok(); + r.json::()["listing_id"].as_str().unwrap().to_owned() +} + +#[tokio::test] +async fn published_listing_shows_up_publicly_with_author() { + let s = server().await; + let owner = common::new_account(); + let id = publish(&s, &owner.to_string(), "Best PvP").await; + + let page: serde_json::Value = s.get("/showcase?sort=new&page=0").await.json(); + let item = page["items"].as_array().unwrap().iter().find(|i| i["id"] == id).expect("listed"); + assert_eq!(item["title"], "Best PvP"); + assert_eq!(item["config_name"], "cfg"); + assert_eq!(item["author"]["nick"], format!("Author-{}", &owner.to_string()[..4])); + assert!(item.get("account_id").is_none()); + + let detail: serde_json::Value = s.get(&format!("/showcase/{id}")).await.json(); + assert_eq!(detail["data"], json!({ "t": "Best PvP" })); +} + +#[tokio::test] +async fn unpublish_removes_listing() { + let s = server().await; + let owner = common::new_account().to_string(); + let id = publish(&s, &owner, "gone").await; + s.delete("/configs/1/publish").add_header(common::ACCOUNT_ID_HEADER, &owner) + .await.assert_status(StatusCode::NO_CONTENT); + s.get(&format!("/showcase/{id}")).await.assert_status(StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn copy_goes_into_a_free_slot_and_counts() { + let s = server().await; + let id = publish(&s, &common::new_account().to_string(), "copy me").await; + let me = common::new_account().to_string(); + + let r = s.post(&format!("/showcase/{id}/copy")).add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "slot": 3 })).await; + r.assert_status(StatusCode::CREATED); + assert_eq!(r.json::()["data"], json!({ "t": "copy me" })); + + s.post(&format!("/showcase/{id}/copy")).add_header(common::ACCOUNT_ID_HEADER, &me) + .json(&json!({ "slot": 3 })).await.assert_status(StatusCode::CONFLICT); + + let detail: serde_json::Value = s.get(&format!("/showcase/{id}")).await.json(); + assert_eq!(detail["copies_count"], 1); +} + +#[tokio::test] +async fn publish_validation_and_auth() { + let s = server().await; + let owner = common::new_account().to_string(); + s.post("/configs/2/publish").add_header(common::ACCOUNT_ID_HEADER, &owner) + .json(&json!({ "title": "x" })).await.assert_status(StatusCode::NOT_FOUND); + s.put("/configs/2").add_header(common::ACCOUNT_ID_HEADER, &owner) + .json(&json!({ "name": "n", "data": {} })).await; + s.post("/configs/2/publish").add_header(common::ACCOUNT_ID_HEADER, &owner) + .json(&json!({ "title": " " })).await.assert_status(StatusCode::BAD_REQUEST); + s.post("/configs/2/publish").json(&json!({ "title": "x" })).await.assert_status_unauthorized(); + s.get("/showcase?sort=bogus").await.assert_status(StatusCode::BAD_REQUEST); +} + +#[tokio::test] +async fn popular_sort_orders_by_copies() { + let s = server().await; + let low = publish(&s, &common::new_account().to_string(), "low").await; + let high = publish(&s, &common::new_account().to_string(), "high").await; + for _ in 0..2 { + s.post(&format!("/showcase/{high}/copy")) + .add_header(common::ACCOUNT_ID_HEADER, common::new_account().to_string()) + .json(&json!({ "slot": 1 })).await.assert_status(StatusCode::CREATED); + } + let page: serde_json::Value = s.get("/showcase?sort=popular").await.json(); + let ids: Vec<&str> = page["items"].as_array().unwrap().iter().map(|i| i["id"].as_str().unwrap()).collect(); + let (hi, lo) = (ids.iter().position(|i| *i == high), ids.iter().position(|i| *i == low)); + assert!(hi < lo || lo.is_none(), "high-copy listing must come first"); +} +``` +(The shared test DB accumulates listings across runs, so tests look up their own ids rather than assuming exact page contents.) + +- [ ] **Step 3: `showcase/repo.rs`** + +```rust +use chrono::{DateTime, Utc}; +use sqlx::PgPool; +use uuid::Uuid; + +pub const PAGE_SIZE: i64 = 20; + +#[derive(Debug, Clone, Copy)] +pub enum Sort { + New, + Popular, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct ListingRow { + pub id: Uuid, + pub account_id: Uuid, + pub title: String, + pub description: String, + pub copies_count: i32, + pub published_at: DateTime, + pub name: String, +} + +const LISTING_SELECT: &str = + "SELECT l.id, l.account_id, l.title, l.description, l.copies_count, l.published_at, s.name + FROM showcase_listings l + JOIN config_slots s ON s.account_id = l.account_id AND s.slot_index = l.slot_index"; + +pub async fn publish(pool: &PgPool, account_id: Uuid, slot: i16, title: &str, description: &str) -> Result, sqlx::Error> { + // The FK to config_slots makes this fail for an empty slot; check first for a clean 404. + let exists: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM config_slots WHERE account_id = $1 AND slot_index = $2)") + .bind(account_id).bind(slot).fetch_one(pool).await?; + if !exists { + return Ok(None); + } + sqlx::query_scalar( + "INSERT INTO showcase_listings (account_id, slot_index, title, description) VALUES ($1, $2, $3, $4) + ON CONFLICT (account_id, slot_index) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description + RETURNING id", + ) + .bind(account_id).bind(slot).bind(title).bind(description) + .fetch_one(pool).await.map(Some) +} + +pub async fn unpublish(pool: &PgPool, account_id: Uuid, slot: i16) -> Result { + let r = sqlx::query("DELETE FROM showcase_listings WHERE account_id = $1 AND slot_index = $2") + .bind(account_id).bind(slot).execute(pool).await?; + Ok(r.rows_affected() == 1) +} + +/// Returns up to PAGE_SIZE + 1 rows; the caller uses the extra one for `has_more`. +pub async fn page(pool: &PgPool, sort: Sort, page: i64) -> Result, sqlx::Error> { + let order = match sort { + Sort::New => "l.published_at DESC", + Sort::Popular => "l.copies_count DESC, l.published_at DESC", + }; + sqlx::query_as(&format!("{LISTING_SELECT} WHERE NOT l.hidden ORDER BY {order} LIMIT $1 OFFSET $2")) + .bind(PAGE_SIZE + 1) + .bind(page * PAGE_SIZE) + .fetch_all(pool) + .await +} + +pub async fn detail(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { + let Some(row) = sqlx::query_as::<_, ListingRow>(&format!("{LISTING_SELECT} WHERE l.id = $1 AND NOT l.hidden")) + .bind(id).fetch_optional(pool).await? + else { + return Ok(None); + }; + let data: serde_json::Value = sqlx::query_scalar( + "SELECT s.data FROM showcase_listings l JOIN config_slots s + ON s.account_id = l.account_id AND s.slot_index = l.slot_index WHERE l.id = $1", + ) + .bind(id).fetch_one(pool).await?; + Ok(Some((row, data))) +} + +pub enum CopyOutcome { + Copied, + ListingMissing, + SlotOccupied, +} + +pub async fn copy_into(pool: &PgPool, listing: Uuid, account_id: Uuid, slot: i16) -> Result { + let mut tx = pool.begin().await?; + let Some((name, data)): Option<(String, serde_json::Value)> = sqlx::query_as( + "SELECT s.name, s.data FROM showcase_listings l JOIN config_slots s + ON s.account_id = l.account_id AND s.slot_index = l.slot_index WHERE l.id = $1 AND NOT l.hidden", + ) + .bind(listing).fetch_optional(&mut *tx).await? + else { + return Ok(CopyOutcome::ListingMissing); + }; + let inserted = sqlx::query( + "INSERT INTO config_slots (account_id, slot_index, name, data, share_code) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (account_id, slot_index) DO NOTHING", + ) + .bind(account_id).bind(slot).bind(&name).bind(&data).bind(crate::sharing::codes::new_code()) + .execute(&mut *tx).await?; + if inserted.rows_affected() == 0 { + return Ok(CopyOutcome::SlotOccupied); + } + sqlx::query("UPDATE showcase_listings SET copies_count = copies_count + 1 WHERE id = $1") + .bind(listing).execute(&mut *tx).await?; + tx.commit().await?; + Ok(CopyOutcome::Copied) +} +``` +(~130 lines. `showcase/` is already at 4 files, so if it ever needs to grow, split a subfolder rather than adding a 5th file.) + +- [ ] **Step 4: `showcase/handlers.rs`** + +```rust +use super::profiles::{Author, ProfileSource}; +use super::repo::{self, CopyOutcome, ListingRow, PAGE_SIZE, Sort}; +use crate::error::AppError; +use crate::slots::handlers::validate_slot; +use axum::{Json, extract::{Path, Query, State}, http::StatusCode}; +use chrono::{DateTime, Utc}; +use common::internal::GatewayIdentity; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Clone)] +pub struct ShowcaseState { + pub pool: sqlx::PgPool, + pub profiles: Arc, +} + +#[derive(Serialize)] +pub struct Listing { + pub id: Uuid, + pub title: String, + pub description: String, + pub config_name: String, + pub copies_count: i32, + pub published_at: DateTime, + pub author: Option, +} + +fn to_listing(row: ListingRow, author: Option) -> Listing { + Listing { + id: row.id, + title: row.title, + description: row.description, + config_name: row.name, + copies_count: row.copies_count, + published_at: row.published_at, + author, + } +} + +#[derive(Deserialize)] +pub struct PublishRequest { + pub title: String, + #[serde(default)] + pub description: String, +} + +pub async fn publish( + State(state): State, + id: GatewayIdentity, + Path(slot): Path, + Json(req): Json, +) -> Result, AppError> { + let slot = validate_slot(slot)?; + let title = req.title.trim(); + if title.is_empty() || title.chars().count() > 64 { + return Err(AppError::Validation("title must be 1..64 characters".into())); + } + if req.description.chars().count() > 500 { + return Err(AppError::Validation("description must be at most 500 characters".into())); + } + let listing = repo::publish(&state.pool, id.account_id, slot, title, req.description.trim()) + .await? + .ok_or_else(|| AppError::NotFound("slot is empty".into()))?; + Ok(Json(serde_json::json!({ "listing_id": listing }))) +} + +pub async fn unpublish(State(state): State, id: GatewayIdentity, Path(slot): Path) -> Result { + let slot = validate_slot(slot)?; + if repo::unpublish(&state.pool, id.account_id, slot).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(AppError::NotFound("slot is not published".into())) + } +} + +#[derive(Deserialize)] +pub struct PageQuery { + pub sort: Option, + pub page: Option, +} + +#[derive(Serialize)] +pub struct PageResponse { + pub items: Vec, + pub page: i64, + pub has_more: bool, +} + +pub async fn browse(State(state): State, Query(q): Query) -> Result, AppError> { + let sort = match q.sort.as_deref() { + None | Some("new") => Sort::New, + Some("popular") => Sort::Popular, + Some(_) => return Err(AppError::Validation("sort must be new or popular".into())), + }; + let page = q.page.unwrap_or(0).clamp(0, 10_000); + let mut rows = repo::page(&state.pool, sort, page).await?; + let has_more = rows.len() as i64 > PAGE_SIZE; + rows.truncate(PAGE_SIZE as usize); + let ids: Vec = rows.iter().map(|r| r.account_id).collect(); + let authors = state.profiles.profiles(&ids).await; + let items = rows.into_iter().map(|r| { let a = authors.get(&r.account_id).cloned(); to_listing(r, a) }).collect(); + Ok(Json(PageResponse { items, page, has_more })) +} + +pub async fn detail(State(state): State, Path(id): Path) -> Result, AppError> { + let (row, data) = repo::detail(&state.pool, id).await?.ok_or_else(|| AppError::NotFound("no such listing".into()))?; + let author = state.profiles.profiles(&[row.account_id]).await.remove(&row.account_id); + let mut body = serde_json::to_value(to_listing(row, author)).map_err(|e| AppError::Internal(e.into()))?; + body["data"] = data; + Ok(Json(body)) +} + +#[derive(Deserialize)] +pub struct CopyRequest { + pub slot: i16, +} + +pub async fn copy( + State(state): State, + id: GatewayIdentity, + Path(listing): Path, + Json(req): Json, +) -> Result<(StatusCode, Json), AppError> { + let slot = validate_slot(req.slot)?; + match repo::copy_into(&state.pool, listing, id.account_id, slot).await? { + CopyOutcome::ListingMissing => Err(AppError::NotFound("no such listing".into())), + CopyOutcome::SlotOccupied => Err(AppError::Conflict("target slot is not empty".into())), + CopyOutcome::Copied => { + let saved = crate::slots::repo::get(&state.pool, id.account_id, slot) + .await? + .ok_or_else(|| AppError::Internal(anyhow::anyhow!("copied slot vanished")))?; + Ok((StatusCode::CREATED, Json(saved))) + } + } +} +``` + +- [ ] **Step 5: Wire routes and main** + +`lib.rs` — `build_app(pool, cfg, profiles: Arc)`: +```rust +let showcase_state = showcase::handlers::ShowcaseState { pool: pool.clone(), profiles }; +let showcase_routes = Router::new() + .route("/configs/{slot}/publish", post(showcase::handlers::publish).delete(showcase::handlers::unpublish)) + .route("/showcase", get(showcase::handlers::browse)) + .route("/showcase/{id}", get(showcase::handlers::detail)) + .route("/showcase/{id}/copy", post(showcase::handlers::copy)) + .with_state(showcase_state); +// merge showcase_routes into `api` before the DefaultBodyLimit / internal-key layers +``` +`main.rs`: `let profiles = Arc::new(configs_service::showcase::profiles::GrpcProfiles::connect_lazy(&cfg.accounts_grpc_url, &cfg.internal_key)?);` and pass it. Update `tests/slots.rs`/`tests/sharing.rs` `build_app` calls to pass `common::no_profiles()`. + +- [ ] **Step 6: Tests pass; commit** + +```bash +git add -A backend/configs-service +git commit -m "feat(configs): public showcase with publish, browse, detail and copy-to-slot" +``` + +--- + +### Task 6: End-to-end through the gateway + docs + +**Files:** +- Modify: `backend/STRUCTURE.md`, `TODO.md` (Фаза 10 status), `backend/.env.example` + +- [ ] **Step 1: Manual e2e (three processes, real DBs)** + +```bash +cd backend && set -a; . ./.env; set +a +CARGO_BUILD_JOBS=4 cargo build -p accounts-service -p configs-service -p gateway +./target/debug/accounts-service & ./target/debug/configs-service & ./target/debug/gateway & +sleep 3 +# register + login as in gateway PLAN Task 11, then: +curl -s -X PUT localhost:8080/configs/1 -H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \ + -d '{"name":"pvp","data":{"Hud":true}}' # -> slot with share_code +curl -s localhost:8080/configs/shared/ # -> {name,data} without auth +curl -s -X POST localhost:8080/configs/1/publish -H "authorization: Bearer $TOKEN" \ + -H 'content-type: application/json' -d '{"title":"My PvP"}' +curl -s 'localhost:8080/showcase?sort=new' # -> author.nick = your nick (via gRPC) +kill %1 %2 %3 +``` +Expected: each call succeeds and the showcase item carries the real nick. + +- [ ] **Step 2: Docs + commit** + +`STRUCTURE.md`: mark configs-service implemented. `TODO.md`: Подсистема 1 backend done; next = frontend (`frontend/PLAN.md`) and mod integration. +```bash +git add -A backend/STRUCTURE.md TODO.md backend/.env.example +git commit -m "docs(backend): configs-service implemented; Подсистема 1 backend complete" +``` diff --git a/backend/configs-service/README.md b/backend/configs-service/README.md deleted file mode 100644 index c4c3c03..0000000 --- a/backend/configs-service/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# configs-service (planned) - -4 cloud config slots per account, share codes, public showcase (Подсистема 1, -часть 2). See `TODO.md` (Фаза 10) and `backend/STRUCTURE.md`. No -implementation plan yet. diff --git a/backend/gateway/PLAN.md b/backend/gateway/PLAN.md new file mode 100644 index 0000000..d3747ff --- /dev/null +++ b/backend/gateway/PLAN.md @@ -0,0 +1,2345 @@ +# Backend: API Gateway + auth hardening (Подсистема 1, часть 2) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** One public entry point (`gateway`) that authenticates every request once (site JWT or mod device token), rate-limits it, and proxies it to internal services — plus the accounts-service changes that make that safe: refresh-token rotation with an httpOnly cookie, persistent revocable device links, a gRPC `AuthenticateDevice` call, and `GET /me` for the site. + +**Architecture:** The gateway is an Axum reverse proxy. External API = REST/JSON (site + mod). Internal: the gateway forwards REST to each service's HTTP port with two trusted headers (`x-lovisual-internal-key`, `x-lovisual-account-id`) and calls accounts-service over **gRPC (tonic)** to resolve device tokens. Services reject any request without the internal key, so they are useless if reached directly. Shared contract code (JWT, internal headers, protobuf) lives in a new `backend/common` crate — its second real consumer (the gateway) now exists, per `backend/STRUCTURE.md`. + +**Why REST forwarding + gRPC, not "everything over gRPC":** accounts-service (and configs-service, next plan) already speak REST with typed Axum handlers and tests. Re-declaring every endpoint twice (REST in gateway, gRPC in service) doubles the surface for zero gain. gRPC is used where there is a real service-to-service call with no public REST equivalent: device-token resolution now, friend/presence lookups in Подсистема 2 later. + +**Tech Stack:** Rust 1.97 edition 2024, Axum 0.8, tonic/tonic-prost/tonic-prost-build 0.14 + prost 0.14 (requires `protoc` on PATH — present, libprotoc 36.1), reqwest 0.13 (`default-features = false, features = ["stream"]`), governor 0.10 (keyed rate limiters), tower-http 0.7 (cors), axum-extra 0.12 (`cookie`), time 0.3, sha2 0.11 + hex 0.4, base64 0.22, rand 0.10, subtle 2. **All verified by compiling spikes on 2026-09-24** (tonic server+client with interceptors, governor keyed `check_key` + `wait_time_from`, cookie builder, sha2/hex, `rand::rng().fill`). + +## Global Constraints + +- Everything in `backend/PLAN.md`'s Global Constraints still applies (no unwrap on request data, argon2id, TDD, ≤250 lines/file, ≤4 files per folder — subfolders don't count, no God-structs, no blocking I/O in async). +- Always build with `CARGO_BUILD_JOBS=4` (the dev machine ran out of memory at higher parallelism). +- Integration tests: `DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db`, MinIO `http://localhost:9000` (both via SSH tunnels to the test VDS). +- Commits: English, conventional style, stage explicit paths only (`git add -A backend/...`), never anything under `mod/`. +- Header names are exact: `x-lovisual-internal-key`, `x-lovisual-account-id`. Device tokens start with `lvd_`, refresh tokens with `lvr_`. Refresh cookie: `lv_refresh`, `Path=/auth`, `HttpOnly`, `SameSite=Strict`, `Secure` unless `COOKIE_SECURE=false`. +- `INTERNAL_KEY` and `JWT_SECRET` are each ≥32 bytes; every binary refuses to start otherwise. + +--- + +## File Structure + +``` +backend/ + Cargo.toml # members += "common", "gateway" + .env.example # + INTERNAL_KEY, GRPC_PORT, COOKIE_SECURE, gateway vars + common/ + Cargo.toml + build.rs # compiles proto/accounts.proto + proto/accounts.proto # AccountsInternal.AuthenticateDevice + src/ + lib.rs # pub mod jwt, internal; pub mod pb + jwt.rs # (moved from accounts-service) Claims, issue/verify access token, bearer_token + internal.rs # header consts, require_internal_key middleware, GatewayIdentity, gRPC key interceptors + accounts-service/ + migrations/ + 0002_refresh_tokens.sql + 0003_device_links_token_unique.sql + src/ + lib.rs # build_app wraps routes in require_internal_key + main.rs # runs HTTP + gRPC servers + config.rs # + internal_key, grpc_port, cookie_secure + error.rs + auth/{mod,password,handlers,tokens}.rs # jwt.rs deleted; tokens.rs = opaque tokens + refresh storage + device/{mod,handlers,store,links}.rs # links.rs = device_links repo + accounts/{mod,model,repo,handlers}.rs # handlers.rs = GET /me + avatars/... # unchanged except GatewayIdentity + grpc/mod.rs # AccountsInternal server impl + tests/ + common/mod.rs # + test_server(), register_account() + auth_flow.rs (+ auth_flow/refresh.rs) + device_flow.rs (+ device_flow/links.rs) + avatar_upload.rs + smoke.rs + gateway/ + Cargo.toml + PLAN.md # this file + src/ + main.rs + lib.rs # build_app(&Config, Arc) -> Router + config.rs + proxy/{mod,routes,forward}.rs + identity/{mod,device}.rs + rate_limit/{mod,rules}.rs + tests/ + common/mod.rs # fake upstream echo server, test_config + proxy.rs + identity.rs + rate_limit.rs +``` + +--- + +### Task 1: `common` crate — shared JWT, internal contract, protobuf + +**Files:** +- Create: `backend/common/Cargo.toml`, `backend/common/build.rs`, `backend/common/proto/accounts.proto` +- Create: `backend/common/src/lib.rs`, `backend/common/src/jwt.rs`, `backend/common/src/internal.rs` +- Modify: `backend/Cargo.toml` (members), `backend/accounts-service/Cargo.toml` (dep `common = { path = "../common" }`) +- Modify: `backend/accounts-service/src/auth/jwt.rs` → keep only `bearer_account_id` + its tests, re-export the rest from `common::jwt` + +**Interfaces:** +- Produces (`common::jwt`): `enum TokenType { Access, Refresh }`, `struct Claims { sub, exp, token_type }`, `issue_access_token(Uuid, &str) -> String`, `issue_refresh_token(Uuid, &str) -> String` (temporary — deleted in Task 4), `verify_token(&str, &str, TokenType) -> Option`, `bearer_token(&HeaderMap) -> Option<&str>`. +- Produces (`common::internal`): `INTERNAL_KEY_HEADER = "x-lovisual-internal-key"`, `ACCOUNT_ID_HEADER = "x-lovisual-account-id"`, `DEVICE_TOKEN_PREFIX = "lvd_"`, `struct InternalKey` (`InternalKey::new(String)`), `async fn require_internal_key(State, Request, Next) -> Response`, `struct GatewayIdentity { pub account_id: Uuid }` (Axum extractor), `struct GrpcKeyCheck`, `struct GrpcKeyAttach` (tonic interceptors). +- Produces (`common::pb::accounts`): tonic-generated `accounts_internal_server::{AccountsInternal, AccountsInternalServer}`, `accounts_internal_client::AccountsInternalClient`, `AuthenticateDeviceRequest { device_token }`, `AuthenticateDeviceReply { account_id }`. + +- [ ] **Step 1: Create the crate files** + +`backend/common/Cargo.toml`: +```toml +[package] +name = "common" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = "0.8" +jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" +uuid = { version = "1", features = ["v4", "serde"] } +subtle = "2" +tonic = "0.14" +tonic-prost = "0.14" +prost = "0.14" + +[build-dependencies] +tonic-prost-build = "0.14" + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +axum-test = "21" +``` + +`backend/common/build.rs`: +```rust +fn main() -> Result<(), Box> { + tonic_prost_build::compile_protos("proto/accounts.proto")?; + Ok(()) +} +``` + +`backend/common/proto/accounts.proto`: +```proto +syntax = "proto3"; +package accounts.v1; + +// Internal-only API of accounts-service. Never exposed publicly; every call +// must carry the x-lovisual-internal-key metadata entry. +service AccountsInternal { + // Resolves a mod's long-lived device token to its account and bumps + // device_links.last_seen. UNAUTHENTICATED if the token is unknown/revoked. + rpc AuthenticateDevice(AuthenticateDeviceRequest) returns (AuthenticateDeviceReply); +} + +message AuthenticateDeviceRequest { string device_token = 1; } +message AuthenticateDeviceReply { string account_id = 1; } +``` + +`backend/common/src/lib.rs`: +```rust +pub mod internal; +pub mod jwt; + +pub mod pb { + pub mod accounts { + tonic::include_proto!("accounts.v1"); + } +} +``` + +In `backend/Cargo.toml` set `members = ["accounts-service", "common"]`. + +- [ ] **Step 2: Move the JWT core into `common/src/jwt.rs`** + +Move from `accounts-service/src/auth/jwt.rs` verbatim: `TokenType`, `Claims`, `issue`, `issue_access_token`, `issue_refresh_token`, `verify_token` and their tests (every test except the `bearer_*` ones). Add the header parser (it replaces the ad-hoc parsing inside `bearer_account_id`): + +```rust +use axum::http::{HeaderMap, header::AUTHORIZATION}; + +/// The raw token from `Authorization: Bearer `, if the header is +/// present, valid ASCII, uses the Bearer scheme and is non-empty. +pub fn bearer_token(headers: &HeaderMap) -> Option<&str> { + let value = headers.get(AUTHORIZATION)?.to_str().ok()?; + let token = value.strip_prefix("Bearer ")?.trim(); + (!token.is_empty()).then_some(token) +} +``` + +Tests to add in `common/src/jwt.rs`: +```rust +#[test] +fn bearer_token_extracts_the_token() { + let mut h = HeaderMap::new(); + h.insert(AUTHORIZATION, "Bearer abc.def".parse().unwrap()); + assert_eq!(bearer_token(&h), Some("abc.def")); +} + +#[test] +fn bearer_token_rejects_missing_wrong_scheme_and_empty() { + let mut h = HeaderMap::new(); + assert_eq!(bearer_token(&h), None); + h.insert(AUTHORIZATION, "Basic abc".parse().unwrap()); + assert_eq!(bearer_token(&h), None); + h.insert(AUTHORIZATION, "Bearer ".parse().unwrap()); + assert_eq!(bearer_token(&h), None); +} +``` + +- [ ] **Step 3: Shrink `accounts-service/src/auth/jwt.rs`** + +```rust +pub use common::jwt::*; + +use crate::error::AppError; +use axum::http::HeaderMap; +use uuid::Uuid; + +/// Account id from `Authorization: Bearer `. Every failure mode +/// is the same `AppError::Unauthorized`. (Deleted in Task 2 — the gateway +/// authenticates and services read `GatewayIdentity` instead.) +pub fn bearer_account_id(headers: &HeaderMap, secret: &str) -> Result { + let token = bearer_token(headers).ok_or(AppError::Unauthorized)?; + let claims = verify_token(token, secret, TokenType::Access).ok_or(AppError::Unauthorized)?; + Uuid::parse_str(&claims.sub).map_err(|_| AppError::Unauthorized) +} +``` +Keep the three existing `bearer_*` tests below it unchanged. Remove `jsonwebtoken` from `accounts-service/Cargo.toml` only if nothing else in the crate uses it (`grep -rn jsonwebtoken backend/accounts-service/src`). + +- [ ] **Step 4: Write `common/src/internal.rs` tests first** + +```rust +#[cfg(test)] +mod tests { + use super::*; + use axum::{Router, routing::get}; + use axum_test::TestServer; + + const KEY: &str = "internal-key-internal-key-internal!!"; + + fn app() -> Router { + Router::new() + .route("/whoami", get(|id: GatewayIdentity| async move { id.account_id.to_string() })) + .route("/open", get(|| async { "open" })) + .layer(axum::middleware::from_fn_with_state(InternalKey::new(KEY.into()), require_internal_key)) + } + + #[tokio::test] + async fn request_without_internal_key_is_forbidden() { + let server = TestServer::new(app()); + server.get("/open").await.assert_status(axum::http::StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn request_with_wrong_internal_key_is_forbidden() { + let server = TestServer::new(app()); + server.get("/open").add_header(INTERNAL_KEY_HEADER, "nope") + .await.assert_status(axum::http::StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn correct_key_passes_and_identity_is_read() { + let server = TestServer::new(app()); + let id = Uuid::new_v4(); + let res = server.get("/whoami") + .add_header(INTERNAL_KEY_HEADER, KEY) + .add_header(ACCOUNT_ID_HEADER, id.to_string()) + .await; + res.assert_status_ok(); + assert_eq!(res.text(), id.to_string()); + } + + #[tokio::test] + async fn missing_or_garbage_identity_is_401() { + let server = TestServer::new(app()); + server.get("/whoami").add_header(INTERNAL_KEY_HEADER, KEY) + .await.assert_status_unauthorized(); + server.get("/whoami").add_header(INTERNAL_KEY_HEADER, KEY) + .add_header(ACCOUNT_ID_HEADER, "not-a-uuid") + .await.assert_status_unauthorized(); + } + + #[test] + fn grpc_key_check_accepts_only_the_right_key() { + let mut check = GrpcKeyCheck::new(KEY); + let mut attach = GrpcKeyAttach::new(KEY).unwrap(); + let ok = attach.call(tonic::Request::new(())).unwrap(); + assert!(check.call(ok).is_ok()); + assert!(check.call(tonic::Request::new(())).is_err()); + } +} +``` + +Run: `cd backend && CARGO_BUILD_JOBS=4 cargo test -p common` → FAIL (items not defined). + +- [ ] **Step 5: Implement `common/src/internal.rs`** + +```rust +//! Contract between the gateway and internal services. Services trust the +//! identity header ONLY because `require_internal_key` guarantees the request +//! came through the gateway (which strips client-supplied copies of both). + +use axum::{ + Json, + extract::{FromRequestParts, Request, State}, + http::{StatusCode, request::Parts}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use serde_json::json; +use std::sync::Arc; +use subtle::ConstantTimeEq; +use tonic::{Status, metadata::{Ascii, MetadataValue}, service::Interceptor}; +use uuid::Uuid; + +pub const INTERNAL_KEY_HEADER: &str = "x-lovisual-internal-key"; +pub const ACCOUNT_ID_HEADER: &str = "x-lovisual-account-id"; +pub const DEVICE_TOKEN_PREFIX: &str = "lvd_"; + +fn keys_match(given: &[u8], expected: &[u8]) -> bool { + given.ct_eq(expected).into() +} + +#[derive(Clone)] +pub struct InternalKey(Arc); + +impl InternalKey { + pub fn new(key: String) -> Self { + InternalKey(key.into()) + } +} + +pub async fn require_internal_key(State(key): State, req: Request, next: Next) -> Response { + let ok = req + .headers() + .get(INTERNAL_KEY_HEADER) + .is_some_and(|v| keys_match(v.as_bytes(), key.0.as_bytes())); + if !ok { + return (StatusCode::FORBIDDEN, Json(json!({ "error": "forbidden" }))).into_response(); + } + next.run(req).await +} + +/// The authenticated account, as resolved by the gateway. Rejects with 401 +/// when the gateway forwarded the request anonymously. +pub struct GatewayIdentity { + pub account_id: Uuid, +} + +impl FromRequestParts for GatewayIdentity { + type Rejection = Response; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + parts + .headers + .get(ACCOUNT_ID_HEADER) + .and_then(|v| v.to_str().ok()) + .and_then(|s| Uuid::parse_str(s).ok()) + .map(|account_id| GatewayIdentity { account_id }) + .ok_or_else(|| { + (StatusCode::UNAUTHORIZED, Json(json!({ "error": "unauthorized" }))).into_response() + }) + } +} + +/// Server-side tonic interceptor: rejects calls without the internal key. +#[derive(Clone)] +pub struct GrpcKeyCheck(Arc); + +impl GrpcKeyCheck { + pub fn new(key: &str) -> Self { + GrpcKeyCheck(key.into()) + } +} + +impl Interceptor for GrpcKeyCheck { + fn call(&mut self, req: tonic::Request<()>) -> Result, Status> { + match req.metadata().get(INTERNAL_KEY_HEADER) { + Some(v) if keys_match(v.as_bytes(), self.0.as_bytes()) => Ok(req), + _ => Err(Status::permission_denied("missing or invalid internal key")), + } + } +} + +/// Client-side tonic interceptor: attaches the internal key to every call. +#[derive(Clone)] +pub struct GrpcKeyAttach(MetadataValue); + +impl GrpcKeyAttach { + pub fn new(key: &str) -> Result { + Ok(GrpcKeyAttach(MetadataValue::try_from(key)?)) + } +} + +impl Interceptor for GrpcKeyAttach { + fn call(&mut self, mut req: tonic::Request<()>) -> Result, Status> { + req.metadata_mut().insert(INTERNAL_KEY_HEADER, self.0.clone()); + Ok(req) + } +} +``` + +- [ ] **Step 6: Run everything** + +Run: `cd backend && CARGO_BUILD_JOBS=4 cargo test -p common && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db CARGO_BUILD_JOBS=4 cargo test -p accounts-service` +Expected: all pass (accounts-service suite unchanged in behavior). + +- [ ] **Step 7: Commit** + +```bash +git add -A backend/Cargo.toml backend/Cargo.lock backend/common backend/accounts-service +git commit -m "feat(backend): add common crate with shared JWT, internal gateway contract and accounts proto" +``` + +--- + +### Task 2: accounts-service only accepts gateway traffic + +**Files:** +- Modify: `backend/accounts-service/src/config.rs` (+ `internal_key`, validate ≥32 bytes) +- Modify: `backend/accounts-service/src/lib.rs` (wrap all routes except `/health` in `require_internal_key`) +- Modify: `backend/accounts-service/src/device/handlers.rs`, `src/avatars/handlers.rs` (use `GatewayIdentity`) +- Delete: `backend/accounts-service/src/auth/jwt.rs` (`auth/mod.rs` drops `pub mod jwt;`; callers import `common::jwt` directly) +- Modify: `backend/.env.example`, `tests/common/mod.rs`, all integration tests + +**Interfaces:** +- Consumes: `common::internal::{InternalKey, require_internal_key, GatewayIdentity, INTERNAL_KEY_HEADER, ACCOUNT_ID_HEADER}`. +- Produces: `Config.internal_key: String` (env `INTERNAL_KEY`); tests/common `TEST_INTERNAL_KEY`, `test_server(Router) -> TestServer` (adds the key to every request), `register_account(&TestServer) -> (Uuid, String /*email*/)`. + +- [ ] **Step 1: Config — failing tests first** + +Add to `config.rs` tests (and `internal_key` to the `config_with_secret` helper, set to `"k".repeat(32)`): +```rust +#[test] +fn short_internal_key_is_rejected() { + let mut cfg = config_with_secret(&"x".repeat(32)); + cfg.internal_key = "short".into(); + assert!(cfg.validate().is_err()); +} +``` +Then add the field (`internal_key: std::env::var("INTERNAL_KEY").context("INTERNAL_KEY not set")?`) and in `validate`: +```rust +if self.internal_key.len() < MIN_JWT_SECRET_BYTES { + anyhow::bail!("INTERNAL_KEY must be at least {MIN_JWT_SECRET_BYTES} bytes"); +} +``` +`.env.example`: add +``` +# Shared secret between gateway and internal services (openssl rand -hex 32) +INTERNAL_KEY= +``` + +- [ ] **Step 2: Wire the middleware in `lib.rs`** + +```rust +let api = Router::new() + .merge(auth_routes) + .merge(device_routes) + .merge(avatar_routes) // whatever Task 9 of backend/PLAN.md named it + .layer(axum::middleware::from_fn_with_state( + common::internal::InternalKey::new(cfg.internal_key.clone()), + common::internal::require_internal_key, + )); + +Router::new() + .route("/health", get(|| async { "ok" })) + .merge(api) +``` +`/health` stays reachable without the key (used by orchestration probes; reveals nothing). + +- [ ] **Step 3: Replace bearer parsing with `GatewayIdentity`** + +`device/handlers.rs::confirm` becomes: +```rust +pub async fn confirm( + State(state): State, + identity: GatewayIdentity, + Json(req): Json, +) -> Result { + if state.store.confirm(&req.user_code, identity.account_id) { + Ok(StatusCode::OK) + } else { + Err(AppError::NotFound("unknown or expired user_code".into())) + } +} +``` +Do the same in the avatar upload handler: remove the `HeaderMap` param and the `bearer_account_id(...)` call, add `identity: GatewayIdentity` (must come before the `Multipart` extractor, which consumes the body), use `identity.account_id`. Drop `jwt_secret` from `AvatarState` if nothing else reads it. `DeviceState.jwt_secret` stays until Task 4 (still used by `token`). Delete `src/auth/jwt.rs` and switch remaining imports (`auth::handlers`, `device::handlers`) to `common::jwt::...`. + +- [ ] **Step 4: Test helpers** + +Append to `tests/common/mod.rs` (and set `internal_key: TEST_INTERNAL_KEY.into()` in `test_config()`): +```rust +use axum_test::TestServer; +use common::internal::INTERNAL_KEY_HEADER; +use uuid::Uuid; + +pub const TEST_INTERNAL_KEY: &str = "internal-key-internal-key-internal!!"; + +/// A server that behaves like it sits behind the gateway. +pub fn test_server(app: axum::Router) -> TestServer { + let mut server = TestServer::new(app); + server.add_header(INTERNAL_KEY_HEADER, TEST_INTERNAL_KEY); + server +} + +/// Registers a fresh random account; returns its id and email. +pub async fn register_account(server: &TestServer) -> (Uuid, String) { + let email = format!("t-{}@example.com", Uuid::new_v4()); + let res = server + .post("/auth/register") + .json(&serde_json::json!({ "email": email, "password": "correct-horse-battery-staple", "nick": "Tester" })) + .await; + res.assert_status(axum::http::StatusCode::CREATED); + let body: serde_json::Value = res.json(); + (Uuid::parse_str(body["id"].as_str().unwrap()).unwrap(), email) +} +``` + +- [ ] **Step 5: Migrate integration tests** + +Every `TestServer::new(app)` → `common::test_server(app)`. Where a test logged in only to get an access token for `/device/confirm` or `/avatars`, it now sends `.add_header(ACCOUNT_ID_HEADER, id.to_string())` with the id from `register_account`. `refresh_token_cannot_confirm_a_device_code` is replaced by: +```rust +#[tokio::test] +async fn confirm_without_identity_is_401() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let code: serde_json::Value = server.post("/device/code").await.json(); + server.post("/device/confirm") + .json(&serde_json::json!({ "user_code": code["user_code"] })) + .await + .assert_status_unauthorized(); +} +``` +Add to `smoke.rs`: +```rust +#[tokio::test] +async fn api_routes_require_internal_key_but_health_does_not() { + let pool = common::test_pool().await; + let server = axum_test::TestServer::new(accounts_service::build_app(pool, &common::test_config())); + server.get("/health").await.assert_status_ok(); + server.post("/device/code").await.assert_status(axum::http::StatusCode::FORBIDDEN); +} +``` + +- [ ] **Step 6: Run full suite, then commit** + +Run: `DATABASE_URL=... CARGO_BUILD_JOBS=4 cargo test -p accounts-service` → all pass. +```bash +git add -A backend/accounts-service backend/.env.example backend/Cargo.lock +git commit -m "feat(accounts): accept only gateway traffic, read identity from gateway header" +``` + +--- + +### Task 3: Refresh tokens — rotation, httpOnly cookie, logout + +**Files:** +- Create: `backend/accounts-service/migrations/0002_refresh_tokens.sql` +- Create: `backend/accounts-service/src/auth/tokens.rs` +- Modify: `src/auth/handlers.rs` (login sets cookie; `refresh`, `logout`), `src/auth/mod.rs`, `src/lib.rs`, `src/config.rs` (+ `cookie_secure`), `Cargo.toml` (+ `axum-extra = { version = "0.12", features = ["cookie"] }`, `time = "0.3"`, `sha2 = "0.11"`, `hex = "0.4"`, `base64 = "0.22"`) +- Create: `tests/auth_flow/refresh.rs` (declared via `mod refresh;` at the top of `tests/auth_flow.rs` — keeps `tests/` at 4 files) + +**Interfaces:** +- Produces (`auth::tokens`): `new_opaque_token(prefix: &str) -> String` (prefix + 43-char base64url of 32 random bytes), `hash_token(&str) -> String` (hex SHA-256), `store_refresh(&PgPool, Uuid) -> Result`, `enum RotateOutcome { Rotated { account_id: Uuid, new_token: String }, Invalid }`, `rotate_refresh(&PgPool, &str) -> Result`, `revoke_refresh(&PgPool, &str) -> Result<(), sqlx::Error>`. +- Produces (HTTP): `POST /auth/login` → `200 {access_token}` + `Set-Cookie: lv_refresh=...`; `POST /auth/refresh` (cookie) → `200 {access_token}` + rotated cookie, or 401; `POST /auth/logout` → 204 + cookie removal. `LoginResponse` loses `refresh_token`. +- Produces: `Config.cookie_secure: bool` (env `COOKIE_SECURE`, default `true`); `AuthState::new(pool, jwt_secret, cookie_secure)`. + +SHA-256 (not Argon2) is correct here: the tokens are 256-bit random, so there is nothing to brute-force; a slow hash would only add latency to every refresh. + +- [ ] **Step 1: Migration** + +```sql +-- Opaque refresh tokens (stored hashed). Rotated on every use; presenting an +-- already-rotated token revokes the whole account's sessions (theft signal). +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_refresh_tokens_account_id ON refresh_tokens(account_id); +``` + +- [ ] **Step 2: Failing unit tests in `auth/tokens.rs`** + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn opaque_tokens_are_prefixed_unique_and_long() { + let a = new_opaque_token("lvr_"); + let b = new_opaque_token("lvr_"); + assert!(a.starts_with("lvr_")); + assert_eq!(a.len(), 4 + 43); + assert_ne!(a, b); + } + + #[test] + fn hash_is_stable_hex_sha256() { + assert_eq!(hash_token("x"), hash_token("x")); + assert_eq!(hash_token("x").len(), 64); + assert_ne!(hash_token("x"), hash_token("y")); + } +} +``` +Run: `cargo test -p accounts-service --lib tokens` → FAIL. + +- [ ] **Step 3: Implement `auth/tokens.rs`** + +```rust +use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; +use rand::RngExt; +use sha2::{Digest, Sha256}; +use sqlx::PgPool; +use uuid::Uuid; + +pub fn new_opaque_token(prefix: &str) -> String { + let mut bytes = [0u8; 32]; + rand::rng().fill(&mut bytes); + format!("{prefix}{}", URL_SAFE_NO_PAD.encode(bytes)) +} + +pub fn hash_token(token: &str) -> String { + hex::encode(Sha256::digest(token.as_bytes())) +} + +pub async fn store_refresh(pool: &PgPool, account_id: Uuid) -> Result { + let token = new_opaque_token("lvr_"); + sqlx::query( + "INSERT INTO refresh_tokens (account_id, token_hash, expires_at) + VALUES ($1, $2, now() + interval '30 days')", + ) + .bind(account_id) + .bind(hash_token(&token)) + .execute(pool) + .await?; + Ok(token) +} + +pub enum RotateOutcome { + Rotated { account_id: Uuid, new_token: String }, + Invalid, +} + +pub async fn rotate_refresh(pool: &PgPool, token: &str) -> Result { + let mut tx = pool.begin().await?; + let row: Option<(Uuid, bool, bool)> = sqlx::query_as( + "SELECT account_id, revoked_at IS NOT NULL, expires_at <= now() + FROM refresh_tokens WHERE token_hash = $1 FOR UPDATE", + ) + .bind(hash_token(token)) + .fetch_optional(&mut *tx) + .await?; + + let outcome = match row { + None => RotateOutcome::Invalid, + Some((account_id, true, _)) => { + // Reuse of a rotated token: someone else holds a copy. Kill all sessions. + sqlx::query("UPDATE refresh_tokens SET revoked_at = now() WHERE account_id = $1 AND revoked_at IS NULL") + .bind(account_id) + .execute(&mut *tx) + .await?; + RotateOutcome::Invalid + } + Some((_, false, true)) => RotateOutcome::Invalid, + Some((account_id, false, false)) => { + sqlx::query("UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1") + .bind(hash_token(token)) + .execute(&mut *tx) + .await?; + let new_token = new_opaque_token("lvr_"); + sqlx::query( + "INSERT INTO refresh_tokens (account_id, token_hash, expires_at) + VALUES ($1, $2, now() + interval '30 days')", + ) + .bind(account_id) + .bind(hash_token(&new_token)) + .execute(&mut *tx) + .await?; + RotateOutcome::Rotated { account_id, new_token } + } + }; + tx.commit().await?; + Ok(outcome) +} + +pub async fn revoke_refresh(pool: &PgPool, token: &str) -> Result<(), sqlx::Error> { + sqlx::query("UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1 AND revoked_at IS NULL") + .bind(hash_token(token)) + .execute(pool) + .await?; + Ok(()) +} +``` +Add `pub mod tokens;` to `auth/mod.rs`. + +- [ ] **Step 4: Failing integration tests — `tests/auth_flow/refresh.rs`** + +```rust +use super::common; +use axum::http::StatusCode; +use axum_extra::extract::cookie::Cookie; +use serde_json::json; + +async fn login(server: &axum_test::TestServer, email: &str) -> (String, String) { + let res = server.post("/auth/login") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple" })) + .await; + res.assert_status_ok(); + let cookie = res.cookie("lv_refresh"); + assert!(cookie.http_only().unwrap_or(false)); + assert_eq!(cookie.path(), Some("/auth")); + let body: serde_json::Value = res.json(); + assert!(body.get("refresh_token").is_none(), "refresh token must not be in the JSON body"); + (body["access_token"].as_str().unwrap().to_owned(), cookie.value().to_owned()) +} + +#[tokio::test] +async fn refresh_rotates_the_cookie_and_issues_a_new_access_token() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, refresh) = login(&server, &email).await; + + let res = server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", refresh.clone())).await; + res.assert_status_ok(); + assert!(res.json::()["access_token"].is_string()); + assert_ne!(res.cookie("lv_refresh").value(), refresh); +} + +#[tokio::test] +async fn reusing_a_rotated_refresh_token_revokes_all_sessions() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, first) = login(&server, &email).await; + + let second = server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", first.clone())) + .await.cookie("lv_refresh").value().to_owned(); + // Attacker replays the old token -> rejected, and the legit new one dies too. + server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", first)) + .await.assert_status(StatusCode::UNAUTHORIZED); + server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", second)) + .await.assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn logout_revokes_the_refresh_token() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (_, email) = common::register_account(&server).await; + let (_, refresh) = login(&server, &email).await; + + server.post("/auth/logout").add_cookie(Cookie::new("lv_refresh", refresh.clone())) + .await.assert_status(StatusCode::NO_CONTENT); + server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", refresh)) + .await.assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn refresh_without_cookie_or_with_garbage_is_401() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + server.post("/auth/refresh").await.assert_status(StatusCode::UNAUTHORIZED); + server.post("/auth/refresh").add_cookie(Cookie::new("lv_refresh", "lvr_garbage")) + .await.assert_status(StatusCode::UNAUTHORIZED); +} +``` +Add `axum-extra = { version = "0.12", features = ["cookie"] }` to `[dev-dependencies]` too if the test crate can't see the normal dependency (it can — integration tests see `[dependencies]`). In `test_config()` set `cookie_secure: false`. In `tests/auth_flow.rs` add `mod refresh;` after `mod common;`, and change `register_then_login_succeeds` to assert `body["refresh_token"].is_null()`. + +Run: `cargo test -p accounts-service --test auth_flow` → FAIL (routes missing). + +- [ ] **Step 5: Implement handlers** + +In `auth/handlers.rs` (add `pub cookie_secure: bool` to `AuthState`, set in `AuthState::new(pool, jwt_secret, cookie_secure)`): +```rust +use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; + +pub const REFRESH_COOKIE: &str = "lv_refresh"; + +fn refresh_cookie(token: String, secure: bool) -> Cookie<'static> { + Cookie::build((REFRESH_COOKIE, token)) + .http_only(true) + .secure(secure) + .same_site(SameSite::Strict) + .path("/auth") + .max_age(time::Duration::days(30)) + .build() +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub access_token: String, +} + +pub async fn login( + State(state): State, + jar: CookieJar, + Json(req): Json, +) -> Result<(CookieJar, Json), AppError> { + // ... unchanged lookup + dummy-hash + verify ... + let refresh = tokens::store_refresh(&state.pool, account.id).await?; + Ok(( + jar.add(refresh_cookie(refresh, state.cookie_secure)), + Json(LoginResponse { access_token: jwt::issue_access_token(account.id, &state.jwt_secret) }), + )) +} + +pub async fn refresh( + State(state): State, + jar: CookieJar, +) -> Result<(CookieJar, Json), AppError> { + let token = jar.get(REFRESH_COOKIE).map(|c| c.value().to_owned()).ok_or(AppError::Unauthorized)?; + match tokens::rotate_refresh(&state.pool, &token).await? { + tokens::RotateOutcome::Rotated { account_id, new_token } => Ok(( + jar.add(refresh_cookie(new_token, state.cookie_secure)), + Json(LoginResponse { access_token: jwt::issue_access_token(account_id, &state.jwt_secret) }), + )), + tokens::RotateOutcome::Invalid => Err(AppError::Unauthorized), + } +} + +pub async fn logout(State(state): State, jar: CookieJar) -> Result<(CookieJar, StatusCode), AppError> { + if let Some(cookie) = jar.get(REFRESH_COOKIE) { + tokens::revoke_refresh(&state.pool, cookie.value()).await?; + } + Ok((jar.remove(Cookie::build(REFRESH_COOKIE).path("/auth")), StatusCode::NO_CONTENT)) +} +``` +Routes in `lib.rs`: `.route("/auth/refresh", post(auth::handlers::refresh)).route("/auth/logout", post(auth::handlers::logout))`. Config: `cookie_secure: std::env::var("COOKIE_SECURE").map(|v| v != "false").unwrap_or(true)`; `.env.example`: `COOKIE_SECURE=false # true in production (HTTPS)`. + +`handlers.rs` is already ~227 lines (after the final-review fixes added `validate_register`, the Argon2 semaphore and `spawn_blocking` wrappers), so this task WILL cross 250. Before adding the new handlers: move the cookie helpers (`REFRESH_COOKIE`, `refresh_cookie`) into `auth/tokens.rs`, and move the bounded Argon2 wrappers (`hash_limiter` semaphore + the async `hash_password`/`verify_password` methods) into `auth/password.rs` as a `PasswordHasher` struct (`new() -> Self`, `async fn hash(&self, String)`, `async fn verify(&self, String, String)`) that `AuthState` holds. `auth/` then has exactly 4 files: `mod, password, handlers, tokens`. + +- [ ] **Step 6: Run full suite, commit** + +```bash +git add -A backend/accounts-service backend/.env.example backend/Cargo.lock +git commit -m "feat(accounts): rotating opaque refresh tokens in httpOnly cookie, /auth/refresh and /auth/logout" +``` + +--- + +### Task 4: Persistent, revocable device links + +**Files:** +- Create: `backend/accounts-service/migrations/0003_device_links_token_unique.sql` +- Create: `backend/accounts-service/src/device/links.rs` +- Modify: `src/device/handlers.rs` (`token` persists a link; `list_links`, `revoke_link`), `src/device/mod.rs`, `src/lib.rs` +- Modify: `backend/common/src/jwt.rs` (delete `issue_refresh_token`, `TokenType::Refresh` and tests that only exist for it) +- Create: `tests/device_flow/links.rs` (declared via `mod links;` in `tests/device_flow.rs`) + +**Interfaces:** +- Consumes: `auth::tokens::{new_opaque_token, hash_token}`, `common::internal::{GatewayIdentity, DEVICE_TOKEN_PREFIX}`. +- Produces (`device::links`): `struct DeviceLink { id: Uuid, linked_at: DateTime, last_seen: Option> }` (Serialize, FromRow), `create(&PgPool, Uuid) -> Result` (returns the plain `lvd_...` token), `authenticate(&PgPool, &str) -> Result, sqlx::Error>` (bumps `last_seen`), `list(&PgPool, Uuid) -> Result, sqlx::Error>`, `revoke(&PgPool, account_id: Uuid, link_id: Uuid) -> Result`. +- Produces (HTTP): `POST /device/token` → `200 {device_token: "lvd_..."}` once confirmed; `GET /device/links` → `[{id, linked_at, last_seen}]`; `DELETE /device/links/{id}` → 204, or 404 if not yours. +- `DeviceState` becomes `{ store: DeviceStore, pool: PgPool }` (no `jwt_secret`). + +- [ ] **Step 1: Migration** + +```sql +-- Device tokens are looked up by hash on every mod request (via gateway gRPC). +CREATE UNIQUE INDEX device_links_token_hash_idx ON device_links (device_token_hash); +``` + +- [ ] **Step 2: Failing integration tests — `tests/device_flow/links.rs`** + +```rust +use super::common; +use axum::http::StatusCode; +use common::ACCOUNT_ID_HEADER; // re-export: add `pub use common::internal::ACCOUNT_ID_HEADER;` in tests/common/mod.rs + +async fn link_device(server: &axum_test::TestServer, account: uuid::Uuid) -> String { + let code: serde_json::Value = server.post("/device/code").await.json(); + server.post("/device/confirm") + .add_header(ACCOUNT_ID_HEADER, account.to_string()) + .json(&serde_json::json!({ "user_code": code["user_code"] })) + .await.assert_status_ok(); + let res = server.post("/device/token").json(&serde_json::json!({ "device_code": code["device_code"] })).await; + res.assert_status_ok(); + res.json::()["device_token"].as_str().unwrap().to_owned() +} + +#[tokio::test] +async fn confirmed_device_gets_an_opaque_token_backed_by_a_link_row() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account, _) = common::register_account(&server).await; + let token = link_device(&server, account).await; + assert!(token.starts_with("lvd_")); + + let resolved = accounts_service::device::links::authenticate(&pool, &token).await.unwrap(); + assert_eq!(resolved, Some(account)); + let links: Vec = server.get("/device/links") + .add_header(ACCOUNT_ID_HEADER, account.to_string()).await.json(); + assert_eq!(links.len(), 1); + assert!(links[0]["last_seen"].is_string(), "authenticate must bump last_seen"); +} + +#[tokio::test] +async fn revoking_a_link_kills_its_token_only() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool.clone(), &common::test_config())); + let (account, _) = common::register_account(&server).await; + let t1 = link_device(&server, account).await; + let t2 = link_device(&server, account).await; + + let links: Vec = server.get("/device/links") + .add_header(ACCOUNT_ID_HEADER, account.to_string()).await.json(); + let first_id = links.iter() + .find(|l| l["id"].is_string()) + .unwrap()["id"].as_str().unwrap().to_owned(); + server.delete(&format!("/device/links/{first_id}")) + .add_header(ACCOUNT_ID_HEADER, account.to_string()) + .await.assert_status(StatusCode::NO_CONTENT); + + let alive = [ + accounts_service::device::links::authenticate(&pool, &t1).await.unwrap(), + accounts_service::device::links::authenticate(&pool, &t2).await.unwrap(), + ]; + assert_eq!(alive.iter().filter(|a| a.is_some()).count(), 1); +} + +#[tokio::test] +async fn cannot_revoke_someone_elses_link() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (owner, _) = common::register_account(&server).await; + let (stranger, _) = common::register_account(&server).await; + link_device(&server, owner).await; + let links: Vec = server.get("/device/links") + .add_header(ACCOUNT_ID_HEADER, owner.to_string()).await.json(); + let id = links[0]["id"].as_str().unwrap(); + + server.delete(&format!("/device/links/{id}")) + .add_header(ACCOUNT_ID_HEADER, stranger.to_string()) + .await.assert_status(StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn unknown_device_token_does_not_authenticate() { + let pool = common::test_pool().await; + let r = accounts_service::device::links::authenticate(&pool, "lvd_nope").await.unwrap(); + assert_eq!(r, None); +} +``` +Update `full_device_link_flow` in `tests/device_flow.rs` to assert `device_token` starts with `lvd_` instead of decoding it as a JWT. Add `mod links;`. + +Run → FAIL. + +- [ ] **Step 3: Implement `device/links.rs`** + +```rust +use crate::auth::tokens::{hash_token, new_opaque_token}; +use chrono::{DateTime, Utc}; +use common::internal::DEVICE_TOKEN_PREFIX; +use serde::Serialize; +use sqlx::PgPool; +use uuid::Uuid; + +#[derive(Debug, Serialize, sqlx::FromRow)] +pub struct DeviceLink { + pub id: Uuid, + pub linked_at: DateTime, + pub last_seen: Option>, +} + +pub async fn create(pool: &PgPool, account_id: Uuid) -> Result { + let token = new_opaque_token(DEVICE_TOKEN_PREFIX); + sqlx::query("INSERT INTO device_links (account_id, device_token_hash) VALUES ($1, $2)") + .bind(account_id) + .bind(hash_token(&token)) + .execute(pool) + .await?; + Ok(token) +} + +pub async fn authenticate(pool: &PgPool, token: &str) -> Result, sqlx::Error> { + sqlx::query_scalar( + "UPDATE device_links SET last_seen = now() WHERE device_token_hash = $1 RETURNING account_id", + ) + .bind(hash_token(token)) + .fetch_optional(pool) + .await +} + +pub async fn list(pool: &PgPool, account_id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as("SELECT id, linked_at, last_seen FROM device_links WHERE account_id = $1 ORDER BY linked_at DESC") + .bind(account_id) + .fetch_all(pool) + .await +} + +pub async fn revoke(pool: &PgPool, account_id: Uuid, link_id: Uuid) -> Result { + let result = sqlx::query("DELETE FROM device_links WHERE id = $1 AND account_id = $2") + .bind(link_id) + .bind(account_id) + .execute(pool) + .await?; + Ok(result.rows_affected() == 1) +} +``` +`device/mod.rs`: `pub mod handlers; pub mod links; pub mod store;`. + +- [ ] **Step 4: Handlers + routes** + +In `device/handlers.rs`: +```rust +PollResult::Confirmed(account_id) => { + let device_token = links::create(&state.pool, account_id).await?; + Ok((StatusCode::OK, Json(Some(TokenResponse { device_token })))) +} +``` +```rust +pub async fn list_links( + State(state): State, + identity: GatewayIdentity, +) -> Result>, AppError> { + Ok(Json(links::list(&state.pool, identity.account_id).await?)) +} + +pub async fn revoke_link( + State(state): State, + identity: GatewayIdentity, + Path(link_id): Path, +) -> Result { + if links::revoke(&state.pool, identity.account_id, link_id).await? { + Ok(StatusCode::NO_CONTENT) + } else { + Err(AppError::NotFound("no such device link".into())) + } +} +``` +Routes: `.route("/device/links", get(device::handlers::list_links)).route("/device/links/{id}", delete(device::handlers::revoke_link))`. `DeviceState { store: DeviceStore::default(), pool: pool.clone() }`. + +- [ ] **Step 5: Remove the dead refresh-JWT code from `common::jwt`** + +Delete `issue_refresh_token`, the `Refresh` variant and every test that used them (`grep -rn "Refresh\b\|issue_refresh_token" backend/` must return nothing outside comments). Keep the `token_type` claim (value always `"access"`) so future token kinds stay distinguishable. + +- [ ] **Step 6: Full suite (`-p common` and `-p accounts-service`), commit** + +```bash +git add -A backend/common backend/accounts-service backend/Cargo.lock +git commit -m "feat(accounts): persist device links with opaque hashed tokens, list and revoke endpoints" +``` + +--- + +### Task 5: accounts-service gRPC server (`AuthenticateDevice`) + +**Files:** +- Create: `backend/accounts-service/src/grpc/mod.rs` +- Modify: `src/lib.rs` (`pub mod grpc;`), `src/main.rs` (serve HTTP + gRPC), `src/config.rs` (+ `grpc_port`, env `GRPC_PORT`, default `50051`), `Cargo.toml` (+ `tonic = "0.14"`, `tokio-stream = { version = "0.1", features = ["net"] }` in dev-deps), `backend/.env.example` (`GRPC_PORT=50051`) + +**Interfaces:** +- Consumes: `device::links::authenticate`, `common::pb::accounts::*`, `common::internal::GrpcKeyCheck`. +- Produces: `grpc::AccountsGrpc::new(PgPool)`, `grpc::server(PgPool, &str /*internal key*/) -> InterceptedService, GrpcKeyCheck>`. + +- [ ] **Step 1: Failing test (inside `src/grpc/mod.rs`, real DB, real network)** + +```rust +#[cfg(test)] +mod tests { + use super::*; + use common::internal::GrpcKeyAttach; + use common::pb::accounts::accounts_internal_client::AccountsInternalClient; + + const KEY: &str = "internal-key-internal-key-internal!!"; + + async fn pool() -> PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); + pool + } + + #[tokio::test] + async fn authenticate_device_over_grpc() { + let pool = pool().await; + let account = crate::accounts::repo::create( + &pool, &format!("g-{}@example.com", Uuid::new_v4()), "x", "Grpc", + ).await.unwrap(); + let token = crate::device::links::create(&pool, account.id).await.unwrap(); + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn( + tonic::transport::Server::builder() + .add_service(server(pool.clone(), KEY)) + .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)), + ); + let channel = tonic::transport::Endpoint::from_shared(format!("http://{addr}")).unwrap().connect_lazy(); + let mut client = AccountsInternalClient::with_interceptor(channel.clone(), GrpcKeyAttach::new(KEY).unwrap()); + + let reply = client + .authenticate_device(AuthenticateDeviceRequest { device_token: token }) + .await.unwrap().into_inner(); + assert_eq!(reply.account_id, account.id.to_string()); + + let err = client + .authenticate_device(AuthenticateDeviceRequest { device_token: "lvd_unknown".into() }) + .await.unwrap_err(); + assert_eq!(err.code(), tonic::Code::Unauthenticated); + + let mut no_key = AccountsInternalClient::new(channel); + let err = no_key + .authenticate_device(AuthenticateDeviceRequest { device_token: "x".into() }) + .await.unwrap_err(); + assert_eq!(err.code(), tonic::Code::PermissionDenied); + + sqlx::query("DELETE FROM accounts WHERE id = $1").bind(account.id).execute(&pool).await.unwrap(); + } +} +``` + +- [ ] **Step 2: Implement** + +```rust +use common::internal::GrpcKeyCheck; +use common::pb::accounts::accounts_internal_server::{AccountsInternal, AccountsInternalServer}; +use common::pb::accounts::{AuthenticateDeviceReply, AuthenticateDeviceRequest}; +use sqlx::PgPool; +use tonic::{Request, Response, Status, service::interceptor::InterceptedService}; +use uuid::Uuid; + +pub struct AccountsGrpc { + pool: PgPool, +} + +impl AccountsGrpc { + pub fn new(pool: PgPool) -> Self { + AccountsGrpc { pool } + } +} + +pub fn server(pool: PgPool, internal_key: &str) -> InterceptedService, GrpcKeyCheck> { + AccountsInternalServer::with_interceptor(AccountsGrpc::new(pool), GrpcKeyCheck::new(internal_key)) +} + +#[tonic::async_trait] +impl AccountsInternal for AccountsGrpc { + async fn authenticate_device( + &self, + request: Request, + ) -> Result, Status> { + let token = request.into_inner().device_token; + match crate::device::links::authenticate(&self.pool, &token).await { + Ok(Some(account_id)) => Ok(Response::new(AuthenticateDeviceReply { account_id: account_id.to_string() })), + Ok(None) => Err(Status::unauthenticated("unknown or revoked device token")), + Err(err) => { + tracing::error!("authenticate_device: {err:?}"); + Err(Status::internal("internal error")) + } + } + } +} +``` +(`Uuid` import only in tests if unused otherwise.) + +- [ ] **Step 3: `main.rs` runs both servers** + +```rust +let http = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; +let grpc_addr = std::net::SocketAddr::from(([0, 0, 0, 0], cfg.grpc_port)); +let grpc = tonic::transport::Server::builder() + .add_service(accounts_service::grpc::server(pool.clone(), &cfg.internal_key)) + .serve(grpc_addr); +let app = accounts_service::build_app(pool, &cfg); +tokio::try_join!( + async { axum::serve(http, app).await.map_err(anyhow::Error::from) }, + async { grpc.await.map_err(anyhow::Error::from) }, +)?; +``` + +- [ ] **Step 4: Tests pass; commit** + +```bash +git add -A backend/accounts-service backend/.env.example backend/Cargo.lock +git commit -m "feat(accounts): internal gRPC AuthenticateDevice guarded by internal key" +``` + +--- + +### Task 6: `GET /me` for the site + +**Files:** +- Create: `backend/accounts-service/src/accounts/handlers.rs` +- Modify: `src/accounts/mod.rs`, `src/accounts/repo.rs` (+ `avatar_key`), `src/lib.rs`, `tests/auth_flow.rs` + +**Interfaces:** +- Produces: `GET /me` → `200 { id, email, display_nick, role, avatar_url: string|null, created_at }`; 401 without identity. `repo::avatar_key(&PgPool, Uuid) -> Result, sqlx::Error>`. `AccountsState { pool, avatar_base_url: String }` (from `cfg.avatar_base_url()` added in backend/PLAN.md Task 9). + +- [ ] **Step 1: Failing test in `tests/auth_flow.rs`** + +```rust +#[tokio::test] +async fn me_returns_profile_without_password_hash() { + let pool = common::test_pool().await; + let server = common::test_server(accounts_service::build_app(pool, &common::test_config())); + let (id, email) = common::register_account(&server).await; + + let res = server.get("/me").add_header(common::ACCOUNT_ID_HEADER, id.to_string()).await; + res.assert_status_ok(); + let body: serde_json::Value = res.json(); + assert_eq!(body["email"], email); + assert_eq!(body["display_nick"], "Tester"); + assert_eq!(body["role"], "user"); + assert!(body["avatar_url"].is_null()); + assert!(body.get("password_hash").is_none()); + + server.get("/me").await.assert_status_unauthorized(); +} +``` + +- [ ] **Step 2: Implement** + +`repo.rs`: +```rust +pub async fn avatar_key(pool: &PgPool, account_id: Uuid) -> Result, sqlx::Error> { + sqlx::query_scalar("SELECT s3_key FROM avatars WHERE account_id = $1") + .bind(account_id) + .fetch_optional(pool) + .await +} +``` +`accounts/handlers.rs`: +```rust +use super::repo; +use crate::error::AppError; +use axum::{Json, extract::State}; +use chrono::{DateTime, Utc}; +use common::internal::GatewayIdentity; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Clone)] +pub struct AccountsState { + pub pool: sqlx::PgPool, + pub avatar_base_url: String, +} + +#[derive(Serialize)] +pub struct MeResponse { + pub id: Uuid, + pub email: String, + pub display_nick: String, + pub role: String, + pub avatar_url: Option, + pub created_at: DateTime, +} + +pub async fn me(State(state): State, identity: GatewayIdentity) -> Result, AppError> { + let account = repo::find_by_id(&state.pool, identity.account_id) + .await? + .ok_or(AppError::Unauthorized)?; + let avatar_url = repo::avatar_key(&state.pool, account.id) + .await? + .map(|key| format!("{}/{key}", state.avatar_base_url)); + Ok(Json(MeResponse { + id: account.id, + email: account.email, + display_nick: account.display_nick, + role: account.role, + avatar_url, + created_at: account.created_at, + })) +} +``` +Route: `.route("/me", get(accounts::handlers::me))` with `AccountsState`, inside the internal-key-guarded `api` router. Add `pub use common::internal::ACCOUNT_ID_HEADER;` to `tests/common/mod.rs` if Task 4 didn't already. + +- [ ] **Step 3: Tests pass; commit** + +```bash +git add -A backend/accounts-service +git commit -m "feat(accounts): GET /me profile endpoint" +``` + +--- + +### Task 7: Gateway scaffold — config, health, `build_app` + +**Files:** +- Create: `backend/gateway/Cargo.toml`, `src/main.rs`, `src/lib.rs`, `src/config.rs`, `tests/common/mod.rs`, `tests/proxy.rs` (health test only for now) +- Modify: `backend/Cargo.toml` (members += "gateway"), `backend/.env.example` + +**Interfaces:** +- Produces: `Config { port: u16, jwt_secret, internal_key, accounts_http_url, accounts_grpc_url, configs_http_url, site_origin: String, trust_proxy: bool }`, `Config::from_env()`, `Config::validate()`; `build_app(cfg: &Config, devices: Arc) -> Router` (Task 7 ships it with only `/health`, taking the `devices` arg already so later tasks don't change the signature — `DeviceAuthenticator` is defined in this task in `identity/device.rs` as the trait only, implementation in Task 9). + +- [ ] **Step 1: `Cargo.toml`** + +```toml +[package] +name = "gateway" +version = "0.1.0" +edition = "2024" + +[lib] +name = "gateway" +path = "src/lib.rs" + +[dependencies] +common = { path = "../common" } +axum = "0.8" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "time"] } +tower-http = { version = "0.7", features = ["cors", "trace"] } +tracing = "0.1" +tracing-subscriber = "0.3" +serde_json = "1" +uuid = { version = "1", features = ["v4"] } +reqwest = { version = "0.13", default-features = false, features = ["stream"] } +tonic = "0.14" +governor = "0.10" +anyhow = "1" +dotenvy = "0.15" + +[dev-dependencies] +axum-test = "21" +``` + +- [ ] **Step 2: `config.rs` with tests first** + +```rust +use anyhow::{Context, Result}; + +#[derive(Clone)] +pub struct Config { + pub port: u16, + pub jwt_secret: String, + pub internal_key: String, + pub accounts_http_url: String, + pub accounts_grpc_url: String, + pub configs_http_url: String, + pub site_origin: String, + /// Behind a reverse proxy (nginx/caddy) that appends the client IP to + /// X-Forwarded-For. Never enable when the gateway is exposed directly. + pub trust_proxy: bool, +} + +fn var(name: &str) -> Result { + std::env::var(name).with_context(|| format!("{name} not set")) +} + +impl Config { + pub fn from_env() -> Result { + Ok(Config { + port: std::env::var("GATEWAY_PORT").unwrap_or_else(|_| "8080".into()).parse().context("GATEWAY_PORT")?, + jwt_secret: var("JWT_SECRET")?, + internal_key: var("INTERNAL_KEY")?, + accounts_http_url: var("ACCOUNTS_HTTP_URL")?, + accounts_grpc_url: var("ACCOUNTS_GRPC_URL")?, + configs_http_url: var("CONFIGS_HTTP_URL")?, + site_origin: var("SITE_ORIGIN")?, + trust_proxy: std::env::var("TRUST_PROXY").is_ok_and(|v| v == "true"), + }) + } + + pub fn validate(&self) -> Result<()> { + for (name, value) in [("JWT_SECRET", &self.jwt_secret), ("INTERNAL_KEY", &self.internal_key)] { + if value.len() < 32 { + anyhow::bail!("{name} must be at least 32 bytes"); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + pub fn sample() -> Config { + Config { + port: 0, + jwt_secret: "j".repeat(32), + internal_key: "k".repeat(32), + accounts_http_url: String::new(), + accounts_grpc_url: String::new(), + configs_http_url: String::new(), + site_origin: "http://localhost:5173".into(), + trust_proxy: false, + } + } + + #[test] + fn valid_config_passes() { + assert!(sample().validate().is_ok()); + } + + #[test] + fn weak_secrets_are_rejected() { + let mut c = sample(); + c.internal_key = "short".into(); + assert!(c.validate().is_err()); + let mut c = sample(); + c.jwt_secret = "short".into(); + assert!(c.validate().is_err()); + } +} +``` + +- [ ] **Step 3: `identity/device.rs` trait (impl comes in Task 9) + `identity/mod.rs`** + +```rust +use std::{future::Future, pin::Pin}; +use uuid::Uuid; + +pub enum DeviceAuth { + Valid(Uuid), + Invalid, + /// accounts-service unreachable — the gateway answers 503, not 401, + /// so the mod doesn't wrongly forget its token. + Unavailable, +} + +pub type DeviceAuthFuture<'a> = Pin + Send + 'a>>; + +pub trait DeviceAuthenticator: Send + Sync + 'static { + fn authenticate<'a>(&'a self, token: &'a str) -> DeviceAuthFuture<'a>; +} +``` +`identity/mod.rs`: `pub mod device;`. + +- [ ] **Step 4: `lib.rs`, `main.rs`, health test** + +`lib.rs`: +```rust +pub mod config; +pub mod identity; + +use axum::{Router, routing::get}; +use config::Config; +use identity::device::DeviceAuthenticator; +use std::sync::Arc; + +pub fn build_app(_cfg: &Config, _devices: Arc) -> Router { + Router::new().route("/health", get(|| async { "ok" })) +} +``` +`main.rs` (the gRPC authenticator doesn't exist yet — pass a stub that says `Unavailable`, replaced in Task 9 Step 5): +```rust +use gateway::identity::device::{DeviceAuth, DeviceAuthFuture, DeviceAuthenticator}; +use std::sync::Arc; + +struct NotYetWired; +impl DeviceAuthenticator for NotYetWired { + fn authenticate<'a>(&'a self, _token: &'a str) -> DeviceAuthFuture<'a> { + Box::pin(async { DeviceAuth::Unavailable }) + } +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + dotenvy::dotenv().ok(); + tracing_subscriber::fmt::init(); + let cfg = gateway::config::Config::from_env()?; + cfg.validate()?; + let app = gateway::build_app(&cfg, Arc::new(NotYetWired)); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; + tracing::info!("gateway listening on {}", cfg.port); + axum::serve(listener, app.into_make_service_with_connect_info::()).await?; + Ok(()) +} +``` +`tests/common/mod.rs`: +```rust +#![allow(dead_code)] + +use gateway::config::Config; +use gateway::identity::device::{DeviceAuth, DeviceAuthFuture, DeviceAuthenticator}; +use std::sync::Arc; +use uuid::Uuid; + +pub const JWT_SECRET: &str = "gateway-test-secret-gateway-test!!"; +pub const INTERNAL_KEY: &str = "internal-key-internal-key-internal!!"; + +pub fn config(accounts: &str, configs: &str) -> Config { + Config { + port: 0, + jwt_secret: JWT_SECRET.into(), + internal_key: INTERNAL_KEY.into(), + accounts_http_url: accounts.into(), + accounts_grpc_url: String::new(), + configs_http_url: configs.into(), + site_origin: "http://localhost:5173".into(), + trust_proxy: true, + } +} + +/// Accepts exactly one device token, mapped to one account. +pub struct FakeDevices { + pub token: String, + pub account: Uuid, +} + +impl DeviceAuthenticator for FakeDevices { + fn authenticate<'a>(&'a self, token: &'a str) -> DeviceAuthFuture<'a> { + Box::pin(async move { + if token == self.token { DeviceAuth::Valid(self.account) } else { DeviceAuth::Invalid } + }) + } +} + +pub fn no_devices() -> Arc { + Arc::new(FakeDevices { token: "lvd_none".into(), account: Uuid::nil() }) +} +``` +`tests/proxy.rs`: +```rust +mod common; + +#[tokio::test] +async fn health_is_ok() { + let app = gateway::build_app(&common::config("http://127.0.0.1:1", "http://127.0.0.1:1"), common::no_devices()); + axum_test::TestServer::new(app).get("/health").await.assert_status_ok(); +} +``` + +- [ ] **Step 5: Run `cargo test -p gateway`, commit** + +`.env.example` additions: +``` +# gateway +GATEWAY_PORT=8080 +ACCOUNTS_HTTP_URL=http://127.0.0.1:8081 +ACCOUNTS_GRPC_URL=http://127.0.0.1:50051 +CONFIGS_HTTP_URL=http://127.0.0.1:8082 +SITE_ORIGIN=http://localhost:5173 +TRUST_PROXY=false +``` +```bash +git add -A backend/Cargo.toml backend/Cargo.lock backend/gateway backend/.env.example +git commit -m "feat(gateway): scaffold crate with config validation and health check" +``` + +--- + +### Task 8: Reverse proxy + +**Files:** +- Create: `backend/gateway/src/proxy/mod.rs`, `proxy/routes.rs`, `proxy/forward.rs` +- Modify: `src/lib.rs`, `tests/common/mod.rs` (echo upstream), `tests/proxy.rs` + +**Interfaces:** +- Produces: `proxy::routes::{Upstream, upstream_for(&str) -> Option}`; `proxy::forward::{Upstreams, proxy}` where `Upstreams { client: reqwest::Client, accounts: String, configs: String, internal_key: HeaderValue }` and `Upstreams::new(&Config) -> anyhow::Result`; `async fn proxy(State>, Request) -> Response` mounted as the router `fallback`. +- Routing table: `auth, device, avatars, me` → accounts; `configs, showcase` → configs; anything else → 404 JSON. + +- [ ] **Step 1: Echo upstream helper (tests/common/mod.rs)** + +```rust +use axum::{Json, Router, body::Bytes, extract::Request, routing::any}; + +/// Starts a fake service that echoes what it received as JSON; returns its base URL. +pub async fn spawn_echo() -> String { + async fn echo(req: Request) -> Json { + let (parts, body) = req.into_parts(); + let body: Bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap_or_default(); + let header = |name: &str| parts.headers.get(name).and_then(|v| v.to_str().ok()).map(str::to_owned); + Json(serde_json::json!({ + "method": parts.method.as_str(), + "path": parts.uri.path(), + "query": parts.uri.query(), + "body": String::from_utf8_lossy(&body), + "account_id": header("x-lovisual-account-id"), + "internal_key": header("x-lovisual-internal-key"), + "authorization": header("authorization"), + })) + } + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { axum::serve(listener, Router::new().fallback(any(echo))).await.unwrap() }); + format!("http://{addr}") +} +``` +Add `serde_json = "1"` to gateway `[dev-dependencies]` if not visible (it is a normal dep — fine). + +- [ ] **Step 2: Failing tests (tests/proxy.rs)** + +```rust +use axum::http::StatusCode; + +async fn server() -> axum_test::TestServer { + let accounts = common::spawn_echo().await; + let configs = common::spawn_echo().await; + let app = gateway::build_app(&common::config(&accounts, &configs), common::no_devices()); + axum_test::TestServer::new(app) +} + +#[tokio::test] +async fn forwards_method_path_query_and_body() { + let res = server().await.put("/configs/2?x=1").text("payload").await; + res.assert_status_ok(); + let echo: serde_json::Value = res.json(); + assert_eq!(echo["method"], "PUT"); + assert_eq!(echo["path"], "/configs/2"); + assert_eq!(echo["query"], "x=1"); + assert_eq!(echo["body"], "payload"); +} + +#[tokio::test] +async fn adds_internal_key_and_strips_spoofed_identity() { + let res = server().await + .post("/auth/login") + .add_header("x-lovisual-account-id", uuid::Uuid::new_v4().to_string()) + .add_header("x-lovisual-internal-key", "spoofed") + .await; + let echo: serde_json::Value = res.json(); + assert_eq!(echo["internal_key"], common::INTERNAL_KEY); + assert!(echo["account_id"].is_null()); +} + +#[tokio::test] +async fn unknown_prefix_is_404() { + server().await.get("/nope").await.assert_status(StatusCode::NOT_FOUND); +} + +#[tokio::test] +async fn dead_upstream_is_502() { + let app = gateway::build_app(&common::config("http://127.0.0.1:1", "http://127.0.0.1:1"), common::no_devices()); + axum_test::TestServer::new(app).get("/me").await.assert_status(StatusCode::BAD_GATEWAY); +} + +#[tokio::test] +async fn oversized_body_is_413() { + let big = "x".repeat(6 * 1024 * 1024 + 1); + server().await.put("/configs/1").text(big).await.assert_status(StatusCode::PAYLOAD_TOO_LARGE); +} +``` +(`adds_internal_key_and_strips_spoofed_identity` fully passes only after Task 9 strips the identity header; in this task `forward` itself also removes `x-lovisual-account-id` — Task 9 moves that into the identity layer and re-adds the resolved value, so forward must NOT strip it after Task 9. Implement stripping in `forward` now and move it in Task 9 Step 4.) + +- [ ] **Step 3: `proxy/routes.rs`** + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Upstream { + Accounts, + Configs, +} + +pub fn upstream_for(path: &str) -> Option { + match path.trim_start_matches('/').split('/').next()? { + "auth" | "device" | "avatars" | "me" => Some(Upstream::Accounts), + "configs" | "showcase" => Some(Upstream::Configs), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn routes_by_first_segment_only() { + assert_eq!(upstream_for("/auth/login"), Some(Upstream::Accounts)); + assert_eq!(upstream_for("/me"), Some(Upstream::Accounts)); + assert_eq!(upstream_for("/configs/shared/ABC"), Some(Upstream::Configs)); + assert_eq!(upstream_for("/showcase"), Some(Upstream::Configs)); + assert_eq!(upstream_for("/authx"), None); + assert_eq!(upstream_for("/"), None); + assert_eq!(upstream_for("/health"), None); + } +} +``` + +- [ ] **Step 4: `proxy/forward.rs`** + +```rust +use super::routes::{Upstream, upstream_for}; +use crate::config::Config; +use axum::{ + Json, + body::Body, + extract::{Request, State}, + http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header}, + response::{IntoResponse, Response}, +}; +use common::internal::INTERNAL_KEY_HEADER; +use serde_json::json; +use std::{sync::Arc, time::Duration}; + +/// 5 MB avatar + multipart overhead; configs are far smaller. +pub const MAX_BODY_BYTES: usize = 6 * 1024 * 1024; + +const HOP_BY_HOP: [HeaderName; 7] = [ + header::CONNECTION, + header::PROXY_AUTHENTICATE, + header::PROXY_AUTHORIZATION, + header::TE, + header::TRAILER, + header::TRANSFER_ENCODING, + header::UPGRADE, +]; + +pub struct Upstreams { + pub client: reqwest::Client, + pub accounts: String, + pub configs: String, + pub internal_key: HeaderValue, +} + +impl Upstreams { + pub fn new(cfg: &Config) -> anyhow::Result { + Ok(Upstreams { + client: reqwest::Client::builder() + // Never follow redirects on behalf of the client — pass them through. + .redirect(reqwest::redirect::Policy::none()) + .timeout(Duration::from_secs(30)) + .build()?, + accounts: cfg.accounts_http_url.trim_end_matches('/').to_owned(), + configs: cfg.configs_http_url.trim_end_matches('/').to_owned(), + internal_key: HeaderValue::from_str(&cfg.internal_key)?, + }) + } +} + +fn error(status: StatusCode, message: &str) -> Response { + (status, Json(json!({ "error": message }))).into_response() +} + +fn strip_hop_by_hop(headers: &mut HeaderMap) { + for name in &HOP_BY_HOP { + headers.remove(name); + } + headers.remove("keep-alive"); +} + +pub async fn proxy(State(up): State>, req: Request) -> Response { + let Some(target) = upstream_for(req.uri().path()) else { + return error(StatusCode::NOT_FOUND, "not found"); + }; + let base = match target { + Upstream::Accounts => &up.accounts, + Upstream::Configs => &up.configs, + }; + let path_and_query = req.uri().path_and_query().map_or("/", |p| p.as_str()); + let url = format!("{base}{path_and_query}"); + + let (parts, body) = req.into_parts(); + let Ok(bytes) = axum::body::to_bytes(body, MAX_BODY_BYTES).await else { + return error(StatusCode::PAYLOAD_TOO_LARGE, "payload too large"); + }; + let mut headers = parts.headers; + strip_hop_by_hop(&mut headers); + headers.remove(header::HOST); + headers.insert(INTERNAL_KEY_HEADER, up.internal_key.clone()); + + let upstream = match up.client.request(parts.method, url).headers(headers).body(bytes).send().await { + Ok(resp) => resp, + Err(err) => { + tracing::warn!("upstream {target:?} failed: {err}"); + return error(StatusCode::BAD_GATEWAY, "upstream unavailable"); + } + }; + + let mut response = Response::builder().status(upstream.status()); + for (name, value) in upstream.headers() { + if !HOP_BY_HOP.contains(name) && name != "keep-alive" { + response = response.header(name, value); + } + } + response + .body(Body::from_stream(upstream.bytes_stream())) + .unwrap_or_else(|_| error(StatusCode::BAD_GATEWAY, "bad upstream response")) +} +``` +In this task also add, at the top of `proxy` (moved to identity in Task 9): `headers.remove(common::internal::ACCOUNT_ID_HEADER);` right after `let mut headers = parts.headers;`. + +`proxy/mod.rs`: `pub mod forward; pub mod routes;`. `lib.rs`: +```rust +pub mod proxy; +// ... +pub fn build_app(cfg: &Config, _devices: Arc) -> Router { + let upstreams = Arc::new(proxy::forward::Upstreams::new(cfg).expect("valid upstream config")); + Router::new() + .route("/health", get(|| async { "ok" })) + .fallback(proxy::forward::proxy) + .with_state(upstreams) +} +``` +`Upstreams::new` only fails on an internal key that isn't a valid header value — a startup misconfiguration, so `expect` at boot is acceptable (not request data). Better: make `build_app` return `anyhow::Result` if the reviewer prefers — then update main/tests accordingly and keep it consistent in Tasks 9–11. + +- [ ] **Step 5: Tests pass; commit** + +```bash +git add -A backend/gateway backend/Cargo.lock +git commit -m "feat(gateway): reverse proxy to accounts and configs services" +``` + +--- + +### Task 9: Identity — JWT or device token, resolved once + +**Files:** +- Modify: `backend/gateway/src/identity/mod.rs` (middleware), `identity/device.rs` (+ `GrpcDevices`), `src/lib.rs`, `src/main.rs`, `src/proxy/forward.rs` (remove the identity-header strip) +- Create: `backend/gateway/tests/identity.rs` + +**Interfaces:** +- Consumes: `common::jwt::{bearer_token, verify_token, issue_access_token, TokenType}`, `common::internal::{ACCOUNT_ID_HEADER, INTERNAL_KEY_HEADER, DEVICE_TOKEN_PREFIX, GrpcKeyAttach}`, `common::pb::accounts::accounts_internal_client::AccountsInternalClient`. +- Produces: `identity::Identity(pub Option)` request extension (read by rate limiting in Task 10); `identity::IdentityState { jwt_secret: Arc, devices: Arc }`; `async fn identify(State, Request, Next) -> Response`; `identity::device::GrpcDevices::connect_lazy(url: &str, internal_key: &str) -> anyhow::Result`. +- Behaviour: no `Authorization` → anonymous. `Bearer lvd_...` → device lookup. Any other Bearer → JWT access token. Invalid credential → **401 at the gateway** (never forwarded). Device backend down → 503. `Authorization` is removed before forwarding. + +- [ ] **Step 1: Failing tests (tests/identity.rs)** + +```rust +mod common; + +use axum::http::StatusCode; +use std::sync::Arc; +use uuid::Uuid; + +async fn server(devices: common::FakeDevices) -> axum_test::TestServer { + let echo = common::spawn_echo().await; + let app = gateway::build_app(&common::config(&echo, &echo), Arc::new(devices)); + axum_test::TestServer::new(app) +} + +fn devices() -> common::FakeDevices { + common::FakeDevices { token: "lvd_good".into(), account: Uuid::new_v4() } +} + +#[tokio::test] +async fn access_jwt_becomes_account_header_and_authorization_is_dropped() { + let account = Uuid::new_v4(); + let token = common::jwt::issue_access_token(account, common::JWT_SECRET); + let echo: serde_json::Value = server(devices()).await + .get("/me").authorization_bearer(token).await.json(); + assert_eq!(echo["account_id"], account.to_string()); + assert!(echo["authorization"].is_null()); +} + +#[tokio::test] +async fn device_token_is_resolved_via_authenticator() { + let d = devices(); + let account = d.account; + let echo: serde_json::Value = server(d).await + .get("/configs").authorization_bearer("lvd_good").await.json(); + assert_eq!(echo["account_id"], account.to_string()); +} + +#[tokio::test] +async fn bad_credentials_are_rejected_at_the_gateway() { + let s = server(devices()).await; + s.get("/me").authorization_bearer("lvd_bad").await.assert_status(StatusCode::UNAUTHORIZED); + s.get("/me").authorization_bearer("not.a.jwt").await.assert_status(StatusCode::UNAUTHORIZED); + let wrong_key = common::jwt::issue_access_token(Uuid::new_v4(), "another-secret-another-secret-12345"); + s.get("/me").authorization_bearer(wrong_key).await.assert_status(StatusCode::UNAUTHORIZED); +} + +#[tokio::test] +async fn anonymous_requests_pass_without_identity() { + let echo: serde_json::Value = server(devices()).await.post("/auth/login").await.json(); + assert!(echo["account_id"].is_null()); +} + +#[tokio::test] +async fn spoofed_identity_header_never_reaches_the_service() { + let echo: serde_json::Value = server(devices()).await + .get("/me").add_header("x-lovisual-account-id", Uuid::new_v4().to_string()).await.json(); + assert!(echo["account_id"].is_null()); +} +``` +In `tests/common/mod.rs` add `pub use common::jwt;` — note the name clash: the test helper module is also called `common`. Rename the dependency import instead: `pub use ::common::jwt;` (leading `::` = the crate). + +- [ ] **Step 2: Implement the middleware (`identity/mod.rs`)** + +```rust +pub mod device; + +use axum::{ + Json, + extract::{Request, State}, + http::{HeaderValue, StatusCode, header::AUTHORIZATION}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use common::internal::{ACCOUNT_ID_HEADER, DEVICE_TOKEN_PREFIX, INTERNAL_KEY_HEADER}; +use common::jwt::{TokenType, bearer_token, verify_token}; +use device::{DeviceAuth, DeviceAuthenticator}; +use serde_json::json; +use std::sync::Arc; +use uuid::Uuid; + +/// Who made the request, as far as the gateway could verify. +#[derive(Clone, Copy)] +pub struct Identity(pub Option); + +#[derive(Clone)] +pub struct IdentityState { + pub jwt_secret: Arc, + pub devices: Arc, +} + +fn reject(status: StatusCode, message: &str) -> Response { + (status, Json(json!({ "error": message }))).into_response() +} + +pub async fn identify(State(state): State, mut req: Request, next: Next) -> Response { + // Client-supplied copies of trusted headers are never forwarded. + req.headers_mut().remove(ACCOUNT_ID_HEADER); + req.headers_mut().remove(INTERNAL_KEY_HEADER); + + let account = match bearer_token(req.headers()) { + None => None, + Some(token) if token.starts_with(DEVICE_TOKEN_PREFIX) => match state.devices.authenticate(token).await { + DeviceAuth::Valid(id) => Some(id), + DeviceAuth::Invalid => return reject(StatusCode::UNAUTHORIZED, "unauthorized"), + DeviceAuth::Unavailable => return reject(StatusCode::SERVICE_UNAVAILABLE, "auth backend unavailable"), + }, + Some(token) => match verify_token(token, &state.jwt_secret, TokenType::Access) + .and_then(|claims| Uuid::parse_str(&claims.sub).ok()) + { + Some(id) => Some(id), + None => return reject(StatusCode::UNAUTHORIZED, "unauthorized"), + }, + }; + + req.headers_mut().remove(AUTHORIZATION); + if let Some(id) = account { + if let Ok(value) = HeaderValue::from_str(&id.to_string()) { + req.headers_mut().insert(ACCOUNT_ID_HEADER, value); + } + } + req.extensions_mut().insert(Identity(account)); + next.run(req).await +} +``` +Remove the temporary `headers.remove(ACCOUNT_ID_HEADER)` from `proxy/forward.rs` (the middleware now owns that). + +- [ ] **Step 3: gRPC authenticator (`identity/device.rs`, below the trait)** + +```rust +use common::internal::GrpcKeyAttach; +use common::pb::accounts::{AuthenticateDeviceRequest, accounts_internal_client::AccountsInternalClient}; +use tonic::{Code, service::interceptor::InterceptedService, transport::{Channel, Endpoint}}; + +pub struct GrpcDevices { + client: AccountsInternalClient>, +} + +impl GrpcDevices { + /// Lazy: the gateway boots even if accounts-service is still starting. + pub fn connect_lazy(url: &str, internal_key: &str) -> anyhow::Result { + let channel = Endpoint::from_shared(url.to_owned())? + .timeout(std::time::Duration::from_secs(5)) + .connect_lazy(); + Ok(GrpcDevices { client: AccountsInternalClient::with_interceptor(channel, GrpcKeyAttach::new(internal_key)?) }) + } +} + +impl DeviceAuthenticator for GrpcDevices { + fn authenticate<'a>(&'a self, token: &'a str) -> DeviceAuthFuture<'a> { + Box::pin(async move { + let mut client = self.client.clone(); + match client.authenticate_device(AuthenticateDeviceRequest { device_token: token.to_owned() }).await { + Ok(reply) => Uuid::parse_str(&reply.into_inner().account_id) + .map_or(DeviceAuth::Invalid, DeviceAuth::Valid), + Err(status) if status.code() == Code::Unauthenticated => DeviceAuth::Invalid, + Err(status) => { + tracing::warn!("AuthenticateDevice failed: {status}"); + DeviceAuth::Unavailable + } + } + }) + } +} +``` +(`tonic`, `anyhow` are already gateway deps.) + +- [ ] **Step 4: Wire it (`lib.rs`) and replace the stub in `main.rs`** + +```rust +pub fn build_app(cfg: &Config, devices: Arc) -> Router { + let upstreams = Arc::new(proxy::forward::Upstreams::new(cfg).expect("valid upstream config")); + let identity = identity::IdentityState { jwt_secret: cfg.jwt_secret.as_str().into(), devices }; + Router::new() + .route("/health", get(|| async { "ok" })) + .fallback(proxy::forward::proxy) + .with_state(upstreams) + .layer(axum::middleware::from_fn_with_state(identity, identity::identify)) +} +``` +`main.rs`: delete `NotYetWired`; use `Arc::new(GrpcDevices::connect_lazy(&cfg.accounts_grpc_url, &cfg.internal_key)?)`. + +- [ ] **Step 5: `cargo test -p gateway` passes; commit** + +```bash +git add -A backend/gateway +git commit -m "feat(gateway): resolve identity once from access JWT or device token via gRPC" +``` + +--- + +### Task 10: Rate limiting + +**Files:** +- Create: `backend/gateway/src/rate_limit/mod.rs`, `rate_limit/rules.rs`, `backend/gateway/tests/rate_limit.rs` +- Modify: `src/lib.rs`, `src/main.rs` + +**Interfaces:** +- Consumes: `identity::Identity` extension, `Config.trust_proxy`. +- Produces: `rate_limit::rules::{KeyBy, Rule, rules() -> Vec}`, `rate_limit::RateLimits::new(trust_proxy: bool) -> Arc`, `RateLimits::purge(&self)`, `async fn enforce(State>, Request, Next) -> Response`. +- 429 body `{"error":"too many requests"}` with `Retry-After: `. + +Limits (from `TODO.md` Фаза 10 §6; one deliberate deviation marked ★): + +| Method + path | Quota | Key | +|---|---|---| +| POST `/auth/login` | 5/min | IP | +| POST `/auth/register` | 3/hour | IP | +| POST `/auth/refresh` | 30/min | IP | +| POST `/device/code` | 10/min | IP | +| POST `/device/token` | ★ 30/min | IP — the mod polls every 2–3 s for up to 10 min; 10/min would break linking | +| PUT `/configs/*` | 20/min | account (IP if anonymous) | +| GET `/configs/shared/*` | 30/min | IP | +| POST `/avatars` | 5/hour | account | +| GET `/showcase*` | 60/min | IP | +| everything (global, in addition) | 300/min | account or IP | + +- [ ] **Step 1: `rules.rs` with tests** + +```rust +use axum::http::Method; +use governor::Quota; +use std::num::NonZeroU32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyBy { + Ip, + Account, +} + +pub struct Rule { + pub method: Method, + /// Exact path, or a prefix when it ends with '*'. + pub pattern: &'static str, + pub quota: Quota, + pub key_by: KeyBy, +} + +impl Rule { + pub fn matches(&self, method: &Method, path: &str) -> bool { + if &self.method != method { + return false; + } + match self.pattern.strip_suffix('*') { + Some(prefix) => path.starts_with(prefix), + None => path == self.pattern, + } + } +} + +fn n(v: u32) -> NonZeroU32 { + NonZeroU32::new(v).expect("rate-limit constants are non-zero") +} + +fn rule(method: Method, pattern: &'static str, quota: Quota, key_by: KeyBy) -> Rule { + Rule { method, pattern, quota, key_by } +} + +pub fn rules() -> Vec { + use KeyBy::*; + vec![ + rule(Method::POST, "/auth/login", Quota::per_minute(n(5)), Ip), + rule(Method::POST, "/auth/register", Quota::per_hour(n(3)), Ip), + rule(Method::POST, "/auth/refresh", Quota::per_minute(n(30)), Ip), + rule(Method::POST, "/device/code", Quota::per_minute(n(10)), Ip), + rule(Method::POST, "/device/token", Quota::per_minute(n(30)), Ip), + rule(Method::PUT, "/configs/*", Quota::per_minute(n(20)), Account), + rule(Method::GET, "/configs/shared/*", Quota::per_minute(n(30)), Ip), + rule(Method::POST, "/avatars", Quota::per_hour(n(5)), Account), + rule(Method::GET, "/showcase*", Quota::per_minute(n(60)), Ip), + ] +} + +pub fn global_quota() -> Quota { + Quota::per_minute(n(300)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn exact_and_prefix_matching() { + let all = rules(); + let find = |m: Method, p: &str| all.iter().position(|r| r.matches(&m, p)); + assert_eq!(find(Method::POST, "/auth/login"), Some(0)); + assert_eq!(find(Method::GET, "/auth/login"), None); + assert_eq!(find(Method::POST, "/auth/login/x"), None); + assert!(find(Method::PUT, "/configs/3").is_some()); + assert!(find(Method::GET, "/configs/shared/ABCD").is_some()); + assert!(find(Method::GET, "/showcase?page=2").is_some() || find(Method::GET, "/showcase").is_some()); + } +} +``` + +- [ ] **Step 2: Failing integration tests (tests/rate_limit.rs)** + +```rust +mod common; + +use axum::http::StatusCode; + +async fn server() -> axum_test::TestServer { + let echo = common::spawn_echo().await; + axum_test::TestServer::new(gateway::build_app(&common::config(&echo, &echo), common::no_devices())) +} + +#[tokio::test] +async fn sixth_login_in_a_minute_from_one_ip_is_429_with_retry_after() { + let s = server().await; + for _ in 0..5 { + s.post("/auth/login").add_header("x-forwarded-for", "203.0.113.7").await.assert_status_ok(); + } + let res = s.post("/auth/login").add_header("x-forwarded-for", "203.0.113.7").await; + res.assert_status(StatusCode::TOO_MANY_REQUESTS); + let retry: u64 = res.header("retry-after").to_str().unwrap().parse().unwrap(); + assert!((1..=60).contains(&retry)); +} + +#[tokio::test] +async fn limits_are_per_ip() { + let s = server().await; + for _ in 0..5 { + s.post("/auth/login").add_header("x-forwarded-for", "203.0.113.8").await; + } + s.post("/auth/login").add_header("x-forwarded-for", "203.0.113.9").await.assert_status_ok(); +} + +#[tokio::test] +async fn rightmost_forwarded_for_entry_is_used() { + // A client can prepend fake entries; only the one our proxy appended counts. + let s = server().await; + for i in 0..5 { + s.post("/auth/login") + .add_header("x-forwarded-for", format!("10.0.0.{i}, 203.0.113.10")) + .await.assert_status_ok(); + } + s.post("/auth/login").add_header("x-forwarded-for", "1.1.1.1, 203.0.113.10") + .await.assert_status(StatusCode::TOO_MANY_REQUESTS); +} +``` + +- [ ] **Step 3: Implement `rate_limit/mod.rs`** + +```rust +pub mod rules; + +use crate::identity::Identity; +use axum::{ + Json, + extract::{ConnectInfo, Request, State}, + http::{HeaderValue, StatusCode, header::RETRY_AFTER}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use governor::{DefaultKeyedRateLimiter, RateLimiter, clock::{Clock, DefaultClock}}; +use rules::{KeyBy, Rule}; +use serde_json::json; +use std::{net::SocketAddr, sync::Arc}; + +pub struct RateLimits { + rules: Vec<(Rule, DefaultKeyedRateLimiter)>, + global: DefaultKeyedRateLimiter, + trust_proxy: bool, + clock: DefaultClock, +} + +impl RateLimits { + pub fn new(trust_proxy: bool) -> Arc { + Arc::new(RateLimits { + rules: rules::rules().into_iter().map(|r| { let l = RateLimiter::keyed(r.quota); (r, l) }).collect(), + global: RateLimiter::keyed(rules::global_quota()), + trust_proxy, + clock: DefaultClock::default(), + }) + } + + /// Drops idle keys so the maps don't grow forever. Call periodically. + pub fn purge(&self) { + for (_, limiter) in &self.rules { + limiter.retain_recent(); + limiter.shrink_to_fit(); + } + self.global.retain_recent(); + self.global.shrink_to_fit(); + } + + fn client_ip(&self, req: &Request) -> String { + if self.trust_proxy { + if let Some(ip) = req.headers().get("x-forwarded-for") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.rsplit(',').next()) + .map(str::trim) + .filter(|s| !s.is_empty()) + { + return ip.to_owned(); + } + } + req.extensions() + .get::>() + .map_or_else(|| "unknown".to_owned(), |c| c.0.ip().to_string()) + } +} + +fn too_many(wait: std::time::Duration) -> Response { + let secs = wait.as_secs_f64().ceil().max(1.0) as u64; + let mut res = (StatusCode::TOO_MANY_REQUESTS, Json(json!({ "error": "too many requests" }))).into_response(); + res.headers_mut().insert(RETRY_AFTER, HeaderValue::from(secs)); + res +} + +pub async fn enforce(State(limits): State>, req: Request, next: Next) -> Response { + let ip_key = format!("ip:{}", limits.client_ip(&req)); + let account_key = req.extensions().get::().and_then(|i| i.0).map(|id| format!("acc:{id}")); + let caller_key = account_key.clone().unwrap_or_else(|| ip_key.clone()); + + let (method, path) = (req.method().clone(), req.uri().path().to_owned()); + if let Some((rule, limiter)) = limits.rules.iter().find(|(r, _)| r.matches(&method, &path)) { + let key = match rule.key_by { + KeyBy::Ip => &ip_key, + KeyBy::Account => &caller_key, + }; + if let Err(not_until) = limiter.check_key(key) { + return too_many(not_until.wait_time_from(limits.clock.now())); + } + } + if let Err(not_until) = limits.global.check_key(&caller_key) { + return too_many(not_until.wait_time_from(limits.clock.now())); + } + next.run(req).await +} +``` +(`Rule` needs no `Clone`; `matches` takes `&Method`.) + +- [ ] **Step 4: Wire it — rate limit runs after identity** + +In `lib.rs` add `pub mod rate_limit;` and in `build_app` (layers run bottom-up: the last `.layer` is outermost, so identity runs first): +```rust +let limits = rate_limit::RateLimits::new(cfg.trust_proxy); +spawn_purger(Arc::clone(&limits)); +Router::new() + .route("/health", get(|| async { "ok" })) + .fallback(proxy::forward::proxy) + .with_state(upstreams) + .layer(axum::middleware::from_fn_with_state(limits, rate_limit::enforce)) + .layer(axum::middleware::from_fn_with_state(identity, identity::identify)) +``` +```rust +fn spawn_purger(limits: Arc) { + if let Ok(handle) = tokio::runtime::Handle::try_current() { + handle.spawn(async move { + let mut tick = tokio::time::interval(std::time::Duration::from_secs(60)); + loop { + tick.tick().await; + limits.purge(); + } + }); + } +} +``` + +- [ ] **Step 5: Tests pass; commit** + +```bash +git add -A backend/gateway +git commit -m "feat(gateway): per-route and global rate limits with Retry-After" +``` + +--- + +### Task 11: CORS, end-to-end run, docs + +**Files:** +- Modify: `backend/gateway/src/lib.rs` (CORS layer), `backend/gateway/tests/proxy.rs` (preflight test) +- Modify: `backend/STRUCTURE.md`, `TODO.md` (Фаза 10 status line), `backend/.env.example` + +**Interfaces:** +- Produces: CORS for `SITE_ORIGIN` only, credentials allowed (refresh cookie), methods GET/POST/PUT/DELETE, headers `authorization`, `content-type`. + +- [ ] **Step 1: Failing preflight test** + +```rust +#[tokio::test] +async fn cors_preflight_allows_only_the_site_origin() { + let app = gateway::build_app(&common::config("http://127.0.0.1:1", "http://127.0.0.1:1"), common::no_devices()); + let s = axum_test::TestServer::new(app); + let ok = s.method(axum::http::Method::OPTIONS, "/auth/login") + .add_header("origin", "http://localhost:5173") + .add_header("access-control-request-method", "POST") + .await; + assert_eq!(ok.header("access-control-allow-origin"), "http://localhost:5173"); + assert_eq!(ok.header("access-control-allow-credentials"), "true"); + + let evil = s.method(axum::http::Method::OPTIONS, "/auth/login") + .add_header("origin", "https://evil.example") + .add_header("access-control-request-method", "POST") + .await; + assert!(evil.maybe_header("access-control-allow-origin").is_none()); +} +``` + +- [ ] **Step 2: Add the layer (outermost, so preflights skip identity/rate limits)** + +```rust +use axum::http::{Method, header::{AUTHORIZATION, CONTENT_TYPE}}; +use tower_http::cors::CorsLayer; + +let cors = CorsLayer::new() + .allow_origin(HeaderValue::from_str(&cfg.site_origin).expect("SITE_ORIGIN is a valid origin")) + .allow_credentials(true) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) + .allow_headers([AUTHORIZATION, CONTENT_TYPE]); +// ... existing chain ... + .layer(cors) +``` + +- [ ] **Step 3: Manual end-to-end check (both services, real DB)** + +```bash +cd backend +set -a; . ./.env; set +a # local .env with real secrets (gitignored) +CARGO_BUILD_JOBS=4 cargo run -p accounts-service & +CARGO_BUILD_JOBS=4 cargo run -p gateway & +sleep 5 +curl -s -X POST localhost:8080/auth/register -H 'content-type: application/json' \ + -d '{"email":"e2e@example.com","password":"correct-horse-battery-staple","nick":"E2E"}' +TOKEN=$(curl -s -c /tmp/lv.jar -X POST localhost:8080/auth/login -H 'content-type: application/json' \ + -d '{"email":"e2e@example.com","password":"correct-horse-battery-staple"}' | sed 's/.*"access_token":"\([^"]*\)".*/\1/') +curl -s localhost:8080/me -H "authorization: Bearer $TOKEN" # -> profile JSON +curl -s -b /tmp/lv.jar -X POST localhost:8080/auth/refresh # -> new access_token +curl -s -o /dev/null -w '%{http_code}\n' localhost:8081/me # -> 403 (direct access blocked) +kill %1 %2 +``` +Expected: register 201, `/me` via gateway 200, refresh 200, direct service access 403. Delete the e2e account afterwards (`DELETE FROM accounts WHERE email='e2e@example.com'`). + +- [ ] **Step 4: Docs** + +- `backend/STRUCTURE.md`: mark `gateway/` and `common/` as implemented, describe the internal contract (internal key + identity header, gRPC for device auth) in 3–5 lines. +- `TODO.md` Фаза 10 status line: gateway + auth hardening done; next: `backend/configs-service/PLAN.md`. + +- [ ] **Step 5: Full workspace test + commit** + +Run: `cd backend && DATABASE_URL=... CARGO_BUILD_JOBS=4 cargo test --workspace` → all pass. +```bash +git add -A backend/gateway backend/STRUCTURE.md backend/.env.example TODO.md +git commit -m "feat(gateway): CORS for the site origin; docs for gateway and internal contract" +``` + +--- + +## Deferred (not in this plan) + +- Admin role enforcement at the gateway (`/admin/*`) — Подсистема 3 plan; `Identity` will then carry the role (add `role` to the access-token claims). +- WebSocket proxying for `chat-service` — Подсистема 2 plan. +- Caching `AuthenticateDevice` results (short TTL) if the per-request gRPC call ever shows up in latency numbers — measure first. +- Redis-backed rate limits for multi-node deploys — single node for now, per TODO §6. diff --git a/backend/gateway/README.md b/backend/gateway/README.md deleted file mode 100644 index 650d52f..0000000 --- a/backend/gateway/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# gateway (planned) - -Routing + single JWT check point + rate limits in front of `accounts-service`, -`configs-service`, `chat-service`. See `TODO.md` (Фаза 10) and -`backend/STRUCTURE.md`. No implementation plan yet — starts after -`accounts-service` (`backend/PLAN.md`) is done. diff --git a/frontend/ARCHITECTURE.md b/frontend/ARCHITECTURE.md index 11ddd04..fd6b2fb 100644 --- a/frontend/ARCHITECTURE.md +++ b/frontend/ARCHITECTURE.md @@ -67,9 +67,30 @@ frontend/src/ из `backend/PLAN.md` (Task 6: `RegisterResponse`, `LoginResponse` и т.д.), не `any`/`unknown` без сужения -## Тестирование (когда дойдёт до реализации) -Колокация тестов рядом с кодом (`Component.test.tsx` рядом с `Component.tsx`), -не отдельное дерево `tests/` — так тест физически не потеряется при -удалении/переносе фичи. +## Тестирование +Тесты живут в подпапке `tests/` внутри фичи (или `shared/*`), которую они +проверяют — не рядом с файлом (`Component.test.tsx`) и не в отдельном дереве +на верхнем уровне. Это всё ещё удаляется вместе с фичей при её удалении/ +переносе, но не съедает бюджет "≤4 файла на папку" — колокация `X.test.tsx` +рядом с `X.tsx` вдвое сокращала бы вместимость каждой папки под этим правилом. + +``` +frontend/src/ + features/ + auth/ + components/ + hooks/ + api.ts + types.ts + tests/ + api.test.ts + components.test.tsx + shared/ + ui/ + Button.tsx + TextField.tsx + tests/ + ui.test.tsx +``` Источники: [Robin Wieruch — React Folder Structure Best Practices 2026](https://www.robinwieruch.de/react-folder-structure/), [Mastering Modern React + Vite Folder Structure, Medium](https://sandeshrathnayake.medium.com/mastering-modern-react-vite-folder-structure-a-production-ready-guide-for-scalable-applications-9ad8e233f8b9). diff --git a/frontend/PLAN.md b/frontend/PLAN.md new file mode 100644 index 0000000..32aa3da --- /dev/null +++ b/frontend/PLAN.md @@ -0,0 +1,1273 @@ +# Frontend: LoVisual site (Подсистема 1) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** The account hub for LoVisual players: sign up / log in, link the game (device code), manage avatar and linked devices, keep 4 cloud config slots with share codes, and browse/copy configs from the public showcase. Plus a landing page that explains all of that in one screen. + +**Architecture:** React 19 SPA (Vite 8), feature folders per `frontend/ARCHITECTURE.md`. Server state via TanStack Query; one fetch wrapper (`shared/api/client.ts`) owns the access token (memory only), silent refresh through the httpOnly `lv_refresh` cookie, and error normalization. Routing via React Router 8 data router. The browser only ever talks to `/api/*` (Vite dev proxy → gateway on :8080; same path behind nginx in prod). + +**Tech Stack (installed and verified 2026-09-24, commit "chore(frontend): add router, react-query, fonts and vitest"):** React 19.2, Vite 8.3, TypeScript 7.0, Tailwind CSS 4.3 (`@tailwindcss/vite`), react-router 8.4 (`createBrowserRouter`/`createMemoryRouter`/`RouterProvider`/`Link`/`NavLink`/`Outlet`/`Navigate`/`useNavigate`/`useParams`/`useSearchParams` all exported from `react-router`), @tanstack/react-query 5.103, Vitest 5 + jsdom 30 + Testing Library (React 16, user-event 14, jest-dom 7 via `@testing-library/jest-dom/vitest`), fonts `@fontsource/comfortaa`, `@fontsource-variable/inter`, `@fontsource/iosevka`. Bun is the package manager (`bun add`, `bun run test`, `bun run build`, `bun run lint`). + +## Global Constraints + +- All user-facing text in Russian, sentence case, active voice. Code identifiers/comments in English. +- ≤250 lines per file, ≤4 files per folder (subfolders don't count). Tests live in a `tests/` subfolder of the feature/shared folder they cover (colocating `X.test.tsx` next to `X.tsx` would halve every folder's capacity under the 4-file rule) — Task 2 updates `ARCHITECTURE.md` accordingly. +- No `any`. DTOs typed from the backend contracts (`backend/PLAN.md`, `backend/gateway/PLAN.md` Tasks 3–6, `backend/configs-service/PLAN.md`). +- No barrel `index.ts` files. No business logic in `pages/`. +- Access token lives in memory only (never localStorage). The refresh token is never visible to JS. +- Every task: `bun run test`, `bun run build` (includes `tsc -b`) and `bun run lint` pass before commit. +- Commits: English, conventional, explicit paths (`git add frontend/...`), never anything under `mod/`. End with `Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ`. +- `erasableSyntaxOnly` is on: no TS parameter properties, no enums — use explicit fields and string-literal unions. + +--- + +## Design + +**Subject:** LoVisual is a legit visuals mod for Minecraft (HUD, particles, themes). The site's audience is its players; its main job is managing cloud configs and linking the game, with the landing explaining those two ideas. + +**Palette** — lifted from the mod's own default ClickGui theme (`mod/.../theme/presets/ThemePresets1.java`: window `#12191B`, header `#142226`, accent `#5CC8E7`, muted `#C6D2CD`), so the site feels like the in-game UI rather than a generic dark template: + +| Token | Hex | Role | +|---|---|---| +| `abyss` | `#0D1416` | page background (teal-tinted, not neutral black) | +| `slate` | `#142226` | panels, top bar (the mod's header colour) | +| `shoal` | `#1F3238` | borders, inputs, raised controls | +| `frost` | `#9FB3B0` | secondary text (≈8:1 on abyss) | +| `snow` | `#EEF5F3` | primary text | +| `ice` | `#5CC8E7` | the one accent: primary actions, focus, share codes | +| `ember` | `#E8835A` | destructive actions and errors only | + +**Type:** Comfortaa (the mod's own main-menu face; rounded, Cyrillic) for headings only; Inter Variable for everything else; Iosevka only where the text is literally typed in-game — share codes, the `%config load` command, device user codes. Scale (≈1.25): 14 / 16 / 20 / 25 / 31 / 39 px, hero 61 px. Body line length ≤ 70ch. + +**Layout:** Left-aligned, max width 1120px, 24px gutters (16px on phones). Top bar: logo left, three nav links (Витрина, Мои конфиги, Привязать игру), account button right. Panels (slate fill, 1px shoal border, 10px radius — the ClickGui window shape) are used only for things that *are* panels in the game: config slots, showcase entries, forms. Everything else sits directly on the page. + +``` +┌ LoVisual ─────── Витрина Мои конфиги Привязать игру ───── [Войти] ┐ +│ │ +│ Твои настройки LoVisual — │ +│ на любом компьютере │ +│ Четыре облачных слота, код для друзей и витрина готовых конфигов. │ +│ [Создать аккаунт] Смотреть витрину │ +│ │ +│ ┌ chat ─────────────────────────────────────────┐ │ +│ │ %config load 7KQ3M9XA▌ │ ← signature │ +│ │ [LoVisual] Конфиг «pvp» загружен │ │ +│ └───────────────────────────────────────────────┘ │ +│ │ +│ Четыре слота ┌slot1┐┌slot2┐ │ +│ ... └─────┘└─────┘ (2×2) │ +│ Привязка игры: 1 → 2 → 3 (real sequence, so numbered) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**Principles:** +1. Speak the game's language: codes and commands appear exactly as typed in-game, in Iosevka. +2. One signature moment: the hero's chat line types `%config load …` once on load, then the confirmation line appears. Nothing else animates on its own (`prefers-reduced-motion` → both lines shown immediately). +3. Panels only where the game has panels; no card grids as decoration. +4. Errors say what happened and what to do, never "Что-то пошло не так". + +*Review against generic defaults:* dark + single accent is on the list of AI defaults. It is kept deliberately because it is the product's real in-game palette; the teal-tinted base (not #111), the ClickGui panel shape, Comfortaa, and the chat-line hero are what make it specific. Rejected alternatives: a stat-number hero ("10 000 игроков") — no real numbers exist; an all-caps eyebrow over each section — the section headings carry that job. + +--- + +## File Structure + +``` +frontend/ + index.html # title, lang="ru", favicon + src/ + main.tsx App.tsx routes.tsx index.css + test/ setup.ts render.tsx fetch.ts smoke.test.tsx + shared/ + types.ts # Me (used by auth + account) + api/ client.ts errors.ts tests/client.test.ts + ui/ Button.tsx TextField.tsx ShareCode.tsx Notice.tsx tests/ui.test.tsx + layout/ AppShell.tsx TopBar.tsx tests/layout.test.tsx + features/ + auth/ api.ts session.tsx RequireAuth.tsx validation.ts + forms/ LoginForm.tsx RegisterForm.tsx + tests/ session.test.tsx forms.test.tsx + landing/ CommandHero.tsx HowItWorks.tsx tests/landing.test.tsx + device-link/ api.ts LinkDeviceForm.tsx tests/link.test.tsx + account/ api.ts components/ ProfileCard.tsx AvatarUpload.tsx DeviceList.tsx tests/account.test.tsx + configs/ api.ts types.ts components/ SlotGrid.tsx SlotPanel.tsx PublishForm.tsx tests/configs.test.tsx + showcase/ api.ts types.ts components/ ListingList.tsx ListingItem.tsx ListingDetail.tsx CopyToSlot.tsx tests/showcase.test.tsx + pages/ + public/ HomePage.tsx LoginPage.tsx RegisterPage.tsx NotFoundPage.tsx + app/ LinkPage.tsx AccountPage.tsx ConfigsPage.tsx + showcase/ ShowcasePage.tsx ListingPage.tsx +``` + +--- + +### Task 1: Tooling — DONE + +Installed router/query/fonts/test tooling, Vitest config in `vite.config.ts` (jsdom, `src/test/setup.ts`), dev proxy `/api` → `http://localhost:8080` with `cookiePathRewrite: { '/auth': '/api/auth' }` (the backend scopes `lv_refresh` to `Path=/auth`), scaffold demo removed, smoke test. Nothing to do. + +--- + +### Task 2: Design tokens, fonts, shared UI kit + +**Files:** +- Modify: `src/index.css`, `index.html`, `frontend/ARCHITECTURE.md` (tests in `tests/` subfolders) +- Create: `src/shared/ui/{Button,TextField,ShareCode,Notice}.tsx`, `src/shared/ui/tests/ui.test.tsx` + +**Interfaces (produces):** +- `Button` props: `ButtonHTMLAttributes & { variant?: 'primary' | 'quiet' | 'danger'; busy?: boolean }` — `busy` disables and sets `aria-busy`. +- `TextField` props: `InputHTMLAttributes & { label: string; error?: string; hint?: string }` — label wired via `useId`, error via `aria-invalid` + `aria-describedby`. +- `ShareCode` props: `{ code: string; label?: string }` — shows the code in Iosevka + a «Скопировать» button (clipboard, then «Скопировано» for 2s, announced via `aria-live="polite"`). +- `Notice` props: `{ tone: 'info' | 'error'; children: ReactNode }` — `role="alert"` for error, `role="status"` for info. + +- [ ] **Step 1: Tokens and fonts — `src/index.css`** + +```css +@import "tailwindcss"; +@import "@fontsource-variable/inter"; +@import "@fontsource/comfortaa/400.css"; +@import "@fontsource/comfortaa/700.css"; +@import "@fontsource/iosevka/400.css"; +@import "@fontsource/iosevka/700.css"; + +@theme { + --color-abyss: #0d1416; + --color-slate: #142226; + --color-shoal: #1f3238; + --color-frost: #9fb3b0; + --color-snow: #eef5f3; + --color-ice: #5cc8e7; + --color-ember: #e8835a; + + --font-display: "Comfortaa", ui-rounded, system-ui, sans-serif; + --font-sans: "Inter Variable", system-ui, sans-serif; + --font-code: "Iosevka", ui-monospace, monospace; + + --text-hero: 3.8125rem; + --text-hero--line-height: 1.05; + --radius-panel: 10px; +} + +@layer base { + html { color-scheme: dark; } + body { + @apply bg-abyss text-snow font-sans antialiased; + min-height: 100dvh; + } + h1, h2, h3 { @apply font-display font-bold; } + :focus-visible { outline: 2px solid var(--color-ice); outline-offset: 2px; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } +} +``` +`index.html`: `lang="ru"`, `LoVisual — аккаунт и облачные конфиги`, ``, ``. + +- [ ] **Step 2: Failing tests — `src/shared/ui/tests/ui.test.tsx`** + +```tsx +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' +import { Button } from '../Button' +import { TextField } from '../TextField' +import { ShareCode } from '../ShareCode' +import { Notice } from '../Notice' + +test('busy button is disabled and announces busy', () => { + render() + const b = screen.getByRole('button', { name: 'Сохранить' }) + expect(b).toBeDisabled() + expect(b).toHaveAttribute('aria-busy', 'true') +}) + +test('text field links label and error', () => { + render() + const input = screen.getByLabelText('Почта') + expect(input).toHaveAttribute('aria-invalid', 'true') + expect(input).toHaveAccessibleDescription('Введи почту') +}) + +test('share code copies to clipboard and confirms', async () => { + const user = userEvent.setup() + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + render() + expect(screen.getByText('7KQ3M9XA')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'Скопировать' })) + expect(writeText).toHaveBeenCalledWith('7KQ3M9XA') + expect(await screen.findByText('Скопировано')).toBeInTheDocument() +}) + +test('error notice is an alert', () => { + render(Неверный код) + expect(screen.getByRole('alert')).toHaveTextContent('Неверный код') +}) +``` +(`userEvent.setup()` installs a clipboard stub on `navigator.clipboard`, which is why `spyOn` works after it.) + +Run: `bun run test` → FAIL. + +- [ ] **Step 3: Implement the kit** + +`Button.tsx`: +```tsx +import type { ButtonHTMLAttributes } from 'react' + +type Variant = 'primary' | 'quiet' | 'danger' + +const VARIANTS: Record = { + primary: 'bg-ice text-abyss hover:bg-ice/90', + quiet: 'bg-transparent text-snow border border-shoal hover:border-frost', + danger: 'bg-transparent text-ember border border-ember/60 hover:border-ember', +} + +export function Button({ + variant = 'primary', + busy = false, + disabled, + className = '', + type = 'button', + ...rest +}: ButtonHTMLAttributes & { variant?: Variant; busy?: boolean }) { + return ( + + {copied ? 'Скопировано' : ''} + + ) +} +``` +`Notice.tsx`: +```tsx +import type { ReactNode } from 'react' + +export function Notice({ tone, children }: { tone: 'info' | 'error'; children: ReactNode }) { + const styles = tone === 'error' ? 'border-ember/60 text-ember' : 'border-shoal text-frost' + return ( +
+ {children} +
+ ) +} +``` + +- [ ] **Step 4: ARCHITECTURE.md** + +Replace the "Тестирование" section's colocation rule with: tests live in a `tests/` subfolder inside the feature (or `shared/*`) folder they cover — still deleted together with the feature, but they don't eat into the 4-files-per-folder budget. Update the example tree accordingly. + +- [ ] **Step 5: test/build/lint pass; commit** + +```bash +git add frontend/src/index.css frontend/index.html frontend/ARCHITECTURE.md frontend/src/shared +git commit -m "feat(frontend): design tokens from the mod theme, fonts and shared UI kit" +``` + +--- + +### Task 3: API client + test helpers + +**Files:** +- Create: `src/shared/api/client.ts`, `src/shared/api/errors.ts`, `src/shared/api/tests/client.test.ts`, `src/shared/types.ts`, `src/test/fetch.ts`, `src/test/render.tsx` + +**Interfaces (produces):** +- `class ApiError extends Error { status: number; retryAfter: number | null }`. +- `createApiClient(baseUrl?: string, fetchImpl?: typeof fetch): ApiClient`, `ApiClient = { request(path, options?: RequestOptions): Promise; refresh(): Promise; setAccessToken(t: string | null): void; hasAccessToken(): boolean }`, `RequestOptions = { method?: string; json?: unknown; body?: BodyInit; signal?: AbortSignal }`. Singleton `api = createApiClient()`. +- Behaviour: JSON body → `content-type: application/json`; token → `authorization: Bearer`; `credentials: 'include'` always; on 401 for a non-`/auth/*` path → one shared (single-flight) `POST /auth/refresh`, then one retry; non-2xx → `ApiError` with the backend's `{error}` message and `Retry-After` seconds; 204/empty body → `undefined`. +- `errors.ts`: `describeError(err: unknown, overrides?: Partial>): string` — Russian message per status (429 includes the wait: «Слишком много попыток. Подожди N с.»; network failure → «Нет связи с сервером. Проверь интернет и попробуй ещё раз.»; 5xx → «Сервер не ответил. Попробуй через минуту.»; 404 → «Не найдено.»; default 400 → «Проверь введённые данные.»). +- `shared/types.ts`: `Me = { id: string; email: string; display_nick: string; role: 'user' | 'admin'; avatar_url: string | null; created_at: string }`. +- `test/fetch.ts`: `mockFetch(routes: Record Response | Promise>)` — key `"METHOD /path"` (path without `/api`, query ignored); unknown routes → 404; returns the `vi.fn` for call assertions; installs via `vi.stubGlobal('fetch', …)`. `json(body, status = 200, headers?)` helper. +- `test/render.tsx`: `renderApp(routes: RouteObject[], initialPath: string)` → wraps in a fresh `QueryClient` (`retry: false`) + `SessionProvider` (Task 4 — until then only QueryClient) + `createMemoryRouter`; returns Testing Library result + `router`. + +- [ ] **Step 1: Test helpers first** + +`src/test/fetch.ts`: +```ts +import { vi } from 'vitest' + +type Handler = (init: RequestInit) => Response | Promise + +export function json(body: unknown, status = 200, headers: Record = {}): Response { + return new Response(body === undefined ? null : JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json', ...headers }, + }) +} + +export function mockFetch(routes: Record) { + const fn = vi.fn(async (input: RequestInfo | URL, init: RequestInit = {}) => { + const url = new URL(String(input), 'http://localhost') + const key = `${(init.method ?? 'GET').toUpperCase()} ${url.pathname.replace(/^\/api/, '')}` + const handler = routes[key] + return handler ? handler(init) : json({ error: `no mock for ${key}` }, 404) + }) + vi.stubGlobal('fetch', fn) + return fn +} +``` +Add `afterEach(() => vi.unstubAllGlobals())` to `src/test/setup.ts`. + +- [ ] **Step 2: Failing client tests — `src/shared/api/tests/client.test.ts`** + +```ts +import { expect, test } from 'vitest' +import { ApiError, createApiClient } from '../client' +import { json, mockFetch } from '../../../test/fetch' + +test('sends json with bearer token and cookies', async () => { + const fetch = mockFetch({ 'PUT /configs/1': () => json({ ok: true }) }) + const api = createApiClient() + api.setAccessToken('tok') + await api.request('/configs/1', { method: 'PUT', json: { a: 1 } }) + const [, init] = fetch.mock.calls[0] + const headers = new Headers(init?.headers) + expect(headers.get('authorization')).toBe('Bearer tok') + expect(headers.get('content-type')).toBe('application/json') + expect(init?.credentials).toBe('include') + expect(init?.body).toBe('{"a":1}') +}) + +test('401 triggers one refresh and one retry', async () => { + let calls = 0 + const fetch = mockFetch({ + 'GET /me': () => (++calls === 1 ? json({ error: 'unauthorized' }, 401) : json({ id: 'x' })), + 'POST /auth/refresh': () => json({ access_token: 'new' }), + }) + const api = createApiClient() + await expect(api.request('/me')).resolves.toEqual({ id: 'x' }) + expect(fetch.mock.calls.filter(([u]) => String(u).endsWith('/auth/refresh'))).toHaveLength(1) + expect(new Headers(fetch.mock.calls[2][1]?.headers).get('authorization')).toBe('Bearer new') +}) + +test('concurrent 401s share a single refresh', async () => { + let refreshes = 0 + mockFetch({ + 'GET /a': (init) => (new Headers(init.headers).get('authorization') ? json(1) : json({}, 401)), + 'GET /b': (init) => (new Headers(init.headers).get('authorization') ? json(2) : json({}, 401)), + 'POST /auth/refresh': () => { refreshes++; return json({ access_token: 't' }) }, + }) + const api = createApiClient() + await Promise.all([api.request('/a'), api.request('/b')]) + expect(refreshes).toBe(1) +}) + +test('failed refresh surfaces the original 401 and clears the token', async () => { + mockFetch({ + 'GET /me': () => json({ error: 'unauthorized' }, 401), + 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401), + }) + const api = createApiClient() + api.setAccessToken('stale') + await expect(api.request('/me')).rejects.toMatchObject({ status: 401 }) + expect(api.hasAccessToken()).toBe(false) +}) + +test('auth endpoints never trigger refresh', async () => { + const fetch = mockFetch({ 'POST /auth/login': () => json({ error: 'unauthorized' }, 401) }) + await expect(createApiClient().request('/auth/login', { method: 'POST', json: {} })).rejects.toBeInstanceOf(ApiError) + expect(fetch).toHaveBeenCalledTimes(1) +}) + +test('429 carries retry-after; 204 resolves undefined', async () => { + mockFetch({ + 'POST /auth/login': () => json({ error: 'too many requests' }, 429, { 'retry-after': '42' }), + 'DELETE /device/links/1': () => new Response(null, { status: 204 }), + }) + const api = createApiClient() + await expect(api.request('/auth/login', { method: 'POST' })).rejects.toMatchObject({ status: 429, retryAfter: 42 }) + await expect(api.request('/device/links/1', { method: 'DELETE' })).resolves.toBeUndefined() +}) +``` + +- [ ] **Step 3: Implement `client.ts`** + +```ts +export class ApiError extends Error { + readonly status: number + readonly retryAfter: number | null + + constructor(status: number, message: string, retryAfter: number | null = null) { + super(message) + this.name = 'ApiError' + this.status = status + this.retryAfter = retryAfter + } +} + +export type RequestOptions = { method?: string; json?: unknown; body?: BodyInit; signal?: AbortSignal } + +export type ApiClient = { + request(path: string, options?: RequestOptions): Promise + refresh(): Promise + setAccessToken(token: string | null): void + hasAccessToken(): boolean +} + +async function toError(res: Response): Promise { + let message = `HTTP ${res.status}` + try { + const body: unknown = await res.json() + if (body && typeof body === 'object' && 'error' in body && typeof body.error === 'string') message = body.error + } catch { + // non-JSON error body: keep the status text + } + const retry = Number(res.headers.get('retry-after')) + return new ApiError(res.status, message, Number.isFinite(retry) && retry > 0 ? retry : null) +} + +export function createApiClient( + baseUrl = '/api', + // Resolved at call time so tests can stub the global fetch. + fetchImpl: typeof fetch = (input, init) => fetch(input, init), +): ApiClient { + let accessToken: string | null = null + let refreshing: Promise | null = null + + async function refreshOnce(): Promise { + try { + const res = await fetchImpl(`${baseUrl}/auth/refresh`, { method: 'POST', credentials: 'include' }) + if (!res.ok) { + accessToken = null + return false + } + const body = (await res.json()) as { access_token: string } + accessToken = body.access_token + return true + } catch { + return false + } + } + + function refresh(): Promise { + refreshing ??= refreshOnce().finally(() => { + refreshing = null + }) + return refreshing + } + + function send(path: string, options: RequestOptions): Promise { + const headers = new Headers() + let body = options.body + if (options.json !== undefined) { + headers.set('content-type', 'application/json') + body = JSON.stringify(options.json) + } + if (accessToken) headers.set('authorization', `Bearer ${accessToken}`) + return fetchImpl(`${baseUrl}${path}`, { + method: options.method ?? 'GET', + headers, + body, + credentials: 'include', + signal: options.signal, + }) + } + + async function request(path: string, options: RequestOptions = {}): Promise { + let res = await send(path, options) + if (res.status === 401 && !path.startsWith('/auth/') && (await refresh())) { + res = await send(path, options) + } + if (!res.ok) throw await toError(res) + const text = await res.text() + return (text ? JSON.parse(text) : undefined) as T + } + + return { + request, + refresh, + setAccessToken: (token) => { + accessToken = token + }, + hasAccessToken: () => accessToken !== null, + } +} + +export const api = createApiClient() +``` + +- [ ] **Step 4: `errors.ts`, `shared/types.ts`, `test/render.tsx`** + +```ts +// errors.ts +import { ApiError } from './client' + +const BY_STATUS: Record = { + 400: 'Проверь введённые данные.', + 401: 'Нужно войти в аккаунт.', + 403: 'Недостаточно прав.', + 404: 'Не найдено.', + 409: 'Уже существует.', +} + +export function describeError(err: unknown, overrides: Partial> = {}): string { + if (err instanceof ApiError) { + if (err.status === 429) { + return err.retryAfter ? `Слишком много попыток. Подожди ${err.retryAfter} с.` : 'Слишком много попыток. Подожди немного.' + } + if (err.status >= 500) return 'Сервер не ответил. Попробуй через минуту.' + return overrides[err.status] ?? BY_STATUS[err.status] ?? 'Запрос не прошёл. Попробуй ещё раз.' + } + return 'Нет связи с сервером. Проверь интернет и попробуй ещё раз.' +} +``` +```ts +// shared/types.ts +export type Me = { + id: string + email: string + display_nick: string + role: 'user' | 'admin' + avatar_url: string | null + created_at: string +} +``` +```tsx +// test/render.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render } from '@testing-library/react' +import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router' + +export function renderApp(routes: RouteObject[], initialPath = '/') { + const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) + const router = createMemoryRouter(routes, { initialEntries: [initialPath] }) + const result = render( + + + , + ) + return { ...result, router, client } +} +``` +Add one test for `describeError` (429 with and without `retryAfter`, a `TypeError` → network message) to `client.test.ts`. + +- [ ] **Step 5: pass; commit** + +```bash +git add frontend/src/shared frontend/src/test +git commit -m "feat(frontend): API client with silent refresh, error descriptions and test helpers" +``` + +--- + +### Task 4: Session, login, register, route guard + +**Files:** +- Create: `src/features/auth/{api.ts,session.tsx,RequireAuth.tsx,validation.ts}`, `src/features/auth/forms/{LoginForm,RegisterForm}.tsx`, `src/features/auth/tests/{session,forms}.test.tsx`, `src/pages/public/{LoginPage,RegisterPage}.tsx` +- Modify: `src/test/render.tsx` (wrap in `SessionProvider`) + +**Interfaces:** +- `auth/api.ts`: `login(email, password): Promise` (stores token via `api.setAccessToken`), `register(email, password, nick): Promise`, `logout(): Promise` (POST `/auth/logout`, clears token even if it fails), `fetchMe(): Promise`. +- `auth/session.tsx`: `SessionProvider`, `useSession(): { status: 'loading' | 'anonymous' | 'authenticated'; me: Me | null; signIn(email, password): Promise; signUp(email, password, nick): Promise; signOut(): Promise }`. On mount: `api.refresh()`; if true → load `['me']`. `signIn` → login + refetch me. `signUp` → register then signIn. `signOut` → logout, `queryClient.clear()`. +- `auth/validation.ts` (mirrors the backend so 400s are rare): `validateEmail`, `validatePassword` (8+ chars, ≤256 bytes via `new TextEncoder().encode(p).length`), `validateNick` (1..32 chars after trim) — each returns `string | undefined` (Russian message). +- `RequireAuth`: loading → quiet placeholder `

Проверяем вход…

`; anonymous → ``; else ``. +- Login form: fields «Почта», «Пароль»; submit «Войти»; 401 → «Неверная почта или пароль.»; success → navigate to `state.from ?? '/configs'`. Register form: «Почта», «Ник на сайте» (hint «Не обязательно совпадает с ником в Minecraft.»), «Пароль» (hint «Минимум 8 символов.»); submit «Создать аккаунт»; 409 → «Эта почта уже занята. Войди или используй другую.» + +- [ ] **Step 1: Failing tests** + +`tests/session.test.tsx`: +```tsx +import { screen } from '@testing-library/react' +import { expect, test } from 'vitest' +import { RequireAuth } from '../RequireAuth' +import { useSession } from '../session' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +const me = { id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '2026-09-24T00:00:00Z' } + +function Whoami() { + const { me } = useSession() + return

Привет, {me?.display_nick}

+} + +const routes = [ + { path: '/login', element:

Страница входа

}, + { element: , children: [{ path: '/configs', element: }] }, +] + +test('restores the session from the refresh cookie', async () => { + mockFetch({ 'POST /auth/refresh': () => json({ access_token: 't' }), 'GET /me': () => json(me) }) + renderApp(routes, '/configs') + expect(await screen.findByText('Привет, Rider')).toBeInTheDocument() +}) + +test('anonymous visitor is sent to login', async () => { + mockFetch({ 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401) }) + renderApp(routes, '/configs') + expect(await screen.findByText('Страница входа')).toBeInTheDocument() +}) +``` +`tests/forms.test.tsx`: +```tsx +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test } from 'vitest' +import { LoginForm } from '../forms/LoginForm' +import { RegisterForm } from '../forms/RegisterForm' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +const me = { id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '2026-09-24T00:00:00Z' } +const anon = { 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401) } + +test('wrong password shows a clear error', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/login': () => json({ error: 'unauthorized' }, 401) }) + renderApp([{ path: '/login', element: }], '/login') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Пароль'), 'wrong-password') + await user.click(screen.getByRole('button', { name: 'Войти' })) + expect(await screen.findByRole('alert')).toHaveTextContent('Неверная почта или пароль.') +}) + +test('successful login goes to configs', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/login': () => json({ access_token: 't' }), 'GET /me': () => json(me) }) + renderApp([{ path: '/login', element: }, { path: '/configs', element:

Мои конфиги

}], '/login') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Пароль'), 'correct-horse') + await user.click(screen.getByRole('button', { name: 'Войти' })) + expect(await screen.findByText('Мои конфиги')).toBeInTheDocument() +}) + +test('register validates before calling the server', async () => { + const user = userEvent.setup() + const fetch = mockFetch(anon) + renderApp([{ path: '/register', element: }], '/register') + await user.type(await screen.findByLabelText('Почта'), 'not-an-email') + await user.type(screen.getByLabelText('Пароль'), 'short') + await user.click(screen.getByRole('button', { name: 'Создать аккаунт' })) + expect(screen.getByLabelText('Почта')).toHaveAttribute('aria-invalid', 'true') + expect(screen.getByLabelText('Пароль')).toHaveAccessibleDescription('Минимум 8 символов.') + expect(fetch.mock.calls.some(([u]) => String(u).includes('/auth/register'))).toBe(false) +}) + +test('taken email explains what to do', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/register': () => json({ error: 'email already registered' }, 409) }) + renderApp([{ path: '/register', element: }], '/register') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Ник на сайте'), 'Rider') + await user.type(screen.getByLabelText('Пароль'), 'correct-horse') + await user.click(screen.getByRole('button', { name: 'Создать аккаунт' })) + expect(await screen.findByRole('alert')).toHaveTextContent('Эта почта уже занята') +}) +``` + +- [ ] **Step 2: Implement `api.ts`, `validation.ts`** + +```ts +// api.ts +import { api } from '../../shared/api/client' +import type { Me } from '../../shared/types' + +export async function login(email: string, password: string): Promise { + const res = await api.request<{ access_token: string }>('/auth/login', { method: 'POST', json: { email, password } }) + api.setAccessToken(res.access_token) +} + +export async function register(email: string, password: string, nick: string): Promise { + await api.request('/auth/register', { method: 'POST', json: { email, password, nick } }) +} + +export async function logout(): Promise { + try { + await api.request('/auth/logout', { method: 'POST' }) + } finally { + api.setAccessToken(null) + } +} + +export const fetchMe = () => api.request('/me') +``` +```ts +// validation.ts +export function validateEmail(email: string): string | undefined { + const v = email.trim() + if (!v) return 'Введи почту.' + if (v.length > 254 || !/^[^@\s]+@[^@\s]+$/.test(v)) return 'Похоже, в почте опечатка.' +} + +export function validatePassword(password: string): string | undefined { + if ([...password].length < 8) return 'Минимум 8 символов.' + if (new TextEncoder().encode(password).length > 256) return 'Слишком длинный пароль.' +} + +export function validateNick(nick: string): string | undefined { + const len = [...nick.trim()].length + if (len === 0) return 'Придумай ник.' + if (len > 32) return 'Не длиннее 32 символов.' +} +``` + +- [ ] **Step 3: Implement `session.tsx`** + +```tsx +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react' +import { api } from '../../shared/api/client' +import type { Me } from '../../shared/types' +import { fetchMe, login, logout, register } from './api' + +type Session = { + status: 'loading' | 'anonymous' | 'authenticated' + me: Me | null + signIn(email: string, password: string): Promise + signUp(email: string, password: string, nick: string): Promise + signOut(): Promise +} + +const SessionContext = createContext(null) + +export function SessionProvider({ children }: { children: ReactNode }) { + const client = useQueryClient() + // Restores the session after a reload: the refresh cookie survives, the + // in-memory access token does not. + const restored = useQuery({ queryKey: ['session-restore'], queryFn: () => api.refresh(), staleTime: Infinity }) + const meQuery = useQuery({ queryKey: ['me'], queryFn: fetchMe, enabled: restored.data === true }) + + const signIn = useCallback(async (email: string, password: string) => { + await login(email, password) + client.setQueryData(['session-restore'], true) + await client.fetchQuery({ queryKey: ['me'], queryFn: fetchMe }) + }, [client]) + + const signUp = useCallback(async (email: string, password: string, nick: string) => { + await register(email, password, nick) + await signIn(email, password) + }, [signIn]) + + const signOut = useCallback(async () => { + await logout() + client.clear() + client.setQueryData(['session-restore'], false) + }, [client]) + + const value = useMemo(() => { + const me = meQuery.data ?? null + const status = restored.isPending || (restored.data && meQuery.isPending) ? 'loading' : me ? 'authenticated' : 'anonymous' + return { status, me, signIn, signUp, signOut } + }, [restored.isPending, restored.data, meQuery.isPending, meQuery.data, signIn, signUp, signOut]) + + return {children} +} + +export function useSession(): Session { + const session = useContext(SessionContext) + if (!session) throw new Error('useSession outside SessionProvider') + return session +} +``` +(React 19: a context object renders directly as a provider.) Update `test/render.tsx` to wrap `RouterProvider` in `` (inside `QueryClientProvider`). + +- [ ] **Step 4: `RequireAuth.tsx`, forms, pages** + +```tsx +// RequireAuth.tsx +import { Navigate, Outlet, useLocation } from 'react-router' +import { useSession } from './session' + +export function RequireAuth() { + const { status } = useSession() + const location = useLocation() + if (status === 'loading') return

Проверяем вход…

+ if (status === 'anonymous') return + return +} +``` +```tsx +// forms/LoginForm.tsx +import { useState, type FormEvent } from 'react' +import { Link, useLocation, useNavigate } from 'react-router' +import { describeError } from '../../../shared/api/errors' +import { Button } from '../../../shared/ui/Button' +import { Notice } from '../../../shared/ui/Notice' +import { TextField } from '../../../shared/ui/TextField' +import { useSession } from '../session' + +export function LoginForm() { + const { signIn } = useSession() + const navigate = useNavigate() + const from = (useLocation().state as { from?: string } | null)?.from ?? '/configs' + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState() + const [busy, setBusy] = useState(false) + + async function submit(e: FormEvent) { + e.preventDefault() + setBusy(true) + setError(undefined) + try { + await signIn(email.trim(), password) + navigate(from, { replace: true }) + } catch (err) { + setError(describeError(err, { 401: 'Неверная почта или пароль.' })) + } finally { + setBusy(false) + } + } + + return ( +
+ setEmail(e.target.value)} /> + setPassword(e.target.value)} /> + {error ? {error} : null} + +

+ Нет аккаунта? Создать +

+ + ) +} +``` +`RegisterForm.tsx` follows the same shape: three fields, per-field `error` from `validation.ts` computed on submit (fields keep their `hint` otherwise), no request when any field is invalid, `describeError(err, { 409: 'Эта почта уже занята. Войди или используй другую.' })`, success → `navigate('/configs', { replace: true })`, footer «Уже есть аккаунт? Войти». Password field: `hint="Минимум 8 символов."`, and when its validation fails show the validation message as `error` (so the test's accessible description `Минимум 8 символов.` holds either way). + +Pages (thin): `LoginPage.tsx` +```tsx +import { LoginForm } from '../../features/auth/forms/LoginForm' + +export default function LoginPage() { + return ( +
+

Вход

+ +
+ ) +} +``` +`RegisterPage.tsx` the same with «Новый аккаунт» and a one-line lead: «Аккаунт нужен для облачных конфигов и привязки игры. Пароль в мод вводить не придётся.» + +- [ ] **Step 5: pass; commit** + +```bash +git add frontend/src/features/auth frontend/src/pages frontend/src/test +git commit -m "feat(frontend): session restore, login and registration with client-side validation" +``` + +--- + +### Task 5: App shell, routes, landing page + +**Files:** +- Create: `src/routes.tsx`, `src/shared/layout/{AppShell,TopBar}.tsx`, `src/shared/layout/tests/layout.test.tsx`, `src/features/landing/{CommandHero,HowItWorks}.tsx`, `src/features/landing/tests/landing.test.tsx`, `src/pages/public/{HomePage,NotFoundPage}.tsx` +- Modify: `src/App.tsx`, `src/test/smoke.test.tsx` (delete — superseded by layout tests) + +**Interfaces:** +- `routes: RouteObject[]` in `routes.tsx` — root `{ element: , children: [...] }`. Pages from Tasks 6–9 are added there by those tasks; lazy-load them with `lazy: () => import('./pages/app/ConfigsPage').then((m) => ({ Component: m.default }))`. +- `App.tsx`: `QueryClientProvider` (singleton `QueryClient`, `staleTime: 30_000`) → `SessionProvider` → `RouterProvider router={createBrowserRouter(routes)}`. +- `TopBar`: logo «LoVisual» (Comfortaa, links to `/`); `NavLink`s «Витрина» `/showcase`, «Мои конфиги» `/configs`, «Привязать игру» `/link` (active link: `text-snow`, inactive `text-frost`); right side: anonymous → «Войти» (quiet button-styled link to `/login`); authenticated → avatar (or first letter of nick in a shoal circle) + nick linking to `/account`. On < 640px nav collapses under a «Меню» disclosure button (`aria-expanded`). +- `CommandHero`: headline «Твои настройки LoVisual — на любом компьютере», lead «Четыре облачных слота, код, которым делишься с друзьями, и витрина готовых конфигов.», CTAs «Создать аккаунт» (→ `/register`, primary; replaced by «Мои конфиги» → `/configs` when authenticated) and «Смотреть витрину» (quiet). Below: the chat bar (`bg-black/55`, Iosevka, like Minecraft's chat) that types `%config load 7KQ3M9XA` (60ms/char, once, via `setInterval` in an effect) then shows `[LoVisual] Конфиг «pvp» загружен` in ice. With `matchMedia('(prefers-reduced-motion: reduce)').matches` both lines render complete immediately. The whole bar has `aria-label="Пример: загрузка конфига по коду в чате игры"` and the animated text is `aria-hidden`, so screen readers get one stable sentence. +- `HowItWorks`: section «Четыре слота в облаке» (2×2 grid of small slate panels: «pvp», «стрим», «ленивый», «пусто» in frost italics) with one sentence each side; section «Привязка игры» as an `
    ` of three steps: «В моде открой вкладку аккаунта — там появится код.» / «Введи код на странице «Привязать игру».» / «Готово: мод входит в аккаунт сам, пароль ему не нужен.»; closing line «Любой конфиг из витрины можно скопировать в свободный слот одной кнопкой.» with a link «Открыть витрину». +- `NotFoundPage`: «Такой страницы нет» + link «На главную». + +- [ ] **Step 1: Failing tests** + +`layout.test.tsx`: +```tsx +import { screen } from '@testing-library/react' +import { expect, test } from 'vitest' +import { routes } from '../../../routes' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +test('anonymous visitor sees nav and a login link', async () => { + mockFetch({ 'POST /auth/refresh': () => json({}, 401) }) + renderApp(routes, '/') + expect(await screen.findByRole('link', { name: 'Войти' })).toHaveAttribute('href', '/login') + expect(screen.getByRole('link', { name: 'Витрина' })).toBeInTheDocument() + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('на любом компьютере') +}) + +test('signed-in visitor sees their nick instead of login', async () => { + mockFetch({ + 'POST /auth/refresh': () => json({ access_token: 't' }), + 'GET /me': () => json({ id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '' }), + }) + renderApp(routes, '/') + expect(await screen.findByRole('link', { name: /Rider/ })).toHaveAttribute('href', '/account') + expect(screen.queryByRole('link', { name: 'Войти' })).not.toBeInTheDocument() +}) + +test('unknown path shows not found', async () => { + mockFetch({ 'POST /auth/refresh': () => json({}, 401) }) + renderApp(routes, '/definitely-not-here') + expect(await screen.findByRole('heading', { name: 'Такой страницы нет' })).toBeInTheDocument() +}) +``` +`landing.test.tsx`: +```tsx +import { render, screen } from '@testing-library/react' +import { expect, test, vi } from 'vitest' +import { CommandHero } from '../CommandHero' +import { createMemoryRouter, RouterProvider } from 'react-router' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { SessionProvider } from '../../auth/session' +import { json, mockFetch } from '../../../test/fetch' + +test('reduced motion shows the full command at once', async () => { + vi.stubGlobal('matchMedia', (q: string) => ({ matches: q.includes('reduce'), addEventListener() {}, removeEventListener() {} })) + mockFetch({ 'POST /auth/refresh': () => json({}, 401) }) + const router = createMemoryRouter([{ path: '/', element: }]) + render( + + + , + ) + expect(await screen.findByText('%config load 7KQ3M9XA')).toBeInTheDocument() + expect(screen.getByText('[LoVisual] Конфиг «pvp» загружен')).toBeInTheDocument() + expect(screen.getByLabelText('Пример: загрузка конфига по коду в чате игры')).toBeInTheDocument() +}) +``` +(jsdom has no `matchMedia`; `CommandHero` must guard `typeof window.matchMedia === 'function'` and treat "unknown" as reduced motion — safe default, also keeps other tests deterministic.) + +- [ ] **Step 2: Implement** `AppShell` (TopBar + `
    ` + ``), `TopBar`, `CommandHero`, `HowItWorks`, pages, `routes.tsx`, `App.tsx` per the interfaces above. `routes.tsx` for now: + +```tsx +import type { RouteObject } from 'react-router' +import { RequireAuth } from './features/auth/RequireAuth' +import { AppShell } from './shared/layout/AppShell' +import HomePage from './pages/public/HomePage' +import LoginPage from './pages/public/LoginPage' +import RegisterPage from './pages/public/RegisterPage' +import NotFoundPage from './pages/public/NotFoundPage' + +export const routes: RouteObject[] = [ + { + element: , + children: [ + { path: '/', element: }, + { path: '/login', element: }, + { path: '/register', element: }, + { element: , children: [] }, // Tasks 6–8 add /link, /account, /configs + { path: '*', element: }, + ], + }, +] +``` +`shared/layout` importing from `features/auth/session` is the one allowed shared→feature edge (the shell needs the session); note it in `ARCHITECTURE.md`. + +The typing effect in `CommandHero`: +```tsx +const COMMAND = '%config load 7KQ3M9XA' + +function prefersReducedMotion(): boolean { + return typeof window.matchMedia !== 'function' || window.matchMedia('(prefers-reduced-motion: reduce)').matches +} + +// inside the component +const [typed, setTyped] = useState(() => (prefersReducedMotion() ? COMMAND.length : 0)) +useEffect(() => { + if (typed >= COMMAND.length) return + const t = setInterval(() => setTyped((n) => Math.min(n + 1, COMMAND.length)), 60) + return () => clearInterval(t) +}, [typed >= COMMAND.length]) +const done = typed >= COMMAND.length +``` +Render `{COMMAND.slice(0, typed)}` + a blinking caret span (`animate-pulse`, hidden once `done`), and the confirmation line only when `done`. + +- [ ] **Step 3: pass; visual check** + +`bun run dev`, open `http://localhost:5173/` (no backend needed — refresh fails → anonymous). Check 1280px and 375px widths: no horizontal scroll, nav collapses, hero line wraps cleanly, focus ring visible when tabbing. Fix anything off before committing. + +- [ ] **Step 4: commit** + +```bash +git add frontend/src +git commit -m "feat(frontend): app shell, routing and landing page with the chat-command hero" +``` + +--- + +### Task 6: Link the game (`/link`) + +**Files:** Create `src/features/device-link/{api.ts,LinkDeviceForm.tsx}`, `src/features/device-link/tests/link.test.tsx`, `src/pages/app/LinkPage.tsx`; modify `src/routes.tsx`. + +**Interfaces:** +- `api.ts`: `confirmDevice(userCode: string): Promise` → `POST /device/confirm {user_code}`; `normalizeUserCode(raw: string): string` → uppercase, strip spaces, insert the dash after 4 chars if missing (`abcd1234` → `ABCD-1234`). +- Form: single field «Код из игры» (Iosevka input, `autoComplete="off"`, `inputMode="text"`, `maxLength={9}`, placeholder `ABCD-1234`), button «Привязать». Success replaces the form with «Игра привязана. Можно возвращаться в Minecraft — мод уже вошёл в аккаунт.» 404 → «Код не найден или устарел. Коды живут 10 минут — возьми новый в моде.» Route `/link` inside `RequireAuth` (anonymous → login → back to `/link` via `state.from`). +- Page: h1 «Привязать игру», lead «Пароль в мод вводить не нужно: мод показывает короткий код, ты подтверждаешь его здесь.» + +- [ ] **Step 1: Failing tests** + +```tsx +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test } from 'vitest' +import { LinkDeviceForm } from '../LinkDeviceForm' +import { normalizeUserCode } from '../api' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +test('normalizes typed codes', () => { + expect(normalizeUserCode(' abcd1234 ')).toBe('ABCD-1234') + expect(normalizeUserCode('abcd-1234')).toBe('ABCD-1234') +}) + +test('confirms a code and shows success', async () => { + const user = userEvent.setup() + const fetch = mockFetch({ 'POST /auth/refresh': () => json({}, 401), 'POST /device/confirm': () => new Response(null, { status: 200 }) }) + renderApp([{ path: '/', element: }]) + await user.type(await screen.findByLabelText('Код из игры'), 'abcd1234') + await user.click(screen.getByRole('button', { name: 'Привязать' })) + expect(await screen.findByText(/Игра привязана/)).toBeInTheDocument() + const body = fetch.mock.calls.find(([u]) => String(u).endsWith('/device/confirm'))?.[1]?.body + expect(body).toBe('{"user_code":"ABCD-1234"}') +}) + +test('expired code explains what to do', async () => { + const user = userEvent.setup() + mockFetch({ 'POST /auth/refresh': () => json({}, 401), 'POST /device/confirm': () => json({ error: 'unknown or expired user_code' }, 404) }) + renderApp([{ path: '/', element: }]) + await user.type(await screen.findByLabelText('Код из игры'), 'ZZZZ-ZZZZ') + await user.click(screen.getByRole('button', { name: 'Привязать' })) + expect(await screen.findByRole('alert')).toHaveTextContent('Коды живут 10 минут') +}) +``` + +- [ ] **Step 2: Implement** per interfaces (`useMutation` for the confirm call), add `{ path: '/link', lazy: … LinkPage }` under `RequireAuth` in `routes.tsx`. + +- [ ] **Step 3: pass; commit** — `feat(frontend): link the game with a device code` + +--- + +### Task 7: Account page (`/account`) — profile, avatar, devices + +**Files:** Create `src/features/account/api.ts`, `src/features/account/components/{ProfileCard,AvatarUpload,DeviceList}.tsx`, `src/features/account/tests/account.test.tsx`, `src/pages/app/AccountPage.tsx`; modify `src/routes.tsx`. + +**Interfaces:** +- `api.ts`: `type DeviceLink = { id: string; linked_at: string; last_seen: string | null }`; `uploadAvatar(file: File): Promise<{ avatar_url: string }>` (FormData field `file`, `POST /avatars`); `listDevices(): Promise` (`GET /device/links`); `revokeDevice(id: string): Promise` (`DELETE /device/links/{id}`); `checkAvatarFile(file: File): string | undefined` — rejects non `image/png|jpeg|webp` («Подойдёт PNG, JPG или WebP.») and > 5 MB («Файл больше 5 МБ.»). +- `ProfileCard`: avatar (or initial), nick (Comfortaa 25px), email (frost), «С нами с {date}» (`Intl.DateTimeFormat('ru-RU', { dateStyle: 'long' })`), «Выйти» (quiet) → `signOut()` → navigate `/`. +- `AvatarUpload`: visually a button «Сменить аватар» wrapping a hidden `` (label-based, keyboard reachable); client check → upload → `invalidateQueries(['me'])`; errors via `Notice`. +- `DeviceList`: heading «Привязанные игры»; each row: «Привязано {date}», «Последний вход: {relative or 'ещё не заходил'}», button «Отвязать» (danger) with a confirm step (button turns into «Точно отвязать?» + «Отмена» for that row); success removes the row (`invalidateQueries(['devices'])`). Empty: «Пока ни одной игры. Привяжи мод на странице «Привязать игру».» with a link. +- Page: two columns on ≥ 900px (profile left, devices right), stacked below. + +- [ ] **Step 1: Failing tests** (`account.test.tsx`) + +```tsx +import { screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test } from 'vitest' +import { checkAvatarFile } from '../api' +import { DeviceList } from '../components/DeviceList' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +test('avatar file checks', () => { + expect(checkAvatarFile(new File(['x'], 'a.gif', { type: 'image/gif' }))).toBe('Подойдёт PNG, JPG или WebP.') + const big = new File([new Uint8Array(5 * 1024 * 1024 + 1)], 'a.png', { type: 'image/png' }) + expect(checkAvatarFile(big)).toBe('Файл больше 5 МБ.') + expect(checkAvatarFile(new File(['x'], 'a.png', { type: 'image/png' }))).toBeUndefined() +}) + +test('revoking a device asks for confirmation first', async () => { + const user = userEvent.setup() + let devices = [{ id: 'd1', linked_at: '2026-09-20T10:00:00Z', last_seen: null }] + const fetch = mockFetch({ + 'POST /auth/refresh': () => json({}, 401), + 'GET /device/links': () => json(devices), + 'DELETE /device/links/d1': () => { devices = []; return new Response(null, { status: 204 }) }, + }) + renderApp([{ path: '/', element: }]) + const row = await screen.findByRole('listitem') + await user.click(within(row).getByRole('button', { name: 'Отвязать' })) + expect(fetch.mock.calls.some(([, i]) => i?.method === 'DELETE')).toBe(false) + await user.click(within(row).getByRole('button', { name: 'Точно отвязать?' })) + expect(await screen.findByText(/Пока ни одной игры/)).toBeInTheDocument() +}) +``` + +- [ ] **Step 2: Implement; add `/account` route under `RequireAuth`.** +- [ ] **Step 3: pass; commit** — `feat(frontend): account page with avatar upload and linked games` + +--- + +### Task 8: My configs (`/configs`) + +**Contract:** `backend/configs-service/PLAN.md` Tasks 2–5. Until configs-service is running, the page is exercised through tests (mocked fetch) only. + +**Files:** Create `src/features/configs/{api.ts,types.ts}`, `src/features/configs/components/{SlotGrid,SlotPanel,PublishForm}.tsx`, `src/features/configs/tests/configs.test.tsx`, `src/pages/app/ConfigsPage.tsx`; modify `src/routes.tsx`. + +**Interfaces:** +- `types.ts`: `SlotSummary = { slot: 1 | 2 | 3 | 4; name: string; updated_at: string; share_code: string; published: boolean }`. +- `api.ts`: `listSlots(): Promise` (`GET /configs`), `regenerateCode(slot): Promise<{ share_code: string }>`, `publish(slot, title, description): Promise<{ listing_id: string }>`, `unpublish(slot): Promise`. Query key `['configs']`; every mutation invalidates it. +- `SlotGrid`: always renders 4 `SlotPanel`s (2×2 ≥ 640px, 1 column below), filling empty ones from the list. Page lead: «Конфиги сохраняются из мода командой `%config save <слот>`. Здесь — коды и публикация.» (command in Iosevka). +- `SlotPanel` (filled): «Слот N», name (Comfortaa 20px), «Обновлён {relative}», `ShareCode` with label «Код для друзей», hint under it: «Друг вводит в игре: %config load {code}» (Iosevka), buttons «Новый код» (quiet; confirm step: «Старый код перестанет работать. Сменить?» + «Сменить» / «Отмена») and «Опубликовать» / «Убрать с витрины» (danger, confirm step) depending on `published`. Empty: «Слот N», «Пусто», one line «Сохрани сюда конфиг из мода: %config save N». +- `PublishForm` (inline in the panel, not a modal): «Название на витрине» (1..64), «Описание» textarea (≤500, counter «{n}/500»), «Опубликовать» / «Отмена». Success → Notice info «Опубликовано. Конфиг виден на витрине.» + +- [ ] **Step 1: Failing tests** — cover: 4 panels always rendered with 1 filled; «Новый код» requires confirm and then shows the new code from the response; publish validates empty title client-side and posts `{title, description}`; published slot shows «Убрать с витрины». + +```tsx +import { screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test } from 'vitest' +import { SlotGrid } from '../components/SlotGrid' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +const filled = { slot: 2, name: 'pvp', updated_at: '2026-09-24T10:00:00Z', share_code: '7KQ3M9XA', published: false } + +test('always shows four slots', async () => { + mockFetch({ 'POST /auth/refresh': () => json({}, 401), 'GET /configs': () => json([filled]) }) + renderApp([{ path: '/', element: }]) + expect(await screen.findAllByRole('article')).toHaveLength(4) + expect(screen.getByText('7KQ3M9XA')).toBeInTheDocument() + expect(screen.getAllByText('Пусто')).toHaveLength(3) +}) + +test('regenerating a code needs confirmation', async () => { + const user = userEvent.setup() + let code = '7KQ3M9XA' + mockFetch({ + 'POST /auth/refresh': () => json({}, 401), + 'GET /configs': () => json([{ ...filled, share_code: code }]), + 'POST /configs/2/regenerate-code': () => { code = 'NEWC0DE2'; return json({ share_code: code }) }, + }) + renderApp([{ path: '/', element: }]) + const panel = (await screen.findByText('pvp')).closest('article') as HTMLElement + await user.click(within(panel).getByRole('button', { name: 'Новый код' })) + expect(within(panel).getByText(/Старый код перестанет работать/)).toBeInTheDocument() + await user.click(within(panel).getByRole('button', { name: 'Сменить' })) + expect(await within(panel).findByText('NEWC0DE2')).toBeInTheDocument() +}) + +test('publish requires a title and sends the form', async () => { + const user = userEvent.setup() + const fetch = mockFetch({ + 'POST /auth/refresh': () => json({}, 401), + 'GET /configs': () => json([filled]), + 'POST /configs/2/publish': () => json({ listing_id: 'l1' }), + }) + renderApp([{ path: '/', element: }]) + const panel = (await screen.findByText('pvp')).closest('article') as HTMLElement + await user.click(within(panel).getByRole('button', { name: 'Опубликовать' })) + await user.click(within(panel).getByRole('button', { name: 'Опубликовать' })) + expect(within(panel).getByLabelText('Название на витрине')).toHaveAttribute('aria-invalid', 'true') + await user.type(within(panel).getByLabelText('Название на витрине'), 'Лучший PvP') + await user.click(within(panel).getByRole('button', { name: 'Опубликовать' })) + expect(await within(panel).findByText(/Опубликовано/)).toBeInTheDocument() + const body = fetch.mock.calls.find(([u]) => String(u).endsWith('/publish'))?.[1]?.body + expect(JSON.parse(String(body))).toEqual({ title: 'Лучший PvP', description: '' }) +}) +``` +(Each `SlotPanel` renders as `
    `. While `PublishForm` is open, the panel's own «Опубликовать» button is hidden so the form's submit is the only one with that name.) + +- [ ] **Step 2: Implement; `/configs` route under `RequireAuth`.** +- [ ] **Step 3: pass; commit** — `feat(frontend): my configs with share codes and publishing` + +--- + +### Task 9: Showcase (`/showcase`, `/showcase/:id`) + +**Contract:** `backend/configs-service/PLAN.md` Task 5. + +**Files:** Create `src/features/showcase/{api.ts,types.ts}`, `src/features/showcase/components/{ListingList,ListingItem,ListingDetail,CopyToSlot}.tsx`, `src/features/showcase/tests/showcase.test.tsx`, `src/pages/showcase/{ShowcasePage,ListingPage}.tsx`; modify `src/routes.tsx`. + +**Interfaces:** +- `types.ts`: `Author = { nick: string; avatar_url: string | null }`; `Listing = { id: string; title: string; description: string; config_name: string; copies_count: number; published_at: string; author: Author | null }`; `ListingDetail = Listing & { data: unknown }`; `ShowcasePage = { items: Listing[]; page: number; has_more: boolean }`. +- `api.ts`: `browse(sort: 'new' | 'popular', page: number)` (key `['showcase', sort, page]`), `getListing(id)` (key `['listing', id]`), `copyToSlot(id, slot)` → `POST /showcase/{id}/copy {slot}`. +- `ShowcasePage`: h1 «Витрина», lead «Конфиги, которыми поделились игроки. Понравился — скопируй в свой слот.», sort toggle «Новые» / «Популярные» (two buttons, `aria-pressed`, synced to `?sort=`), list, pager «Назад» / «Дальше» (synced to `?page=`, «Дальше» disabled when `!has_more`). Anonymous access allowed. +- `ListingItem` (`
    `, links to detail): title, author nick (or «Автор скрыт» when `author` is null), config name, «Скопировали {n} раз» with correct Russian plural (`Intl.PluralRules('ru')`: раз/раза/раз), date. +- `ListingDetail`: title, description (preserve line breaks, `whitespace-pre-line`), author, stats; `CopyToSlot` below. +- `CopyToSlot`: anonymous → «Войди, чтобы скопировать конфиг к себе» (link to `/login` with `state.from`); authenticated → four slot buttons «Слот 1»…«Слот 4», occupied slots (from `['configs']` query) disabled with `title="Занят"`; success → Notice «Скопировано в слот N» + link «Открыть мои конфиги»; 409 → «Этот слот уже занят. Выбери свободный или освободи слот в моде.» +- Empty list: «На витрине пока пусто. Опубликуй свой конфиг на странице «Мои конфиги».» + +- [ ] **Step 1: Failing tests** — cover: list renders items + pluralization (1 раз / 2 раза / 5 раз); sort toggle updates `?sort=popular` and refetches; anonymous detail shows the login prompt; authenticated copy disables occupied slots and posts `{slot}`; 409 message. + +```tsx +import { expect, test } from 'vitest' +import { copiesLabel } from '../components/ListingItem' + +test('russian plural for copies', () => { + expect(copiesLabel(1)).toBe('Скопировали 1 раз') + expect(copiesLabel(3)).toBe('Скопировали 3 раза') + expect(copiesLabel(5)).toBe('Скопировали 5 раз') + expect(copiesLabel(21)).toBe('Скопировали 21 раз') +}) +``` +(plus rendering tests in the same file, same `renderApp` + `mockFetch` pattern as Tasks 7–8; export `copiesLabel` from `ListingItem.tsx`.) + +- [ ] **Step 2: Implement; routes `/showcase` and `/showcase/:id` (public, lazy).** +- [ ] **Step 3: pass; commit** — `feat(frontend): public showcase with sorting, detail and copy to slot` + +--- + +### Task 10: Visual QA pass + docs + +**Files:** any `src/**` fixes; `public/favicon.svg` (replace Vite's), `frontend/ARCHITECTURE.md`, `TODO.md` (Фаза 10 status). + +- [ ] **Step 1:** `bun run dev`; with Chrome DevTools MCP (or a browser) take screenshots of `/`, `/login`, `/register`, `/showcase` (mocked empty state is fine — with no backend running, refresh fails → anonymous; showcase shows the network error Notice, which must read well too) at 1280×800 and 375×812. +- [ ] **Step 2:** Critique against the Design section: one accent only, panels only where specified, no horizontal scroll at 375px, visible focus on every interactive element (tab through each page), contrast of `frost` text on `slate` panels ≥ 4.5:1, hero typing runs once. Fix and re-screenshot. +- [ ] **Step 3:** Favicon: simple SVG — ice rounded square (10px radius shape scaled) with a snow «L» in Comfortaa-like geometry; `theme-color` already set in Task 2. +- [ ] **Step 4:** `TODO.md`: frontend Подсистема 1 done; next = mod integration (device link + `%config` commands against the gateway). +- [ ] **Step 5:** test/build/lint; commit — `feat(frontend): visual QA fixes, favicon, docs` + +--- + +## Deferred + +- Admin panel (`/admin`) — Подсистема 3 plan. +- Friends/chat/presence UI — Подсистема 2 plan. +- Showcase search/filters, config diff preview — after real usage. +- i18n (English UI) — not requested. diff --git a/frontend/bun.lock b/frontend/bun.lock index 759cb22..99cb2fb 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,23 +5,67 @@ "": { "name": "frontend", "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource/comfortaa": "^5.3.0", + "@fontsource/iosevka": "^5.3.0", "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query": "^5.103.2", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-router": "^8.4.0", "tailwindcss": "^4.3.3", }, "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/node": "^26.6.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", + "jsdom": "^30.1.1", "oxlint": "^1.81.0", "typescript": "^7.0.2", "vite": "^8.3.0", + "vitest": "^5.0.1", }, }, }, "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@7.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.4.0", "@csstools/css-color-parser": "^4.2.3", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.1", "lru-cache": "^11.5.3" } }, "sha512-C9duntabagkBZ1LebM7FKmphR4Q1pBclLxVbZETQV0akkFjV0ooFxo8FlvAQyKj9F6l8rEnmidgcTlpyxFYizg=="], + + "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@9.2.1", "", { "dependencies": { "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.3" } }, "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="], + + "@csstools/css-calc": ["@csstools/css-calc@3.4.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.3", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.4.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], + + "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.14", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.1", "", {}, "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw=="], + + "@exodus/bytes": ["@exodus/bytes@1.16.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA=="], + + "@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="], + + "@fontsource/comfortaa": ["@fontsource/comfortaa@5.3.0", "", {}, "sha512-ab4DERH0q6ko8QCpEZzOV9ebK39LgQ2oM91AkLNqu0VIxxwT0nV2D3sOuI7iLirWWq4IMooaIN6/2llxrDN2JA=="], + + "@fontsource/iosevka": ["@fontsource/iosevka@5.3.0", "", {}, "sha512-RcG0v/65e4PTuThR/d7n9jj6PLlex7YUGaLa4ZpL+NnqArWQmD8Z+6gYBTxPMVWxYMRcEw6OF4rdt8XisnC5zA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -72,6 +116,8 @@ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.85.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA=="], + "@remix-run/route-pattern": ["@remix-run/route-pattern@0.22.1", "", {}, "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.10", "", { "os": "android", "cpu": "arm" }, "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.10", "", { "os": "android", "cpu": "arm64" }, "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA=="], @@ -134,6 +180,26 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.103.2", "", {}, "sha512-I8DkFXls5jXLqtm8+QpOhEmG07hIblhSZFzafg9wHsMSEvMzULy9hK17wU1T/ahfhMbtITJhbxutwwCoihkR7A=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.103.2", "", { "dependencies": { "@tanstack/query-core": "5.103.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-B+fWiYZBc+0uUD5zDZAeLw9dKj7XEsdnuu6zRZ+no6LNKpeE3P3bJ+N6HINjcIlWR6fW3vUoTneEaGa2V6ehqw=="], + + "@testing-library/dom": ["@testing-library/dom@10.4.2", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@7.0.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" }, "peerDependencies": { "@testing-library/dom": ">=10 <11", "vitest": ">= 0.32" }, "optionalPeers": ["vitest"] }, "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw=="], + + "@testing-library/react": ["@testing-library/react@16.3.3", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.7", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg=="], + + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="], "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], @@ -182,20 +248,68 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="], + "@vitest/mocker": ["@vitest/mocker@5.0.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q=="], + + "@vitest/spy": ["@vitest/spy@5.0.1", "", {}, "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], + + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], + "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], + + "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "html-encoding-sniffer": ["html-encoding-sniffer@7.0.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1" } }, "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsdom": ["jsdom@30.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^7.0.0", "@asamuzakjp/dom-selector": "^9.2.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.13", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^7.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "tough-cookie": "^6.0.2", "undici": "^8.10.2", "w3c-xmlserializer": "^6.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg=="], + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], @@ -220,42 +334,108 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + + "magic-string": ["magic-string@1.4.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g=="], + + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], + "oxlint": ["oxlint@1.85.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.85.0", "@oxlint/binding-android-arm64": "1.85.0", "@oxlint/binding-darwin-arm64": "1.85.0", "@oxlint/binding-darwin-x64": "1.85.0", "@oxlint/binding-freebsd-x64": "1.85.0", "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", "@oxlint/binding-linux-arm-musleabihf": "1.85.0", "@oxlint/binding-linux-arm64-gnu": "1.85.0", "@oxlint/binding-linux-arm64-musl": "1.85.0", "@oxlint/binding-linux-ppc64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-musl": "1.85.0", "@oxlint/binding-linux-s390x-gnu": "1.85.0", "@oxlint/binding-linux-x64-gnu": "1.85.0", "@oxlint/binding-linux-x64-musl": "1.85.0", "@oxlint/binding-openharmony-arm64": "1.85.0", "@oxlint/binding-win32-arm64-msvc": "1.85.0", "@oxlint/binding-win32-ia32-msvc": "1.85.0", "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg=="], + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + + "react-router": ["react-router@8.4.0", "", { "dependencies": { "@remix-run/route-pattern": "^0.22.1", "cookie-es": "^3.1.1" }, "peerDependencies": { "react": ">=19.2.7", "react-dom": ">=19.2.7" }, "optionalPeers": ["react-dom"] }, "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "rolldown": ["rolldown@1.2.10", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.10", "@rolldown/binding-android-arm64": "1.2.10", "@rolldown/binding-darwin-arm64": "1.2.10", "@rolldown/binding-darwin-x64": "1.2.10", "@rolldown/binding-freebsd-x64": "1.2.10", "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", "@rolldown/binding-linux-arm64-gnu": "1.2.10", "@rolldown/binding-linux-arm64-musl": "1.2.10", "@rolldown/binding-linux-ppc64-gnu": "1.2.10", "@rolldown/binding-linux-s390x-gnu": "1.2.10", "@rolldown/binding-linux-x64-gnu": "1.2.10", "@rolldown/binding-linux-x64-musl": "1.2.10", "@rolldown/binding-openharmony-arm64": "1.2.10", "@rolldown/binding-win32-arm64-msvc": "1.2.10", "@rolldown/binding-win32-x64-msvc": "1.2.10" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog=="], + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "tinybench": ["tinybench@6.1.4", "", {}, "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ=="], + + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tldts": ["tldts@7.4.15", "", { "dependencies": { "tldts-core": "^7.4.15" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ=="], + + "tldts-core": ["tldts-core@7.4.15", "", {}, "sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A=="], + + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + + "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "undici": ["undici@8.11.2", "", {}, "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ=="], + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], + "vitest": ["vitest@5.0.1", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.1", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "6.1.4", "tinyexec": "1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.1", "@vitest/browser-preview": "5.0.1", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.1", "@vitest/coverage-v8": "5.0.1", "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@6.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q=="], + + "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], + + "whatwg-url": ["whatwg-url@17.1.2", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + "@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], @@ -268,6 +448,12 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], + + "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + + "data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], diff --git a/frontend/index.html b/frontend/index.html index 0fca6f0..6f34f9f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,15 @@ - + - frontend + + + LoVisual — аккаунт и облачные конфиги
    diff --git a/frontend/package.json b/frontend/package.json index 3ff622e..246ba67 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,21 +7,33 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "@fontsource/comfortaa": "^5.3.0", + "@fontsource/iosevka": "^5.3.0", "@tailwindcss/vite": "^4.3.3", + "@tanstack/react-query": "^5.103.2", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-router": "^8.4.0", "tailwindcss": "^4.3.3" }, "devDependencies": { + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "^16.3.3", + "@testing-library/user-event": "^14.6.7", "@types/node": "^26.6.2", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.7", "@vitejs/plugin-react": "^6.1.1", + "jsdom": "^30.1.1", "oxlint": "^1.81.0", "typescript": "^7.0.2", - "vite": "^8.3.0" + "vite": "^8.3.0", + "vitest": "^5.0.1" } } diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9ca8e8d..3bfca3c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,122 +1,7 @@ -import { useState } from 'react' -import heroImg from './assets/hero.png' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import './App.css' +import { createBrowserRouter, RouterProvider } from 'react-router' -function App() { - const [count, setCount] = useState(0) +const router = createBrowserRouter([{ path: '/', element:

    LoVisual

    }]) - return ( - <> -
    -
    - - React logo - Vite logo -
    -
    -

    Get started

    -

    - Edit src/App.tsx and save to test HMR -

    -
    - -
    - -
    - -
    -
    - -

    Documentation

    -

    Your questions, answered

    - -
    -
    - -

    Connect with us

    -

    Join the Vite community

    - -
    -
    - -
    -
    - - ) +export default function App() { + return } - -export default App diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index 02251f4b956c55af2d76fd0788124d7eee2b45eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/features/auth/RequireAuth.tsx b/frontend/src/features/auth/RequireAuth.tsx new file mode 100644 index 0000000..5f04e52 --- /dev/null +++ b/frontend/src/features/auth/RequireAuth.tsx @@ -0,0 +1,10 @@ +import { Navigate, Outlet, useLocation } from 'react-router' +import { useSession } from './session' + +export function RequireAuth() { + const { status } = useSession() + const location = useLocation() + if (status === 'loading') return

    Проверяем вход…

    + if (status === 'anonymous') return + return +} diff --git a/frontend/src/features/auth/api.ts b/frontend/src/features/auth/api.ts new file mode 100644 index 0000000..0f91b07 --- /dev/null +++ b/frontend/src/features/auth/api.ts @@ -0,0 +1,21 @@ +import { api } from '../../shared/api/client' +import type { Me } from '../../shared/types' + +export async function login(email: string, password: string): Promise { + const res = await api.request<{ access_token: string }>('/auth/login', { method: 'POST', json: { email, password } }) + api.setAccessToken(res.access_token) +} + +export async function register(email: string, password: string, nick: string): Promise { + await api.request('/auth/register', { method: 'POST', json: { email, password, nick } }) +} + +export async function logout(): Promise { + try { + await api.request('/auth/logout', { method: 'POST' }) + } finally { + api.setAccessToken(null) + } +} + +export const fetchMe = () => api.request('/me') diff --git a/frontend/src/features/auth/forms/LoginForm.tsx b/frontend/src/features/auth/forms/LoginForm.tsx new file mode 100644 index 0000000..145b95d --- /dev/null +++ b/frontend/src/features/auth/forms/LoginForm.tsx @@ -0,0 +1,51 @@ +import { useState, type FormEvent } from 'react' +import { Link, useLocation, useNavigate } from 'react-router' +import { describeError } from '../../../shared/api/errors' +import { Button } from '../../../shared/ui/Button' +import { Notice } from '../../../shared/ui/Notice' +import { TextField } from '../../../shared/ui/TextField' +import { useSession } from '../session' + +export function LoginForm() { + const { signIn } = useSession() + const navigate = useNavigate() + const from = (useLocation().state as { from?: string } | null)?.from ?? '/configs' + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState() + const [busy, setBusy] = useState(false) + + async function submit(e: FormEvent) { + e.preventDefault() + setBusy(true) + setError(undefined) + try { + await signIn(email.trim(), password) + navigate(from, { replace: true }) + } catch (err) { + setError(describeError(err, { 401: 'Неверная почта или пароль.' })) + } finally { + setBusy(false) + } + } + + return ( +
    + setEmail(e.target.value)} /> + setPassword(e.target.value)} + /> + {error ? {error} : null} + +

    + Нет аккаунта? Создать +

    + + ) +} diff --git a/frontend/src/features/auth/forms/RegisterForm.tsx b/frontend/src/features/auth/forms/RegisterForm.tsx new file mode 100644 index 0000000..0980ee6 --- /dev/null +++ b/frontend/src/features/auth/forms/RegisterForm.tsx @@ -0,0 +1,81 @@ +import { useState, type FormEvent } from 'react' +import { Link, useNavigate } from 'react-router' +import { describeError } from '../../../shared/api/errors' +import { Button } from '../../../shared/ui/Button' +import { Notice } from '../../../shared/ui/Notice' +import { TextField } from '../../../shared/ui/TextField' +import { useSession } from '../session' +import { validateEmail, validateNick, validatePassword } from '../validation' + +export function RegisterForm() { + const { signUp } = useSession() + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [nick, setNick] = useState('') + const [password, setPassword] = useState('') + const [emailError, setEmailError] = useState() + const [nickError, setNickError] = useState() + const [passwordError, setPasswordError] = useState() + const [error, setError] = useState() + const [busy, setBusy] = useState(false) + + async function submit(e: FormEvent) { + e.preventDefault() + setError(undefined) + + const emailErr = validateEmail(email) + const nickErr = validateNick(nick) + const passwordErr = validatePassword(password) + setEmailError(emailErr) + setNickError(nickErr) + setPasswordError(passwordErr) + if (emailErr || nickErr || passwordErr) return + + setBusy(true) + try { + await signUp(email.trim(), password, nick.trim()) + navigate('/configs', { replace: true }) + } catch (err) { + setError(describeError(err, { 409: 'Эта почта уже занята. Войди или используй другую.' })) + } finally { + setBusy(false) + } + } + + return ( +
    + setEmail(e.target.value)} + /> + setNick(e.target.value)} + /> + setPassword(e.target.value)} + /> + {error ? {error} : null} + +

    + Уже есть аккаунт? Войти +

    + + ) +} diff --git a/frontend/src/features/auth/session.tsx b/frontend/src/features/auth/session.tsx new file mode 100644 index 0000000..70ccd90 --- /dev/null +++ b/frontend/src/features/auth/session.tsx @@ -0,0 +1,60 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react' +import { api } from '../../shared/api/client' +import type { Me } from '../../shared/types' +import { fetchMe, login, logout, register } from './api' + +type Session = { + status: 'loading' | 'anonymous' | 'authenticated' + me: Me | null + signIn(email: string, password: string): Promise + signUp(email: string, password: string, nick: string): Promise + signOut(): Promise +} + +const SessionContext = createContext(null) + +export function SessionProvider({ children }: { children: ReactNode }) { + const client = useQueryClient() + // Restores the session after a reload: the refresh cookie survives, the + // in-memory access token does not. + const restored = useQuery({ queryKey: ['session-restore'], queryFn: () => api.refresh(), staleTime: Infinity }) + const meQuery = useQuery({ queryKey: ['me'], queryFn: fetchMe, enabled: restored.data === true }) + + const signIn = useCallback( + async (email: string, password: string) => { + await login(email, password) + client.setQueryData(['session-restore'], true) + await client.fetchQuery({ queryKey: ['me'], queryFn: fetchMe }) + }, + [client], + ) + + const signUp = useCallback( + async (email: string, password: string, nick: string) => { + await register(email, password, nick) + await signIn(email, password) + }, + [signIn], + ) + + const signOut = useCallback(async () => { + await logout() + client.clear() + client.setQueryData(['session-restore'], false) + }, [client]) + + const value = useMemo(() => { + const me = meQuery.data ?? null + const status = restored.isPending || (restored.data && meQuery.isPending) ? 'loading' : me ? 'authenticated' : 'anonymous' + return { status, me, signIn, signUp, signOut } + }, [restored.isPending, restored.data, meQuery.isPending, meQuery.data, signIn, signUp, signOut]) + + return {children} +} + +export function useSession(): Session { + const session = useContext(SessionContext) + if (!session) throw new Error('useSession outside SessionProvider') + return session +} diff --git a/frontend/src/features/auth/tests/forms.test.tsx b/frontend/src/features/auth/tests/forms.test.tsx new file mode 100644 index 0000000..be0c5fc --- /dev/null +++ b/frontend/src/features/auth/tests/forms.test.tsx @@ -0,0 +1,53 @@ +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test } from 'vitest' +import { LoginForm } from '../forms/LoginForm' +import { RegisterForm } from '../forms/RegisterForm' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +const me = { id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '2026-09-24T00:00:00Z' } +const anon = { 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401) } + +test('wrong password shows a clear error', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/login': () => json({ error: 'unauthorized' }, 401) }) + renderApp([{ path: '/login', element: }], '/login') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Пароль'), 'wrong-password') + await user.click(screen.getByRole('button', { name: 'Войти' })) + expect(await screen.findByRole('alert')).toHaveTextContent('Неверная почта или пароль.') +}) + +test('successful login goes to configs', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/login': () => json({ access_token: 't' }), 'GET /me': () => json(me) }) + renderApp([{ path: '/login', element: }, { path: '/configs', element:

    Мои конфиги

    }], '/login') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Пароль'), 'correct-horse') + await user.click(screen.getByRole('button', { name: 'Войти' })) + expect(await screen.findByText('Мои конфиги')).toBeInTheDocument() +}) + +test('register validates before calling the server', async () => { + const user = userEvent.setup() + const fetch = mockFetch(anon) + renderApp([{ path: '/register', element: }], '/register') + await user.type(await screen.findByLabelText('Почта'), 'not-an-email') + await user.type(screen.getByLabelText('Пароль'), 'short') + await user.click(screen.getByRole('button', { name: 'Создать аккаунт' })) + expect(screen.getByLabelText('Почта')).toHaveAttribute('aria-invalid', 'true') + expect(screen.getByLabelText('Пароль')).toHaveAccessibleDescription('Минимум 8 символов.') + expect(fetch.mock.calls.some(([u]) => String(u).includes('/auth/register'))).toBe(false) +}) + +test('taken email explains what to do', async () => { + const user = userEvent.setup() + mockFetch({ ...anon, 'POST /auth/register': () => json({ error: 'email already registered' }, 409) }) + renderApp([{ path: '/register', element: }], '/register') + await user.type(await screen.findByLabelText('Почта'), 'a@b.c') + await user.type(screen.getByLabelText('Ник на сайте'), 'Rider') + await user.type(screen.getByLabelText('Пароль'), 'correct-horse') + await user.click(screen.getByRole('button', { name: 'Создать аккаунт' })) + expect(await screen.findByRole('alert')).toHaveTextContent('Эта почта уже занята') +}) diff --git a/frontend/src/features/auth/tests/session.test.tsx b/frontend/src/features/auth/tests/session.test.tsx new file mode 100644 index 0000000..967c96a --- /dev/null +++ b/frontend/src/features/auth/tests/session.test.tsx @@ -0,0 +1,30 @@ +import { screen } from '@testing-library/react' +import { expect, test } from 'vitest' +import { RequireAuth } from '../RequireAuth' +import { useSession } from '../session' +import { renderApp } from '../../../test/render' +import { json, mockFetch } from '../../../test/fetch' + +const me = { id: '1', email: 'a@b.c', display_nick: 'Rider', role: 'user', avatar_url: null, created_at: '2026-09-24T00:00:00Z' } + +function Whoami() { + const { me } = useSession() + return

    Привет, {me?.display_nick}

    +} + +const routes = [ + { path: '/login', element:

    Страница входа

    }, + { element: , children: [{ path: '/configs', element: }] }, +] + +test('restores the session from the refresh cookie', async () => { + mockFetch({ 'POST /auth/refresh': () => json({ access_token: 't' }), 'GET /me': () => json(me) }) + renderApp(routes, '/configs') + expect(await screen.findByText('Привет, Rider')).toBeInTheDocument() +}) + +test('anonymous visitor is sent to login', async () => { + mockFetch({ 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401) }) + renderApp(routes, '/configs') + expect(await screen.findByText('Страница входа')).toBeInTheDocument() +}) diff --git a/frontend/src/features/auth/validation.ts b/frontend/src/features/auth/validation.ts new file mode 100644 index 0000000..7b80ae0 --- /dev/null +++ b/frontend/src/features/auth/validation.ts @@ -0,0 +1,16 @@ +export function validateEmail(email: string): string | undefined { + const v = email.trim() + if (!v) return 'Введи почту.' + if (v.length > 254 || !/^[^@\s]+@[^@\s]+$/.test(v)) return 'Похоже, в почте опечатка.' +} + +export function validatePassword(password: string): string | undefined { + if ([...password].length < 8) return 'Минимум 8 символов.' + if (new TextEncoder().encode(password).length > 256) return 'Слишком длинный пароль.' +} + +export function validateNick(nick: string): string | undefined { + const len = [...nick.trim()].length + if (len === 0) return 'Придумай ник.' + if (len > 32) return 'Не длиннее 32 символов.' +} diff --git a/frontend/src/index.css b/frontend/src/index.css index f1d8c73..1d1c83a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1 +1,52 @@ @import "tailwindcss"; +@import "@fontsource-variable/inter"; +@import "@fontsource/comfortaa/400.css"; +@import "@fontsource/comfortaa/700.css"; +@import "@fontsource/iosevka/400.css"; +@import "@fontsource/iosevka/700.css"; + +@theme { + --color-abyss: #0d1416; + --color-slate: #142226; + --color-shoal: #1f3238; + --color-frost: #9fb3b0; + --color-snow: #eef5f3; + --color-ice: #5cc8e7; + --color-ember: #e8835a; + + --font-display: "Comfortaa", ui-rounded, system-ui, sans-serif; + --font-sans: "Inter Variable", system-ui, sans-serif; + --font-code: "Iosevka", ui-monospace, monospace; + + --text-hero: 3.8125rem; + --text-hero--line-height: 1.05; + --radius-panel: 10px; +} + +@layer base { + html { + color-scheme: dark; + } + body { + @apply bg-abyss text-snow font-sans antialiased; + min-height: 100dvh; + } + h1, + h2, + h3 { + @apply font-display font-bold; + } + :focus-visible { + outline: 2px solid var(--color-ice); + outline-offset: 2px; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/frontend/src/pages/public/LoginPage.tsx b/frontend/src/pages/public/LoginPage.tsx new file mode 100644 index 0000000..6fdade4 --- /dev/null +++ b/frontend/src/pages/public/LoginPage.tsx @@ -0,0 +1,10 @@ +import { LoginForm } from '../../features/auth/forms/LoginForm' + +export default function LoginPage() { + return ( +
    +

    Вход

    + +
    + ) +} diff --git a/frontend/src/pages/public/RegisterPage.tsx b/frontend/src/pages/public/RegisterPage.tsx new file mode 100644 index 0000000..9779246 --- /dev/null +++ b/frontend/src/pages/public/RegisterPage.tsx @@ -0,0 +1,13 @@ +import { RegisterForm } from '../../features/auth/forms/RegisterForm' + +export default function RegisterPage() { + return ( +
    +

    Новый аккаунт

    +

    + Аккаунт нужен для облачных конфигов и привязки игры. Пароль в мод вводить не придётся. +

    + +
    + ) +} diff --git a/frontend/src/shared/api/client.ts b/frontend/src/shared/api/client.ts new file mode 100644 index 0000000..bf447e6 --- /dev/null +++ b/frontend/src/shared/api/client.ts @@ -0,0 +1,101 @@ +export class ApiError extends Error { + readonly status: number + readonly retryAfter: number | null + + constructor(status: number, message: string, retryAfter: number | null = null) { + super(message) + this.name = 'ApiError' + this.status = status + this.retryAfter = retryAfter + } +} + +export type RequestOptions = { method?: string; json?: unknown; body?: BodyInit; signal?: AbortSignal } + +export type ApiClient = { + request(path: string, options?: RequestOptions): Promise + refresh(): Promise + setAccessToken(token: string | null): void + hasAccessToken(): boolean +} + +async function toError(res: Response): Promise { + let message = `HTTP ${res.status}` + try { + const body: unknown = await res.json() + if (body && typeof body === 'object' && 'error' in body && typeof body.error === 'string') message = body.error + } catch { + // non-JSON error body: keep the status text + } + const retry = Number(res.headers.get('retry-after')) + return new ApiError(res.status, message, Number.isFinite(retry) && retry > 0 ? retry : null) +} + +export function createApiClient( + baseUrl = '/api', + // Resolved at call time so tests can stub the global fetch. + fetchImpl: typeof fetch = (input, init) => fetch(input, init), +): ApiClient { + let accessToken: string | null = null + let refreshing: Promise | null = null + + async function refreshOnce(): Promise { + try { + const res = await fetchImpl(`${baseUrl}/auth/refresh`, { method: 'POST', credentials: 'include' }) + if (!res.ok) { + accessToken = null + return false + } + const body = (await res.json()) as { access_token: string } + accessToken = body.access_token + return true + } catch { + return false + } + } + + function refresh(): Promise { + refreshing ??= refreshOnce().finally(() => { + refreshing = null + }) + return refreshing + } + + function send(path: string, options: RequestOptions): Promise { + const headers = new Headers() + let body = options.body + if (options.json !== undefined) { + headers.set('content-type', 'application/json') + body = JSON.stringify(options.json) + } + if (accessToken) headers.set('authorization', `Bearer ${accessToken}`) + return fetchImpl(`${baseUrl}${path}`, { + method: options.method ?? 'GET', + headers, + body, + credentials: 'include', + signal: options.signal, + }) + } + + async function request(path: string, options: RequestOptions = {}): Promise { + let res = await send(path, options) + if (res.status === 401 && !path.startsWith('/auth/') && (await refresh())) { + res = await send(path, options) + } + if (!res.ok) throw await toError(res) + const text = await res.text() + return (text ? JSON.parse(text) : undefined) as T + } + + return { + request, + refresh, + setAccessToken: (token) => { + accessToken = token + }, + hasAccessToken: () => accessToken !== null, + } +} + +export const api = createApiClient() diff --git a/frontend/src/shared/api/errors.ts b/frontend/src/shared/api/errors.ts new file mode 100644 index 0000000..6425b0e --- /dev/null +++ b/frontend/src/shared/api/errors.ts @@ -0,0 +1,22 @@ +import { ApiError } from './client' + +const BY_STATUS: Record = { + 400: 'Проверь введённые данные.', + 401: 'Нужно войти в аккаунт.', + 403: 'Недостаточно прав.', + 404: 'Не найдено.', + 409: 'Уже существует.', +} + +export function describeError(err: unknown, overrides: Partial> = {}): string { + if (err instanceof ApiError) { + if (err.status === 429) { + return err.retryAfter + ? `Слишком много попыток. Подожди ${err.retryAfter} с.` + : 'Слишком много попыток. Подожди немного.' + } + if (err.status >= 500) return 'Сервер не ответил. Попробуй через минуту.' + return overrides[err.status] ?? BY_STATUS[err.status] ?? 'Проверь введённые данные.' + } + return 'Нет связи с сервером. Проверь интернет и попробуй ещё раз.' +} diff --git a/frontend/src/shared/api/tests/client.test.ts b/frontend/src/shared/api/tests/client.test.ts new file mode 100644 index 0000000..d1afeef --- /dev/null +++ b/frontend/src/shared/api/tests/client.test.ts @@ -0,0 +1,82 @@ +import { expect, test } from 'vitest' +import { ApiError, createApiClient } from '../client' +import { describeError } from '../errors' +import { json, mockFetch } from '../../../test/fetch' + +test('sends json with bearer token and cookies', async () => { + const fetch = mockFetch({ 'PUT /configs/1': () => json({ ok: true }) }) + const api = createApiClient() + api.setAccessToken('tok') + await api.request('/configs/1', { method: 'PUT', json: { a: 1 } }) + const [, init] = fetch.mock.calls[0] + const headers = new Headers(init?.headers) + expect(headers.get('authorization')).toBe('Bearer tok') + expect(headers.get('content-type')).toBe('application/json') + expect(init?.credentials).toBe('include') + expect(init?.body).toBe('{"a":1}') +}) + +test('401 triggers one refresh and one retry', async () => { + let calls = 0 + const fetch = mockFetch({ + 'GET /me': () => (++calls === 1 ? json({ error: 'unauthorized' }, 401) : json({ id: 'x' })), + 'POST /auth/refresh': () => json({ access_token: 'new' }), + }) + const api = createApiClient() + await expect(api.request('/me')).resolves.toEqual({ id: 'x' }) + expect(fetch.mock.calls.filter(([u]) => String(u).endsWith('/auth/refresh'))).toHaveLength(1) + expect(new Headers(fetch.mock.calls[2][1]?.headers).get('authorization')).toBe('Bearer new') +}) + +test('concurrent 401s share a single refresh', async () => { + let refreshes = 0 + mockFetch({ + 'GET /a': (init) => (new Headers(init.headers).get('authorization') ? json(1) : json({}, 401)), + 'GET /b': (init) => (new Headers(init.headers).get('authorization') ? json(2) : json({}, 401)), + 'POST /auth/refresh': () => { + refreshes++ + return json({ access_token: 't' }) + }, + }) + const api = createApiClient() + await Promise.all([api.request('/a'), api.request('/b')]) + expect(refreshes).toBe(1) +}) + +test('failed refresh surfaces the original 401 and clears the token', async () => { + mockFetch({ + 'GET /me': () => json({ error: 'unauthorized' }, 401), + 'POST /auth/refresh': () => json({ error: 'unauthorized' }, 401), + }) + const api = createApiClient() + api.setAccessToken('stale') + await expect(api.request('/me')).rejects.toMatchObject({ status: 401 }) + expect(api.hasAccessToken()).toBe(false) +}) + +test('auth endpoints never trigger refresh', async () => { + const fetch = mockFetch({ 'POST /auth/login': () => json({ error: 'unauthorized' }, 401) }) + await expect(createApiClient().request('/auth/login', { method: 'POST', json: {} })).rejects.toBeInstanceOf( + ApiError, + ) + expect(fetch).toHaveBeenCalledTimes(1) +}) + +test('429 carries retry-after; 204 resolves undefined', async () => { + mockFetch({ + 'POST /auth/login': () => json({ error: 'too many requests' }, 429, { 'retry-after': '42' }), + 'DELETE /device/links/1': () => new Response(null, { status: 204 }), + }) + const api = createApiClient() + await expect(api.request('/auth/login', { method: 'POST' })).rejects.toMatchObject({ status: 429, retryAfter: 42 }) + await expect(api.request('/device/links/1', { method: 'DELETE' })).resolves.toBeUndefined() +}) + +test('describeError covers 429, network failure and status defaults', () => { + expect(describeError(new ApiError(429, 'x', 42))).toBe('Слишком много попыток. Подожди 42 с.') + expect(describeError(new ApiError(429, 'x', null))).toBe('Слишком много попыток. Подожди немного.') + expect(describeError(new TypeError('Failed to fetch'))).toBe( + 'Нет связи с сервером. Проверь интернет и попробуй ещё раз.', + ) + expect(describeError(new ApiError(404, 'x'))).toBe('Не найдено.') +}) diff --git a/frontend/src/shared/types.ts b/frontend/src/shared/types.ts new file mode 100644 index 0000000..b86e8d5 --- /dev/null +++ b/frontend/src/shared/types.ts @@ -0,0 +1,8 @@ +export type Me = { + id: string + email: string + display_nick: string + role: 'user' | 'admin' + avatar_url: string | null + created_at: string +} diff --git a/frontend/src/shared/ui/Button.tsx b/frontend/src/shared/ui/Button.tsx new file mode 100644 index 0000000..66be9c0 --- /dev/null +++ b/frontend/src/shared/ui/Button.tsx @@ -0,0 +1,28 @@ +import type { ButtonHTMLAttributes } from 'react' + +type Variant = 'primary' | 'quiet' | 'danger' + +const VARIANTS: Record = { + primary: 'bg-ice text-abyss hover:bg-ice/90', + quiet: 'bg-transparent text-snow border border-shoal hover:border-frost', + danger: 'bg-transparent text-ember border border-ember/60 hover:border-ember', +} + +export function Button({ + variant = 'primary', + busy = false, + disabled, + className = '', + type = 'button', + ...rest +}: ButtonHTMLAttributes & { variant?: Variant; busy?: boolean }) { + return ( + + + {copied ? 'Скопировано' : ''} + + + ) +} diff --git a/frontend/src/shared/ui/TextField.tsx b/frontend/src/shared/ui/TextField.tsx new file mode 100644 index 0000000..8ac3425 --- /dev/null +++ b/frontend/src/shared/ui/TextField.tsx @@ -0,0 +1,35 @@ +import { useId, type InputHTMLAttributes } from 'react' + +export function TextField({ + label, + error, + hint, + className = '', + ...rest +}: InputHTMLAttributes & { label: string; error?: string; hint?: string }) { + const id = useId() + const describedBy = error ? `${id}-error` : hint ? `${id}-hint` : undefined + return ( +
    + + + {error ? ( +

    + {error} +

    + ) : hint ? ( +

    + {hint} +

    + ) : null} +
    + ) +} diff --git a/frontend/src/shared/ui/tests/ui.test.tsx b/frontend/src/shared/ui/tests/ui.test.tsx new file mode 100644 index 0000000..0c3a687 --- /dev/null +++ b/frontend/src/shared/ui/tests/ui.test.tsx @@ -0,0 +1,36 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { expect, test, vi } from 'vitest' +import { Button } from '../Button' +import { TextField } from '../TextField' +import { ShareCode } from '../ShareCode' +import { Notice } from '../Notice' + +test('busy button is disabled and announces busy', () => { + render() + const b = screen.getByRole('button', { name: 'Сохранить' }) + expect(b).toBeDisabled() + expect(b).toHaveAttribute('aria-busy', 'true') +}) + +test('text field links label and error', () => { + render() + const input = screen.getByLabelText('Почта') + expect(input).toHaveAttribute('aria-invalid', 'true') + expect(input).toHaveAccessibleDescription('Введи почту') +}) + +test('share code copies to clipboard and confirms', async () => { + const user = userEvent.setup() + const writeText = vi.spyOn(navigator.clipboard, 'writeText').mockResolvedValue() + render() + expect(screen.getByText('7KQ3M9XA')).toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'Скопировать' })) + expect(writeText).toHaveBeenCalledWith('7KQ3M9XA') + expect(await screen.findByText('Скопировано')).toBeInTheDocument() +}) + +test('error notice is an alert', () => { + render(Неверный код) + expect(screen.getByRole('alert')).toHaveTextContent('Неверный код') +}) diff --git a/frontend/src/test/fetch.ts b/frontend/src/test/fetch.ts new file mode 100644 index 0000000..850a157 --- /dev/null +++ b/frontend/src/test/fetch.ts @@ -0,0 +1,21 @@ +import { vi } from 'vitest' + +type Handler = (init: RequestInit) => Response | Promise + +export function json(body: unknown, status = 200, headers: Record = {}): Response { + return new Response(body === undefined ? null : JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json', ...headers }, + }) +} + +export function mockFetch(routes: Record) { + const fn = vi.fn(async (input: RequestInfo | URL, init: RequestInit = {}) => { + const url = new URL(String(input), 'http://localhost') + const key = `${(init.method ?? 'GET').toUpperCase()} ${url.pathname.replace(/^\/api/, '')}` + const handler = routes[key] + return handler ? handler(init) : json({ error: `no mock for ${key}` }, 404) + }) + vi.stubGlobal('fetch', fn) + return fn +} diff --git a/frontend/src/test/render.tsx b/frontend/src/test/render.tsx new file mode 100644 index 0000000..98bc2f7 --- /dev/null +++ b/frontend/src/test/render.tsx @@ -0,0 +1,17 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { render } from '@testing-library/react' +import { createMemoryRouter, RouterProvider, type RouteObject } from 'react-router' +import { SessionProvider } from '../features/auth/session' + +export function renderApp(routes: RouteObject[], initialPath = '/') { + const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) + const router = createMemoryRouter(routes, { initialEntries: [initialPath] }) + const result = render( + + + + + , + ) + return { ...result, router, client } +} diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..2ac6f26 --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1,6 @@ +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterEach, vi } from 'vitest' + +afterEach(() => cleanup()) +afterEach(() => vi.unstubAllGlobals()) diff --git a/frontend/src/test/smoke.test.tsx b/frontend/src/test/smoke.test.tsx new file mode 100644 index 0000000..7e25e6d --- /dev/null +++ b/frontend/src/test/smoke.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/react' +import { createMemoryRouter, RouterProvider } from 'react-router' +import { expect, test } from 'vitest' + +test('router renders a route', () => { + const router = createMemoryRouter([{ path: '/', element:

    LoVisual

    }]) + render() + expect(screen.getByRole('heading', { name: 'LoVisual' })).toBeInTheDocument() +}) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9f9b446..d4e9126 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,8 +1,24 @@ import tailwindcss from '@tailwindcss/vite' import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import { defineConfig } from 'vitest/config' -// https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + server: { + // Same-origin in dev: the browser talks to /api, Vite forwards to the gateway. + proxy: { + '/api': { + target: 'http://localhost:8080', + rewrite: (path) => path.replace(/^\/api/, ''), + // The refresh cookie is scoped to Path=/auth by the backend; in dev the + // browser sees it under /api/auth, so rewrite the path accordingly. + cookiePathRewrite: { '/auth': '/api/auth' }, + }, + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + css: false, + }, }) diff --git a/mod/TODO.md b/mod/TODO.md index 8ae521c..e0de8fc 100644 --- a/mod/TODO.md +++ b/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) — ПЛАН diff --git a/mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java b/mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java index a8d022c..01aecbe 100644 --- a/mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java +++ b/mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/HudRender.java b/mod/src/main/java/dev/loki/lovisual/HudRender.java new file mode 100644 index 0000000..8390d1b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/HudRender.java @@ -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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/Lifecycle.java b/mod/src/main/java/dev/loki/lovisual/Lifecycle.java index 111d4b9..d793601 100644 --- a/mod/src/main/java/dev/loki/lovisual/Lifecycle.java +++ b/mod/src/main/java/dev/loki/lovisual/Lifecycle.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/LoVisual.java b/mod/src/main/java/dev/loki/lovisual/LoVisual.java index ce6a561..862e4fc 100644 --- a/mod/src/main/java/dev/loki/lovisual/LoVisual.java +++ b/mod/src/main/java/dev/loki/lovisual/LoVisual.java @@ -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(); diff --git a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java index 31f8747..e0e8f9e 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java index 94f71ba..69b8e90 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java index 3a6f2eb..5245661 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java index f933e8b..8b2523f 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java index 88a9979..a3ed385 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java index f871b4c..716372a 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java @@ -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( diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java index 4cb3ed7..b903a0c 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java @@ -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. + * + *

    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.

    + */ public enum AddonManager { ; public static final String ADDON_METADATA_KEY = "lovisual:addon"; - private static final Map ADDONS = new LinkedHashMap<>(); - private static final Map ENABLED_OVERRIDES = new LinkedHashMap<>(); - private static final Map MODULE_OWNERS = new LinkedHashMap<>(); - private static final Map DRAGGABLE_HUD_OWNERS = new LinkedHashMap<>(); - private static final Map STATIC_HUD_OWNERS = new LinkedHashMap<>(); + static final Map ADDONS = new LinkedHashMap<>(); + static final Map 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 snapshots() { @@ -117,7 +100,7 @@ public enum AddonManager { init(); List 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 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); } } diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java index d137c90..a163c56 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonDescriptors.java b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonDescriptors.java new file mode 100644 index 0000000..1553988 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonDescriptors.java @@ -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 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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonLifecycle.java b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonLifecycle.java new file mode 100644 index 0000000..0596d51 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonLifecycle.java @@ -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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonProfiles.java b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonProfiles.java new file mode 100644 index 0000000..fa18749 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonProfiles.java @@ -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::module:} 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 MODULE_OWNERS = new LinkedHashMap<>(); + private static final Map DRAGGABLE_HUD_OWNERS = new LinkedHashMap<>(); + private static final Map 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::...} 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)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonRuntime.java b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonRuntime.java new file mode 100644 index 0000000..a0a0c27 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/addon/core/impl/AddonRuntime.java @@ -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(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java b/mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java index eb40baf..0a311e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java b/mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java index f4492ce..05fa37c 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java index 827f56e..30e38d8 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java index 4a79e9b..ccbd3e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java index 995252d..b38c117 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java index a40ab2d..5790c6d 100644 --- a/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java +++ b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java @@ -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 { diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java index f13e6fb..fead11f 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java @@ -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; /** diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java index 13bf253..1c8a918 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java index 026f715..5ddcfc6 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java index dedc896..b777c60 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java index fd0232c..bf96a79 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java index c18dc7c..d4df927 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java @@ -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 { diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java b/mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java index b370510..57c7602 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java b/mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java index f8a9919..e41c7ee 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java index 918769e..da53119 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java index c382722..88b5aa0 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java index 57057aa..82720c5 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java @@ -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 { diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java index c12ea92..ec6e640 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java index 711197b..59344e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java @@ -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 diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java index 0f4181f..fbbf12f 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java index c00c925..4e89e33 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java @@ -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 diff --git a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java index 21cf930..5d40fc2 100644 --- a/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java index f3fd349..5ed3139 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java index dbf6dc3..f74bc3b 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java index d5a6458..6bac310 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java index 7c21659..bd870ea 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java index d081bf9..1f76869 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java index dc5337d..a98fed9 100644 --- a/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java +++ b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java index c58a274..aa53ed5 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java index 7f91e8c..ab19978 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java index 6deaef1..2394a72 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java index d1bd633..b032d1e 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java index abbc4a3..c0f36c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java index dca513d..4503edd 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java index 401461e..af49971 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java index b6fbcfd..538b6ee 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java index 58177d6..149baf8 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java index 8e190b3..70b8029 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java index d5aacfa..02969dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java index 8eaba4b..d1036d5 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java index 794e948..6be4ac7 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java index df41b21..2aa2996 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java +++ b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java index 16aaa14..9f8a16b 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java @@ -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; /** diff --git a/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java index 8fc33cd..af0f10f 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java +++ b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java index 42c2f20..22dd907 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java +++ b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java index 25d0cca..7d39e8e 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java +++ b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java index b5972ba..901d834 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java +++ b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java index 550e4dd..422bd6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java index 1124a6d..e6eb09c 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java +++ b/mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java index 03bfa9b..26411cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java index 354d713..4967971 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java +++ b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java index 3aeb5a7..0cf2b6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java +++ b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java @@ -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 PENDING_TEXT = new HashMap<>(); - private static final Map> PENDING_TASKS = new HashMap<>(); - private static final Map 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 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 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 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 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 out = new ArrayList<>(); - for (JsonElement child : el.getAsJsonArray()) { - out.add(unwrapJsonElement(child)); - } - return out; - } - if (el.isJsonObject()) { - Map 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 out = new ArrayList<>(); - for (Json5Element child : el.getAsJson5Array()) { - out.add(unwrapJson5(child)); - } - return out; - } - if (el.isJson5Object()) { - Map 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); } } diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java b/mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java index d516ea5..75c10ca 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java +++ b/mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java b/mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java index 797ec5c..641703c 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigFilePaths.java b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigFilePaths.java new file mode 100644 index 0000000..b908937 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigFilePaths.java @@ -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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigGsonSupport.java b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigGsonSupport.java new file mode 100644 index 0000000..9c5d68b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigGsonSupport.java @@ -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 out = new ArrayList<>(); + for (JsonElement child : el.getAsJsonArray()) out.add(unwrapJsonElement(child)); + return out; + } + if (el.isJsonObject()) { + Map 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 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); } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigJson5Support.java b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigJson5Support.java new file mode 100644 index 0000000..dd4fc1d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigJson5Support.java @@ -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 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 out = new ArrayList<>(); + for (Json5Element child : el.getAsJson5Array()) out.add(unwrapJson5(child)); + return out; + } + if (el.isJson5Object()) { + Map 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 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 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); } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigSaveScheduler.java b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigSaveScheduler.java new file mode 100644 index 0000000..00df9ca --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/config/io/serializer/ConfigSaveScheduler.java @@ -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 PENDING_TEXT = new HashMap<>(); + private static final Map> PENDING_TASKS = new HashMap<>(); + private static final Map 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) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java index 8610d2e..c145bea 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java @@ -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.*; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java index 15d0b0a..c6d80d9 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java index d16d00f..cf16d36 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java index b379390..13ae0b7 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java index 096305d..304cc3c 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java index 912d619..289c843 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java index dd87d76..3c5410f 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java @@ -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.*; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java index 15c60fc..83df242 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java index 4bfb788..f08cc27 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java index 9ca9955..83ee6d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java index 5d2bece..fcb243d 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java index ab3a141..a21e18f 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java index cc3f217..0d88638 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java index 64f8e70..a87c614 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java @@ -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; /** diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java index fc939dd..8d735b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java @@ -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.*; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java index 95043a8..8973ea2 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java index 1c1b97f..f7576ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java index 20736ce..9dc60cb 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java @@ -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 { diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java index 9656957..72b55bd 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java index a061750..1bbd5a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java index 7cfe511..58ae15f 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java index a51f86d..4dfe271 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java @@ -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 { diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java index 89f4a46..883a83f 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java index 41e0e78..16d5cf3 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java @@ -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.*; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java index 17cbfda..2f98147 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java @@ -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; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java index 511bcd7..122eef1 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java @@ -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.primitive; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java index ed957f1..84bf201 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java @@ -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.primitive; import dev.loki.lovisual.config.values.collection.ConfigValue; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java index 9ade29e..8b1ee61 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java @@ -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.primitive; import java.text.DecimalFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java index c32faab..3dbc25b 100644 --- a/mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java +++ b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java @@ -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.primitive; import dev.loki.lovisual.config.values.collection.ConfigValue; diff --git a/mod/src/main/java/dev/loki/lovisual/events/Event.java b/mod/src/main/java/dev/loki/lovisual/events/Event.java index 52776fd..d99b431 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/Event.java +++ b/mod/src/main/java/dev/loki/lovisual/events/Event.java @@ -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.events; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/EventBus.java b/mod/src/main/java/dev/loki/lovisual/events/EventBus.java index 9207fa4..7145fac 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/EventBus.java +++ b/mod/src/main/java/dev/loki/lovisual/events/EventBus.java @@ -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.events; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/events/Events.java b/mod/src/main/java/dev/loki/lovisual/events/Events.java index 402fd7c..3a3b938 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/Events.java +++ b/mod/src/main/java/dev/loki/lovisual/events/Events.java @@ -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.events; public enum Events { diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java index 74d1234..aba64f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java @@ -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.events.impl.entity; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java index b385417..36ece64 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java @@ -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.events.impl.entity; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java index 3c50733..6177f3d 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java @@ -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.events.impl.entity; import net.minecraft.world.entity.LivingEntity; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java index 05019d2..de44f38 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java @@ -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.events.impl.entity; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java index 041f9c7..d6a53a5 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java @@ -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.events.impl.input; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java index 219fa32..cb9a941 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java @@ -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.events.impl.input; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java index 123c6ed..6818031 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java @@ -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.events.impl.input; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java index 4c1584c..4469969 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java @@ -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.events.impl.input; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java index 78f81fe..e8a4569 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java @@ -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.events.impl.movement; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java index c5fb53e..70b95bd 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java @@ -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.events.impl.movement; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java index 2075601..8ae3d14 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java @@ -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.events.impl.movement; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java index 7552f36..710067d 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java @@ -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.events.impl.movement; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java index 5e953ec..ba13dbc 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java @@ -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.events.impl.network; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java index 3eed9a5..f1bfcde 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java @@ -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.events.impl.network; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java index 4df3ece..57e7ca3 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java @@ -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.events.impl.network; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java index eab4f80..02e7b7e 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java @@ -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.events.impl.network; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java index 54b9ef6..99b73de 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java @@ -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.events.impl.player; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java index a7665e9..344ff3b 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java @@ -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.events.impl.player; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java index 13586c9..518f541 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java @@ -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.events.impl.player; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java index abd5983..7aa2d4c 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java @@ -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.events.impl.player; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java index 51c94b8..6b3ead3 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java @@ -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.events.impl.pvp; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java index edbaf09..253721b 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java @@ -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.events.impl.pvp; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java index 02e7a52..dcfdb3f 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java @@ -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.events.impl.pvp; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java index 25211da..c273d8b 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java @@ -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.events.impl.pvp; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java index 2d92a20..29c1ab3 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java @@ -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.events.impl.render; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java index 10b4803..7195ed0 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java @@ -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.events.impl.render; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java index 7c86791..e19886d 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java @@ -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.events.impl.render; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java index 3154dc3..c1f413d 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java @@ -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.events.impl.render; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java index b75996a..fa8d524 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java @@ -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.events.impl.render.particles; import net.minecraft.core.particles.ParticleOptions; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java index 6b0e182..cd65c11 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java @@ -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.events.impl.world; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java index 37e5821..9f32fd5 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java @@ -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.events.impl.world; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java index 67cf688..0eee5aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java @@ -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.events.impl.world; import dev.loki.lovisual.events.Event; diff --git a/mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java index 59c8bad..d33dff6 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java +++ b/mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java @@ -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.events.impl.world; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java b/mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java index 368bc2a..e947515 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java @@ -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.events.meta; import java.lang.annotation.ElementType; diff --git a/mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java b/mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java index 3250c38..b30a944 100644 --- a/mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java +++ b/mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java @@ -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.events.meta; import java.lang.annotation.ElementType; diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java index bf226eb..15a6d0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java +++ b/mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java @@ -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.account; public enum AccountAuthType { diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java index 6032b7d..739be50 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java @@ -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.account; import net.minecraft.resources.Identifier; @@ -14,48 +7,54 @@ import dev.loki.lovisual.config.io.JsonConfigObject; import dev.loki.lovisual.config.values.collection.ConfigValue; import dev.loki.lovisual.config.values.primitive.StringValue; import dev.loki.lovisual.util.session.MicrosoftSessionResult; -import dev.loki.lovisual.util.session.SessionChanger; -import dev.loki.lovisual.util.session.microsoft.auth.MicrosoftAuthService; -import dev.loki.lovisual.util.logging.DebugLog; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +/** + * Singleton config + API for the alt-account manager. Holds the persisted account list and the + * active / authorization-required account fields, and stays the public entry point consumed by the + * ClickGUI alt screens. Behavior is delegated within the {@code features.account} package: + * {@link AccountsValue} (JSON), {@link AccountStore} (collection CRUD) and {@link AccountSession} + * (active-account application + auth flag), keeping this facade under the file-size cap. + */ public final class AccountConfig implements JsonConfigObject, ConfigNameProvider { public static final AccountConfig INSTANCE = new AccountConfig(); private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); - private final AccountsValue accounts = new AccountsValue("accounts"); - private final StringValue activeAccountName = new StringValue("active_name", ""); - private final StringValue activeAccountDate = new StringValue("active_date", ""); - private final StringValue activeAccountSkin = new StringValue("active_skin", ""); - private String microsoftAuthorizationRequiredName = ""; + final AccountsValue accounts = new AccountsValue("accounts"); + final StringValue activeAccountName = new StringValue("active_name", ""); + final StringValue activeAccountDate = new StringValue("active_date", ""); + final StringValue activeAccountSkin = new StringValue("active_skin", ""); + String microsoftAuthorizationRequiredName = ""; private AccountConfig() { ConfigSerializer.load(this); - applyActiveSession(); + AccountSession.applyActiveSession(this); } public static AccountConfig get() { return INSTANCE; } - private static boolean isBlank(String value) { + static boolean isBlank(String value) { return value == null || value.isBlank(); } - private static String normalizeName(String name) { + static String normalizeName(String name) { return name == null ? "" : name.trim(); } - private static String nowString() { + static String nowString() { return LocalDateTime.now().format(DATE_FORMAT); } - private static Identifier parseIdentifier(String raw) { + static Identifier parseIdentifier(String raw) { if (isBlank(raw)) { return null; } @@ -66,7 +65,7 @@ public final class AccountConfig implements JsonConfigObject, ConfigNameProvider } } - private static UUID parseUuid(String raw) { + static UUID parseUuid(String raw) { if (isBlank(raw)) { return null; } @@ -78,211 +77,87 @@ public final class AccountConfig implements JsonConfigObject, ConfigNameProvider } public List getAccounts() { - return accounts.get(); + return AccountStore.getAccounts(this); } public List getSortedAccounts() { - List sorted = new ArrayList<>(accounts.get()); - sorted.sort(Comparator - .comparing(AccountEntry::isPinned).reversed() - .thenComparingInt(AccountEntry::getOriginalIndex)); - return sorted; + return AccountStore.getSortedAccounts(this); } public AccountEntry getAccountBySortedIndex(int sortedIndex) { - List sorted = getSortedAccounts(); - if (sortedIndex < 0 || sortedIndex >= sorted.size()) { - return null; - } - return sorted.get(sortedIndex); + return AccountStore.getAccountBySortedIndex(this, sortedIndex); } public AccountEntry findByName(String name) { - String normalized = normalizeName(name); - if (normalized.isEmpty()) { - return null; - } - for (AccountEntry entry : accounts.get()) { - if (normalizeName(entry.getName()).equals(normalized)) { - return entry; - } - } - return null; + return AccountStore.findByName(this, name); } public AccountEntry addAccount(String name) { - return addAccount(name, nowString(), null); + return AccountStore.addAccount(this, name); } public AccountEntry addAccount(String name, String date, Identifier skin) { - String cleanName = normalizeName(name); - if (cleanName.isEmpty()) { - return null; - } - - AccountEntry existing = findByName(cleanName); - if (existing != null) { - existing.setAuthType(AccountAuthType.OFFLINE); - existing.setOnlineUuid(null); - existing.setMicrosoftRefreshToken(""); - clearAuthorizationRequired(existing.getName()); - if (!isBlank(date)) { - existing.setDate(date); - } - if (skin != null) { - existing.setSkin(skin); - } - save(); - return existing; - } - - AccountEntry entry = new AccountEntry(cleanName, isBlank(date) ? nowString() : date, skin, false, accounts.get().size()); - accounts.get().add(entry); - save(); - return entry; + return AccountStore.addAccount(this, name, date, skin); } public AccountEntry addMicrosoftAccount(MicrosoftSessionResult session) { - if (session == null || isBlank(session.name()) || session.uuid() == null || isBlank(session.refreshToken())) { - return null; - } - - AccountEntry existing = findByName(session.name()); - if (existing != null) { - existing.applyMicrosoftSession(session); - existing.setDate(nowString()); - clearAuthorizationRequired(existing.getName()); - save(); - return existing; - } - - AccountEntry entry = new AccountEntry( - session.name(), - nowString(), - null, - false, - accounts.get().size(), - AccountAuthType.MICROSOFT, - session.uuid(), - session.refreshToken() - ); - accounts.get().add(entry); - clearAuthorizationRequired(entry.getName()); - save(); - return entry; + return AccountStore.addMicrosoftAccount(this, session); } public void removeAccount(AccountEntry entry) { - if (entry == null) { - return; - } - if (!accounts.get().remove(entry)) { - return; - } - - if (isActive(entry)) { - clearActiveAccount(); - } else { - reindexAccounts(); - save(); - } + AccountStore.removeAccount(this, entry); } public void removeAccountBySortedIndex(int sortedIndex) { - removeAccount(getAccountBySortedIndex(sortedIndex)); + AccountStore.removeAccountBySortedIndex(this, sortedIndex); } public void clearAllAccounts() { - accounts.get().clear(); - activeAccountName.set(""); - activeAccountDate.set(""); - activeAccountSkin.set(""); - microsoftAuthorizationRequiredName = ""; - save(); + AccountStore.clearAllAccounts(this); } public void togglePin(int sortedIndex) { - AccountEntry entry = getAccountBySortedIndex(sortedIndex); - if (entry == null) { - return; - } - entry.togglePinned(); - save(); + AccountStore.togglePin(this, sortedIndex); } public void setActiveAccount(AccountEntry entry) { - if (entry == null) { - clearActiveAccount(); - return; - } - - activeAccountName.set(normalizeName(entry.getName())); - activeAccountDate.set(entry.getDate() == null ? "" : entry.getDate()); - Identifier skin = entry.getSkin(); - activeAccountSkin.set(skin == null ? "" : skin.toString()); - applySession(entry); - save(); + AccountSession.setActiveAccount(this, entry); } public void setActiveAccount(String name, String date, Identifier skin) { - String cleanName = normalizeName(name); - if (cleanName.isEmpty()) { - clearActiveAccount(); - return; - } - - AccountEntry entry = findByName(cleanName); - if (entry == null) { - entry = addAccount(cleanName, date, skin); - } else { - if (!isBlank(date)) { - entry.setDate(date); - } - if (skin != null) { - entry.setSkin(skin); - } - } - setActiveAccount(entry); + AccountSession.setActiveAccount(this, name, date, skin); } public void clearActiveAccount() { - activeAccountName.set(""); - activeAccountDate.set(""); - activeAccountSkin.set(""); - reindexAccounts(); - save(); + AccountSession.clearActiveAccount(this); } public String getActiveAccountName() { - return activeAccountName.get(); + return AccountSession.getActiveAccountName(this); } public String getActiveAccountDate() { - return activeAccountDate.get(); + return AccountSession.getActiveAccountDate(this); } public Identifier getActiveAccountSkin() { - return parseIdentifier(activeAccountSkin.get()); + return AccountSession.getActiveAccountSkin(this); } public boolean isMicrosoftAuthorizationRequired() { - return getMicrosoftAuthorizationRequiredAccount() != null; + return AccountSession.isMicrosoftAuthorizationRequired(this); } public AccountEntry getMicrosoftAuthorizationRequiredAccount() { - if (isBlank(microsoftAuthorizationRequiredName)) { - return null; - } - AccountEntry entry = findByName(microsoftAuthorizationRequiredName); - return entry != null && entry.isMicrosoft() ? entry : null; + return AccountSession.getMicrosoftAuthorizationRequiredAccount(this); } public void clearMicrosoftAuthorizationRequired() { - microsoftAuthorizationRequiredName = ""; + AccountSession.clearMicrosoftAuthorizationRequired(this); } public void save() { - reindexAccounts(); + AccountStore.reindex(this); ConfigSerializer.requestSave(this); } @@ -300,174 +175,4 @@ public final class AccountConfig implements JsonConfigObject, ConfigNameProvider values.add(activeAccountSkin); return values; } - - private void applyActiveSession() { - if (isBlank(activeAccountName.get())) { - return; - } - AccountEntry entry = findByName(activeAccountName.get()); - if (entry != null) { - applySession(entry); - } else { - SessionChanger.changeUsername(activeAccountName.get()); - } - } - - private void applySession(AccountEntry entry) { - if (entry == null) { - return; - } - - if (!entry.isMicrosoft()) { - clearAuthorizationRequired(entry.getName()); - SessionChanger.changeUsername(entry.getName()) - .exceptionally(t -> { - DebugLog.error("[AccountConfig] Failed to apply offline account session: " + entry.getName(), t); - return null; - }); - return; - } - - String refreshToken = entry.getMicrosoftRefreshToken(); - if (isBlank(refreshToken)) { - if (SessionChanger.canUseOriginalLauncherSession(entry.getName(), entry.getOnlineUuid())) { - clearAuthorizationRequired(entry.getName()); - SessionChanger.restoreOriginalLauncherSession(false) - .exceptionally(t -> { - DebugLog.error("[AccountConfig] Failed to restore launcher Microsoft session: " + entry.getName(), t); - markAuthorizationRequired(entry); - return null; - }); - return; - } - markAuthorizationRequired(entry); - DebugLog.warnOnce("alt-microsoft-token-empty-" + entry.getName(), "[AccountConfig] Microsoft account requires authorization: %s", entry.getName()); - return; - } - - clearAuthorizationRequired(entry.getName()); - MicrosoftAuthService.loginWithRefreshToken(refreshToken) - .thenCompose(session -> { - entry.applyMicrosoftSession(session); - activeAccountName.set(session.name()); - activeAccountDate.set(entry.getDate() == null ? "" : entry.getDate()); - save(); - return SessionChanger.changeSession(session.toLoginData()); - }) - .exceptionally(t -> { - DebugLog.error("[AccountConfig] Failed to apply Microsoft account session: " + entry.getName(), t); - markAuthorizationRequired(entry); - return null; - }); - } - - private void markAuthorizationRequired(AccountEntry entry) { - microsoftAuthorizationRequiredName = entry == null ? "" : normalizeName(entry.getName()); - } - - private void clearAuthorizationRequired(String name) { - if (normalizeName(name).equalsIgnoreCase(normalizeName(microsoftAuthorizationRequiredName))) { - microsoftAuthorizationRequiredName = ""; - } - } - - private boolean isActive(AccountEntry entry) { - if (entry == null) { - return false; - } - return normalizeName(entry.getName()).equals(normalizeName(activeAccountName.get())); - } - - private void reindexAccounts() { - List list = accounts.get(); - for (int i = 0; i < list.size(); i++) { - list.get(i).setOriginalIndex(i); - } - } - - private static final class AccountsValue extends ConfigValue> { - - private AccountsValue(String name) { - super(name, new ArrayList<>()); - } - - private static String stringValue(Object value) { - if (value == null) { - return ""; - } - return String.valueOf(value).trim(); - } - - private static boolean boolValue(Object value) { - if (value instanceof Boolean bool) { - return bool; - } - if (value instanceof String string) { - return Boolean.parseBoolean(string); - } - return false; - } - - private static int intValue(Object value, int fallback) { - if (value instanceof Number number) { - return Math.max(0, number.intValue()); - } - if (value instanceof String string) { - try { - return Math.max(0, Integer.parseInt(string.trim())); - } catch (NumberFormatException ignored) { - return fallback; - } - } - return fallback; - } - - @Override - public Object toJson() { - List out = new ArrayList<>(); - for (AccountEntry entry : value) { - Map row = new LinkedHashMap<>(); - row.put("name", entry.getName()); - row.put("date", entry.getDate()); - row.put("skin", entry.getSkin() == null ? "" : entry.getSkin().toString()); - row.put("pinned", entry.isPinned()); - row.put("originalIndex", entry.getOriginalIndex()); - row.put("authType", entry.getAuthType().id()); - row.put("onlineUuid", entry.getOnlineUuid() == null ? "" : entry.getOnlineUuid().toString()); - row.put("microsoftRefreshToken", ""); - out.add(row); - } - return out; - } - - @Override - public void fromJson(Object json) { - List loaded = new ArrayList<>(); - if (json instanceof Iterable iterable) { - int fallbackIndex = 0; - for (Object item : iterable) { - if (!(item instanceof Map row)) { - continue; - } - - String name = stringValue(row.get("name")); - if (name.isBlank()) { - continue; - } - - String date = stringValue(row.get("date")); - Identifier skin = parseIdentifier(stringValue(row.get("skin"))); - boolean pinned = boolValue(row.get("pinned")); - int originalIndex = intValue(row.get("originalIndex"), fallbackIndex); - AccountAuthType authType = AccountAuthType.byId(stringValue(row.get("authType"))); - UUID onlineUuid = parseUuid(stringValue(row.get("onlineUuid"))); - loaded.add(new AccountEntry(name, date, skin, pinned, originalIndex, authType, onlineUuid, "")); - fallbackIndex++; - } - } - - loaded.sort(Comparator.comparingInt(AccountEntry::getOriginalIndex)); - value = loaded; - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java index 29569e0..76f15e1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java @@ -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.account; import dev.loki.lovisual.util.session.MicrosoftSessionResult; diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java b/mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java index 6731d72..ae505df 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java @@ -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.account; import com.google.gson.JsonObject; diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountSession.java b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountSession.java new file mode 100644 index 0000000..1cb759a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountSession.java @@ -0,0 +1,169 @@ +package dev.loki.lovisual.features.account; + +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.session.SessionChanger; +import dev.loki.lovisual.util.session.microsoft.auth.MicrosoftAuthService; +import net.minecraft.resources.Identifier; + +/** + * Owns the "current session" concerns of {@link AccountConfig}: applying the active account + * (offline rename, launcher-session restore, or Microsoft refresh-token login) and the active / + * authorization-required account fields. Async failures are logged and, for Microsoft, recorded + * via the authorization-required flag. Takes the owning {@link AccountConfig} for package-private + * field access; lives in a subfolder to keep both this class and the manager under the size caps. + */ +final class AccountSession { + + private AccountSession() { } + + /** Re-apply the persisted active account on startup. */ + static void applyActiveSession(AccountConfig owner) { + if (AccountConfig.isBlank(owner.activeAccountName.get())) { + return; + } + AccountEntry entry = AccountStore.findByName(owner, owner.activeAccountName.get()); + if (entry != null) { + applySession(owner, entry); + } else { + SessionChanger.changeUsername(owner.activeAccountName.get()); + } + } + + /** Apply the given account's session, dispatching by auth type and token availability. */ + static void applySession(AccountConfig owner, AccountEntry entry) { + if (entry == null) { + return; + } + if (!entry.isMicrosoft()) { + applyOfflineSession(entry); + return; + } + String refreshToken = entry.getMicrosoftRefreshToken(); + if (AccountConfig.isBlank(refreshToken)) { + applyMicrosoftWithoutToken(owner, entry); + return; + } + applyMicrosoftSession(owner, entry, refreshToken); + } + + private static void applyOfflineSession(AccountEntry entry) { + SessionChanger.changeUsername(entry.getName()) + .exceptionally(t -> { + DebugLog.error("[AccountConfig] Failed to apply offline account session: " + entry.getName(), t); + return null; + }); + } + + private static void applyMicrosoftWithoutToken(AccountConfig owner, AccountEntry entry) { + if (SessionChanger.canUseOriginalLauncherSession(entry.getName(), entry.getOnlineUuid())) { + clearAuthorizationRequired(owner, entry.getName()); + SessionChanger.restoreOriginalLauncherSession(false) + .exceptionally(t -> { + DebugLog.error("[AccountConfig] Failed to restore launcher Microsoft session: " + entry.getName(), t); + markAuthorizationRequired(owner, entry); + return null; + }); + return; + } + markAuthorizationRequired(owner, entry); + DebugLog.warnOnce("alt-microsoft-token-empty-" + entry.getName(), "[AccountConfig] Microsoft account requires authorization: %s", entry.getName()); + } + + private static void applyMicrosoftSession(AccountConfig owner, AccountEntry entry, String refreshToken) { + clearAuthorizationRequired(owner, entry.getName()); + MicrosoftAuthService.loginWithRefreshToken(refreshToken) + .thenCompose(session -> { + entry.applyMicrosoftSession(session); + owner.activeAccountName.set(session.name()); + owner.activeAccountDate.set(entry.getDate() == null ? "" : entry.getDate()); + owner.save(); + return SessionChanger.changeSession(session.toLoginData()); + }) + .exceptionally(t -> { + DebugLog.error("[AccountConfig] Failed to apply Microsoft account session: " + entry.getName(), t); + markAuthorizationRequired(owner, entry); + return null; + }); + } + + static void setActiveAccount(AccountConfig owner, AccountEntry entry) { + if (entry == null) { + clearActiveAccount(owner); + return; + } + + owner.activeAccountName.set(AccountConfig.normalizeName(entry.getName())); + owner.activeAccountDate.set(entry.getDate() == null ? "" : entry.getDate()); + Identifier skin = entry.getSkin(); + owner.activeAccountSkin.set(skin == null ? "" : skin.toString()); + applySession(owner, entry); + owner.save(); + } + + static void setActiveAccount(AccountConfig owner, String name, String date, Identifier skin) { + String cleanName = AccountConfig.normalizeName(name); + if (cleanName.isEmpty()) { + clearActiveAccount(owner); + return; + } + + AccountEntry entry = AccountStore.findByName(owner, cleanName); + if (entry == null) { + entry = AccountStore.addAccount(owner, cleanName, date, skin); + } else { + if (!AccountConfig.isBlank(date)) { + entry.setDate(date); + } + if (skin != null) { + entry.setSkin(skin); + } + } + setActiveAccount(owner, entry); + } + + static void clearActiveAccount(AccountConfig owner) { + owner.activeAccountName.set(""); + owner.activeAccountDate.set(""); + owner.activeAccountSkin.set(""); + AccountStore.reindex(owner); + owner.save(); + } + + static String getActiveAccountName(AccountConfig owner) { + return owner.activeAccountName.get(); + } + + static String getActiveAccountDate(AccountConfig owner) { + return owner.activeAccountDate.get(); + } + + static Identifier getActiveAccountSkin(AccountConfig owner) { + return AccountConfig.parseIdentifier(owner.activeAccountSkin.get()); + } + + static boolean isMicrosoftAuthorizationRequired(AccountConfig owner) { + return getMicrosoftAuthorizationRequiredAccount(owner) != null; + } + + static AccountEntry getMicrosoftAuthorizationRequiredAccount(AccountConfig owner) { + if (AccountConfig.isBlank(owner.microsoftAuthorizationRequiredName)) { + return null; + } + AccountEntry entry = AccountStore.findByName(owner, owner.microsoftAuthorizationRequiredName); + return entry != null && entry.isMicrosoft() ? entry : null; + } + + static void clearMicrosoftAuthorizationRequired(AccountConfig owner) { + owner.microsoftAuthorizationRequiredName = ""; + } + + static void markAuthorizationRequired(AccountConfig owner, AccountEntry entry) { + owner.microsoftAuthorizationRequiredName = entry == null ? "" : AccountConfig.normalizeName(entry.getName()); + } + + static void clearAuthorizationRequired(AccountConfig owner, String name) { + if (AccountConfig.normalizeName(name).equalsIgnoreCase(AccountConfig.normalizeName(owner.microsoftAuthorizationRequiredName))) { + owner.microsoftAuthorizationRequiredName = ""; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountStore.java b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountStore.java new file mode 100644 index 0000000..be46f7e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountStore.java @@ -0,0 +1,169 @@ +package dev.loki.lovisual.features.account; + +import dev.loki.lovisual.util.session.MicrosoftSessionResult; +import net.minecraft.resources.Identifier; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Collection operations for {@link AccountConfig}: account lookup, add (offline / Microsoft), + * remove, pin and index maintenance. Each method takes the owning {@link AccountConfig} so the + * singleton keeps holding the state and public API while this collaborator stays small; active + * -session and auth concerns live in {@link AccountSession}. Shares the {@code features.account} + * package (lives in a subfolder) for package-private access to the owner's config values. + */ +final class AccountStore { + + private AccountStore() { } + + static List getAccounts(AccountConfig owner) { + return owner.accounts.get(); + } + + static List getSortedAccounts(AccountConfig owner) { + List sorted = new ArrayList<>(owner.accounts.get()); + sorted.sort(Comparator + .comparing(AccountEntry::isPinned).reversed() + .thenComparingInt(AccountEntry::getOriginalIndex)); + return sorted; + } + + static AccountEntry getAccountBySortedIndex(AccountConfig owner, int sortedIndex) { + List sorted = getSortedAccounts(owner); + if (sortedIndex < 0 || sortedIndex >= sorted.size()) { + return null; + } + return sorted.get(sortedIndex); + } + + static AccountEntry findByName(AccountConfig owner, String name) { + String normalized = AccountConfig.normalizeName(name); + if (normalized.isEmpty()) { + return null; + } + for (AccountEntry entry : owner.accounts.get()) { + if (AccountConfig.normalizeName(entry.getName()).equals(normalized)) { + return entry; + } + } + return null; + } + + static AccountEntry addAccount(AccountConfig owner, String name) { + return addAccount(owner, name, AccountConfig.nowString(), null); + } + + static AccountEntry addAccount(AccountConfig owner, String name, String date, Identifier skin) { + String cleanName = AccountConfig.normalizeName(name); + if (cleanName.isEmpty()) { + return null; + } + + AccountEntry existing = findByName(owner, cleanName); + if (existing != null) { + existing.setAuthType(AccountAuthType.OFFLINE); + existing.setOnlineUuid(null); + existing.setMicrosoftRefreshToken(""); + AccountSession.clearAuthorizationRequired(owner, existing.getName()); + if (!AccountConfig.isBlank(date)) { + existing.setDate(date); + } + if (skin != null) { + existing.setSkin(skin); + } + owner.save(); + return existing; + } + + AccountEntry entry = new AccountEntry(cleanName, AccountConfig.isBlank(date) ? AccountConfig.nowString() : date, + skin, false, owner.accounts.get().size()); + owner.accounts.get().add(entry); + owner.save(); + return entry; + } + + static AccountEntry addMicrosoftAccount(AccountConfig owner, MicrosoftSessionResult session) { + if (session == null || AccountConfig.isBlank(session.name()) || session.uuid() == null + || AccountConfig.isBlank(session.refreshToken())) { + return null; + } + + AccountEntry existing = findByName(owner, session.name()); + if (existing != null) { + existing.applyMicrosoftSession(session); + existing.setDate(AccountConfig.nowString()); + AccountSession.clearAuthorizationRequired(owner, existing.getName()); + owner.save(); + return existing; + } + + AccountEntry entry = new AccountEntry( + session.name(), + AccountConfig.nowString(), + null, + false, + owner.accounts.get().size(), + AccountAuthType.MICROSOFT, + session.uuid(), + session.refreshToken() + ); + owner.accounts.get().add(entry); + AccountSession.clearAuthorizationRequired(owner, entry.getName()); + owner.save(); + return entry; + } + + static void removeAccount(AccountConfig owner, AccountEntry entry) { + if (entry == null) { + return; + } + if (!owner.accounts.get().remove(entry)) { + return; + } + + if (isActive(owner, entry)) { + AccountSession.clearActiveAccount(owner); + } else { + reindex(owner); + owner.save(); + } + } + + static void removeAccountBySortedIndex(AccountConfig owner, int sortedIndex) { + removeAccount(owner, getAccountBySortedIndex(owner, sortedIndex)); + } + + static void clearAllAccounts(AccountConfig owner) { + owner.accounts.get().clear(); + owner.activeAccountName.set(""); + owner.activeAccountDate.set(""); + owner.activeAccountSkin.set(""); + owner.microsoftAuthorizationRequiredName = ""; + owner.save(); + } + + static void togglePin(AccountConfig owner, int sortedIndex) { + AccountEntry entry = getAccountBySortedIndex(owner, sortedIndex); + if (entry == null) { + return; + } + entry.togglePinned(); + owner.save(); + } + + static boolean isActive(AccountConfig owner, AccountEntry entry) { + if (entry == null) { + return false; + } + return AccountConfig.normalizeName(entry.getName()).equals(AccountConfig.normalizeName(owner.activeAccountName.get())); + } + + static void reindex(AccountConfig owner) { + List list = owner.accounts.get(); + for (int i = 0; i < list.size(); i++) { + list.get(i).setOriginalIndex(i); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountsValue.java b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountsValue.java new file mode 100644 index 0000000..5f8f100 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/account/impl/AccountsValue.java @@ -0,0 +1,104 @@ +package dev.loki.lovisual.features.account; + +import dev.loki.lovisual.config.values.collection.ConfigValue; +import net.minecraft.resources.Identifier; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * The {@code accounts} config entry: a {@link ConfigValue} holding the list of {@link AccountEntry} + * records and their JSON (de)serialization. Microsoft refresh tokens are intentionally never + * written to disk ({@code microsoftRefreshToken} is always persisted empty). Shares the + * {@code features.account} package (lives in a subfolder) so it can reuse {@link + * AccountConfig}'s parse helpers and {@link AccountEntry}'s package-private constructors. + */ +final class AccountsValue extends ConfigValue> { + + AccountsValue(String name) { + super(name, new ArrayList<>()); + } + + private static String stringValue(Object value) { + if (value == null) { + return ""; + } + return String.valueOf(value).trim(); + } + + private static boolean boolValue(Object value) { + if (value instanceof Boolean bool) { + return bool; + } + if (value instanceof String string) { + return Boolean.parseBoolean(string); + } + return false; + } + + private static int intValue(Object value, int fallback) { + if (value instanceof Number number) { + return Math.max(0, number.intValue()); + } + if (value instanceof String string) { + try { + return Math.max(0, Integer.parseInt(string.trim())); + } catch (NumberFormatException ignored) { + return fallback; + } + } + return fallback; + } + + @Override + public Object toJson() { + List out = new ArrayList<>(); + for (AccountEntry entry : value) { + Map row = new LinkedHashMap<>(); + row.put("name", entry.getName()); + row.put("date", entry.getDate()); + row.put("skin", entry.getSkin() == null ? "" : entry.getSkin().toString()); + row.put("pinned", entry.isPinned()); + row.put("originalIndex", entry.getOriginalIndex()); + row.put("authType", entry.getAuthType().id()); + row.put("onlineUuid", entry.getOnlineUuid() == null ? "" : entry.getOnlineUuid().toString()); + row.put("microsoftRefreshToken", ""); + out.add(row); + } + return out; + } + + @Override + public void fromJson(Object json) { + List loaded = new ArrayList<>(); + if (json instanceof Iterable iterable) { + int fallbackIndex = 0; + for (Object item : iterable) { + if (!(item instanceof Map row)) { + continue; + } + + String name = stringValue(row.get("name")); + if (name.isBlank()) { + continue; + } + + String date = stringValue(row.get("date")); + Identifier skin = AccountConfig.parseIdentifier(stringValue(row.get("skin"))); + boolean pinned = boolValue(row.get("pinned")); + int originalIndex = intValue(row.get("originalIndex"), fallbackIndex); + AccountAuthType authType = AccountAuthType.byId(stringValue(row.get("authType"))); + UUID onlineUuid = AccountConfig.parseUuid(stringValue(row.get("onlineUuid"))); + loaded.add(new AccountEntry(name, date, skin, pinned, originalIndex, authType, onlineUuid, "")); + fallbackIndex++; + } + } + + loaded.sort(Comparator.comparingInt(AccountEntry::getOriginalIndex)); + value = loaded; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java index 2d6f60e..63f1084 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java @@ -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.core; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java index 9887ffb..394224d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java @@ -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.core; import dev.loki.lovisual.events.meta.UsedImplicitly; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java index f248fc6..b5db6f6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java @@ -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.core; import com.mojang.brigadier.context.StringRange; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java index 9395fd0..f4f441e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java @@ -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.core; import net.minecraft.client.resources.language.I18n; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java index 9a6b29d..8f2f601 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java @@ -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.module.core.Module; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java index c444681..4a36fe7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java @@ -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.client; import dev.loki.lovisual.addon.core.AddonIssue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java index 4376e71..9cc869b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java @@ -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.client; import dev.loki.lovisual.config.io.ConfigPaths; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java index 5859a17..b8b8320 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java @@ -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.client; import dev.loki.lovisual.config.io.ConfigSerializer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java index bccf6f9..25e998d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java @@ -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.client; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java index 0bc09e0..ab1c2d6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java @@ -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.info; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java index 7de4f32..30cb60a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java @@ -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.info; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java index b01e12d..b0e62b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java @@ -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.info; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java index 02c5d93..78595ca 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java @@ -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.info; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java index 291ffd6..713aec8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java @@ -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.module; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java index a000ebc..c6df275 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java @@ -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.module; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java index 3d0d069..d32aabe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java @@ -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.module; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java index 2e6784c..f9719f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java @@ -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.module; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java index 7355769..0e82126 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java @@ -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.world; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java index 38601a4..b423cb7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java @@ -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.world; import dev.loki.lovisual.features.command.core.CommandContext; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java index f5ba4d7..3c60bc2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java @@ -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.world; import dev.loki.lovisual.features.command.core.CommandContext; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java index 77481ca..84b27a7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java @@ -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.world.integration; import dev.loki.lovisual.features.command.util.ClientCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java index 5bc2093..f6ac5d7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java @@ -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.world.integration; import dev.loki.lovisual.compat.xaero.XaeroWaypointStore; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java index 3713c67..d8bc889 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java @@ -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.util; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java index 3f60767..00fc5d8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java @@ -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.util; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java index 25179ed..c2b87e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java @@ -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.util; /** diff --git a/mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java index 26bcb9e..d36eb18 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java +++ b/mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java @@ -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.util; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java index f579ed1..a34555b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java @@ -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.gui.chat; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java index 239bcd6..2154125 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java @@ -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.gui.chat.input; import dev.loki.lovisual.mixins.accessors.widget.TextFieldWidgetAccessor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java index 5e561b9..0536c0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java @@ -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.gui.chat.input; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java index a3a24ee..c491749 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.chat.input; import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor; import dev.loki.lovisual.render.engine.text.font.info.FontInfo; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java index b85dd12..a17af40 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java @@ -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.gui.chat.input; import com.mojang.blaze3d.platform.InputConstants; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java index de6fe24..9f8411b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.chat.menu; import dev.loki.lovisual.features.command.core.CommandManager; import dev.loki.lovisual.features.command.util.CommandPrefix; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java index f111a39..a1d26e6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java @@ -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.gui.chat.menu; import java.util.Collections; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java index 26f82b7..e021163 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java @@ -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.gui.chat.menu; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips.BetterTooltips; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java index 3bfa011..261b809 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java @@ -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.gui.chat.messages; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java index 74e0cce..b7f0275 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.chat.messages; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java index 329dc80..2965eb1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java @@ -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.gui.chat.messages; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java index 5cbe3b3..d60e86b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java @@ -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.gui.chat.messages; import com.mojang.brigadier.suggestion.Suggestion; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java index d2db1d2..4948b30 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java @@ -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.gui.chat.model; import net.minecraft.network.chat.HoverEvent; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java index f7f7fea..4741bc2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java @@ -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.gui.chat.model; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java index 37e4baa..4f65527 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java @@ -1,59 +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.features.gui.chat.model; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ServerData; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.Style; import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.config.io.ConfigPaths; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; -import dev.loki.lovisual.util.logging.DebugLog; import dev.loki.lovisual.util.text.format.LegacyTextUtil; -import dev.loki.lovisual.util.text.format.TextJsonUtil; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.CompletableFuture; import dev.loki.lovisual.features.gui.chat.render.BetterChatRenderer; import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + /** * Per-world chat storage + async Gson persistence. + * + *

    Facade keeping the public API and the active-store state; key/path resolution lives in + * {@link ChatStoreKeys}, the async persistence engine in {@link ChatStorePersistence} and hover + * capture in {@link ChatHoverCapture}. */ public enum BetterChatStoreManager { ; - private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); private static final Map STORES = new HashMap<>(); - private static final Map HOVER_CACHES = new HashMap<>(); - private static final Map> SAVE_JOBS = new HashMap<>(); - private static final Map> CACHE_SAVE_JOBS = new HashMap<>(); - private static final Map LAST_SAVED_REVISIONS = new HashMap<>(); - private static final Map REQUESTED_SAVE_REVISIONS = new HashMap<>(); - private static final Map SERIALIZED_LINE_CACHE = - Collections.synchronizedMap(new WeakHashMap<>()); private static String currentKey = null; private static BetterChatStore active = null; private static BetterChatHoverCache activeCache = null; public static BetterChatStore getActiveStore(Minecraft mc) { - String key = key(mc); + String key = ChatStoreKeys.key(mc); if (key == null) return null; if (!Objects.equals(key, currentKey)) { switchStore(key); @@ -73,22 +50,22 @@ public enum BetterChatStoreManager { text = LegacyTextUtil.convertLegacyCodes(text); store.add(text); - boolean cacheChanged = captureHovers(text, activeCache); + boolean cacheChanged = ChatHoverCapture.captureHovers(text, activeCache); BetterChatRenderer.onNewMessage(); BetterChat cfg = BetterChat.get(); boolean historyOn = cfg == null || cfg.historyEnabled(); if (historyOn) { - scheduleSave(key(mc), store); + ChatStorePersistence.scheduleSave(ChatStoreKeys.key(mc), store); } if (cacheChanged) { - scheduleCacheSave(key(mc), activeCache); + ChatStorePersistence.scheduleCacheSave(ChatStoreKeys.key(mc), activeCache); } } public static void clearActive() { if (active != null) { active.clear(); - scheduleSave(currentKey, active); + ChatStorePersistence.scheduleSave(currentKey, active); } } @@ -97,62 +74,16 @@ public enum BetterChatStoreManager { */ public static void flushActive() { if (currentKey != null && active != null) { - saveSync(currentKey, active); + ChatStorePersistence.saveSync(currentKey, active); } } private static void switchStore(String key) { BetterChatRenderer.resetScroll(); currentKey = key; - BetterChatHoverCache cache = cacheFor(key); + BetterChatHoverCache cache = ChatStoreKeys.cacheFor(key); activeCache = cache; - active = STORES.computeIfAbsent(key, k -> loadSync(k, cache)); - } - - private static BetterChatStore loadSync(String key, BetterChatHoverCache cache) { - File f = fileFor(key); - if (!f.exists()) return new BetterChatStore(); - try (FileReader reader = new FileReader(f, StandardCharsets.UTF_8)) { - JsonArray arr = GSON.fromJson(reader, JsonArray.class); - BetterChatStore store = new BetterChatStore(); - if (arr != null) { - for (var el : arr) { - if (!el.isJsonObject()) continue; - JsonObject obj = el.getAsJsonObject(); - String json = obj.has("text") ? obj.get("text").getAsString() : null; - long ts = obj.has("time") ? obj.get("time").getAsLong() : System.currentTimeMillis(); - if (json != null) { - var txt = TextJsonUtil.fromJson(json); - if (txt != null) { - store.add(txt, ts); - captureHovers(txt, cache); - } - } - } - } - DebugLog.info("[BetterChat] Loaded %d messages for %s", store.snapshot().size(), key); - return store; - } catch (Exception e) { - DebugLog.error("BetterChat load failed for %s", e, key); - return new BetterChatStore(); - } - } - - private static void scheduleSave(String key, BetterChatStore store) { - if (key == null || store == null) return; - long revision = store.revision(); - synchronized (BetterChatStoreManager.class) { - long lastSaved = LAST_SAVED_REVISIONS.getOrDefault(key, -1L); - if (revision <= lastSaved) { - return; - } - REQUESTED_SAVE_REVISIONS.put(key, revision); - CompletableFuture inFlight = SAVE_JOBS.get(key); - if (inFlight != null && !inFlight.isDone()) { - return; - } - SAVE_JOBS.put(key, CompletableFuture.runAsync(() -> drainSaveQueue(key, store))); - } + active = STORES.computeIfAbsent(key, k -> ChatStorePersistence.loadSync(k, cache)); } /** @@ -160,24 +91,13 @@ public enum BetterChatStoreManager { */ public static void flushAll() { // wait for pending async saves - for (CompletableFuture f : new ArrayList<>(SAVE_JOBS.values())) { - try { - f.join(); - } catch (Throwable ignored) { - } - } - for (CompletableFuture f : new ArrayList<>(CACHE_SAVE_JOBS.values())) { - try { - f.join(); - } catch (Throwable ignored) { - } - } + ChatStorePersistence.awaitPending(); // save active store synchronously if (currentKey != null && active != null) { - saveSync(currentKey, active); + ChatStorePersistence.saveSync(currentKey, active); } // save caches synchronously - for (var entry : HOVER_CACHES.entrySet()) { + for (var entry : ChatStoreKeys.hoverCaches().entrySet()) { try { entry.getValue().save(entry.getKey()); } catch (Exception ignored) { @@ -185,198 +105,6 @@ public enum BetterChatStoreManager { } } - private static void saveSync(String key, BetterChatStore store) { - try { - SavePayload payload = captureSavePayload(store); - File f = fileFor(key); - f.getParentFile().mkdirs(); - JsonArray arr = new JsonArray(); - for (ChatLine line : payload.lines()) { - JsonObject obj = new JsonObject(); - String json = serializedText(line); - obj.addProperty("text", json); - obj.addProperty("time", line.timestampMs()); - arr.add(obj); - } - try (FileWriter writer = new FileWriter(f, StandardCharsets.UTF_8)) { - GSON.toJson(arr, writer); - } - synchronized (BetterChatStoreManager.class) { - LAST_SAVED_REVISIONS.put(key, payload.revision()); - } - } catch (Exception e) { - DebugLog.error("BetterChat save failed for %s", e, key); - } - } - - private static void drainSaveQueue(String key, BetterChatStore store) { - try { - while (true) { - long requested; - long saved; - synchronized (BetterChatStoreManager.class) { - requested = REQUESTED_SAVE_REVISIONS.getOrDefault(key, -1L); - saved = LAST_SAVED_REVISIONS.getOrDefault(key, -1L); - if (requested <= saved) { - SAVE_JOBS.remove(key); - return; - } - } - saveSync(key, store); - } - } finally { - synchronized (BetterChatStoreManager.class) { - CompletableFuture current = SAVE_JOBS.get(key); - if (current != null && current.isDone()) { - SAVE_JOBS.remove(key); - } - } - } - } - - private static SavePayload captureSavePayload(BetterChatStore store) { - synchronized (store) { - return new SavePayload(store.revision(), store.snapshot()); - } - } - - private static String serializedText(ChatLine line) { - String cached = SERIALIZED_LINE_CACHE.get(line); - if (cached != null) { - return cached; - } - String created = TextJsonUtil.toJson(line.text()); - SERIALIZED_LINE_CACHE.put(line, created); - return created; - } - - private static File fileFor(String key) { - return betterChatFile(key + ".json"); - } - - private static File betterChatFile(String fileName) { - Minecraft mc = Minecraft.getInstance(); - return mc.gameDirectory.toPath() - .resolve(ConfigPaths.root()) - .resolve("betterchat") - .resolve(fileName) - .toFile(); - } - - private static BetterChatHoverCache cacheFor(String key) { - return HOVER_CACHES.computeIfAbsent(key, BetterChatHoverCache::load); - } - - private static String key(Minecraft mc) { - // IMPORTANT: Do not split history by dimension. One store + one cache per save/server. - if (mc == null) return null; - - if (mc.hasSingleplayerServer()) { - // Singleplayer worlds stored under singleplayer/ - String levelName = mc.getSingleplayerServer() != null ? mc.getSingleplayerServer().getWorldData().getLevelName() : "world"; - return "singleplayer/" + sanitizeKeyPart(levelName); - } - - ServerData info = mc.getCurrentServer(); - if (info != null) { - // Multiplayer servers stored under servers/ (ignore port) - String host = stripPort(info.ip); - return "servers/" + sanitizeKeyPart(host); - } - - // Fallback - return "singleplayer/world"; - } - - private static String stripPort(String address) { - if (address == null) return "unknown"; - String s = address.trim(); - if (s.isEmpty()) return "unknown"; - - // IPv6 with brackets: [addr]:port - if (s.startsWith("[")) { - int end = s.indexOf(']'); - if (end > 1) { - return s.substring(1, end); - } - return s; - } - - int lastColon = s.lastIndexOf(':'); - if (lastColon > 0 && lastColon + 1 < s.length()) { - String tail = s.substring(lastColon + 1); - boolean digits = true; - for (int i = 0; i < tail.length(); i++) { - char c = tail.charAt(i); - if (c < '0' || c > '9') { - digits = false; - break; - } - } - if (digits) { - return s.substring(0, lastColon); - } - } - - return s; - } - - /** - * Sanitizes a key part so it is safe as a file path component. - */ - private static String sanitizeKeyPart(String s) { - if (s == null) return "unknown"; - String trimmed = s.trim(); - if (trimmed.isEmpty()) return "unknown"; - StringBuilder out = null; - int length = trimmed.length(); - for (int i = 0; i < length; i++) { - char c = trimmed.charAt(i); - boolean ok = (c >= 'A' && c <= 'Z') - || (c >= 'a' && c <= 'z') - || (c >= '0' && c <= '9') - || c == '.' - || c == '_' - || c == '-'; - if (ok) { - if (out != null) { - out.append(c); - } - continue; - } - if (out == null) { - out = new StringBuilder(length); - out.append(trimmed, 0, i); - } - out.append('_'); - } - return out == null ? trimmed : out.toString(); - } - - private static boolean captureHovers(Component text, BetterChatHoverCache cache) { - if (text == null || cache == null) return false; - final boolean[] changed = {false}; - text.visit((style, string) -> { - if (style != null) { - HoverEvent hover = style.getHoverEvent(); - if (hover instanceof HoverEvent.ShowItem(net.minecraft.world.item.ItemStackTemplate itemTemplate)) { - ItemStack item = itemTemplate.create(); - String keyId = hoverItemKey(item); - if (cache.putItem(keyId, item)) { - changed[0] = true; - } - } else if (hover instanceof HoverEvent.ShowEntity(HoverEvent.EntityTooltipInfo entity)) { - String keyId = hoverEntityKey(entity); - if (cache.putEntity(keyId, entity)) { - changed[0] = true; - } - } - } - return Optional.empty(); - }, Style.EMPTY); - return changed[0]; - } - public static String hoverItemKey(ItemStack stack) { String id = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); int compHash = stack.getComponents().hashCode(); @@ -387,16 +115,6 @@ public enum BetterChatStoreManager { return "ent:" + content.uuid; } - private static void scheduleCacheSave(String key, BetterChatHoverCache cache) { - if (key == null || cache == null) return; - CACHE_SAVE_JOBS.compute(key, (k, prev) -> { - CompletableFuture base = prev == null - ? CompletableFuture.completedFuture(null) - : prev.exceptionally(ex -> null); - return base.thenRunAsync(() -> cache.save(k)); - }); - } - public static BetterChatRenderer.Layout loadLayout() { return new BetterChatRenderer.Layout(16f, 120f); } @@ -420,7 +138,4 @@ public enum BetterChatStoreManager { */ public static void cacheLayout(float x, float yBottom, float screenH) { } - - private record SavePayload(long revision, List lines) { - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java index 03892f8..e5bc4d6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java @@ -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.gui.chat.model; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatHoverCapture.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatHoverCapture.java new file mode 100644 index 0000000..e8ccc76 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatHoverCapture.java @@ -0,0 +1,42 @@ +package dev.loki.lovisual.features.gui.chat.model; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.world.item.ItemStack; +import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache; + +import java.util.Optional; + +/** + * Hover-event extraction for {@link BetterChatStoreManager}: walks a message component tree and + * records item/entity tooltips into the per-key {@link BetterChatHoverCache}. + */ +final class ChatHoverCapture { + private ChatHoverCapture() { + } + + static boolean captureHovers(Component text, BetterChatHoverCache cache) { + if (text == null || cache == null) return false; + final boolean[] changed = {false}; + text.visit((style, string) -> { + if (style != null) { + HoverEvent hover = style.getHoverEvent(); + if (hover instanceof HoverEvent.ShowItem(net.minecraft.world.item.ItemStackTemplate itemTemplate)) { + ItemStack item = itemTemplate.create(); + String keyId = BetterChatStoreManager.hoverItemKey(item); + if (cache.putItem(keyId, item)) { + changed[0] = true; + } + } else if (hover instanceof HoverEvent.ShowEntity(HoverEvent.EntityTooltipInfo entity)) { + String keyId = BetterChatStoreManager.hoverEntityKey(entity); + if (cache.putEntity(keyId, entity)) { + changed[0] = true; + } + } + } + return Optional.empty(); + }, Style.EMPTY); + return changed[0]; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStoreKeys.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStoreKeys.java new file mode 100644 index 0000000..7007f86 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStoreKeys.java @@ -0,0 +1,129 @@ +package dev.loki.lovisual.features.gui.chat.model; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ServerData; +import dev.loki.lovisual.config.io.ConfigPaths; +import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * World/server key derivation, on-disk file resolution and per-key hover-cache ownership for + * {@link BetterChatStoreManager}. Extracted so the manager facade keeps only the public API and + * the active-store state. + */ +final class ChatStoreKeys { + private static final Map HOVER_CACHES = new HashMap<>(); + + private ChatStoreKeys() { + } + + static Map hoverCaches() { + return HOVER_CACHES; + } + + static BetterChatHoverCache cacheFor(String key) { + return HOVER_CACHES.computeIfAbsent(key, BetterChatHoverCache::load); + } + + static File fileFor(String key) { + return betterChatFile(key + ".json"); + } + + private static File betterChatFile(String fileName) { + Minecraft mc = Minecraft.getInstance(); + return mc.gameDirectory.toPath() + .resolve(ConfigPaths.root()) + .resolve("betterchat") + .resolve(fileName) + .toFile(); + } + + static String key(Minecraft mc) { + // IMPORTANT: Do not split history by dimension. One store + one cache per save/server. + if (mc == null) return null; + + if (mc.hasSingleplayerServer()) { + // Singleplayer worlds stored under singleplayer/ + String levelName = mc.getSingleplayerServer() != null ? mc.getSingleplayerServer().getWorldData().getLevelName() : "world"; + return "singleplayer/" + sanitizeKeyPart(levelName); + } + + ServerData info = mc.getCurrentServer(); + if (info != null) { + // Multiplayer servers stored under servers/ (ignore port) + String host = stripPort(info.ip); + return "servers/" + sanitizeKeyPart(host); + } + + // Fallback + return "singleplayer/world"; + } + + private static String stripPort(String address) { + if (address == null) return "unknown"; + String s = address.trim(); + if (s.isEmpty()) return "unknown"; + + // IPv6 with brackets: [addr]:port + if (s.startsWith("[")) { + int end = s.indexOf(']'); + if (end > 1) { + return s.substring(1, end); + } + return s; + } + + int lastColon = s.lastIndexOf(':'); + if (lastColon > 0 && lastColon + 1 < s.length()) { + String tail = s.substring(lastColon + 1); + boolean digits = true; + for (int i = 0; i < tail.length(); i++) { + char c = tail.charAt(i); + if (c < '0' || c > '9') { + digits = false; + break; + } + } + if (digits) { + return s.substring(0, lastColon); + } + } + + return s; + } + + /** + * Sanitizes a key part so it is safe as a file path component. + */ + private static String sanitizeKeyPart(String s) { + if (s == null) return "unknown"; + String trimmed = s.trim(); + if (trimmed.isEmpty()) return "unknown"; + StringBuilder out = null; + int length = trimmed.length(); + for (int i = 0; i < length; i++) { + char c = trimmed.charAt(i); + boolean ok = (c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9') + || c == '.' + || c == '_' + || c == '-'; + if (ok) { + if (out != null) { + out.append(c); + } + continue; + } + if (out == null) { + out = new StringBuilder(length); + out.append(trimmed, 0, i); + } + out.append('_'); + } + return out == null ? trimmed : out.toString(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStorePersistence.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStorePersistence.java new file mode 100644 index 0000000..82e830e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/impl/ChatStorePersistence.java @@ -0,0 +1,181 @@ +package dev.loki.lovisual.features.gui.chat.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.text.format.TextJsonUtil; +import dev.loki.lovisual.features.gui.chat.text.util.BetterChatHoverCache; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; + +/** + * Async Gson persistence engine for {@link BetterChatStoreManager}: revision-gated save scheduling, + * synchronous flush-on-shutdown, JSON (de)serialization and the serialized-line cache. Extracted so + * the manager facade keeps only the public API and active-store state. + */ +final class ChatStorePersistence { + private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + private static final Map> SAVE_JOBS = new HashMap<>(); + private static final Map> CACHE_SAVE_JOBS = new HashMap<>(); + private static final Map LAST_SAVED_REVISIONS = new HashMap<>(); + private static final Map REQUESTED_SAVE_REVISIONS = new HashMap<>(); + private static final Map SERIALIZED_LINE_CACHE = + Collections.synchronizedMap(new WeakHashMap<>()); + + private ChatStorePersistence() { + } + + static BetterChatStore loadSync(String key, BetterChatHoverCache cache) { + File f = ChatStoreKeys.fileFor(key); + if (!f.exists()) return new BetterChatStore(); + try (FileReader reader = new FileReader(f, StandardCharsets.UTF_8)) { + JsonArray arr = GSON.fromJson(reader, JsonArray.class); + BetterChatStore store = new BetterChatStore(); + if (arr != null) { + for (var el : arr) { + if (!el.isJsonObject()) continue; + JsonObject obj = el.getAsJsonObject(); + String json = obj.has("text") ? obj.get("text").getAsString() : null; + long ts = obj.has("time") ? obj.get("time").getAsLong() : System.currentTimeMillis(); + if (json != null) { + var txt = TextJsonUtil.fromJson(json); + if (txt != null) { + store.add(txt, ts); + ChatHoverCapture.captureHovers(txt, cache); + } + } + } + } + DebugLog.info("[BetterChat] Loaded %d messages for %s", store.snapshot().size(), key); + return store; + } catch (Exception e) { + DebugLog.error("BetterChat load failed for %s", e, key); + return new BetterChatStore(); + } + } + + static void scheduleSave(String key, BetterChatStore store) { + if (key == null || store == null) return; + long revision = store.revision(); + synchronized (ChatStorePersistence.class) { + long lastSaved = LAST_SAVED_REVISIONS.getOrDefault(key, -1L); + if (revision <= lastSaved) { + return; + } + REQUESTED_SAVE_REVISIONS.put(key, revision); + CompletableFuture inFlight = SAVE_JOBS.get(key); + if (inFlight != null && !inFlight.isDone()) { + return; + } + SAVE_JOBS.put(key, CompletableFuture.runAsync(() -> drainSaveQueue(key, store))); + } + } + + static void scheduleCacheSave(String key, BetterChatHoverCache cache) { + if (key == null || cache == null) return; + CACHE_SAVE_JOBS.compute(key, (k, prev) -> { + CompletableFuture base = prev == null + ? CompletableFuture.completedFuture(null) + : prev.exceptionally(ex -> null); + return base.thenRunAsync(() -> cache.save(k)); + }); + } + + /** + * Blocks until all pending async store/cache save jobs have settled. + */ + static void awaitPending() { + for (CompletableFuture f : new ArrayList<>(SAVE_JOBS.values())) { + try { + f.join(); + } catch (Throwable ignored) { + } + } + for (CompletableFuture f : new ArrayList<>(CACHE_SAVE_JOBS.values())) { + try { + f.join(); + } catch (Throwable ignored) { + } + } + } + + static void saveSync(String key, BetterChatStore store) { + try { + SavePayload payload = captureSavePayload(store); + File f = ChatStoreKeys.fileFor(key); + f.getParentFile().mkdirs(); + JsonArray arr = new JsonArray(); + for (ChatLine line : payload.lines()) { + JsonObject obj = new JsonObject(); + String json = serializedText(line); + obj.addProperty("text", json); + obj.addProperty("time", line.timestampMs()); + arr.add(obj); + } + try (FileWriter writer = new FileWriter(f, StandardCharsets.UTF_8)) { + GSON.toJson(arr, writer); + } + synchronized (ChatStorePersistence.class) { + LAST_SAVED_REVISIONS.put(key, payload.revision()); + } + } catch (Exception e) { + DebugLog.error("BetterChat save failed for %s", e, key); + } + } + + private static void drainSaveQueue(String key, BetterChatStore store) { + try { + while (true) { + long requested; + long saved; + synchronized (ChatStorePersistence.class) { + requested = REQUESTED_SAVE_REVISIONS.getOrDefault(key, -1L); + saved = LAST_SAVED_REVISIONS.getOrDefault(key, -1L); + if (requested <= saved) { + SAVE_JOBS.remove(key); + return; + } + } + saveSync(key, store); + } + } finally { + synchronized (ChatStorePersistence.class) { + CompletableFuture current = SAVE_JOBS.get(key); + if (current != null && current.isDone()) { + SAVE_JOBS.remove(key); + } + } + } + } + + private static SavePayload captureSavePayload(BetterChatStore store) { + synchronized (store) { + return new SavePayload(store.revision(), store.snapshot()); + } + } + + private static String serializedText(ChatLine line) { + String cached = SERIALIZED_LINE_CACHE.get(line); + if (cached != null) { + return cached; + } + String created = TextJsonUtil.toJson(line.text()); + SERIALIZED_LINE_CACHE.put(line, created); + return created; + } + + private record SavePayload(long revision, List lines) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java index 9ef634d..7cf7b97 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java @@ -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.gui.chat.render; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java index e147bae..b7bb726 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java @@ -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.gui.chat.render; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java index 65f2d11..94d673e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java @@ -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.gui.chat.render; import com.mojang.brigadier.suggestion.Suggestion; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java index e6b4302..8fafb48 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java @@ -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.gui.chat.suggestions; import dev.loki.lovisual.features.command.util.CommandOutput; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java index 55799f5..178e5cb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.chat.suggestions; import dev.loki.lovisual.features.command.util.CommandPrefix; import dev.loki.lovisual.mixins.accessors.chat.ChatScreenAccessor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java index ef735a5..42793f5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java @@ -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.gui.chat.suggestions; import dev.loki.lovisual.features.command.core.CommandManager; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java index f4aad55..e73caf8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java @@ -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.gui.chat.suggestions; import com.mojang.brigadier.context.StringRange; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java index 982a496..553ef1e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java @@ -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.gui.chat.text; import dev.loki.lovisual.features.command.util.CommandOutput; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java index d71119f..69381e4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.chat.text; import dev.loki.lovisual.features.command.util.CommandOutput; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java index f507477..5c80c72 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java @@ -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.gui.chat.text; import net.minecraft.network.chat.HoverEvent; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java index f22a10d..ce6fb7c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java @@ -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.gui.chat.text; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java index 60b89cd..d98df73 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java @@ -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.gui.chat.text.util; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java index 8bc632c..65a3425 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java @@ -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.gui.chat.text.util; import dev.loki.lovisual.render.engine.text.font.info.FontInfo; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java index d8bc587..61aac4d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java @@ -1,184 +1,50 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.chat.text.util; -import com.google.gson.*; -import com.mojang.serialization.JsonOps; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientPacketListener; -import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.HoverEvent; import net.minecraft.resources.Identifier; -import net.minecraft.resources.RegistryOps; import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.config.io.ConfigPaths; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.util.logging.DebugLog; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.nio.charset.StandardCharsets; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; /** * Per-key cache for ShowItem/ShowEntity hover payloads to persist across sessions. * Stores compact deduped entries; keyed by current chat store key (dimension/server). + * + *

    Disk (de)serialization, registry-ops codec and display-name indexing live in + * {@link ChatHoverCacheCodec}; this class keeps the public put/get/find API and per-instance maps. */ public final class BetterChatHoverCache { - private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); - - private static volatile HolderLookup.Provider cachedLookup; - private static volatile RegistryOps cachedOps; - private final Map itemCache = new HashMap<>(); - private final Map entityCache = new HashMap<>(); + final Map itemCache = new HashMap<>(); + final Map entityCache = new HashMap<>(); private final Map decodedItemCache = new HashMap<>(); - private final Map itemLookupByName = new HashMap<>(); - private final Map entityLookupByName = new HashMap<>(); - private final java.util.Set loadedItemKeys = new java.util.HashSet<>(); - private final java.util.Set loadedEntityKeys = new java.util.HashSet<>(); - private boolean loadedFromDisk = false; - private boolean itemLookupIndexBuilt = false; - private boolean entityLookupIndexBuilt = false; + final Map itemLookupByName = new HashMap<>(); + final Map entityLookupByName = new HashMap<>(); + final Set loadedItemKeys = new HashSet<>(); + final Set loadedEntityKeys = new HashSet<>(); + boolean loadedFromDisk = false; + boolean itemLookupIndexBuilt = false; + boolean entityLookupIndexBuilt = false; + private BetterChatHoverCache() { } public static BetterChatHoverCache load(String key) { - File f = fileFor(key); BetterChatHoverCache cache = new BetterChatHoverCache(); - if (!f.exists()) return cache; - try (FileReader r = new FileReader(f, StandardCharsets.UTF_8)) { - JsonObject obj = GSON.fromJson(r, JsonObject.class); - cache.loadedFromDisk = true; - if (obj != null && obj.has("items")) { - JsonArray arr = obj.getAsJsonArray("items"); - arr.forEach(el -> { - if (!el.isJsonObject()) return; - JsonObject o = el.getAsJsonObject(); - String keyId = o.has("k") ? o.get("k").getAsString() : null; - String id = o.has("id") ? o.get("id").getAsString() : null; - String comp = o.has("comp") ? o.get("comp").getAsString() : null; - if (keyId != null && id != null && comp != null) { - cache.itemCache.put(keyId, new ItemEntry(id, comp)); - cache.loadedItemKeys.add(keyId); - } - }); - } - if (obj != null && obj.has("entities")) { - JsonArray arr = obj.getAsJsonArray("entities"); - arr.forEach(el -> { - if (!el.isJsonObject()) return; - JsonObject o = el.getAsJsonObject(); - String keyId = o.has("k") ? o.get("k").getAsString() : null; - String name = o.has("name") ? o.get("name").getAsString() : ""; - String type = o.has("type") ? o.get("type").getAsString() : ""; - String uuid = o.has("uuid") ? o.get("uuid").getAsString() : null; - if (keyId != null && uuid != null && !name.isEmpty()) { - try { - Identifier tid = Identifier.tryParse(type); - if (tid != null) { - var et = BuiltInRegistries.ENTITY_TYPE.getValue(tid); - if (et != net.minecraft.world.entity.EntityTypes.PLAYER) { - String def = et.getDescription().getString().trim(); - if (!def.isEmpty() && def.equalsIgnoreCase(name.trim())) { - return; - } - } - } - } catch (Exception ignored) { - } - cache.entityCache.put(keyId, new EntityEntry(name, type, uuid)); - cache.loadedEntityKeys.add(keyId); - } - }); - } - } catch (Exception e) { - DebugLog.error("BetterChatHoverCache load failed for %s", e, key); - } + ChatHoverCacheCodec.populate(cache, key); return cache; } - private static File fileFor(String key) { - Minecraft mc = Minecraft.getInstance(); - File dir = mc.gameDirectory.toPath() - .resolve(ConfigPaths.root()) - .resolve("betterchat") - .resolve("cache") - .toFile(); - return new File(dir, key + ".json"); - } - - private static HolderLookup.Provider lookup() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return null; - ClientPacketListener net = mc.getConnection(); - if (net != null) return net.registryAccess(); - if (mc.level != null) return mc.level.registryAccess(); - return null; - } - - private static RegistryOps ops() { - HolderLookup.Provider lookup = lookup(); - if (lookup == null) { - return null; - } - RegistryOps cached = cachedOps; - if (cached != null && cachedLookup == lookup) { - return cached; - } - synchronized (BetterChatHoverCache.class) { - if (cachedOps == null || cachedLookup != lookup) { - cachedLookup = lookup; - cachedOps = RegistryOps.create(JsonOps.INSTANCE, lookup); - } - return cachedOps; - } - } - - private static String normalizeLookup(String raw) { - return raw == null ? "" : raw.trim().toLowerCase(); - } - public void save(String key) { - try { - File f = fileFor(key); - f.getParentFile().mkdirs(); - JsonObject root = new JsonObject(); - JsonArray items = new JsonArray(); - for (var e : itemCache.entrySet()) { - JsonObject o = new JsonObject(); - o.addProperty("k", e.getKey()); - o.addProperty("id", e.getValue().getId()); - o.addProperty("comp", e.getValue().componentsJson()); - items.add(o); - } - JsonArray ents = new JsonArray(); - for (var e : entityCache.entrySet()) { - JsonObject o = new JsonObject(); - o.addProperty("k", e.getKey()); - o.addProperty("name", e.getValue().name()); - o.addProperty("type", e.getValue().typeId()); - o.addProperty("uuid", e.getValue().uuid()); - ents.add(o); - } - root.add("items", items); - root.add("entities", ents); - try (FileWriter w = new FileWriter(f, StandardCharsets.UTF_8)) { - GSON.toJson(root, w); - } - } catch (Exception e) { - DebugLog.error("BetterChatHoverCache save failed for %s", e, key); - } + ChatHoverCacheCodec.write(this, key); } public boolean putItem(String keyId, ItemStack stack) { @@ -187,13 +53,8 @@ public final class BetterChatHoverCache { if (keyId == null || stack == null || stack.isEmpty()) return false; try { String itemId = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); - RegistryOps ops = ops(); - if (ops == null) return false; - var encoded = ItemStack.CODEC.encodeStart(ops, stack.copy()); - var res = encoded.result(); - if (res.isEmpty()) return false; - String compJson = res.get().toString(); - if (compJson.length() > 20000) return false; + String compJson = ChatHoverCacheCodec.encodeComponents(stack); + if (compJson == null) return false; ItemEntry entry = new ItemEntry(itemId, compJson); ItemEntry prev = itemCache.put(keyId, entry); decodedItemCache.put(keyId, stack.copy()); @@ -239,17 +100,10 @@ public final class BetterChatHoverCache { if (cached != null && !cached.isEmpty()) { return cached.copy(); } - try { - RegistryOps ops = ops(); - if (ops == null) return ItemStack.EMPTY; - var parsed = ItemStack.CODEC.parse(ops, JsonParser.parseString(e.componentsJson())); - if (parsed.result().isPresent()) { - ItemStack decoded = parsed.result().get(); - decodedItemCache.put(keyId, decoded.copy()); - return decoded; - } - } catch (Exception ex) { - DebugLog.error("BetterChatHoverCache getItem decode failed", ex); + ItemStack decoded = ChatHoverCacheCodec.decodeItem(e.componentsJson()); + if (decoded != null) { + decodedItemCache.put(keyId, decoded.copy()); + return decoded; } return ItemStack.EMPTY; } @@ -336,9 +190,9 @@ public final class BetterChatHoverCache { if (displayName == null || displayName.isEmpty()) return new ItemLookup(ItemStack.EMPTY, false); BetterChat cfg = BetterChat.get(); if (cfg != null && !cfg.cacheItems()) return new ItemLookup(ItemStack.EMPTY, false); - String needle = normalizeLookup(displayName); + String needle = ChatHoverCacheCodec.normalizeLookup(displayName); if (needle.isEmpty()) return new ItemLookup(ItemStack.EMPTY, false); - ensureItemLookupIndex(); + ChatHoverCacheCodec.ensureItemLookupIndex(this); ItemLookup lookup = itemLookupByName.get(needle); if (lookup == null || lookup.stack().isEmpty()) { return new ItemLookup(ItemStack.EMPTY, false); @@ -354,9 +208,9 @@ public final class BetterChatHoverCache { if (name == null || name.isEmpty()) return new EntityLookup(null, false); BetterChat cfg = BetterChat.get(); if (cfg != null && !cfg.cacheEntities()) return new EntityLookup(null, false); - String needle = normalizeLookup(name); + String needle = ChatHoverCacheCodec.normalizeLookup(name); if (needle.isEmpty()) return new EntityLookup(null, false); - ensureEntityLookupIndex(); + ChatHoverCacheCodec.ensureEntityLookupIndex(this); EntityLookup lookup = entityLookupByName.get(needle); return lookup != null ? lookup : new EntityLookup(null, false); } @@ -365,43 +219,6 @@ public final class BetterChatHoverCache { return findEntityByNameWithMeta(name).content(); } - private void ensureItemLookupIndex() { - if (itemLookupIndexBuilt) return; - itemLookupByName.clear(); - for (var kv : itemCache.entrySet()) { - String key = kv.getKey(); - ItemStack stack = getItem(key); - if (stack.isEmpty()) continue; - String normalized = normalizeLookup(stack.getHoverName().getString()); - if (normalized.isEmpty() || itemLookupByName.containsKey(normalized)) continue; - boolean loaded = loadedFromDisk && loadedItemKeys.contains(key); - itemLookupByName.put(normalized, new ItemLookup(stack.copy(), loaded)); - } - itemLookupIndexBuilt = true; - } - - private void ensureEntityLookupIndex() { - if (entityLookupIndexBuilt) return; - entityLookupByName.clear(); - for (var kv : entityCache.entrySet()) { - String key = kv.getKey(); - EntityEntry entry = kv.getValue(); - String normalized = normalizeLookup(entry.name()); - if (normalized.isEmpty() || entityLookupByName.containsKey(normalized)) continue; - try { - HoverEvent.EntityTooltipInfo content = new HoverEvent.EntityTooltipInfo( - BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.tryParse(entry.typeId())), - UUID.fromString(entry.uuid()), - entry.name().isEmpty() ? null : Component.literal(entry.name()) - ); - boolean loaded = loadedFromDisk && loadedEntityKeys.contains(key); - entityLookupByName.put(normalized, new EntityLookup(content, loaded)); - } catch (Exception ignored) { - } - } - entityLookupIndexBuilt = true; - } - public record ItemEntry(String id, String componentsJson) { public String getId() { return id; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java index 15abe3c..6d1ae95 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java @@ -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.gui.chat.text.util; import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/impl/ChatHoverCacheCodec.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/impl/ChatHoverCacheCodec.java new file mode 100644 index 0000000..36c2971 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/impl/ChatHoverCacheCodec.java @@ -0,0 +1,230 @@ +package dev.loki.lovisual.features.gui.chat.text.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.serialization.JsonOps; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.RegistryOps; +import net.minecraft.world.item.ItemStack; +import dev.loki.lovisual.config.io.ConfigPaths; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * Disk (de)serialization, registry-ops ItemStack codec and display-name index construction for + * {@link BetterChatHoverCache}. Extracted so the cache class keeps its public put/get/find API and + * per-instance maps; operates on the cache's package-private fields (same package). + */ +final class ChatHoverCacheCodec { + static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); + private static final int MAX_COMP_JSON = 20000; + + private static volatile HolderLookup.Provider cachedLookup; + private static volatile RegistryOps cachedOps; + + private ChatHoverCacheCodec() { + } + + static void populate(BetterChatHoverCache cache, String key) { + File f = fileFor(key); + if (!f.exists()) return; + try (FileReader r = new FileReader(f, StandardCharsets.UTF_8)) { + JsonObject obj = GSON.fromJson(r, JsonObject.class); + cache.loadedFromDisk = true; + if (obj != null && obj.has("items")) { + JsonArray arr = obj.getAsJsonArray("items"); + arr.forEach(el -> { + if (!el.isJsonObject()) return; + JsonObject o = el.getAsJsonObject(); + String keyId = o.has("k") ? o.get("k").getAsString() : null; + String id = o.has("id") ? o.get("id").getAsString() : null; + String comp = o.has("comp") ? o.get("comp").getAsString() : null; + if (keyId != null && id != null && comp != null) { + cache.itemCache.put(keyId, new BetterChatHoverCache.ItemEntry(id, comp)); + cache.loadedItemKeys.add(keyId); + } + }); + } + if (obj != null && obj.has("entities")) { + JsonArray arr = obj.getAsJsonArray("entities"); + arr.forEach(el -> { + if (!el.isJsonObject()) return; + JsonObject o = el.getAsJsonObject(); + String keyId = o.has("k") ? o.get("k").getAsString() : null; + String name = o.has("name") ? o.get("name").getAsString() : ""; + String type = o.has("type") ? o.get("type").getAsString() : ""; + String uuid = o.has("uuid") ? o.get("uuid").getAsString() : null; + if (keyId != null && uuid != null && !name.isEmpty()) { + try { + Identifier tid = Identifier.tryParse(type); + if (tid != null) { + var et = BuiltInRegistries.ENTITY_TYPE.getValue(tid); + if (et != net.minecraft.world.entity.EntityTypes.PLAYER) { + String def = et.getDescription().getString().trim(); + if (!def.isEmpty() && def.equalsIgnoreCase(name.trim())) { + return; + } + } + } + } catch (Exception ignored) { + } + cache.entityCache.put(keyId, new BetterChatHoverCache.EntityEntry(name, type, uuid)); + cache.loadedEntityKeys.add(keyId); + } + }); + } + } catch (Exception e) { + DebugLog.error("BetterChatHoverCache load failed for %s", e, key); + } + } + + static void write(BetterChatHoverCache cache, String key) { + try { + File f = fileFor(key); + f.getParentFile().mkdirs(); + JsonObject root = new JsonObject(); + JsonArray items = new JsonArray(); + for (var e : cache.itemCache.entrySet()) { + JsonObject o = new JsonObject(); + o.addProperty("k", e.getKey()); + o.addProperty("id", e.getValue().getId()); + o.addProperty("comp", e.getValue().componentsJson()); + items.add(o); + } + JsonArray ents = new JsonArray(); + for (var e : cache.entityCache.entrySet()) { + JsonObject o = new JsonObject(); + o.addProperty("k", e.getKey()); + o.addProperty("name", e.getValue().name()); + o.addProperty("type", e.getValue().typeId()); + o.addProperty("uuid", e.getValue().uuid()); + ents.add(o); + } + root.add("items", items); + root.add("entities", ents); + try (FileWriter w = new FileWriter(f, StandardCharsets.UTF_8)) { + GSON.toJson(root, w); + } + } catch (Exception e) { + DebugLog.error("BetterChatHoverCache save failed for %s", e, key); + } + } + + static String normalizeLookup(String raw) { + return raw == null ? "" : raw.trim().toLowerCase(); + } + + static String encodeComponents(ItemStack stack) { + RegistryOps ops = ops(); + if (ops == null) return null; + var encoded = ItemStack.CODEC.encodeStart(ops, stack.copy()); + var res = encoded.result(); + if (res.isEmpty()) return null; + String compJson = res.get().toString(); + if (compJson.length() > MAX_COMP_JSON) return null; + return compJson; + } + + static ItemStack decodeItem(String componentsJson) { + RegistryOps ops = ops(); + if (ops == null) return null; + try { + var parsed = ItemStack.CODEC.parse(ops, JsonParser.parseString(componentsJson)); + if (parsed.result().isPresent()) { + return parsed.result().get(); + } + } catch (Exception ex) { + DebugLog.error("BetterChatHoverCache getItem decode failed", ex); + } + return null; + } + + static void ensureItemLookupIndex(BetterChatHoverCache cache) { + if (cache.itemLookupIndexBuilt) return; + cache.itemLookupByName.clear(); + for (var kv : cache.itemCache.entrySet()) { + String key = kv.getKey(); + ItemStack stack = cache.getItem(key); + if (stack.isEmpty()) continue; + String normalized = normalizeLookup(stack.getHoverName().getString()); + if (normalized.isEmpty() || cache.itemLookupByName.containsKey(normalized)) continue; + boolean loaded = cache.loadedFromDisk && cache.loadedItemKeys.contains(key); + cache.itemLookupByName.put(normalized, new BetterChatHoverCache.ItemLookup(stack.copy(), loaded)); + } + cache.itemLookupIndexBuilt = true; + } + + static void ensureEntityLookupIndex(BetterChatHoverCache cache) { + if (cache.entityLookupIndexBuilt) return; + cache.entityLookupByName.clear(); + for (var kv : cache.entityCache.entrySet()) { + String key = kv.getKey(); + BetterChatHoverCache.EntityEntry entry = kv.getValue(); + String normalized = normalizeLookup(entry.name()); + if (normalized.isEmpty() || cache.entityLookupByName.containsKey(normalized)) continue; + try { + HoverEvent.EntityTooltipInfo content = new HoverEvent.EntityTooltipInfo( + BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.tryParse(entry.typeId())), + UUID.fromString(entry.uuid()), + entry.name().isEmpty() ? null : Component.literal(entry.name()) + ); + boolean loaded = cache.loadedFromDisk && cache.loadedEntityKeys.contains(key); + cache.entityLookupByName.put(normalized, new BetterChatHoverCache.EntityLookup(content, loaded)); + } catch (Exception ignored) { + } + } + cache.entityLookupIndexBuilt = true; + } + + private static HolderLookup.Provider lookup() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return null; + ClientPacketListener net = mc.getConnection(); + if (net != null) return net.registryAccess(); + if (mc.level != null) return mc.level.registryAccess(); + return null; + } + + private static RegistryOps ops() { + HolderLookup.Provider lookup = lookup(); + if (lookup == null) { + return null; + } + RegistryOps cached = cachedOps; + if (cached != null && cachedLookup == lookup) { + return cached; + } + synchronized (ChatHoverCacheCodec.class) { + if (cachedOps == null || cachedLookup != lookup) { + cachedLookup = lookup; + cachedOps = RegistryOps.create(JsonOps.INSTANCE, lookup); + } + return cachedOps; + } + } + + private static File fileFor(String key) { + Minecraft mc = Minecraft.getInstance(); + File dir = mc.gameDirectory.toPath() + .resolve(ConfigPaths.root()) + .resolve("betterchat") + .resolve("cache") + .toFile(); + return new File(dir, key + ".json"); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java index 1a75241..8fadc39 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java @@ -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.gui.clickgui.editor; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java index 3a1e28e..9b6bd45 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java @@ -1,267 +1,145 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.editor; import com.mojang.blaze3d.platform.InputConstants; -import net.minecraft.client.Minecraft; -import org.lwjgl.glfw.GLFW; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; import dev.loki.lovisual.features.gui.clickgui.settings.owner.TextEditorOwner; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; import dev.loki.lovisual.util.text.edit.ClipboardUtil; -import dev.loki.lovisual.util.text.edit.TextSelection; +import net.minecraft.client.Minecraft; +import org.lwjgl.glfw.GLFW; -import static dev.loki.lovisual.features.theme.Theme.theme; +import static dev.loki.lovisual.features.gui.clickgui.editor.ClickGuiEditorCaret.clamp; +/** + * State container for the modal text editor shown from ClickGui string / list + * settings. Owns the mutable buffer, caret, selection, scroll offsets, and the + * per-frame modal / text-view / footer rectangles. Delegates caret math and + * viewport adjustment to {@link ClickGuiEditorCaret} and the draw pass to + * {@link ClickGuiEditorPainter}. + */ public final class ClickGuiTextEditorState { - private static final float WIDTH = 360f; - private static final float HEIGHT = 240f; - private static final float PADDING = 14f; - private static final float TEXT_PADDING = 8f; - private static final float BUTTON_W = 70f; - private static final float BUTTON_H = 24f; - private static final float LINE_H = 18f; - private final TextEditorOwner owner; - private final String title; - private final StringBuilder buffer; - private final boolean singleLine; - private final float anchorX; - private final float anchorY; - private final float anchorW; - private final TextSelection selection = new TextSelection(); - private int caret; - private float modalX, modalY; - private float textX, textY, textW, textH; - private float saveX, saveY, saveW, saveH; - private float cancelX, cancelY, cancelW, cancelH; - private float scroll; - private float scrollX; - private int preferredColumn = -1; - private boolean dragging = false; + static final float WIDTH = 360f; + static final float HEIGHT = 240f; + static final float PADDING = 14f; + static final float TEXT_PADDING = 8f; + static final float BUTTON_W = 70f; + static final float BUTTON_H = 24f; + static final float LINE_H = 18f; + + // Package-private so sibling helpers in {@code impl/} can read + write. + final TextEditorOwner owner; + final String title; + final StringBuilder buffer; + final boolean singleLine; + final float anchorX, anchorY, anchorW; + final ClickGuiEditorCaret caretHelper; + final ClickGuiEditorPainter painter; + final dev.loki.lovisual.util.text.edit.TextSelection selection = new dev.loki.lovisual.util.text.edit.TextSelection(); + int caret; + float modalX, modalY; + float textX, textY, textW, textH; + float saveX, saveY, saveW, saveH; + float cancelX, cancelY, cancelW, cancelH; + float scroll; + float scrollX; + int preferredColumn = -1; + boolean dragging = false; public ClickGuiTextEditorState(TextEditorOwner owner, String title, String seed, float anchorX, float anchorY, float anchorW) { this.owner = owner; this.title = title == null ? "Edit list" : title; this.singleLine = owner != null && owner.isSingleLine(); - String normalizedSeed = singleLine ? normalizeSingleLine(seed) : seed; + String normalizedSeed = singleLine ? ClickGuiEditorCaret.normalizeSingleLine(seed) : seed; this.buffer = new StringBuilder(normalizedSeed == null ? "" : normalizedSeed); this.caret = buffer.length(); this.anchorX = anchorX; this.anchorY = anchorY; this.anchorW = anchorW; - this.scroll = 0f; - this.scrollX = 0f; - } - - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static float clamp(float v, float min, float max) { - return Math.max(min, Math.min(max, v)); - } - - private static int clamp(int v, int min, int max) { - return Math.max(min, Math.min(max, v)); - } - - private static String normalizeSingleLine(String text) { - if (text == null) return ""; - String normalized = text.replace("\r\n", "\n").replace('\r', '\n'); - return normalized.replace('\n', ' '); - } - - private static int[] caretLineAndColumn(String[] lines, int caretIndex) { - int remaining = caretIndex; - for (int i = 0; i < lines.length; i++) { - int len = lines[i].length(); - if (remaining <= len) { - return new int[]{i, remaining}; - } - remaining -= len; - if (i < lines.length - 1) remaining -= 1; - } - return new int[]{Math.max(0, lines.length - 1), lines.length == 0 ? 0 : lines[lines.length - 1].length()}; - } - - private static String safeSubstring(String s, int from, int to) { - int start = Math.max(0, Math.min(s.length(), from)); - int end = Math.max(start, Math.min(s.length(), to)); - return s.substring(start, end); + this.caretHelper = new ClickGuiEditorCaret(this); + this.painter = new ClickGuiEditorPainter(this, caretHelper); } public void handleMouseDown(float mx, float my, int button) { if (button != 0) return; - - layout(ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); - - if (!inside(mx, my, modalX, modalY, WIDTH, HEIGHT)) { - ClickGuiRenderer.closeTextEditor(false); - return; - } - - if (inside(mx, my, saveX, saveY, saveW, saveH)) { - ClickGuiRenderer.closeTextEditor(true); - return; - } - - if (inside(mx, my, cancelX, cancelY, cancelW, cancelH)) { - ClickGuiRenderer.closeTextEditor(false); - return; - } - - if (inside(mx, my, textX, textY, textW, textH)) { - int target = caretFromPoint(mx, my); - if (isShiftDown()) { - if (!selection.appliesToLine(0)) selection.update(0, caret); - selection.updateCaret(target); - } else { - selection.clear(); - } + painter.layout(ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); + if (!ClickGuiEditorCaret.inside(mx, my, modalX, modalY, WIDTH, HEIGHT)) { ClickGuiRenderer.closeTextEditor(false); return; } + if (ClickGuiEditorCaret.inside(mx, my, saveX, saveY, saveW, saveH)) { ClickGuiRenderer.closeTextEditor(true); return; } + if (ClickGuiEditorCaret.inside(mx, my, cancelX, cancelY, cancelW, cancelH)) { ClickGuiRenderer.closeTextEditor(false); return; } + if (ClickGuiEditorCaret.inside(mx, my, textX, textY, textW, textH)) { + int target = caretHelper.caretFromPoint(mx, my); + caretHelper.updateSelectionForMove(target, isShiftDown()); caret = target; preferredColumn = -1; - ensureCaretVisible(); + caretHelper.ensureCaretVisible(); dragging = true; return; } - selection.clear(); } - public void handleMouseUp(float mx, float my, int button) { - if (button == 0) { - dragging = false; - } - } + public void handleMouseUp(float mx, float my, int button) { if (button == 0) dragging = false; } public void handleMouseMove(float mx, float my) { if (!dragging) return; - int target = caretFromPoint(mx, my); + int target = caretHelper.caretFromPoint(mx, my); selection.update(0, target); caret = target; preferredColumn = -1; - ensureCaretVisible(); - } - - public void insertChar(char c) { - if (Character.isISOControl(c)) return; - if (deleteSelection()) { - } - buffer.insert(caret, c); - caret++; - selection.clear(); - preferredColumn = -1; - ensureCaretVisible(); - dragging = false; - } - - public void insertNewline() { - if (singleLine) return; - if (deleteSelection()) { - } - buffer.insert(caret, '\n'); - caret++; - selection.clear(); - preferredColumn = -1; - ensureCaretVisible(); - dragging = false; + caretHelper.ensureCaretVisible(); } + public void insertChar(char c) { if (!Character.isISOControl(c)) insertRaw(String.valueOf(c), 1); } + public void insertNewline() { if (!singleLine) insertRaw("\n", 1); } public void pasteFromClipboard() { String clip = ClipboardUtil.get(); if (clip == null || clip.isEmpty()) return; String normalized = clip.replace("\r\n", "\n").replace('\r', '\n'); - if (singleLine) normalized = normalizeSingleLine(normalized); - insertText(normalized); + if (singleLine) normalized = ClickGuiEditorCaret.normalizeSingleLine(normalized); + insertRaw(normalized, normalized.length()); } public void backspace() { - if (deleteSelection()) { - preferredColumn = -1; - ensureCaretVisible(); - return; - } + if (caretHelper.deleteSelection()) { preferredColumn = -1; caretHelper.ensureCaretVisible(); return; } if (caret == 0) return; buffer.deleteCharAt(caret - 1); caret--; selection.clear(); preferredColumn = -1; - ensureCaretVisible(); + caretHelper.ensureCaretVisible(); dragging = false; } public void deleteForward() { - if (deleteSelection()) { - preferredColumn = -1; - ensureCaretVisible(); - return; - } + if (caretHelper.deleteSelection()) { preferredColumn = -1; caretHelper.ensureCaretVisible(); return; } if (caret >= buffer.length()) return; buffer.deleteCharAt(caret); selection.clear(); preferredColumn = -1; - ensureCaretVisible(); + caretHelper.ensureCaretVisible(); dragging = false; } - public void moveCaret(int delta, boolean extend) { - int target = clamp(caret + delta, 0, buffer.length()); - updateSelectionForMove(target, extend); - caret = target; - preferredColumn = -1; - ensureCaretVisible(); - } - - public void moveCaretToStart(boolean extend) { - updateSelectionForMove(0, extend); - caret = 0; - preferredColumn = -1; - ensureCaretVisible(); - } - - public void moveCaretToEnd(boolean extend) { - int end = buffer.length(); - updateSelectionForMove(end, extend); - caret = end; - preferredColumn = -1; - ensureCaretVisible(); - } + public void moveCaret(int delta, boolean extend) { moveInternal(clamp(caret + delta, 0, buffer.length()), extend, true); } + public void moveCaretToStart(boolean extend) { moveInternal(0, extend, true); } + public void moveCaretToEnd(boolean extend) { moveInternal(buffer.length(), extend, true); } public void moveCaretVertical(int dir, boolean extend) { String[] lines = buffer.toString().split("\\n", -1); - int[] pos = caretLineAndColumn(lines, caret); - int currentCol = pos[1]; - if (preferredColumn < 0) preferredColumn = currentCol; + int[] pos = ClickGuiEditorCaret.caretLineAndColumn(lines, caret); + if (preferredColumn < 0) preferredColumn = pos[1]; int targetLine = clamp(pos[0] + dir, 0, Math.max(0, lines.length - 1)); int targetCol = Math.min(preferredColumn, lines[targetLine].length()); - int targetIndex = indexForLineColumn(lines, targetLine, targetCol); - updateSelectionForMove(targetIndex, extend); - caret = targetIndex; - ensureCaretVisible(); + int targetIndex = caretHelper.indexForLineColumn(lines, targetLine, targetCol); + moveInternal(targetIndex, extend, false); } public void copySelection() { if (selection.appliesToLine(0) && selection.hasRange()) { - int start = Math.min(selection.start(), selection.end()); - int end = Math.max(selection.start(), selection.end()) + 1; - start = clamp(start, 0, buffer.length()); - end = clamp(end, 0, buffer.length()); - if (end > start) { - ClipboardUtil.copy(buffer.substring(start, end)); - return; - } - } - if (buffer.length() > 0) { - ClipboardUtil.copy(buffer.toString()); + int start = clamp(Math.min(selection.start(), selection.end()), 0, buffer.length()); + int end = clamp(Math.max(selection.start(), selection.end()) + 1, 0, buffer.length()); + if (end > start) { ClipboardUtil.copy(buffer.substring(start, end)); return; } } + if (buffer.length() > 0) ClipboardUtil.copy(buffer.toString()); } public void selectAll() { @@ -269,270 +147,41 @@ public final class ClickGuiTextEditorState { selection.updateCaret(buffer.length()); caret = buffer.length(); preferredColumn = -1; - ensureCaretVisible(); + caretHelper.ensureCaretVisible(); } - public void save() { - owner.applyEditorText(buffer.toString()); - } + public void save() { owner.applyEditorText(buffer.toString()); } public void scroll(double delta) { String[] lines = buffer.toString().split("\\n", -1); float visible = textH - TEXT_PADDING * 2f; float content = lines.length * LINE_H; float minScroll = Math.min(0f, visible - content); - scroll += (float) (delta * LINE_H * 1.5f); - scroll = clamp(scroll, minScroll, 0f); + scroll = clamp(scroll + (float) (delta * LINE_H * 1.5f), minScroll, 0f); } - public void render(int fbw, int fbh) { - layout(fbw, fbh); + public void render(int fbw, int fbh) { painter.render(fbw, fbh); } - ClickGuiRenderer.drawRoundedRect(modalX, modalY, WIDTH, HEIGHT, 12f, theme().windowBg()); - ClickGuiRenderer.drawRoundedRectStroke(modalX, modalY, WIDTH, HEIGHT, 12f, 1.2f, theme().windowStroke()); - - TextRenderer titleFont = ClickGuiRenderer.getIosevkaRegular(); - float titleSize = 16f; - ClickGuiRenderer.drawText(titleFont, title, modalX + PADDING, modalY + 12f, titleSize, theme().textPrimary(), false); - if (!singleLine) { - ClickGuiRenderer.drawText(ClickGuiRenderer.getIosevkaRegular(), "One entry per line", modalX + PADDING, modalY + 30f, 12f, theme().textMuted(), false); - } - - ClickGuiRenderer.drawRoundedRect(textX, textY, textW, textH, 8f, theme().surface()); - ClickGuiRenderer.drawRoundedRectStroke(textX, textY, textW, textH, 8f, 1.0f, theme().strokeSoft()); - - String[] lines = buffer.toString().split("\\n", -1); - TextRenderer textFont = ClickGuiRenderer.getIosevkaRegular(); - float fontSize = 15f; - float viewX = textX + TEXT_PADDING - scrollX; - float viewY = textY + TEXT_PADDING + scroll; - float lineTop = viewY; - - float scissorX = textX + TEXT_PADDING; - float scissorY = textY + TEXT_PADDING; - float scissorW = textW - TEXT_PADDING * 2f; - float scissorH = textH - TEXT_PADDING * 2f; - - boolean clipped = ScissorFunction.pushRaw(scissorX, scissorY, scissorW, scissorH); - int globalOffset = 0; - for (int idx = 0; idx < lines.length; idx++) { - String line = lines[idx]; - if (selection.appliesToLine(0) && selection.hasRange()) { - int selStart = Math.min(selection.start(), selection.end()); - int selEnd = Math.max(selection.start(), selection.end()) + 1; - int selA = Math.max(globalOffset, selStart); - int selB = Math.min(globalOffset + line.length(), selEnd); - if (selB > selA) { - int colA = selA - globalOffset; - int colB = selB - globalOffset; - float x0 = viewX + ClickGuiRenderer.textWidth(textFont, safeSubstring(line, 0, colA), fontSize); - float x1 = viewX + ClickGuiRenderer.textWidth(textFont, safeSubstring(line, 0, colB), fontSize); - float y0 = lineTop; - ClickGuiRenderer.drawRoundedRect(x0, y0, Math.max(1f, x1 - x0), LINE_H, 3f, theme().accentSoft()); - } - } - float textY = lineTop + (LINE_H - ClickGuiRenderer.textHeight(textFont, fontSize)) * 0.5f; - ClickGuiRenderer.drawText(textFont, line, viewX, textY, fontSize, theme().textPrimary(), false); - lineTop += LINE_H; - globalOffset += line.length(); - if (idx < lines.length - 1) globalOffset++; - } - - int[] caretPos = caretLineAndColumn(lines, caret); - float caretLineTop = viewY + caretPos[0] * LINE_H; - String caretLine = caretPos[0] < lines.length ? lines[caretPos[0]] : ""; - float caretOffsetX = ClickGuiRenderer.textWidth(textFont, safeSubstring(caretLine, 0, caretPos[1]), fontSize); - float cx = viewX + caretOffsetX; - float cy0 = caretLineTop + 2f; - float cy1 = caretLineTop + LINE_H - 2f; - if (AnimationUtility.blink(500L)) { - ClickGuiRenderer.drawLine(cx, cy0, cx, cy1, theme().textPrimary()); - } - if (clipped) ScissorFunction.pop(); - - float contentHeight = lines.length * LINE_H; - float visibleHeight = scissorH; - if (contentHeight > visibleHeight + 0.5f) { - float trackX = scissorX + scissorW - 6f; - float trackY = scissorY; - float trackW = 4f; - float trackH = scissorH; - - float thumbH = Math.max(18f, trackH * (visibleHeight / contentHeight)); - float maxScroll = contentHeight - visibleHeight; - float scrollNorm = Math.min(1f, Math.max(0f, -scroll / maxScroll)); - float thumbY = trackY + (trackH - thumbH) * scrollNorm; - - ClickGuiRenderer.drawRoundedRect(trackX, trackY, trackW, trackH, 3f, theme().surface()); - ClickGuiRenderer.drawRoundedRect(trackX, thumbY, trackW, thumbH, 3f, theme().strokeSoft()); - } - - int saveBg = theme().accent(); - int cancelBg = theme().surface(); - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - boolean hoverCancel = inside(mx, my, cancelX, cancelY, cancelW, cancelH); - boolean hoverSave = inside(mx, my, saveX, saveY, saveW, saveH); - - int cancelFill = hoverCancel ? theme().surfaceHover() : cancelBg; - int saveFill = hoverSave ? ClickGuiRenderer.mixColor(saveBg, theme().surfaceHover(), 0.35f) : saveBg; - - ClickGuiRenderer.drawRoundedRect(cancelX, cancelY, cancelW, cancelH, 8f, cancelFill); - ClickGuiRenderer.drawRoundedRectStroke(cancelX, cancelY, cancelW, cancelH, 8f, 1.05f, theme().strokeSoft()); - float cancelTextY = cancelY + (cancelH - ClickGuiRenderer.textHeight(textFont, 14f)) * 0.5f; - ClickGuiRenderer.drawText(textFont, "Cancel", cancelX + 14f, cancelTextY, 14f, theme().textPrimary(), false); - - ClickGuiRenderer.drawRoundedRect(saveX, saveY, saveW, saveH, 8f, saveFill); - ClickGuiRenderer.drawRoundedRectStroke(saveX, saveY, saveW, saveH, 8f, 1.05f, theme().strokeSoft()); - float saveTextY = saveY + (saveH - ClickGuiRenderer.textHeight(textFont, 14f)) * 0.5f; - ClickGuiRenderer.drawText(textFont, "Save", saveX + 20f, saveTextY, 14f, theme().textPrimary(), false); + private void moveInternal(int target, boolean extend, boolean resetPreferred) { + caretHelper.updateSelectionForMove(target, extend); + caret = target; + if (resetPreferred) preferredColumn = -1; + caretHelper.ensureCaretVisible(); } - private void layout(int fbw, int fbh) { - float margin = 12f; - modalX = clamp(anchorX - WIDTH + anchorW + margin, margin, fbw - WIDTH - margin); - modalY = clamp(anchorY - HEIGHT / 2f, margin, fbh - HEIGHT - margin); - - textX = modalX + PADDING; - textY = modalY + 58f; - textW = WIDTH - PADDING * 2f; - textH = HEIGHT - PADDING * 3f - BUTTON_H; - - saveW = BUTTON_W; - saveH = BUTTON_H; - saveX = modalX + WIDTH - PADDING - saveW - 14f; - saveY = modalY + HEIGHT - PADDING - saveH; - - cancelW = BUTTON_W; - cancelH = BUTTON_H; - cancelX = saveX - cancelW - 8f; - cancelY = saveY; - } - - private boolean isShiftDown() { - var window = Minecraft.getInstance().getWindow(); - return InputConstants.isKeyDown(window, GLFW.GLFW_KEY_LEFT_SHIFT) || - InputConstants.isKeyDown(window, GLFW.GLFW_KEY_RIGHT_SHIFT); - } - - private int caretFromPoint(float mx, float my) { - String[] lines = buffer.toString().split("\\n", -1); - float localY = my - (textY + TEXT_PADDING) - scroll; - int lineIdx = clamp((int) Math.floor(localY / LINE_H), 0, Math.max(0, lines.length - 1)); - int lineStart = 0; - for (int i = 0; i < lineIdx; i++) { - lineStart += lines[i].length(); - if (i < lines.length - 1) lineStart++; - } - String line = lines.length > 0 ? lines[lineIdx] : ""; - float relX = mx - (textX + TEXT_PADDING) + scrollX; - if (relX < 0f) relX = 0f; - int col = columnForX(line, relX); - return Math.min(buffer.length(), lineStart + col); - } - - private int columnForX(String line, float relX) { - if (line == null || line.isEmpty()) return 0; - TextRenderer tr = ClickGuiRenderer.getIosevkaRegular(); - float size = 15f; - float acc = 0f; - for (int i = 0; i < line.length(); i++) { - String ch = String.valueOf(line.charAt(i)); - float w = ClickGuiRenderer.textWidth(tr, ch, size); - if (acc + w * 0.5f >= relX) return i; - acc += w; - } - return line.length(); - } - - private void insertText(String text) { - if (text == null || text.isEmpty()) return; - String insert = singleLine ? normalizeSingleLine(text) : text; - if (deleteSelection()) { - } - buffer.insert(caret, insert); - caret += insert.length(); + /** Shared body for insertChar / insertNewline / paste: delete selection, splice text in, refresh caret. */ + private void insertRaw(String text, int caretAdvance) { + caretHelper.deleteSelection(); + buffer.insert(caret, text); + caret += caretAdvance; selection.clear(); preferredColumn = -1; - ensureCaretVisible(); + caretHelper.ensureCaretVisible(); dragging = false; } - private int indexForLineColumn(String[] lines, int line, int column) { - int idx = 0; - for (int i = 0; i < lines.length; i++) { - if (i == line) { - return idx + column; - } - idx += lines[i].length(); - if (i < lines.length - 1) idx++; - } - return buffer.length(); - } - - private void updateSelectionForMove(int target, boolean extend) { - if (extend) { - if (!selection.appliesToLine(0)) selection.update(0, caret); - selection.updateCaret(target); - } else { - selection.clear(); - } - } - - private boolean deleteSelection() { - if (!selection.appliesToLine(0) || !selection.hasRange()) return false; - int start = Math.min(selection.start(), selection.end()); - int end = Math.max(selection.start(), selection.end()) + 1; - start = clamp(start, 0, buffer.length()); - end = clamp(end, 0, buffer.length()); - if (end <= start) { - selection.clear(); - return false; - } - buffer.delete(start, end); - caret = start; - selection.clear(); - return true; - } - - private void ensureCaretVisible() { - String[] lines = buffer.toString().split("\\n", -1); - float visibleY = textH - TEXT_PADDING * 2f; - float contentY = lines.length * LINE_H; - float minScrollY = Math.min(0f, visibleY - contentY); - int[] caretPos = caretLineAndColumn(lines, caret); - float caretY = caretPos[0] * LINE_H + scroll; - float bottom = caretY + LINE_H; - - if (caretY < 0f) { - scroll = clamp(scroll - caretY, minScrollY, 0f); - } else if (bottom > visibleY) { - float diff = bottom - visibleY; - scroll = clamp(scroll - diff, minScrollY, 0f); - } else { - scroll = clamp(scroll, minScrollY, 0f); - } - - TextRenderer tr = ClickGuiRenderer.getIosevkaRegular(); - float size = 15f; - float visibleW = Math.max(1f, textW - TEXT_PADDING * 2f); - float caretOffsetX = 0f; - if (lines.length > 0 && caretPos[0] >= 0 && caretPos[0] < lines.length) { - caretOffsetX = ClickGuiRenderer.textWidth(tr, safeSubstring(lines[caretPos[0]], 0, caretPos[1]), size); - } - float maxWidth = 0f; - for (String line : lines) { - float w = ClickGuiRenderer.textWidth(tr, line, size); - if (w > maxWidth) maxWidth = w; - } - float maxScrollX = Math.max(0f, maxWidth - visibleW); - float caretRelativeX = caretOffsetX - scrollX; - if (caretRelativeX < 0f) { - scrollX = clamp(scrollX + caretRelativeX, 0f, maxScrollX); - } else if (caretRelativeX > visibleW) { - scrollX = clamp(scrollX + (caretRelativeX - visibleW), 0f, maxScrollX); - } else { - scrollX = clamp(scrollX, 0f, maxScrollX); - } + private static boolean isShiftDown() { + var window = Minecraft.getInstance().getWindow(); + return InputConstants.isKeyDown(window, GLFW.GLFW_KEY_LEFT_SHIFT) || InputConstants.isKeyDown(window, GLFW.GLFW_KEY_RIGHT_SHIFT); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorCaret.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorCaret.java new file mode 100644 index 0000000..85c31ab --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorCaret.java @@ -0,0 +1,148 @@ +package dev.loki.lovisual.features.gui.clickgui.editor; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.text.edit.TextSelection; + +/** + * Caret / selection / scroll math for {@link ClickGuiTextEditorState}. Owns the + * line-splitting helpers, the pixel-to-caret mapping used by mouse clicks and the + * viewport adjustment that keeps the caret visible after edits or navigation. + * Lives in a sibling folder sharing the parent's package so the editor state class + * stays within the two-hundred-line cap. + */ +final class ClickGuiEditorCaret { + + private final ClickGuiTextEditorState owner; + + ClickGuiEditorCaret(ClickGuiTextEditorState owner) { + this.owner = owner; + } + + static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + static float clamp(float v, float min, float max) { return Math.max(min, Math.min(max, v)); } + static int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); } + + static String safeSubstring(String s, int from, int to) { + int start = Math.max(0, Math.min(s.length(), from)); + int end = Math.max(start, Math.min(s.length(), to)); + return s.substring(start, end); + } + + /** Strip CR and collapse newlines to spaces so a single-line editor never sees embedded breaks. */ + static String normalizeSingleLine(String text) { + if (text == null) return ""; + return text.replace("\r\n", "\n").replace('\r', '\n').replace('\n', ' '); + } + + /** Global character index → {line, column} pair, treating the split by "\n" as the source of truth. */ + static int[] caretLineAndColumn(String[] lines, int caretIndex) { + int remaining = caretIndex; + for (int i = 0; i < lines.length; i++) { + int len = lines[i].length(); + if (remaining <= len) return new int[]{i, remaining}; + remaining -= len; + if (i < lines.length - 1) remaining -= 1; + } + return new int[]{Math.max(0, lines.length - 1), lines.length == 0 ? 0 : lines[lines.length - 1].length()}; + } + + /** Line-major character index at {@code (line, column)}; returns buffer length if past the end. */ + int indexForLineColumn(String[] lines, int line, int column) { + int idx = 0; + for (int i = 0; i < lines.length; i++) { + if (i == line) return idx + column; + idx += lines[i].length(); + if (i < lines.length - 1) idx++; + } + return owner.buffer.length(); + } + + /** Binary-style column search: walk the line, accumulate widths, stop where the caret would land. */ + int columnForX(String line, float relX) { + if (line == null || line.isEmpty()) return 0; + TextRenderer tr = ClickGuiRenderer.getIosevkaRegular(); + float size = 15f; + float acc = 0f; + for (int i = 0; i < line.length(); i++) { + float w = ClickGuiRenderer.textWidth(tr, String.valueOf(line.charAt(i)), size); + if (acc + w * 0.5f >= relX) return i; + acc += w; + } + return line.length(); + } + + /** Pixel coordinates (framebuffer space) → global caret index. */ + int caretFromPoint(float mx, float my) { + String[] lines = owner.buffer.toString().split("\\n", -1); + float localY = my - (owner.textY + ClickGuiTextEditorState.TEXT_PADDING) - owner.scroll; + int lineIdx = clamp((int) Math.floor(localY / ClickGuiTextEditorState.LINE_H), 0, Math.max(0, lines.length - 1)); + int lineStart = 0; + for (int i = 0; i < lineIdx; i++) { + lineStart += lines[i].length(); + if (i < lines.length - 1) lineStart++; + } + String line = lines.length > 0 ? lines[lineIdx] : ""; + float relX = Math.max(0f, mx - (owner.textX + ClickGuiTextEditorState.TEXT_PADDING) + owner.scrollX); + return Math.min(owner.buffer.length(), lineStart + columnForX(line, relX)); + } + + /** Extend or clear the selection to reflect a move to {@code target}. */ + void updateSelectionForMove(int target, boolean extend) { + TextSelection sel = owner.selection; + if (extend) { + if (!sel.appliesToLine(0)) sel.update(0, owner.caret); + sel.updateCaret(target); + } else { + sel.clear(); + } + } + + /** Remove the current selection; returns false when there was nothing selected. */ + boolean deleteSelection() { + TextSelection sel = owner.selection; + if (!sel.appliesToLine(0) || !sel.hasRange()) return false; + int start = clamp(Math.min(sel.start(), sel.end()), 0, owner.buffer.length()); + int end = clamp(Math.max(sel.start(), sel.end()) + 1, 0, owner.buffer.length()); + sel.clear(); + if (end <= start) return false; + owner.buffer.delete(start, end); + owner.caret = start; + return true; + } + + /** Scroll the viewport so the caret stays inside the visible rectangle (both axes). */ + void ensureCaretVisible() { + String[] lines = owner.buffer.toString().split("\\n", -1); + float visibleY = owner.textH - ClickGuiTextEditorState.TEXT_PADDING * 2f; + float contentY = lines.length * ClickGuiTextEditorState.LINE_H; + float minScrollY = Math.min(0f, visibleY - contentY); + int[] caretPos = caretLineAndColumn(lines, owner.caret); + float caretY = caretPos[0] * ClickGuiTextEditorState.LINE_H + owner.scroll; + float bottom = caretY + ClickGuiTextEditorState.LINE_H; + if (caretY < 0f) owner.scroll = clamp(owner.scroll - caretY, minScrollY, 0f); + else if (bottom > visibleY) owner.scroll = clamp(owner.scroll - (bottom - visibleY), minScrollY, 0f); + else owner.scroll = clamp(owner.scroll, minScrollY, 0f); + + TextRenderer tr = ClickGuiRenderer.getIosevkaRegular(); + float size = 15f; + float visibleW = Math.max(1f, owner.textW - ClickGuiTextEditorState.TEXT_PADDING * 2f); + float caretOffsetX = 0f; + if (lines.length > 0 && caretPos[0] >= 0 && caretPos[0] < lines.length) { + caretOffsetX = ClickGuiRenderer.textWidth(tr, safeSubstring(lines[caretPos[0]], 0, caretPos[1]), size); + } + float maxWidth = 0f; + for (String line : lines) { + float w = ClickGuiRenderer.textWidth(tr, line, size); + if (w > maxWidth) maxWidth = w; + } + float maxScrollX = Math.max(0f, maxWidth - visibleW); + float caretRelativeX = caretOffsetX - owner.scrollX; + if (caretRelativeX < 0f) owner.scrollX = clamp(owner.scrollX + caretRelativeX, 0f, maxScrollX); + else if (caretRelativeX > visibleW) owner.scrollX = clamp(owner.scrollX + (caretRelativeX - visibleW), 0f, maxScrollX); + else owner.scrollX = clamp(owner.scrollX, 0f, maxScrollX); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorPainter.java new file mode 100644 index 0000000..8f818d0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/impl/ClickGuiEditorPainter.java @@ -0,0 +1,132 @@ +package dev.loki.lovisual.features.gui.clickgui.editor; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +/** + * Draw pass for {@link ClickGuiTextEditorState}: modal chrome, title, multiline + * text view with selection highlighting, animated caret, vertical scrollbar and + * the Save / Cancel footer buttons. Also computes the modal + button rectangles + * from the anchor coordinates. Sibling folder sharing the editor package. + */ +final class ClickGuiEditorPainter { + + private final ClickGuiTextEditorState owner; + private final ClickGuiEditorCaret caret; + + ClickGuiEditorPainter(ClickGuiTextEditorState owner, ClickGuiEditorCaret caret) { + this.owner = owner; + this.caret = caret; + } + + /** Anchor-relative modal placement, clamped so the dialog never leaves the framebuffer. */ + void layout(int fbw, int fbh) { + float margin = 12f; + float w = ClickGuiTextEditorState.WIDTH, h = ClickGuiTextEditorState.HEIGHT, p = ClickGuiTextEditorState.PADDING; + owner.modalX = ClickGuiEditorCaret.clamp(owner.anchorX - w + owner.anchorW + margin, margin, fbw - w - margin); + owner.modalY = ClickGuiEditorCaret.clamp(owner.anchorY - h / 2f, margin, fbh - h - margin); + owner.textX = owner.modalX + p; + owner.textY = owner.modalY + 58f; + owner.textW = w - p * 2f; + owner.textH = h - p * 3f - ClickGuiTextEditorState.BUTTON_H; + owner.saveW = ClickGuiTextEditorState.BUTTON_W; + owner.saveH = ClickGuiTextEditorState.BUTTON_H; + owner.saveX = owner.modalX + w - p - owner.saveW - 14f; + owner.saveY = owner.modalY + h - p - owner.saveH; + owner.cancelW = ClickGuiTextEditorState.BUTTON_W; + owner.cancelH = ClickGuiTextEditorState.BUTTON_H; + owner.cancelX = owner.saveX - owner.cancelW - 8f; + owner.cancelY = owner.saveY; + } + + /** Full paint: modal chrome, text view, selection, caret, scrollbar, footer buttons. */ + void render(int fbw, int fbh) { + layout(fbw, fbh); + float w = ClickGuiTextEditorState.WIDTH, h = ClickGuiTextEditorState.HEIGHT; + float tp = ClickGuiTextEditorState.TEXT_PADDING, lh = ClickGuiTextEditorState.LINE_H; + ClickGuiRenderer.drawRoundedRect(owner.modalX, owner.modalY, w, h, 12f, theme().windowBg()); + ClickGuiRenderer.drawRoundedRectStroke(owner.modalX, owner.modalY, w, h, 12f, 1.2f, theme().windowStroke()); + TextRenderer titleFont = ClickGuiRenderer.getIosevkaRegular(); + ClickGuiRenderer.drawText(titleFont, owner.title, owner.modalX + ClickGuiTextEditorState.PADDING, owner.modalY + 12f, 16f, theme().textPrimary(), false); + if (!owner.singleLine) { + ClickGuiRenderer.drawText(titleFont, "One entry per line", owner.modalX + ClickGuiTextEditorState.PADDING, owner.modalY + 30f, 12f, theme().textMuted(), false); + } + ClickGuiRenderer.drawRoundedRect(owner.textX, owner.textY, owner.textW, owner.textH, 8f, theme().surface()); + ClickGuiRenderer.drawRoundedRectStroke(owner.textX, owner.textY, owner.textW, owner.textH, 8f, 1.0f, theme().strokeSoft()); + + String[] lines = owner.buffer.toString().split("\\n", -1); + float fontSize = 15f; + float viewX = owner.textX + tp - owner.scrollX; + float viewY = owner.textY + tp + owner.scroll; + float lineTop = viewY; + float scissorX = owner.textX + tp, scissorY = owner.textY + tp; + float scissorW = owner.textW - tp * 2f, scissorH = owner.textH - tp * 2f; + + boolean clipped = ScissorFunction.pushRaw(scissorX, scissorY, scissorW, scissorH); + int globalOffset = 0; + for (int idx = 0; idx < lines.length; idx++) { + String line = lines[idx]; + drawSelectionForLine(line, globalOffset, viewX, lineTop, fontSize, tp, lh); + float textYLine = lineTop + (lh - ClickGuiRenderer.textHeight(titleFont, fontSize)) * 0.5f; + ClickGuiRenderer.drawText(titleFont, line, viewX, textYLine, fontSize, theme().textPrimary(), false); + lineTop += lh; + globalOffset += line.length(); + if (idx < lines.length - 1) globalOffset++; + } + drawCaret(lines, viewX, viewY, titleFont, fontSize, lh); + if (clipped) ScissorFunction.pop(); + drawScrollbar(scissorX, scissorY, scissorW, scissorH, lines.length * lh); + drawFooter(titleFont); + } + + private void drawSelectionForLine(String line, int globalOffset, float viewX, float lineTop, float fontSize, float tp, float lh) { + if (!owner.selection.appliesToLine(0) || !owner.selection.hasRange()) return; + int selStart = Math.min(owner.selection.start(), owner.selection.end()); + int selEnd = Math.max(owner.selection.start(), owner.selection.end()) + 1; + int selA = Math.max(globalOffset, selStart); + int selB = Math.min(globalOffset + line.length(), selEnd); + if (selB <= selA) return; + int colA = selA - globalOffset, colB = selB - globalOffset; + float x0 = viewX + ClickGuiRenderer.textWidth(ClickGuiRenderer.getIosevkaRegular(), ClickGuiEditorCaret.safeSubstring(line, 0, colA), fontSize); + float x1 = viewX + ClickGuiRenderer.textWidth(ClickGuiRenderer.getIosevkaRegular(), ClickGuiEditorCaret.safeSubstring(line, 0, colB), fontSize); + ClickGuiRenderer.drawRoundedRect(x0, lineTop, Math.max(1f, x1 - x0), lh, 3f, theme().accentSoft()); + } + + private void drawCaret(String[] lines, float viewX, float viewY, TextRenderer font, float fontSize, float lh) { + int[] pos = ClickGuiEditorCaret.caretLineAndColumn(lines, owner.caret); + float caretLineTop = viewY + pos[0] * lh; + String caretLine = pos[0] < lines.length ? lines[pos[0]] : ""; + float caretOffsetX = ClickGuiRenderer.textWidth(font, ClickGuiEditorCaret.safeSubstring(caretLine, 0, pos[1]), fontSize); + float cx = viewX + caretOffsetX; + if (AnimationUtility.blink(500L)) ClickGuiRenderer.drawLine(cx, caretLineTop + 2f, cx, caretLineTop + lh - 2f, theme().textPrimary()); + } + + private void drawScrollbar(float scissorX, float scissorY, float scissorW, float scissorH, float contentHeight) { + if (contentHeight <= scissorH + 0.5f) return; + float trackX = scissorX + scissorW - 6f, trackY = scissorY, trackW = 4f, trackH = scissorH; + float thumbH = Math.max(18f, trackH * (scissorH / contentHeight)); + float maxScroll = contentHeight - scissorH; + float scrollNorm = ClickGuiEditorCaret.clamp(-owner.scroll / maxScroll, 0f, 1f); + float thumbY = trackY + (trackH - thumbH) * scrollNorm; + ClickGuiRenderer.drawRoundedRect(trackX, trackY, trackW, trackH, 3f, theme().surface()); + ClickGuiRenderer.drawRoundedRect(trackX, thumbY, trackW, thumbH, 3f, theme().strokeSoft()); + } + + private void drawFooter(TextRenderer font) { + float mx = ClickGuiRenderer.getMouseX(), my = ClickGuiRenderer.getMouseY(); + boolean hoverCancel = caret.inside(mx, my, owner.cancelX, owner.cancelY, owner.cancelW, owner.cancelH); + boolean hoverSave = caret.inside(mx, my, owner.saveX, owner.saveY, owner.saveW, owner.saveH); + int cancelFill = hoverCancel ? theme().surfaceHover() : theme().surface(); + int saveFill = hoverSave ? ClickGuiRenderer.mixColor(theme().accent(), theme().surfaceHover(), 0.35f) : theme().accent(); + ClickGuiRenderer.drawRoundedRect(owner.cancelX, owner.cancelY, owner.cancelW, owner.cancelH, 8f, cancelFill); + ClickGuiRenderer.drawRoundedRectStroke(owner.cancelX, owner.cancelY, owner.cancelW, owner.cancelH, 8f, 1.05f, theme().strokeSoft()); + ClickGuiRenderer.drawText(font, "Cancel", owner.cancelX + 14f, owner.cancelY + (owner.cancelH - ClickGuiRenderer.textHeight(font, 14f)) * 0.5f, 14f, theme().textPrimary(), false); + ClickGuiRenderer.drawRoundedRect(owner.saveX, owner.saveY, owner.saveW, owner.saveH, 8f, saveFill); + ClickGuiRenderer.drawRoundedRectStroke(owner.saveX, owner.saveY, owner.saveW, owner.saveH, 8f, 1.05f, theme().strokeSoft()); + ClickGuiRenderer.drawText(font, "Save", owner.saveX + 20f, owner.saveY + (owner.saveH - ClickGuiRenderer.textHeight(font, 14f)) * 0.5f, 14f, theme().textPrimary(), false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java index 4f363f1..f304a1b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java @@ -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.gui.clickgui.layout.screen.modules; import org.lwjgl.glfw.GLFW; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java index 327df72..0f4f6bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java index 2d6f2cf..d0ef50a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.config.i18n.MainConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java index de9d6f0..e8e4ff5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java @@ -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.gui.clickgui.layout.screen.modules.model; import dev.loki.lovisual.features.module.core.ModuleCategory; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java index 73ff8b9..b06a38a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java @@ -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.gui.clickgui.layout.screen.modules.model; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java index 0613b21..a02e78a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.config.i18n.MainConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java index 441539c..a7a661c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java index 6a1f5c5..9f36b77 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java index 40ac6c5..14c9867 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java @@ -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.gui.clickgui.layout.screen.modules; import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuCategory; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java index a9bf959..c8061a9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java @@ -1,107 +1,26 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import org.lwjgl.glfw.GLFW; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigProfilesComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main.MainSettingsComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.SettingsPanelComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.BackgroundComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.CategoryContainerComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.SearchComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations.RelationsComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeEditorPreviewComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.theme.EditableClickGuiTheme; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.MenuSettingsResolver; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; public final class MenuScreen { - private static final float INPUT_READY_PROGRESS = 0.72f; - private static final float S = 2f; - private static final float MENU_W = 400f; - private static final float MENU_H = 250f; - private final BackgroundComponent backgroundComponent = new BackgroundComponent(); - private final CategoryContainerComponent categoryContainer = new CategoryContainerComponent(); - private final SearchComponent searchComponent = new SearchComponent(); - private final ModuleComponent moduleComponent = new ModuleComponent(); - private final ThemeComponent themeComponent = new ThemeComponent(); - private final ThemeEditorPreviewComponent themeEditorPreview = new ThemeEditorPreviewComponent(); - private final MainSettingsComponent mainSettingsComponent = new MainSettingsComponent(); - private final RelationsComponent relationsComponent = new RelationsComponent(); - private final ConfigProfilesComponent configProfilesComponent = new ConfigProfilesComponent(); - private final SettingsPanelComponent settingsPanel = new SettingsPanelComponent(); - private Category category = Category.HUD; - private float menuX; - private float menuY; - private float menuW; - private float menuH; - private float shellX; - private float shellY; - private float shellW; - private float shellH; - private float categoryX; - private float categoryY; - private float searchX; - private float searchY; - private float menuLinePulsePhase; - private float menuLineHSweep; - private float menuLineVSweep; - private float menuLineHDir = 1f; - private float menuLineVDir = 1f; - private float menuMaskAnim = 0f; - private float screenAnim = 0f; - private boolean openTarget = false; - private EditableClickGuiTheme activeThemeEditor; + private final MenuScreenState state; + private final MenuScreenLayout layout; + private final MenuScreenRenderer renderer; + private final MenuScreenInput input; - private static float easeOutCubic(float t) { - t = AnimationUtility.clamp(t, 0.0f, 1.0f); - float inv = 1.0f - t; - return 1.0f - inv * inv * inv; - } - - private static float easeOutQuint(float t) { - t = AnimationUtility.clamp(t, 0.0f, 1.0f); - float inv = 1.0f - t; - return 1.0f - inv * inv * inv * inv * inv; - } - - private static float smootherStep(float t) { - t = AnimationUtility.clamp(t, 0.0f, 1.0f); - return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); - } - - private static List visibleEntries(List src) { - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) return src; - List out = new ArrayList<>(); - for (ModuleComponent.CardEntry e : src) { - if (ClickGuiSearch.matches(e.title(), e.searchAliases())) out.add(e); - } - return out; + public MenuScreen() { + MenuScreenState createdState = new MenuScreenState(); + MenuScreenLayout createdLayout = new MenuScreenLayout(); + MenuScreenRenderer createdRenderer = new MenuScreenRenderer(createdLayout); + MenuScreenInput createdInput = new MenuScreenInput(); + this.state = createdState; + this.layout = createdLayout; + this.renderer = createdRenderer; + this.input = createdInput; } public void clearScreens() { @@ -111,517 +30,50 @@ public final class MenuScreen { } public void open() { - openTarget = true; + state.openTarget = true; } public void close() { - openTarget = false; - settingsPanel.mouseReleased(0f, 0f, 0); - relationsComponent.resetScroll(); - activeThemeEditor = null; - themeEditorPreview.reset(); + state.openTarget = false; + state.settingsPanel.mouseReleased(0f, 0f, 0); + state.relationsComponent.resetScroll(); + state.activeThemeEditor = null; + state.themeEditorPreview.reset(); if (!ClickGuiRenderer.isClosingForExit()) { - screenAnim = 0.0f; + state.screenAnim = 0.0f; } } public boolean isVisible() { - return openTarget || screenAnim > 0.001f; + return state.isVisible(); } public void render(float areaX, float areaY, float areaW, float areaH, float mx, float my) { - SettingsGuiPalette palette = SettingsGuiPalette.current(); - float dt = AnimationUtility.deltaTime(); - screenAnim = AnimationUtility.approach(screenAnim, openTarget ? 1.0f : 0.0f, dt, openTarget ? 8.5f : 7.2f); - screenAnim = AnimationUtility.snap(screenAnim, openTarget ? 1.0f : 0.0f, 0.002f); - if (screenAnim <= 0.001f && !openTarget) return; - - layout(areaX, areaY, areaW, areaH); - boolean hudPanelActive = settingsPanel.shouldMaskMenu(); - float maskTarget = hudPanelActive ? 1f : 0f; - menuMaskAnim = AnimationUtility.approach(menuMaskAnim, maskTarget, dt, 10f); - menuMaskAnim = AnimationUtility.snap(menuMaskAnim, maskTarget, 0.01f); - - boolean renderMain = !hudPanelActive; - if (activeThemeEditor != null && !settingsPanel.isActive()) { - activeThemeEditor = null; - themeEditorPreview.reset(); - } - float distanceProgress = easeOutQuint(screenAnim); - float fadeProgress = smootherStep(screenAnim); - float previousAlpha = ClickGuiRenderer.setRenderAlphaMultiplier(fadeProgress); - Renderer2D.COLOR.setAlpha(fadeProgress); - try (RenderWarpStack.Scope ignored = pushLifecycleWarp(distanceProgress)) { - if (renderMain) { - renderPanelShadow(palette); - - backgroundComponent.render(shellX, shellY, shellW, shellH, S); - renderShellChrome(palette); - - categoryContainer.render(categoryX, categoryY, mx, my, category, S); - searchComponent.render(searchX, searchY, 80f * S, 15f * S); - - if (category == Category.THEMES) { - themeComponent.render(menuX, menuY, menuW, menuH, mx, my, S); - } else if (category == Category.MAIN_SETTINGS) { - mainSettingsComponent.render(menuX, menuY, menuW, menuH, mx, my, S); - } else if (category == Category.RELATIONS) { - relationsComponent.render(menuX, menuY, menuW, menuH, mx, my, S); - } else if (category == Category.CONFIGS) { - configProfilesComponent.render(menuX, menuY, menuW, menuH, mx, my, S); - } else { - List entries = visibleEntries(MenuSettingsResolver.buildCards(category)); - moduleComponent.render(menuX, menuY, menuW, menuH, entries, S); - } - } - - if (renderMain && menuMaskAnim > 0.01f) { - int c0 = LayoutRender2D.alpha(palette.menuMaskLeft(), menuMaskAnim * fadeProgress); - int c1 = LayoutRender2D.alpha(palette.menuMaskRight(), menuMaskAnim * fadeProgress); - LayoutRender2D.roundedQuad(shellX, shellY, shellW, shellH, 8f * S, c0, c1, c1, c0); - } - - if (activeThemeEditor != null && settingsPanel.isActive()) { - themeEditorPreview.render(activeThemeEditor, menuX, menuY, menuW, menuH, mx, my, S); - } - - settingsPanel.render(menuX, menuY, menuW, menuH, mx, my, S); - } finally { - Renderer2D.COLOR.setAlpha(1.0f); - ClickGuiRenderer.restoreRenderAlphaMultiplier(previousAlpha); - } - - if (renderMain && !settingsPanel.isActive()) { - renderContextHints(areaX, areaY, areaW, areaH, fadeProgress); - } - } - - private void renderContextHints(float areaX, float areaY, float areaW, float areaH, float alpha) { - MainConfig config = MainConfig.get(); - if (!config.isClickGuiHintsEnabled()) return; - - String primary; - String secondary; - String tertiary; - switch (category) { - case HUD -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.hud.toggle", "LMB - toggle HUD element"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.hud.settings", "RMB - open element settings"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.hud.drag", "Open settings - edit widget position"); - } - case UI -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.ui.toggle", "LMB - toggle UI element"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.ui.settings", "RMB - open element settings"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.ui.search", "Ctrl+F - search cards"); - } - case THEMES -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.themes.select", "LMB - select theme"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.themes.edit", "Edit - open theme editor"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.themes.scroll", "Wheel - scroll themes"); - } - case MAIN_SETTINGS -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.main.select", "LMB - change setting"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.main.category", "Left list - switch setting group"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.main.scroll", "Wheel - scroll settings"); - } - case RELATIONS -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.relations.mode", "Top pills - choose relation list"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.relations.add", "User-plus icon - pick online player"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.relations.search", "Search - filter names"); - } - case CONFIGS -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.configs.select", "LMB - select config"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.configs.rename", "Click name - rename config"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.configs.enter", "Enter/Esc - confirm or cancel text edit"); - } - default -> { - primary = ClickGuiI18n.tr("clickgui.hints.settings.generic.select", "LMB - select"); - secondary = ClickGuiI18n.tr("clickgui.hints.settings.generic.scroll", "Wheel - scroll"); - tertiary = ClickGuiI18n.tr("clickgui.hints.settings.generic.search", "Ctrl+F - search"); - } - } - - ClickGuiHintOverlay.renderBottomLeft( - areaX, - areaY, - areaW, - areaH, - S, - alpha, - primary, - secondary, - tertiary, - ClickGuiI18n.tr("clickgui.hints.settings.close", "Esc - close ClickGUI"), - ClickGuiI18n.tr("clickgui.hints.settings.hide", "Alt+H - hide hints") - ); - } - - private static boolean isHintToggle(int keyCode, int modifiers) { - return keyCode == GLFW.GLFW_KEY_H && (modifiers & GLFW.GLFW_MOD_ALT) != 0; + renderer.render(state, areaX, areaY, areaW, areaH, mx, my); } public boolean mouseClicked(float mx, float my, int button) { - if (!isInteractive()) return false; - if (button != 0 && button != 1) return false; - - if (activeThemeEditor != null && themeEditorPreview.mouseClicked(activeThemeEditor, mx, my, button)) { - settingsPanel.close(); - activeThemeEditor = null; - themeEditorPreview.reset(); - return true; - } - - if (settingsPanel.mouseClicked(mx, my, button, S)) return true; - if (settingsPanel.blocksMenuInput()) return true; - - if (searchComponent.click(searchX, searchY, 80f * S, 15f * S, mx, my, button)) { - return true; - } - - Category hitCategory = categoryContainer.click(categoryX, categoryY, mx, my, S); - if (hitCategory != null) { - if (hitCategory != category) { - category = hitCategory; - moduleComponent.resetScroll(); - themeComponent.resetScroll(); - mainSettingsComponent.resetScroll(); - relationsComponent.resetScroll(); - configProfilesComponent.resetScroll(); - } - return true; - } - - if (category == Category.THEMES) { - if (themeComponent.mousePressedScrollbar(mx, my, button)) return true; - ThemeComponent.ThemeAction action = themeComponent.click(mx, my, button); - if (action == null) return false; - if (action.type() == ThemeComponent.ThemeActionType.SELECT && action.themeId() != null) { - Theme.setTheme(action.themeId()); - } else if (action.type() == ThemeComponent.ThemeActionType.ADD) { - openThemeEditor(EditableClickGuiTheme.createFromCurrent()); - } else if (action.type() == ThemeComponent.ThemeActionType.EDIT && action.themeId() != null) { - openThemeEditor(EditableClickGuiTheme.existing(action.themeId())); - } else if (action.type() == ThemeComponent.ThemeActionType.DELETE && action.themeId() != null) { - Theme.deleteCustomTheme(action.themeId()); - } - return true; - } - - if (category == Category.CONFIGS) { - if (configProfilesComponent.mousePressedScrollbar(mx, my, button)) return true; - return configProfilesComponent.click(mx, my, button); - } - - if (category == Category.MAIN_SETTINGS) { - if (mainSettingsComponent.mousePressedScrollbar(mx, my, button)) return true; - return mainSettingsComponent.mouseClicked(mx, my, button); - } - - if (category == Category.RELATIONS) { - if (relationsComponent.mousePressedScrollbar(mx, my, button)) return true; - return relationsComponent.click(mx, my, button); - } - - if (moduleComponent.mousePressedScrollbar(mx, my, button)) return true; - ModuleComponent.CardAction action = moduleComponent.click(mx, my, button); - if (action == null) return false; - if (action.toggleable() && (action.statusClick() || action.leftClick())) { - toggleEntry(action.getId()); - } - if (action.rightClick()) { - openSettings(action.getId()); - } - return true; + return input.mouseClicked(state, mx, my, button); } public void mouseReleased(float mx, float my, int button) { - if (!isInteractive()) return; - settingsPanel.mouseReleased(mx, my, button); - themeComponent.mouseReleased(button); - mainSettingsComponent.mouseReleased(mx, my, button); - relationsComponent.mouseReleased(button); - configProfilesComponent.mouseReleased(button); - moduleComponent.mouseReleased(button); + input.mouseReleased(state, mx, my, button); } public void mouseScrolled(float mx, float my, double delta) { - if (!isInteractive()) return; - if (settingsPanel.mouseScrolled(mx, my, delta)) return; - if (settingsPanel.blocksMenuInput()) return; - if (category == Category.THEMES) { - themeComponent.scroll(mx, my, menuX, menuY, menuW, menuH, delta, S); - } else if (category == Category.MAIN_SETTINGS) { - mainSettingsComponent.scroll(mx, my, delta); - } else if (category == Category.RELATIONS) { - relationsComponent.scroll(mx, my, delta); - } else if (category == Category.CONFIGS) { - configProfilesComponent.scroll(mx, my, menuX, menuY, menuW, menuH, delta, S); - } else { - moduleComponent.scroll(mx, my, menuX, menuY, menuW, menuH, delta, S); - } + input.mouseScrolled(state, mx, my, delta); } public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!isInteractive()) return false; - - if (settingsPanel.isActive() && settingsPanel.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - if (isHintToggle(keyCode, modifiers)) { - MainConfig config = MainConfig.get(); - config.setClickGuiHintsEnabled(!config.isClickGuiHintsEnabled()); - return true; - } - - if (category == Category.MAIN_SETTINGS && mainSettingsComponent.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - if (category == Category.CONFIGS && configProfilesComponent.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - if (category == Category.RELATIONS && relationsComponent.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; - - if (ctrl && keyCode == GLFW.GLFW_KEY_F) { - ClickGuiSearch.setActive(true); - return true; - } - - if (ClickGuiSearch.isActive()) { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - ClickGuiSearch.deactivate(); - return true; - } - - if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { - ClickGuiSearch.backspace(); - return true; - } - - return true; - } - - return false; + return input.keyPressed(state, keyCode, scanCode, modifiers); } public boolean charTyped(char chr, int modifiers) { - if (!isInteractive()) return false; - if (category == Category.MAIN_SETTINGS && mainSettingsComponent.charTyped(chr, modifiers)) { - return true; - } - - if (category == Category.CONFIGS && configProfilesComponent.charTyped(chr, modifiers)) { - return true; - } - - if (category == Category.RELATIONS && relationsComponent.charTyped(chr, modifiers)) { - return true; - } - - if (ClickGuiSearch.isActive()) { - ClickGuiSearch.append(chr); - return true; - } - - return settingsPanel.charTyped(chr, modifiers); + return input.charTyped(state, chr, modifiers); } public boolean onFilesDrop(List paths) { - if (!isInteractive() || category != Category.CONFIGS) return false; - return configProfilesComponent.importDroppedFiles(paths); - } - - private void layout(float areaX, float areaY, float areaW, float areaH) { - menuW = MENU_W * S; - menuH = MENU_H * S; - menuX = areaX + (areaW - menuW) * 0.5f; - menuY = areaY + (areaH - menuH) * 0.5f; - - shellX = menuX - 20f * S; - shellY = menuY; - shellW = menuW + 40f * S; - shellH = menuH; - - categoryX = menuX - 20f * S; - categoryY = menuY + 40f * S; - searchX = menuX + 330f * S; - searchY = menuY + 7.5f * S; - } - - private boolean isInteractive() { - return openTarget && screenAnim >= INPUT_READY_PROGRESS; - } - - private RenderWarpStack.Scope pushLifecycleWarp(float progress) { - if (progress >= 0.999f) return Renderer2D.pushWarp(null); - return Renderer2D.pushPerspectiveWarp( - shellX, - shellY, - shellW, - shellH, - 0.0f, - 0.0f, - 0.0f, - 3.6f, - 0.80f, - 0.90f + 0.10f * progress - ); - } - - private void renderShellChrome(SettingsGuiPalette palette) { - float vLineX = shellX + 42.5f * S; - float hLineY = shellY + 28f * S; - float lineThickness = 0.5f * S; - float vLineH = shellH; - float hLineW = shellW - 43f * S; - float hSegW = 56f * S; - float vSegH = 52f * S; - float hTravel = Math.max(1f, hLineW - hSegW); - float vTravel = Math.max(1f, vLineH - vSegH); - float dt = AnimationUtility.deltaTime(); - - LayoutRender2D.rectQuad( - vLineX, shellY, lineThickness, vLineH, - palette.menuLineLow(), - palette.menuLineMid(), - palette.menuLineLow(), - palette.menuLineStrong() - ); - LayoutRender2D.rectQuad( - shellX + 43f * S, hLineY, hLineW, lineThickness, - palette.menuLineStrong(), - palette.menuLineLow(), - palette.menuLineStrong(), - palette.menuLineLow() - ); - - menuLineHSweep = AnimationUtility.clamp(menuLineHSweep + menuLineHDir * 120f * dt, 0f, hTravel); - if (menuLineHSweep >= hTravel) menuLineHDir = -1f; - else if (menuLineHSweep <= 0f) menuLineHDir = 1f; - - menuLineVSweep = AnimationUtility.clamp(menuLineVSweep + menuLineVDir * 95f * dt, 0f, vTravel); - if (menuLineVSweep >= vTravel) menuLineVDir = -1f; - else if (menuLineVSweep <= 0f) menuLineVDir = 1f; - - menuLinePulsePhase += 4.1f * dt; - float pulse = 0.65f + 0.35f * (float) Math.sin(menuLinePulsePhase); - - float hSegX = shellX + 43f * S + menuLineHSweep; - int hA0 = LayoutRender2D.alpha(palette.menuSweepStart(), pulse); - int hA1 = LayoutRender2D.alpha(palette.menuSweepEnd(), pulse); - LayoutRender2D.rectQuad( - hSegX, hLineY, hSegW, lineThickness, - hA0, - hA1, - hA1, - hA0 - ); - - float vSegY = shellY + menuLineVSweep; - int vA0 = LayoutRender2D.alpha(palette.menuSweepStart(), pulse * 0.86f); - int vA1 = LayoutRender2D.alpha(palette.menuSweepEnd(), pulse * 0.93f); - LayoutRender2D.rectQuad( - vLineX, vSegY, lineThickness, vSegH, - vA0, - vA1, - vA0, - vA1 - ); - - if (category.iconToken()) { - TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); - float iconSize = 11.5f * S; - float textSize = 11.5f * S; - float baseX = shellX + 50f * S; - float baseY = shellY + 13.5f * S; - ClickGuiRenderer.drawText( - icons, - category.token(), - baseX, - baseY, - iconSize, - palette.menuHeaderText(), - false - ); - float iconW = ClickGuiRenderer.textWidth(icons, category.token(), iconSize); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - " " + category.title(), - baseX + iconW + 1.5f * S, - baseY, - textSize, - palette.menuHeaderText(), - false - ); - } else if (category.svgIcon()) { - float iconSize = 11.5f * S; - float textSize = 11.5f * S; - float baseX = shellX + 50f * S; - float baseY = shellY + 13.5f * S; - Renderer2D.COLOR.svg( - category.token(), - baseX, - baseY + 0.5f * S, - iconSize, - iconSize, - SvgRenderOptions.overrideColor(palette.menuHeaderText()) - ); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - " " + category.title(), - baseX + iconSize + 1.5f * S, - baseY, - textSize, - palette.menuHeaderText(), - false - ); - } else { - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - "| " + category.title(), - shellX + 50f * S, - shellY + 13.5f * S, - 11.5f * S, - palette.menuHeaderText(), - false - ); - } - } - - private void renderPanelShadow(SettingsGuiPalette palette) { - LayoutRender2D.roundedSoftShadow( - shellX, - shellY, - shellW, - shellH, - 8f * S, - 8f * S, - 0.018f, - palette.menuShadow() - ); - } - - private void toggleEntry(String id) { - MenuSettingsResolver.toggleEntry(id); - } - - private void openSettings(String id) { - MenuSettingsResolver.ResolvedSettings resolved = MenuSettingsResolver.resolveSettings(id); - if (resolved == null || resolved.settings().isEmpty()) return; - settingsPanel.open(resolved.getId(), resolved.title(), resolved.settings(), resolved.hudContext()); - } - - private void openThemeEditor(EditableClickGuiTheme editor) { - if (editor == null) return; - activeThemeEditor = editor; - themeEditorPreview.reset(); - settingsPanel.openEditor("theme:" + editor.id(), editor.title(), editor.buildSettings()); + return input.onFilesDrop(state, paths); } public enum Category { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java index d153009..9cce486 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java @@ -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.gui.clickgui.layout.screen.settings; import dev.loki.lovisual.config.io.ConfigSerializer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java index 18d5592..1e14cdd 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java @@ -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.gui.clickgui.layout.screen.settings; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java index 651e5e4..86799cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.category; import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java index f52119c..105c95b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.config; import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java index 8381333..2e8fed5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.config; import dev.loki.lovisual.config.profile.diff.ConfigProfileDateFormatter; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java index 066502a..dd18ba7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java @@ -1,13 +1,5 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; import dev.loki.lovisual.config.i18n.MainConfig; import dev.loki.lovisual.config.definition.SettingDef; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; @@ -19,9 +11,7 @@ import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderCont import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; import java.util.ArrayList; import java.util.EnumMap; @@ -29,29 +19,13 @@ import java.util.List; import java.util.Map; public final class MainSettingsComponent { - private static final float PARALLAX_MAX_ANGLE = 4.0f; - private static final float PARALLAX_DEPTH = 2.2f; - private static final float PARALLAX_PERSPECTIVE = 1.0f; - private static final float PARALLAX_SCALE_BOOST = 0.010f; - private static final float PARALLAX_CONTENT_SHIFT = 1.1f; private final Map> settingsByCategory = new EnumMap<>(Category.class); private final List rows = new ArrayList<>(); private final List settingHits = new ArrayList<>(); private final List categoryHits = new ArrayList<>(); private final Map categoryHoverAnim = new EnumMap<>(Category.class); + private final MainSettingsScroll scrollState = new MainSettingsScroll(); private Category selected = Category.IMAGE; - private float scroll; - private float smoothedScroll; - private boolean draggingScrollbar; - private boolean scrollbarVisible; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; private float rightX; private float rightY; private float rightW; @@ -78,38 +52,8 @@ public final class MainSettingsComponent { return Math.max(1.0f, menuScale * 0.66f); } - private static Parallax computeParallax(float mx, float my, float x, float y, float w, float h, float hover) { - float ease = smooth(Math.max(0f, Math.min(1f, hover))); - if (ease <= 0.001f || w <= 0f || h <= 0f) return Parallax.NONE; - float nx = clamp((mx - x) / w * 2f - 1f, -1f, 1f); - float ny = clamp((my - y) / h * 2f - 1f, -1f, 1f); - return new Parallax( - -nx * PARALLAX_MAX_ANGLE * ease, - ny * PARALLAX_MAX_ANGLE * ease, - 1f + PARALLAX_SCALE_BOOST * ease, - nx * PARALLAX_CONTENT_SHIFT * ease, - ny * PARALLAX_CONTENT_SHIFT * ease, - true - ); - } - - private static RenderWarpStack.Scope pushParallax(Parallax p, float x, float y, float w, float h) { - if (p == null || !p.active()) return Renderer2D.pushWarp(null); - return Renderer2D.pushPerspectiveWarp(x, y, w, h, p.yawDeg(), p.pitchDeg(), 0f, PARALLAX_DEPTH, PARALLAX_PERSPECTIVE, p.scale()); - } - - private static float smooth(float t) { - return t * t * (3f - 2f * t); - } - - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); - } - public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; + scrollState.reset(); } public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { @@ -139,18 +83,12 @@ public final class MainSettingsComponent { } public boolean mousePressedScrollbar(float mx, float my, int button) { - if (button != 0 || !isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; + return scrollState.press(mx, my, button); } public boolean mouseClicked(float mx, float my, int button) { if (button != 0 && button != 1) return false; - if (isScrollbarHovered(mx, my)) return true; + if (scrollState.isHovered(mx, my)) return true; for (CategoryHit hit : categoryHits) { if (!ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; @@ -175,7 +113,7 @@ public final class MainSettingsComponent { } public void mouseReleased(float mx, float my, int button) { - if (button == 0) draggingScrollbar = false; + scrollState.release(button); List settings = settingsByCategory.get(selected); if (settings == null) return; try (SettingRenderContext.Scope ignored = SettingRenderContext.push(SettingRenderSurface.SETTINGS, lastSettingScale)) { @@ -187,7 +125,7 @@ public final class MainSettingsComponent { public void scroll(float mx, float my, double amount) { if (!ClickGuiMath.insideRect(mx, my, rightX, rightY, rightW, rightH)) return; - scroll += (float) (amount * 22f); + scrollState.addWheel(amount); } public boolean keyPressed(int keyCode, int scanCode, int modifiers) { @@ -273,22 +211,15 @@ public final class MainSettingsComponent { contentX = rightX + pad; contentY = rightY + 27f * scale; - contentW = Math.max(1f, rightW - pad * 2f - (needsScrollbar() ? 7f * scale : 0f)); + contentW = Math.max(1f, rightW - pad * 2f - (scrollState.needsScrollbar() ? 7f * scale : 0f)); contentH = Math.max(1f, rightH - 35f * scale); float totalH = totalRowsHeight(); - float maxScroll = Math.max(0f, totalH - contentH); - updateScrollbarMetrics(maxScroll, totalH, scale); - if (draggingScrollbar) { - scrollToMouse(my); - } - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); + scrollState.layout(rightX, rightW, contentY, contentH, totalH, my, scale); settingHits.clear(); boolean clipped = ScissorFunction.pushRaw(contentX, contentY, contentW, contentH); - float y = contentY + smoothedScroll; + float y = contentY + scrollState.smoothed(); try (SettingRenderContext.Scope ignored = SettingRenderContext.push(SettingRenderSurface.SETTINGS, lastSettingScale)) { for (SettingRow row : rows) { float h = row.height(); @@ -303,7 +234,7 @@ public final class MainSettingsComponent { } if (clipped) ScissorFunction.pop(); - renderScrollbar(scale, palette); + scrollState.render(scale, palette); } private void buildRows() { @@ -328,74 +259,6 @@ public final class MainSettingsComponent { return out; } - private boolean needsScrollbar() { - return scrollbarVisible; - } - - private void updateScrollbarMetrics(float maxScroll, float totalH, float scale) { - scrollbarVisible = maxScroll > 0f && totalH > contentH + 0.5f; - scrollbarMaxScroll = maxScroll; - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - - scrollbarW = 4f * scale; - scrollbarX = rightX + rightW - 10f * scale; - scrollbarY = contentY; - scrollbarH = contentH; - scrollbarThumbH = Math.max(18f * scale, scrollbarH * (contentH / Math.max(contentH, totalH))); - float ratio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private void renderScrollbar(float scale, SettingsGuiPalette palette) { - if (!scrollbarVisible) return; - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - if (isScrollbarHovered(mx, my) || draggingScrollbar) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - LayoutRender2D.roundedQuad( - scrollbarX, - scrollbarY, - scrollbarW, - scrollbarH, - 2f * scale, - palette.moduleScrollTrackA(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackA() - ); - LayoutRender2D.roundedQuad( - scrollbarX, - scrollbarThumbY, - scrollbarW, - scrollbarThumbH, - 2f * scale, - palette.moduleScrollHandleA(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleA() - ); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - private void drawPanel(float x, float y, float w, float h, float scale, SettingsGuiPalette palette) { ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 200f / 255f); LayoutRender2D.roundedQuad( @@ -425,30 +288,4 @@ public final class MainSettingsComponent { settingsByCategory.put(Category.SECURITY, buildSettings(config.getSecuritySettingDefs(), config)); settingsByCategory.put(Category.UTILITY, buildSettings(config.getUtilitySettingDefs(), config)); } - - private enum Category { - IMAGE("Image"), - SECURITY("Security"), - UTILITY("Utility"), - MISCELLANIOUS("Miscellanious"); - - private final String title; - - Category(String title) { - this.title = title; - } - } - - private record SettingRow(Setting setting, float anim, float height, float gap) { - } - - private record SettingHit(Setting setting, float x, float y, float w, float h) { - } - - private record CategoryHit(Category category, float x, float y, float w, float h) { - } - - private record Parallax(float yawDeg, float pitchDeg, float scale, float shiftX, float shiftY, boolean active) { - private static final Parallax NONE = new Parallax(0f, 0f, 1f, 0f, 0f, false); - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsModel.java new file mode 100644 index 0000000..4a4912e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsModel.java @@ -0,0 +1,30 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main; + +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; + +/** + * Data types for {@link MainSettingsComponent}: the top-level settings categories and + * the per-frame hit-test/layout records. Package-private, kept together so the component + * class stays focused on behavior. + */ +enum Category { + IMAGE("Image"), + SECURITY("Security"), + UTILITY("Utility"), + MISCELLANIOUS("Miscellanious"); + + final String title; + + Category(String title) { + this.title = title; + } +} + +record SettingRow(Setting setting, float anim, float height, float gap) { +} + +record SettingHit(Setting setting, float x, float y, float w, float h) { +} + +record CategoryHit(Category category, float x, float y, float w, float h) { +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsScroll.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsScroll.java new file mode 100644 index 0000000..9eabea8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsScroll.java @@ -0,0 +1,112 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +/** + * Scroll and scrollbar state for {@link MainSettingsComponent}'s settings panel: owns the + * scroll offset, its smoothing animation and the scrollbar geometry/hit-testing. The + * component drives it once per frame via {@link #layout} and forwards input events here. + */ +final class MainSettingsScroll { + private float scroll; + private float smoothed; + private boolean dragging; + private boolean visible; + private float sbX; + private float sbY; + private float sbW; + private float sbH; + private float thumbY; + private float thumbH; + private float maxScroll; + private float dragOffset; + + void reset() { + scroll = 0f; + smoothed = 0f; + dragging = false; + } + + void addWheel(double amount) { + scroll += (float) (amount * 22f); + } + + float smoothed() { + return smoothed; + } + + boolean needsScrollbar() { + return visible; + } + + boolean press(float mx, float my, int button) { + if (button != 0 || !isHovered(mx, my)) return false; + dragging = true; + dragOffset = ClickGuiMath.insideRect(mx, my, sbX, thumbY, sbW, thumbH) ? my - thumbY : thumbH * 0.5f; + scrollToMouse(my); + return true; + } + + void release(int button) { + if (button == 0) dragging = false; + } + + boolean isHovered(float mx, float my) { + if (!visible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, sbX - pad, sbY - pad, sbW + pad * 2f, sbH + pad * 2f); + } + + void layout(float rightX, float rightW, float contentY, float contentH, float totalH, float my, float scale) { + float max = Math.max(0f, totalH - contentH); + updateMetrics(max, totalH, contentY, contentH, rightX, rightW, scale); + if (dragging) scrollToMouse(my); + scroll = ClickGuiMath.clamp(scroll, -max, 0f); + smoothed = AnimationUtility.approach(smoothed, scroll, dragging ? 0.55f : 0.2f); + smoothed = AnimationUtility.snap(smoothed, scroll, dragging ? 0.01f : 0.05f); + } + + private void updateMetrics(float max, float totalH, float contentY, float contentH, float rightX, float rightW, float scale) { + visible = max > 0f && totalH > contentH + 0.5f; + maxScroll = max; + if (!visible) { + dragging = false; + sbX = sbY = sbW = sbH = thumbY = thumbH = 0f; + return; + } + sbW = 4f * scale; + sbX = rightX + rightW - 10f * scale; + sbY = contentY; + sbH = contentH; + thumbH = Math.max(18f * scale, sbH * (contentH / Math.max(contentH, totalH))); + float ratio = maxScroll <= 0f ? 0f : (-smoothed / maxScroll); + thumbY = sbY + (sbH - thumbH) * AnimationUtility.clamp(ratio, 0f, 1f); + } + + private void scrollToMouse(float my) { + if (!visible || maxScroll <= 0f) return; + float span = sbH - thumbH; + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - dragOffset, sbY, sbY + span); + float ratio = (thumbTop - sbY) / span; + scroll = -maxScroll * AnimationUtility.clamp(ratio, 0f, 1f); + } + + void render(float scale, SettingsGuiPalette palette) { + if (!visible) return; + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + if (isHovered(mx, my) || dragging) { + SystemCursor.set(SystemCursor.CursorType.SCROLL); + } + LayoutRender2D.roundedQuad(sbX, sbY, sbW, sbH, 2f * scale, + palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); + LayoutRender2D.roundedQuad(sbX, thumbY, sbW, thumbH, 2f * scale, + palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java index dd09623..0cb384c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java @@ -1,528 +1,42 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.StatusRender; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; - -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import dev.loki.lovisual.features.module.core.Module; public final class ModuleComponent { - private static final float PARALLAX_MAX_ANGLE = 4.6f; - private static final float PARALLAX_DEPTH = 4.8f; - private static final float PARALLAX_PERSPECTIVE = 0.92f; - private static final float PARALLAX_SCALE_BOOST = 0.012f; - private static final float PARALLAX_CONTENT_SHIFT = 0.0f; - private final List hits = new ArrayList<>(); - private final Map stateAnimById = new HashMap<>(); - private final Map hoverAnimById = new HashMap<>(); - private final Map statusRenderById = new HashMap<>(); - private float scroll; - private float smoothedScroll; - private boolean scrollbarVisible; - private boolean draggingScrollbar; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; + private final ModuleCardState state; + private final ModuleCardLayout layout; + private final ModuleCardRenderer renderer; + private final ModuleCardInput input; - private static String normalizeDescription(String description) { - if (description == null || description.isBlank()) { - return ClickGuiI18n.tr("clickgui.settings.module.description_missing", "Module description is missing."); - } - return description; - } - - private static int brightenRgb(int color, int delta) { - int a = (color >>> 24) & 0xFF; - int r = Math.min(255, ((color >>> 16) & 0xFF) + delta); - int g = Math.min(255, ((color >>> 8) & 0xFF) + delta); - int b = Math.min(255, (color & 0xFF) + delta); - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static Parallax computeParallax(float mx, float my, float x, float y, float w, float h, float hover) { - float ease = smooth(Math.max(0f, Math.min(1f, hover))); - if (ease <= 0.001f || w <= 0f || h <= 0f) return Parallax.NONE; - float nx = clamp((mx - x) / w * 2f - 1f, -1f, 1f); - float ny = clamp((my - y) / h * 2f - 1f, -1f, 1f); - return new Parallax( - -nx * PARALLAX_MAX_ANGLE * ease, - ny * PARALLAX_MAX_ANGLE * ease, - 1f + PARALLAX_SCALE_BOOST * ease, - nx * PARALLAX_CONTENT_SHIFT * ease, - ny * PARALLAX_CONTENT_SHIFT * ease, - true - ); - } - - private static RenderWarpStack.Scope pushParallax(Parallax p, float x, float y, float w, float h) { - if (p == null || !p.active()) return Renderer2D.pushWarp(null); - return Renderer2D.pushPerspectiveWarp(x, y, w, h, p.yawDeg(), p.pitchDeg(), 0f, PARALLAX_DEPTH, PARALLAX_PERSPECTIVE, p.scale()); - } - - private static float smooth(float t) { - return t * t * (3f - 2f * t); - } - - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); + public ModuleComponent() { + this.state = new ModuleCardState(); + this.layout = new ModuleCardLayout(state); + this.renderer = new ModuleCardRenderer(state, layout); + this.input = new ModuleCardInput(state, layout); } public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; + state.resetScroll(); } public void scroll(float mx, float my, float menuX, float menuY, float menuW, float menuH, double amount, float scale) { - float hitX = menuX + 35f * scale; - float hitY = menuY + 13f * scale; - float hitW = menuW - 35f * scale + 7f * scale; - float hitH = menuH - 13f * scale + 15f * scale; - if (!ClickGuiMath.insideRect(mx, my, hitX, hitY, hitW, hitH)) return; - scroll += (float) (amount * 20f); + input.scroll(mx, my, menuX, menuY, menuW, menuH, amount, scale); } - public void render(float menuX, - float menuY, - float menuW, - float menuH, - List entries, - float scale) { - hits.clear(); - if (entries.isEmpty()) return; - - int[] offsets = calculateOffsets(entries, scale); - int column = 0; - int maxScroll = 0; - float columnWidth = 142f * scale; - SettingsGuiPalette palette = SettingsGuiPalette.current(); - - float scissorX = menuX + 35f * scale - 75f * scale; - float scissorY = menuY + 35.5f * scale; - float scissorW = menuW - 35f * scale + 150f * scale; - float scissorBottom = menuY + menuH - 3.5f * scale; - float scissorH = Math.max(1f, scissorBottom - scissorY); - boolean clipped = ScissorFunction.pushRaw(scissorX, scissorY, scissorW, scissorH); - - for (int i = entries.size() - 1; i >= 0; i--) { - CardEntry entry = entries.get(i); - int componentHeight = Math.round(getComponentHeight(entry, columnWidth + 40f * scale, scale) + 9f * scale); - - float x = menuX + 32f * scale + (column * (columnWidth + 48f * scale)); - float y = menuY + 35f * scale + offsets[column] - componentHeight + smoothedScroll; - float w = columnWidth + 40f * scale; - float h = componentHeight - 9f * scale; - - if (y + h > scissorY + 1.0f * scale && scissorBottom - 1.0f * scale > y) { - CardHit hit = drawCard(entry, x, y, w, h, scale, palette); - hits.add(hit); - } - - offsets[column] -= componentHeight; - maxScroll = Math.max(maxScroll, offsets[column]); - column = (column + 1) % 2; - } - - int clamped = ClickGuiMath.clamp(Math.round(maxScroll - (menuH * 0.5f + 35f * scale)), 0, maxScroll); - updateScrollbarMetrics(menuX, menuY, menuH, clamped, scale); - if (draggingScrollbar) { - scrollToMouse(ClickGuiRenderer.getMouseY()); - } - scroll = ClickGuiMath.clamp(scroll, -clamped, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); - if (clipped) ScissorFunction.pop(); - - renderScrollbar(menuX, menuY, menuH, clamped, scale, palette); + public void render(float menuX, float menuY, float menuW, float menuH, List entries, float scale) { + renderer.render(menuX, menuY, menuW, menuH, entries, scale); } public boolean mousePressedScrollbar(float mx, float my, int button) { - if (button != 0) return false; - if (!isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; + return input.mousePressedScrollbar(mx, my, button); } public void mouseReleased(int button) { - if (button == 0) { - draggingScrollbar = false; - } + input.mouseReleased(button); } public CardAction click(float mx, float my, int button) { - if (isScrollbarHovered(mx, my)) return null; - for (CardHit hit : hits) { - if (!ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; - if (button == 1 && !hit.hasSettings()) continue; - return new CardAction( - hit.getId(), - button == 0, - button == 1, - hit.statusHit(mx, my), - hit.bindHit(mx, my), - hit.toggleable() - ); - } - return null; - } - - private CardHit drawCard(CardEntry entry, float x, float y, float w, float h, float scale, SettingsGuiPalette palette) { - float enabledAnim = stateAnimById.compute(entry.getId(), (k, v) -> AnimationUtility.approach(v == null ? 0f : v, entry.enabled() ? 1f : 0f, 0.16f)); - boolean hovered = ClickGuiMath.insideRect(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY(), x, y, w, h); - float hoverAnim = hoverAnimById.compute(entry.getId(), (k, v) -> AnimationUtility.approach(v == null ? 0f : v, hovered ? 1f : 0f, 0.18f)); - Parallax parallax = computeParallax(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY(), x, y, w, h, hoverAnim); - int brightnessOffset = Math.round(9f * enabledAnim); - int alphaOffset = 150 + Math.round(105f * enabledAnim); - String description = normalizeDescription(entry.description()); - float descHeight = descriptionHeight(description, w, scale); - int cardTopStrong = brightenRgb(palette.moduleCardTopStrong(), brightnessOffset); - int cardBottomStrong = brightenRgb(palette.moduleCardBottomStrong(), brightnessOffset); - BindRect bindRect; - float statusX = x + w - 24f * scale; - float statusY = y + descHeight + 31f * scale; - - ClickGuiRenderer.drawRoundedRectShadow( - x, - y + 1.5f * scale, - w, - h, - 5.4f * scale, - 9.0f * scale, - 1.0f * scale, - LayoutRender2D.alpha(0xFF000000, 0.22f + 0.10f * hoverAnim) - ); - try (var ignored = pushParallax(parallax, x, y, w, h)) { - ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 200f / 255f); - LayoutRender2D.roundedQuad( - x, y, w, h, - 5f * scale, - palette.moduleCardTop(), - cardTopStrong, - palette.moduleCardBottom(), - cardBottomStrong - ); - LayoutRender2D.rectQuad( - x, y + descHeight + 25f * scale, w, 0.5f * scale, - palette.moduleDividerStart(), - palette.moduleDividerEnd(), - palette.moduleDividerEnd(), - palette.moduleDividerStart() - ); - Renderer2D.COLOR.radialGlowMasked( - x, - y, - w, - h, - 5f * scale, - 0f, - 48f * scale, - x + w * 0.18f, - y + h * 0.08f, - LayoutRender2D.alpha(palette.moduleCardTopStrong(), 0.12f + 0.10f * hoverAnim) - ); - - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - "• " + entry.title(), - x + 8.8f * scale, - y + 5.4f * scale, - 13f * scale, - SettingsGuiPalette.withAlpha(palette.moduleTitleText(), alphaOffset), - false - ); - - drawDescription(description, x, y, w, scale, palette); - if (!entry.hasSettings()) { - drawNoSettingsHint(x, y, descHeight, scale, palette); - } - bindRect = drawBind(entry, x, y, w, descHeight, scale, palette); - if (entry.toggleable()) { - StatusRender status = statusRenderById.computeIfAbsent(entry.getId(), k -> new StatusRender()); - status.render(statusX, statusY, enabledAnim, scale); - } - } - - return new CardHit( - entry.getId(), - x, - y, - w, - h, - bindRect.hitX(), - bindRect.hitY(), - bindRect.hitW(), - bindRect.hitH(), - statusX, - statusY, - 16f * scale, - 8f * scale, - entry.hasSettings(), - entry.toggleable() - ); - } - - private void drawDescription(String description, float x, float y, float w, float scale, SettingsGuiPalette palette) { - float maxWidth = w - 25f * scale; - float currentX = x + 10f * scale; - float currentY = y + 19f * scale; - String[] words = description.split(" "); - StringBuilder line = new StringBuilder(); - int currentLine = 1; - float textSize = 10f * scale; - TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); - int iconColor = palette.moduleDescriptionIcon(); - int textColor = palette.moduleDescriptionText(); - - for (String word : words) { - float wordWidth = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), word + " ", textSize); - if (currentX + wordWidth > x + maxWidth) { - if (currentLine == 1) { - ClickGuiRenderer.drawText( - icons, - "J", - x + 6.5f * scale, - currentY + 0.75f * scale, - 9f * scale, - iconColor, - false - ); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - line.toString(), - x + 15f * scale, - currentY, - textSize, - textColor, - false - ); - } else { - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - line.toString(), - x + 5f * scale, - currentY, - textSize, - textColor, - false - ); - } - line = new StringBuilder(); - currentY += ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), textSize) - 5.75f * scale; - currentX = x + 10f * scale; - currentLine++; - } - line.append(word).append(" "); - currentX += wordWidth; - } - - if (!line.isEmpty()) { - if (currentLine == 1) { - ClickGuiRenderer.drawText( - icons, - "J", - x + 6.5f * scale, - currentY + 0.75f * scale, - 9f * scale, - iconColor, - false - ); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - line.toString(), - x + 15f * scale, - currentY, - textSize, - textColor, - false - ); - } else { - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - line.toString(), - x + 7f * scale, - currentY, - textSize, - textColor, - false - ); - } - } - } - - private BindRect drawBind(CardEntry entry, float x, float y, float w, float descHeight, float scale, SettingsGuiPalette palette) { - String label = entry.bindLabel(); - if (label == null || label.isBlank()) { - return new BindRect(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f); - } - float labelW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), label, 12f * scale); - float bindX = x + w - 37.5f * scale - labelW; - float bindY = y + descHeight + 29.75f * scale; - float bindW = labelW + 6f * scale; - float bindH = 10f * scale; - float hitY = y + descHeight + 32.5f * scale; - float hitH = 9f * scale; - - LayoutRender2D.roundedQuad( - bindX + 0.25f * scale, bindY, bindW, bindH, 3f * scale, - palette.moduleBindBg0(), - palette.moduleBindBg1(), - palette.moduleBindBg2(), - palette.moduleBindBg3() - ); - LayoutRender2D.roundedStroke( - bindX + 0.25f * scale, bindY, bindW, bindH, 3f * scale, 0.5f * scale, - palette.moduleBindStroke() - ); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - label, - x + w - 34.5f * scale - labelW, - y + descHeight + 34f * scale, - 12f * scale, - palette.moduleBindText(), - false - ); - return new BindRect(bindX, bindY, bindW, bindH, bindX, hitY, bindW, hitH); - } - - private void drawNoSettingsHint(float x, float y, float descHeight, float scale, SettingsGuiPalette palette) { - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - ClickGuiI18n.tr("clickgui.settings.module.no_settings", "No settings"), - x + 10f * scale, - y + descHeight + 33.5f * scale, - 9f * scale, - palette.moduleNoSettingsText(), - false - ); - } - - private void renderScrollbar(float menuX, float menuY, float menuH, int clamped, float scale, SettingsGuiPalette palette) { - updateScrollbarMetrics(menuX, menuY, menuH, clamped, scale); - if (!scrollbarVisible) return; - - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - boolean hover = isScrollbarHovered(mx, my); - if (hover || draggingScrollbar) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - - LayoutRender2D.roundedQuad( - scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - palette.moduleScrollTrackA(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackA() - ); - - LayoutRender2D.roundedQuad( - scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - palette.moduleScrollHandleA(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleA() - ); - } - - private void updateScrollbarMetrics(float menuX, float menuY, float menuH, int clamped, float scale) { - scrollbarVisible = clamped > 0; - scrollbarMaxScroll = Math.max(0f, clamped); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - - float offsetY = 14f * scale; - scrollbarW = 4f * scale; - scrollbarX = menuX + 400f * scale - 35f * scale - scrollbarW + 50f * scale; - scrollbarY = menuY + offsetY + 22f * scale; - scrollbarH = menuH - offsetY * 2f - 14f * scale; - - float contentHeight = scrollbarMaxScroll; - float viewHeight = menuH - offsetY * 2f; - scrollbarThumbH = Math.max(20f * scale, viewHeight * (viewHeight / (contentHeight + viewHeight))); - float scrollRatio = contentHeight <= 0f ? 0f : (-smoothedScroll / contentHeight); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(scrollRatio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private float descriptionHeight(String description, float w, float scale) { - float maxWidth = w - 25f * scale; - float currentX = 0f; - int lineCount = 1; - for (String word : description.split(" ")) { - float wordW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), word + " ", 10f * scale); - if (currentX + wordW > maxWidth) { - lineCount++; - currentX = 0f; - } - currentX += wordW; - } - float lineH = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), 10f * scale); - float compactStep = Math.max(scale, lineH - 5.75f * scale); - return Math.max(0f, lineH - 4f * scale + (lineCount - 1) * compactStep); - } - - private int getComponentHeight(CardEntry entry, float w, float scale) { - return Math.round(45f * scale + descriptionHeight(normalizeDescription(entry.description()), w, scale)); - } - - private int[] calculateOffsets(List entries, float scale) { - int[] offsets = new int[2]; - int column = 0; - for (int i = entries.size() - 1; i >= 0; i--) { - int componentHeight = Math.round(getComponentHeight(entries.get(i), 182f * scale, scale) + 9f * scale); - offsets[column] += componentHeight; - column = (column + 1) % 2; - } - return offsets; + return input.click(mx, my, button); } public record CardEntry(String id, @@ -574,40 +88,4 @@ public final class ModuleComponent { return id; } } - - private record Parallax(float yawDeg, float pitchDeg, float scale, float shiftX, float shiftY, boolean active) { - private static final Parallax NONE = new Parallax(0f, 0f, 1f, 0f, 0f, false); - } - - private record BindRect(float x, float y, float w, float h, float hitX, float hitY, float hitW, float hitH) { - } - - private record CardHit(String id, - float x, - float y, - float w, - float h, - float bindX, - float bindY, - float bindW, - float bindH, - float statusX, - float statusY, - float statusW, - float statusH, - boolean hasSettings, - boolean toggleable) { - public String getId() { - return id; - } - - boolean statusHit(float mx, float my) { - if (!toggleable) return false; - return ClickGuiMath.insideRect(mx, my, statusX, statusY, statusW, statusH); - } - - boolean bindHit(float mx, float my) { - return ClickGuiMath.insideRect(mx, my, bindX, bindY, bindW, bindH); - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java index 79aa554..db1f705 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java @@ -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.gui.clickgui.layout.screen.settings.implement.module; public final class ModuleSettingsWindow { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java index b4e4d76..1d3c0d1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java @@ -1,715 +1,74 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import net.minecraft.client.Minecraft; -import org.lwjgl.glfw.GLFW; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; -import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; -import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.gui.clickgui.util.tooltip.SettingTooltip; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import java.util.ArrayList; import java.util.List; public final class SettingsPanelComponent { - private static final float DROPDOWN_PANEL_W = 115.0f; - private static final float DROPDOWN_PANEL_H = 240.0f; - private static final float DROPDOWN_HEADER_H = 24.0f; - private static final float DROPDOWN_SEPARATOR_H = 4.0f; - private static final float DROPDOWN_RADIUS = 10.0f; - private final List settings = new ArrayList<>(); - private final List rows = new ArrayList<>(); - private final List hits = new ArrayList<>(); - private final SettingRenderSurface renderSurface; - private String targetId = ""; - private String title = "Settings"; - private boolean hudContext = false; - private boolean editorContext = false; - private boolean openTarget = false; - private float openAnim = 0f; - private float panelHeight = 120f; - private float scroll = 0f; - private float smoothedScroll = 0f; - private float closeHoverAnim = 0f; - private boolean dragging = false; - private float dragOffsetX = 0f; - private float dragOffsetY = 0f; - private boolean manualPos = false; - private float manualX = 0f; - private float manualY = 0f; - private boolean limitedHeight = false; - private float hudExtraTop = 0f; - private float panelX; - private float panelY; - private float panelW; - private float panelH; - private float closeX; - private float closeY; - private float closeW; - private float closeH; - private float contentX; - private float contentY; - private float contentW; - private float contentH; - private float maxScroll; - private float contentPadLeft; - private float contentPadRight; - private float pillX; - private float pillY; - private float pillW; - private float pillH; - private float pillActiveAnim = 1f; - private HudPreviewMode hudPreviewMode = HudPreviewMode.ONLY_CURRENT; - private float lastSettingScale = 1.0f; + private final SettingsPanelState state; + private final SettingsPanelLayout layout; + private final SettingsPanelChrome chrome; + private final SettingsPanelRenderer renderer; + private final SettingsPanelInput input; + public SettingsPanelComponent() { this(SettingRenderSurface.MODULES); } - public SettingsPanelComponent(SettingRenderSurface renderSurface) { - this.renderSurface = renderSurface == null ? SettingRenderSurface.SETTINGS : renderSurface; - } - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static boolean isHintToggle(int keyCode, int modifiers) { - return keyCode == GLFW.GLFW_KEY_H && (modifiers & GLFW.GLFW_MOD_ALT) != 0; - } - - private static int mixColor(int from, int to, float t) { - t = Math.max(0f, Math.min(1f, t)); - int a = (int) (((from >>> 24) & 0xFF) * (1 - t) + ((to >>> 24) & 0xFF) * t); - int r = (int) (((from >>> 16) & 0xFF) * (1 - t) + ((to >>> 16) & 0xFF) * t); - int g = (int) (((from >>> 8) & 0xFF) * (1 - t) + ((to >>> 8) & 0xFF) * t); - int b = (int) (((from) & 0xFF) * (1 - t) + ((to) & 0xFF) * t); - return (a << 24) | (r << 16) | (g << 8) | b; + public SettingsPanelComponent(SettingRenderSurface surface) { + this.state = new SettingsPanelState(surface); + this.layout = new SettingsPanelLayout(state); + this.chrome = new SettingsPanelChrome(state, layout); + this.renderer = new SettingsPanelRenderer(state, layout, chrome); + this.input = new SettingsPanelInput(state, layout); } public void open(String id, String title, List source, boolean hudContext) { - if (id == null) return; - resetHudPreview(); - this.targetId = id; - this.title = title == null || title.isBlank() ? id : title; - this.settings.clear(); - if (source != null) this.settings.addAll(source); - this.hudContext = hudContext; - this.editorContext = false; - this.openTarget = true; - this.scroll = 0f; - this.smoothedScroll = 0f; - this.closeHoverAnim = 0f; - this.dragging = false; - this.rows.clear(); - this.hits.clear(); - if (hudContext) { - this.manualPos = false; - } - if (hudContext) { - this.hudPreviewMode = HudPreviewMode.ONLY_CURRENT; - this.pillActiveAnim = 1f; - applyHudPreviewMode(); - } + state.open(id, title, source, hudContext); } public void openEditor(String id, String title, List source) { - open(id, title, source, false); - this.editorContext = true; - this.manualPos = false; + state.openEditor(id, title, source); } public void close() { - openTarget = false; - dragging = false; + state.close(); } public boolean isActive() { - return openTarget || openAnim > 0.001f; + return state.isActive(); } public boolean shouldMaskMenu() { - return (hudContext || editorContext) && isActive(); + return state.shouldMaskMenu(); } public boolean blocksMenuInput() { - return (hudContext || editorContext) && (openTarget || openAnim > 0.35f); + return state.blocksMenuInput(); } public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { - float dt = AnimationUtility.deltaTime(); - float openTargetValue = openTarget ? 1f : 0f; - openAnim = AnimationUtility.approach(openAnim, openTargetValue, dt, openTarget ? 12f : 13f); - openAnim = AnimationUtility.snap(openAnim, openTargetValue, 0.01f); - float pillTarget = hudPreviewMode == HudPreviewMode.ALL_ENABLED ? 0f : 1f; - pillActiveAnim = AnimationUtility.approach(pillActiveAnim, pillTarget, dt, 14f); - pillActiveAnim = AnimationUtility.snap(pillActiveAnim, pillTarget, 0.002f); - - if (!openTarget && openAnim <= 0.001f) { - openAnim = 0f; - if (hudContext) { - resetHudPreview(); - hudContext = false; - } - editorContext = false; - return; - } - - float panelScale = panelScale(scale); - lastSettingScale = settingScale(panelScale); - buildRows(panelScale); - - float baseW = DROPDOWN_PANEL_W * panelScale; - float baseX = menuX + menuW + 24f * panelScale; - float baseY = menuY + 4f * panelScale; - if (manualPos) { - baseX = manualX; - baseY = manualY; - } - hudExtraTop = hudContext ? 20f * panelScale : 0f; - float targetH = AnimationUtility.clamp( - totalRowsHeight() + (DROPDOWN_HEADER_H + DROPDOWN_SEPARATOR_H) * panelScale + 5f * panelScale + hudExtraTop, - 0f, - DROPDOWN_PANEL_H * panelScale - ); - panelHeight = AnimationUtility.approach(panelHeight, targetH, dt, 14f); - panelHeight = AnimationUtility.snap(panelHeight, targetH, 0.25f); - - float distanceProgress = AnimationUtility.easeInOutCubic(openAnim); - float fadeProgress = AnimationUtility.easeInOutCubic(openAnim); - if (dragging) { - manualPos = true; - manualX = mx + dragOffsetX; - manualY = my + dragOffsetY; - panelX = manualX; - panelY = manualY; - } else { - panelX = baseX; - panelY = baseY; - } - panelW = baseW; - panelH = panelHeight; - limitedHeight = panelH >= (DROPDOWN_PANEL_H * panelScale - 0.25f * panelScale); - SettingsGuiPalette palette = SettingsGuiPalette.current(); - - try (RenderWarpStack.Scope lifecycleScope = pushLifecycleWarp(distanceProgress)) { - int bgTl = LayoutRender2D.alpha(palette.panelBgLeft(), fadeProgress); - int bgTr = LayoutRender2D.alpha(palette.panelBgRight(), fadeProgress); - int bgBr = LayoutRender2D.alpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.08f), fadeProgress); - int bgBl = LayoutRender2D.alpha(SettingsGuiPalette.darken(palette.panelBgLeft(), 0.05f), fadeProgress); - int stroke = LayoutRender2D.alpha(palette.panelStroke(), fadeProgress); - int line = LayoutRender2D.alpha(palette.panelDivider(), 0.52f * fadeProgress); - int text = LayoutRender2D.alpha(palette.panelText(), fadeProgress); - int mutedBase = LayoutRender2D.alpha(palette.panelMuted(), fadeProgress); - - String header = title; - closeW = 16f * panelScale; - closeH = 14f * panelScale; - closeX = panelX + panelW - closeW - 6f * panelScale; - closeY = panelY + (DROPDOWN_HEADER_H * panelScale - closeH) * 0.5f; - boolean closeHover = inside(mx, my, closeX, closeY, closeW, closeH); - closeHoverAnim = AnimationUtility.approach(closeHoverAnim, closeHover ? 1f : 0f, dt, 10f); - int muted = mixColor(mutedBase, text, closeHoverAnim * 0.7f); - - renderMatteChrome(header, panelScale, bgTl, bgTr, bgBr, bgBl, stroke, line, text, muted, palette); - - float contentStartY = panelY + (DROPDOWN_HEADER_H + DROPDOWN_SEPARATOR_H) * panelScale; - if (hudContext) { - renderHudModePill(panelScale, text, muted, palette); - contentStartY += hudExtraTop; - } - - contentPadLeft = 6f * panelScale; - contentPadRight = 13f * panelScale; - float contentClipInset = 2.2f * panelScale; - contentX = panelX + contentPadLeft; - contentY = contentStartY + contentClipInset; - contentW = Math.max(1f, panelW - contentPadLeft - contentPadRight); - contentH = Math.max(0f, panelY + panelH - contentY - 7.0f * panelScale); - - maxScroll = Math.max(0f, totalRowsHeight() - contentH); - scroll = AnimationUtility.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, dt, 14f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, 0.1f); - - hits.clear(); - float rowsReveal = fadeProgress; - boolean clipped = ScissorFunction.pushRaw(contentX, contentY, contentW, contentH); - - float y = contentY + smoothedScroll; - for (SettingRow row : rows) { - float sh = row.height() * rowsReveal; - float gap = row.gap() * rowsReveal; - if (sh > 0.5f) { - hits.add(new SettingHit(row.setting(), contentX, y, contentW, sh, row.scale())); - if (y + sh >= contentY - 1.0f * panelScale && y <= contentY + contentH + 1.0f * panelScale) { - float slide = (1f - row.anim()) * 6f * panelScale; - boolean itemClip = ScissorFunction.pushRaw(contentX, y, contentW, sh); - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, row.scale())) { - row.setting().render(contentX, y + slide, contentW, mx, my); - } - ClickGuiRenderer.flushRenderer(); - if (itemClip) { - ScissorFunction.pop(); - } - } - } - y += sh + gap; - } - - ClickGuiRenderer.flushRenderer(); - if (clipped) { - ScissorFunction.pop(); - } - - renderScrollbar(panelScale, palette); - } - renderSettingTooltip(mx, my, panelScale, palette); - renderHudEditorHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); - renderSettingsPanelHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); - } - - /** Пузырь под курсором с {@code .desc}, когда у наведенной строки есть описание. */ - private void renderSettingTooltip(float mx, float my, float panelScale, SettingsGuiPalette tooltipPalette) { - for (SettingHit hit : hits) { - if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { - SettingTooltip.render(hit.setting(), mx, my, hit.y() + hit.h(), panelScale, tooltipPalette); - return; - } - } + renderer.render(menuX, menuY, menuW, menuH, mx, my, scale); } public boolean mouseClicked(float mx, float my, int button, float scale) { - if (!isActive()) return false; - if (openAnim <= 0.01f) return false; - float panelScale = panelScale(scale); - - if (button == 0 && inside(mx, my, closeX, closeY, closeW, closeH)) { - close(); - return true; - } - - if (!inside(mx, my, panelX, panelY, panelW, panelH)) { - return hudContext || editorContext; - } - - if (button == 0 && inside(mx, my, panelX, panelY, panelW, DROPDOWN_HEADER_H * panelScale)) { - dragging = true; - dragOffsetX = panelX - mx; - dragOffsetY = panelY - my; - return true; - } - - if (hudContext && button == 0 && inside(mx, my, pillX, pillY, pillW, pillH)) { - float half = pillW * 0.5f; - hudPreviewMode = (mx < pillX + half) ? HudPreviewMode.ALL_ENABLED : HudPreviewMode.ONLY_CURRENT; - applyHudPreviewMode(); - return true; - } - - if (inside(mx, my, contentX, contentY, contentW, contentH)) { - SettingHit target = null; - for (SettingHit hit : hits) { - if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { - target = hit; - break; - } - } - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, target == null ? settingScale(panelScale) : target.scale())) { - for (Setting setting : settings) { - if (target != null && target.setting() == setting) continue; - setting.mouseClickedOutside(mx, my, button); - } - if (target != null) { - if (!target.setting().isAvailable()) { - return true; - } - target.setting().mouseClicked(mx, my, button); - return true; - } - } - } else { - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, settingScale(panelScale))) { - for (Setting setting : settings) setting.mouseClickedOutside(mx, my, button); - } - } - return true; + return input.mouseClicked(mx, my, button, scale); } public boolean mouseReleased(float mx, float my, int button) { - if (!isActive()) return false; - if (button == 0) dragging = false; - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, lastSettingScale)) { - for (Setting setting : settings) { - if (!setting.isAvailable()) continue; - setting.mouseReleased(mx, my, button); - } - } - return inside(mx, my, panelX, panelY, panelW, panelH); + return input.mouseReleased(mx, my, button); } public boolean mouseScrolled(float mx, float my, double amount) { - if (!isActive()) return false; - if (!limitedHeight) { - return inside(mx, my, panelX, panelY, panelW, panelH); - } - if (!inside(mx, my, panelX, panelY, panelW, panelH)) { - return hudContext || editorContext; - } - for (SettingHit hit : hits) { - if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; - if (!hit.setting().isAvailable()) return true; - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, hit.scale())) { - if (hit.setting().mouseScrolled(mx, my, amount)) return true; - } - break; - } - scroll += (float) (amount * 20f * Math.max(0.25f, lastSettingScale)); - return true; + return input.mouseScrolled(mx, my, amount); } public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!isActive()) return false; - if (isHintToggle(keyCode, modifiers)) { - MainConfig config = MainConfig.get(); - if (hudContext) { - config.setClickGuiHudEditorHintsEnabled(!config.isClickGuiHudEditorHintsEnabled()); - } else { - config.setClickGuiHintsEnabled(!config.isClickGuiHintsEnabled()); - } - return true; - } - - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, lastSettingScale)) { - for (Setting setting : settings) { - if (!setting.isAvailable()) continue; - if (setting.keyPressed(keyCode, scanCode, modifiers)) return true; - } - } - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - close(); - return true; - } - return false; + return input.keyPressed(keyCode, scanCode, modifiers); } public boolean charTyped(char chr, int modifiers) { - if (!isActive()) return false; - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, lastSettingScale)) { - for (Setting setting : settings) { - if (!setting.isAvailable()) continue; - if (setting.charTyped(chr, modifiers)) return true; - } - } - return false; - } - - private void renderHudModePill(float scale, int text, int muted, SettingsGuiPalette palette) { - updateHudModePillBounds(scale); - - float dt = AnimationUtility.deltaTime(); - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - float segmentW = pillW * 0.5f; - boolean enabledHover = inside(mx, my, pillX, pillY, segmentW, pillH); - boolean onlyHover = inside(mx, my, pillX + segmentW, pillY, segmentW, pillH); - int baseA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.32f), openAnim); - int baseB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.40f), openAnim); - int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.32f), openAnim); - int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.20f), openAnim); - - ClickGuiRenderer.drawBlur(pillX, pillY, pillW, pillH, 5.5f * scale, palette.panelBlurTint(), (105f / 255f) * openAnim); - LayoutRender2D.roundedQuad(pillX, pillY, pillW, pillH, 5.5f * scale, baseA, baseB, SettingsGuiPalette.darken(baseB, 0.05f), baseA); - LayoutRender2D.roundedStrokeQuad(pillX, pillY, pillW, pillH, 5.5f * scale, 0.45f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.82f), LayoutRender2D.alpha(strokeA, 0.88f)); - - float innerPad = 1.3f * scale; - float activeW = segmentW - innerPad * 2f; - float activeH = pillH - innerPad * 2f; - float pillT = AnimationUtility.easeInOutCubic(pillActiveAnim); - float activeX = pillX + innerPad + (segmentW * pillT); - int activeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.18f), openAnim); - int activeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.24f), openAnim); - int activeC = LayoutRender2D.alpha(SettingsGuiPalette.darken(activeB, 0.10f), openAnim); - LayoutRender2D.roundedSoftShadow(activeX, pillY + innerPad, activeW, activeH, 4.4f * scale, 4.0f * scale, 0.018f, LayoutRender2D.alpha(palette.menuShadow(), openAnim * 0.55f)); - LayoutRender2D.roundedQuad(activeX, pillY + innerPad, activeW, activeH, 4.4f * scale, activeA, activeB, activeC, activeA); - LayoutRender2D.roundedStrokeQuad(activeX, pillY + innerPad, activeW, activeH, 4.4f * scale, 0.28f * scale, LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * 0.86f), LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * 0.92f), LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * 0.72f), LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * 0.78f)); - - float sepX = pillX + segmentW; - LayoutRender2D.roundedQuad(sepX - 0.42f * scale, pillY + 4.0f * scale, 0.84f * scale, pillH - 8.0f * scale, 0.42f * scale, - LayoutRender2D.alpha(palette.moduleDividerStart(), 0.35f * openAnim), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.50f * openAnim), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.42f * openAnim), - LayoutRender2D.alpha(palette.moduleDividerStart(), 0.28f * openAnim)); - - float ty = pillY + (pillH - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), 6.2f * scale)) * 0.5f; - int enabledColor = mixColor(mixColor(text, muted, pillT), text, enabledHover ? 0.10f : 0f); - int onlyColor = mixColor(mixColor(muted, text, pillT), text, onlyHover ? 0.10f : 0f); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Enabled", pillX + 8.0f * scale, ty, 6.2f * scale, enabledColor, false); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Only this", pillX + segmentW + 8.0f * scale, ty, 6.2f * scale, onlyColor, false); - } - - private void updateHudModePillBounds(float scale) { - pillX = panelX + 7f * scale; - pillY = panelY + DROPDOWN_HEADER_H * scale + 4f * scale; - pillW = panelW - 14f * scale; - pillH = 16f * scale; - } - - private void renderScrollbar(float scale, SettingsGuiPalette palette) { - if (!limitedHeight || maxScroll <= 0f) return; - - float trackW = 2.5f * scale; - float trackX = panelX + panelW - 5f * scale; - float trackY = contentY + 5f * scale; - float viewableH = Math.max(1f, contentH); - float trackH = Math.max(8f * scale, viewableH - 10f * scale); - - float rowsH = Math.max(1f, totalRowsHeight()); - float handleH = Math.min(trackH, Math.max(20f * scale, (viewableH / rowsH) * trackH)); - float ratio = maxScroll <= 0f ? 0f : (-smoothedScroll / maxScroll); - float handleY = trackY + (trackH - handleH) * ratio; - - int trTl = LayoutRender2D.alpha(palette.panelScrollTrackA(), openAnim); - int trTr = LayoutRender2D.alpha(palette.panelScrollTrackB(), openAnim); - int hdTl = LayoutRender2D.alpha(palette.panelScrollHandleA(), openAnim); - int hdTr = LayoutRender2D.alpha(palette.panelScrollHandleB(), openAnim); - - LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, 1.25f * scale, trTl, trTr, trTr, trTl); - LayoutRender2D.roundedQuad(trackX, handleY, trackW, handleH, 1.25f * scale, hdTl, hdTr, hdTr, hdTl); - } - - private void renderHudEditorHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { - Minecraft mc = Minecraft.getInstance(); - MainConfig config = MainConfig.get(); - if (!hudContext || !config.isClickGuiHudEditorHintsEnabled()) return; - - float areaX = 0f; - float areaY = 0f; - float areaW = Math.max(1f, fallbackX + fallbackW); - float areaH = Math.max(1f, fallbackY + fallbackH); - if (mc != null && mc.getWindow() != null) { - int screenW = mc.getWindow().getWidth(); - int screenH = mc.getWindow().getHeight(); - areaW = Math.max(1f, HudScale.virtualWidth(screenW, screenH)); - areaH = Math.max(1f, HudScale.virtualHeight(screenW, screenH)); - } - - ClickGuiHintOverlay.renderBottomLeft( - areaX, - areaY, - areaW, - areaH, - scale, - alpha, - ClickGuiI18n.tr("clickgui.hints.hud.alt", "Alt - drag without widget linking"), - ClickGuiI18n.tr("clickgui.hints.hud.close", "Esc - close editor"), - ClickGuiI18n.tr("clickgui.hints.hud.hide", "Alt+H - hide hints") - ); - } - - private void renderSettingsPanelHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { - Minecraft mc = Minecraft.getInstance(); - MainConfig config = MainConfig.get(); - if (hudContext || !isActive() || !config.isClickGuiHintsEnabled()) return; - - float areaX = 0f; - float areaY = 0f; - float areaW = Math.max(1f, fallbackX + fallbackW); - float areaH = Math.max(1f, fallbackY + fallbackH); - if (mc != null && mc.getWindow() != null) { - int screenW = mc.getWindow().getWidth(); - int screenH = mc.getWindow().getHeight(); - areaW = Math.max(1f, HudScale.virtualWidth(screenW, screenH)); - areaH = Math.max(1f, HudScale.virtualHeight(screenW, screenH)); - } - - ClickGuiHintOverlay.renderBottomLeft( - areaX, - areaY, - areaW, - areaH, - scale, - alpha, - ClickGuiI18n.tr("clickgui.hints.settings_panel.change", "LMB - change focused setting"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.drag", "Drag header - move this panel"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.scroll", "Wheel - scroll settings"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.back", "Esc - back to category"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.hide", "Alt+H - hide hints") - ); - } - - private void renderMatteChrome(String header, - float scale, - int bgTl, - int bgTr, - int bgBr, - int bgBl, - int stroke, - int line, - int text, - int muted, - SettingsGuiPalette palette) { - if (panelW <= 0.5f || panelH <= 0.5f || openAnim <= 0.001f) return; - - float radius = DROPDOWN_RADIUS * scale; - float headerBottom = panelY + DROPDOWN_HEADER_H * scale; - int shadow = LayoutRender2D.alpha(palette.panelShadow(), 0.72f * openAnim); - int blurTint = LayoutRender2D.alpha(palette.panelBlurTint(), openAnim); - int topWashA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.16f), openAnim); - int topWashB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.18f), openAnim); - int bodyA = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTl, palette.menuWindowBgLeft(), 0.18f), openAnim); - int bodyB = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTr, palette.menuWindowBgRight(), 0.18f), openAnim); - int bodyC = LayoutRender2D.alpha(SettingsGuiPalette.darken(bgBr, 0.06f), openAnim); - int accent = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.18f), 0.55f * openAnim); - int accentSoft = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.panelBgRight(), 0.42f), 0.22f * openAnim); - int strokeTop = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.moduleDividerEnd(), 0.22f), openAnim); - int strokeBottom = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.panelBgRight(), 0.22f), 0.82f * openAnim); - - LayoutRender2D.roundedSoftShadow(panelX - 1.4f * scale, panelY + 1.2f * scale, panelW + 2.8f * scale, panelH + 2.4f * scale, radius + 1.6f * scale, 9.5f * scale, 0.028f, shadow); - ClickGuiRenderer.drawBlur(panelX, panelY, panelW, panelH, radius, blurTint, (150f / 255f) * openAnim); - LayoutRender2D.roundedQuad(panelX, panelY, panelW, panelH, radius, bodyA, bodyB, bodyC, bgBl); - LayoutRender2D.roundedStrokeQuad(panelX, panelY, panelW, panelH, radius, 0.58f * scale, strokeTop, LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * 0.72f), strokeBottom, LayoutRender2D.alpha(strokeTop, 0.82f)); - - LayoutRender2D.roundedQuad(panelX + 1.0f * scale, panelY + 1.0f * scale, panelW - 2.0f * scale, DROPDOWN_HEADER_H * scale - 1.0f * scale, Math.max(0f, radius - 1.0f * scale), topWashA, topWashB, LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashB, 0.18f), 0.64f), LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashA, 0.14f), 0.64f)); - LayoutRender2D.rectQuad(panelX + 8.0f * scale, headerBottom, panelW - 16.0f * scale, Math.max(0.45f, 0.55f * scale), LayoutRender2D.alpha(line, 0.55f), line, LayoutRender2D.alpha(line, 0.28f), LayoutRender2D.alpha(line, 0.28f)); - LayoutRender2D.rectQuad(panelX + 8.0f * scale, headerBottom + 0.6f * scale, panelW - 16.0f * scale, Math.max(0.45f, 0.65f * scale), accent, accentSoft, 0x00000000, 0x00000000); - - float markW = 2.4f * scale; - float markH = 9.2f * scale; - LayoutRender2D.roundedQuad(panelX + 8.0f * scale, panelY + 7.2f * scale, markW, markH, markW * 0.5f, accent, accentSoft, accentSoft, accent); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), header, panelX + 14.0f * scale, panelY + 7.0f * scale, 8.0f * scale, text, false); - - renderCloseButton(scale, text, muted, palette); - } - - private void renderCloseButton(float scale, int text, int muted, SettingsGuiPalette palette) { - int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.16f + closeHoverAnim * 0.26f); - int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.18f + closeHoverAnim * 0.28f); - int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelBgRight(), 0.06f), palette.menuCategoryHoverRight(), 0.12f + closeHoverAnim * 0.20f); - int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.26f), openAnim * (0.72f + closeHoverAnim * 0.22f)); - int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.22f), openAnim * (0.78f + closeHoverAnim * 0.20f)); - float radius = closeH * 0.50f; - - ClickGuiRenderer.drawBlur(closeX, closeY, closeW, closeH, radius, palette.panelBlurTint(), (72f / 255f) * openAnim); - LayoutRender2D.roundedSoftShadow(closeX, closeY + 0.4f * scale, closeW, closeH, radius, 4.0f * scale, 0.018f + closeHoverAnim * 0.018f, LayoutRender2D.alpha(palette.menuShadow(), openAnim * (0.38f + closeHoverAnim * 0.36f))); - LayoutRender2D.roundedQuad(closeX, closeY, closeW, closeH, radius, LayoutRender2D.alpha(bgA, openAnim), LayoutRender2D.alpha(bgB, openAnim), LayoutRender2D.alpha(bgC, openAnim), LayoutRender2D.alpha(bgA, openAnim)); - LayoutRender2D.roundedStrokeQuad(closeX, closeY, closeW, closeH, radius, 0.42f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.74f), LayoutRender2D.alpha(strokeA, 0.78f)); - - float icon = 6.8f * scale; - int iconColor = LayoutRender2D.alpha(SettingsGuiPalette.mix(muted, text, 0.42f + closeHoverAnim * 0.48f), openAnim); - Renderer2D.COLOR.svg("x", closeX + (closeW - icon) * 0.5f, closeY + (closeH - icon) * 0.5f, icon, icon, SvgRenderOptions.overrideColor(iconColor)); - if (closeHoverAnim > 0.08f) SystemCursor.set(SystemCursor.CursorType.HAND); - } - - private void buildRows(float menuScale) { - rows.clear(); - for (Setting setting : settings) { - float rowScale = settingScale(setting, menuScale); - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(renderSurface, rowScale)) { - float anim = setting.updateVisibilityAnim(); - boolean targetVisible = setting.isVisibilityTargetVisible(); - if (!targetVisible && anim <= 0.01f) continue; - float h = setting.getHeight() * anim; - float gap = 0f; - rows.add(new SettingRow(setting, anim, h, gap, rowScale)); - } - } - } - - private float totalRowsHeight() { - float out = 0f; - for (SettingRow row : rows) { - out += row.height() + row.gap(); - } - return out; - } - - private void applyHudPreviewMode() { - if (!hudContext) return; - DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); - if (hudPreviewMode == HudPreviewMode.ONLY_CURRENT) { - if (target != null) { - target.setPreviewEnabled(true); - } - DraggableHudElementRegistry.setEditorWidgetId(targetId); - DraggableHudElementRegistry.setForceVisible(true); - } else { - if (target != null) { - target.setPreviewEnabled(false); - } - DraggableHudElementRegistry.setEditorWidgetId(null); - DraggableHudElementRegistry.setForceVisible(true); - } - } - - private void resetHudPreview() { - DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); - if (target != null) { - target.setPreviewEnabled(false); - } - DraggableHudElementRegistry.setEditorWidgetId(null); - DraggableHudElementRegistry.setForceVisible(false); - } - - private float panelScale(float menuScale) { - float base = Math.max(0.25f, menuScale); - if (renderSurface != SettingRenderSurface.MODULES) return base; - return AnimationUtility.clamp(base * 1.5f, 2.55f, 3.0f); - } - - private float settingScale(float menuScale) { - return Math.max(0.25f, menuScale); - } - - private float settingScale(Setting setting, float menuScale) { - return settingScale(menuScale); - } - - private RenderWarpStack.Scope pushLifecycleWarp(float progress) { - if (progress >= 0.999f || dragging) { - return Renderer2D.pushWarp(null); - } - return Renderer2D.pushPerspectiveWarp( - panelX, - panelY, - panelW, - panelH, - 0.0f, - 0.0f, - 0.0f, - 3.6f, - 0.80f, - 0.92f + 0.08f * progress - ); - } - - private enum HudPreviewMode { - ALL_ENABLED, - ONLY_CURRENT - } - - private record SettingRow(Setting setting, float anim, float height, float gap, float scale) { - } - - private record SettingHit(Setting setting, float x, float y, float w, float h, float scale) { + return input.charTyped(chr, modifiers); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardInput.java new file mode 100644 index 0000000..ed898e2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardInput.java @@ -0,0 +1,51 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; + +final class ModuleCardInput { + private final ModuleCardState state; + private final ModuleCardLayout layout; + + ModuleCardInput(ModuleCardState state, ModuleCardLayout layout) { + this.state = state; + this.layout = layout; + } + + void scroll(float mx, float my, float menuX, float menuY, float menuW, float menuH, double amount, float scale) { + float hitX = menuX + 35f * scale; + float hitY = menuY + 13f * scale; + float hitW = menuW - 35f * scale + 7f * scale; + float hitH = menuH - 13f * scale + 15f * scale; + if (!ClickGuiMath.insideRect(mx, my, hitX, hitY, hitW, hitH)) return; + state.scroll += (float) (amount * 20f); + } + + boolean mousePressedScrollbar(float mx, float my, int button) { + if (button != 0) return false; + if (!layout.isScrollbarHovered(mx, my)) return false; + state.draggingScrollbar = true; + state.scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, state.scrollbarX, state.scrollbarThumbY, + state.scrollbarW, state.scrollbarThumbH) + ? my - state.scrollbarThumbY + : state.scrollbarThumbH * 0.5f; + layout.scrollToMouse(my); + return true; + } + + void mouseReleased(int button) { + if (button == 0) { + state.draggingScrollbar = false; + } + } + + ModuleComponent.CardAction click(float mx, float my, int button) { + if (layout.isScrollbarHovered(mx, my)) return null; + for (CardHit hit : state.hits) { + if (!ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; + if (button == 1 && !hit.hasSettings()) continue; + return new ModuleComponent.CardAction(hit.getId(), button == 0, button == 1, + hit.statusHit(mx, my), hit.bindHit(mx, my), hit.toggleable()); + } + return null; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardLayout.java new file mode 100644 index 0000000..726aea2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardLayout.java @@ -0,0 +1,164 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +import java.util.List; + +final class ModuleCardLayout { + private final ModuleCardState state; + + ModuleCardLayout(ModuleCardState state) { + this.state = state; + } + + float descriptionHeight(String description, float w, float scale) { + float maxWidth = w - 25f * scale; + float currentX = 0f; + int lineCount = 1; + for (String word : description.split(" ")) { + float wordW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), word + " ", 10f * scale); + if (currentX + wordW > maxWidth) { + lineCount++; + currentX = 0f; + } + currentX += wordW; + } + float lineH = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), 10f * scale); + float compactStep = Math.max(scale, lineH - 5.75f * scale); + return Math.max(0f, lineH - 4f * scale + (lineCount - 1) * compactStep); + } + + int getComponentHeight(ModuleComponent.CardEntry entry, float w, float scale) { + return Math.round(45f * scale + descriptionHeight(normalizeDescription(entry.description()), w, scale)); + } + + int[] calculateOffsets(List entries, float scale) { + int[] offsets = new int[2]; + int column = 0; + for (int i = entries.size() - 1; i >= 0; i--) { + int componentHeight = Math.round(getComponentHeight(entries.get(i), 182f * scale, scale) + 9f * scale); + offsets[column] += componentHeight; + column = (column + 1) % 2; + } + return offsets; + } + + void updateScrollbarMetrics(float menuX, float menuY, float menuH, int clamped, float scale) { + state.scrollbarVisible = clamped > 0; + state.scrollbarMaxScroll = Math.max(0f, clamped); + if (!state.scrollbarVisible) { + state.draggingScrollbar = false; + state.scrollbarX = 0f; + state.scrollbarY = 0f; + state.scrollbarW = 0f; + state.scrollbarH = 0f; + state.scrollbarThumbY = 0f; + state.scrollbarThumbH = 0f; + return; + } + float offsetY = 14f * scale; + state.scrollbarW = 4f * scale; + state.scrollbarX = menuX + 400f * scale - 35f * scale - state.scrollbarW + 50f * scale; + state.scrollbarY = menuY + offsetY + 22f * scale; + state.scrollbarH = menuH - offsetY * 2f - 14f * scale; + float contentHeight = state.scrollbarMaxScroll; + float viewHeight = menuH - offsetY * 2f; + state.scrollbarThumbH = Math.max(20f * scale, viewHeight * (viewHeight / (contentHeight + viewHeight))); + float scrollRatio = contentHeight <= 0f ? 0f : (-state.smoothedScroll / contentHeight); + state.scrollbarThumbY = state.scrollbarY + (state.scrollbarH - state.scrollbarThumbH) + * AnimationUtility.clamp(scrollRatio, 0f, 1f); + } + + boolean isScrollbarHovered(float mx, float my) { + if (!state.scrollbarVisible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, state.scrollbarX - pad, state.scrollbarY - pad, + state.scrollbarW + pad * 2f, state.scrollbarH + pad * 2f); + } + + void scrollToMouse(float my) { + if (!state.scrollbarVisible || state.scrollbarMaxScroll <= 0f) return; + float span = state.scrollbarH - state.scrollbarThumbH; + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - state.scrollbarDragOffset, state.scrollbarY, state.scrollbarY + span); + float ratio = (thumbTop - state.scrollbarY) / span; + state.scroll = -state.scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); + } + + private static String normalizeDescription(String description) { + if (description == null || description.isBlank()) { + return dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n + .tr("clickgui.settings.module.description_missing", "Module description is missing."); + } + return description; + } + + void drawDescription(String description, float x, float y, float w, float scale, SettingsGuiPalette palette) { + float maxWidth = w - 25f * scale; + float currentX = x + 10f * scale; + float currentY = y + 19f * scale; + String[] words = description.split(" "); + StringBuilder line = new StringBuilder(); + int currentLine = 1; + float textSize = 10f * scale; + TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); + int iconColor = palette.moduleDescriptionIcon(); + int textColor = palette.moduleDescriptionText(); + for (String word : words) { + float wordWidth = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), word + " ", textSize); + if (currentX + wordWidth > x + maxWidth) { + if (currentLine == 1) { + ClickGuiRenderer.drawText(icons, "J", x + 6.5f * scale, currentY + 0.75f * scale, 9f * scale, iconColor, false); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), line.toString(), x + 15f * scale, currentY, textSize, textColor, false); + } else { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), line.toString(), x + 5f * scale, currentY, textSize, textColor, false); + } + line = new StringBuilder(); + currentY += ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), textSize) - 5.75f * scale; + currentX = x + 10f * scale; + currentLine++; + } + line.append(word).append(" "); + currentX += wordWidth; + } + if (!line.isEmpty()) { + if (currentLine == 1) { + ClickGuiRenderer.drawText(icons, "J", x + 6.5f * scale, currentY + 0.75f * scale, 9f * scale, iconColor, false); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), line.toString(), x + 15f * scale, currentY, textSize, textColor, false); + } else { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), line.toString(), x + 7f * scale, currentY, textSize, textColor, false); + } + } + } + + BindRect drawBind(ModuleComponent.CardEntry entry, float x, float y, float w, float descHeight, float scale, SettingsGuiPalette palette) { + String label = entry.bindLabel(); + if (label == null || label.isBlank()) return new BindRect(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f); + float labelW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), label, 12f * scale); + float bindX = x + w - 37.5f * scale - labelW; + float bindY = y + descHeight + 29.75f * scale; + float bindW = labelW + 6f * scale; + float bindH = 10f * scale; + float hitY = y + descHeight + 32.5f * scale; + float hitH = 9f * scale; + LayoutRender2D.roundedQuad(bindX + 0.25f * scale, bindY, bindW, bindH, 3f * scale, + palette.moduleBindBg0(), palette.moduleBindBg1(), palette.moduleBindBg2(), palette.moduleBindBg3()); + LayoutRender2D.roundedStroke(bindX + 0.25f * scale, bindY, bindW, bindH, 3f * scale, 0.5f * scale, palette.moduleBindStroke()); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), label, x + w - 34.5f * scale - labelW, y + descHeight + 34f * scale, 12f * scale, palette.moduleBindText(), false); + return new BindRect(bindX, bindY, bindW, bindH, bindX, hitY, bindW, hitH); + } + + void drawNoSettingsHint(float x, float y, float descHeight, float scale, SettingsGuiPalette palette) { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), + ClickGuiI18n.tr("clickgui.settings.module.no_settings", "No settings"), + x + 10f * scale, y + descHeight + 33.5f * scale, 9f * scale, palette.moduleNoSettingsText(), false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardRenderer.java new file mode 100644 index 0000000..31707b2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardRenderer.java @@ -0,0 +1,184 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.StatusRender; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +import java.util.List; + +final class ModuleCardRenderer { + private static final float PARALLAX_MAX_ANGLE = 4.6f; + private static final float PARALLAX_DEPTH = 4.8f; + private static final float PARALLAX_PERSPECTIVE = 0.92f; + private static final float PARALLAX_SCALE_BOOST = 0.012f; + private static final float PARALLAX_CONTENT_SHIFT = 0.0f; + + private final ModuleCardState state; + private final ModuleCardLayout layout; + + ModuleCardRenderer(ModuleCardState state, ModuleCardLayout layout) { + this.state = state; + this.layout = layout; + } + + void render(float menuX, float menuY, float menuW, float menuH, + List entries, float scale) { + state.hits.clear(); + if (entries.isEmpty()) return; + + int[] offsets = layout.calculateOffsets(entries, scale); + int column = 0; + int maxScroll = 0; + float columnWidth = 142f * scale; + SettingsGuiPalette palette = SettingsGuiPalette.current(); + + float scissorX = menuX + 35f * scale - 75f * scale; + float scissorY = menuY + 35.5f * scale; + float scissorW = menuW - 35f * scale + 150f * scale; + float scissorBottom = menuY + menuH - 3.5f * scale; + float scissorH = Math.max(1f, scissorBottom - scissorY); + boolean clipped = ScissorFunction.pushRaw(scissorX, scissorY, scissorW, scissorH); + + for (int i = entries.size() - 1; i >= 0; i--) { + ModuleComponent.CardEntry entry = entries.get(i); + int componentHeight = Math.round(layout.getComponentHeight(entry, columnWidth + 40f * scale, scale) + 9f * scale); + float x = menuX + 32f * scale + (column * (columnWidth + 48f * scale)); + float y = menuY + 35f * scale + offsets[column] - componentHeight + state.smoothedScroll; + float w = columnWidth + 40f * scale; + float h = componentHeight - 9f * scale; + if (y + h > scissorY + 1.0f * scale && scissorBottom - 1.0f * scale > y) { + CardHit hit = drawCard(entry, x, y, w, h, scale, palette); + state.hits.add(hit); + } + offsets[column] -= componentHeight; + maxScroll = Math.max(maxScroll, offsets[column]); + column = (column + 1) % 2; + } + + int clamped = ClickGuiMath.clamp(Math.round(maxScroll - (menuH * 0.5f + 35f * scale)), 0, maxScroll); + layout.updateScrollbarMetrics(menuX, menuY, menuH, clamped, scale); + if (state.draggingScrollbar) { + layout.scrollToMouse(ClickGuiRenderer.getMouseY()); + } + state.scroll = ClickGuiMath.clamp(state.scroll, -clamped, 0f); + state.smoothedScroll = AnimationUtility.approach(state.smoothedScroll, state.scroll, + state.draggingScrollbar ? 0.55f : 0.2f); + state.smoothedScroll = AnimationUtility.snap(state.smoothedScroll, state.scroll, + state.draggingScrollbar ? 0.01f : 0.05f); + if (clipped) ScissorFunction.pop(); + renderScrollbar(menuX, menuY, menuH, clamped, scale, palette); + } + + private CardHit drawCard(ModuleComponent.CardEntry entry, float x, float y, float w, float h, + float scale, SettingsGuiPalette palette) { + float enabledAnim = state.stateAnimById.compute(entry.getId(), + (k, v) -> AnimationUtility.approach(v == null ? 0f : v, entry.enabled() ? 1f : 0f, 0.16f)); + boolean hovered = ClickGuiMath.insideRect(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY(), x, y, w, h); + float hoverAnim = state.hoverAnimById.compute(entry.getId(), + (k, v) -> AnimationUtility.approach(v == null ? 0f : v, hovered ? 1f : 0f, 0.18f)); + Parallax parallax = computeParallax(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY(), x, y, w, h, hoverAnim); + int brightnessOffset = Math.round(9f * enabledAnim); + int alphaOffset = 150 + Math.round(105f * enabledAnim); + String description = normalizeDescription(entry.description()); + float descHeight = layout.descriptionHeight(description, w, scale); + int cardTopStrong = brightenRgb(palette.moduleCardTopStrong(), brightnessOffset); + int cardBottomStrong = brightenRgb(palette.moduleCardBottomStrong(), brightnessOffset); + BindRect bindRect; + float statusX = x + w - 24f * scale; + float statusY = y + descHeight + 31f * scale; + + ClickGuiRenderer.drawRoundedRectShadow(x, y + 1.5f * scale, w, h, 5.4f * scale, 9.0f * scale, 1.0f * scale, + LayoutRender2D.alpha(0xFF000000, 0.22f + 0.10f * hoverAnim)); + try (var ignored = pushParallax(parallax, x, y, w, h)) { + ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 200f / 255f); + LayoutRender2D.roundedQuad(x, y, w, h, 5f * scale, + palette.moduleCardTop(), cardTopStrong, palette.moduleCardBottom(), cardBottomStrong); + LayoutRender2D.rectQuad(x, y + descHeight + 25f * scale, w, 0.5f * scale, + palette.moduleDividerStart(), palette.moduleDividerEnd(), + palette.moduleDividerEnd(), palette.moduleDividerStart()); + Renderer2D.COLOR.radialGlowMasked(x, y, w, h, 5f * scale, 0f, 48f * scale, + x + w * 0.18f, y + h * 0.08f, + LayoutRender2D.alpha(palette.moduleCardTopStrong(), 0.12f + 0.10f * hoverAnim)); + + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), "• " + entry.title(), + x + 8.8f * scale, y + 5.4f * scale, 13f * scale, + SettingsGuiPalette.withAlpha(palette.moduleTitleText(), alphaOffset), false); + layout.drawDescription(description, x, y, w, scale, palette); + if (!entry.hasSettings()) { + layout.drawNoSettingsHint(x, y, descHeight, scale, palette); + } + bindRect = layout.drawBind(entry, x, y, w, descHeight, scale, palette); + if (entry.toggleable()) { + StatusRender status = state.statusRenderById.computeIfAbsent(entry.getId(), k -> new StatusRender()); + status.render(statusX, statusY, enabledAnim, scale); + } + } + + return new CardHit(entry.getId(), x, y, w, h, + bindRect.hitX(), bindRect.hitY(), bindRect.hitW(), bindRect.hitH(), + statusX, statusY, 16f * scale, 8f * scale, + entry.hasSettings(), entry.toggleable()); + } + + private void renderScrollbar(float menuX, float menuY, float menuH, int clamped, float scale, SettingsGuiPalette palette) { + layout.updateScrollbarMetrics(menuX, menuY, menuH, clamped, scale); + if (!state.scrollbarVisible) return; + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + boolean hover = layout.isScrollbarHovered(mx, my); + if (hover || state.draggingScrollbar) { + SystemCursor.set(SystemCursor.CursorType.SCROLL); + } + LayoutRender2D.roundedQuad(state.scrollbarX, state.scrollbarY, state.scrollbarW, state.scrollbarH, 2f * scale, + palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), + palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); + LayoutRender2D.roundedQuad(state.scrollbarX, state.scrollbarThumbY, state.scrollbarW, state.scrollbarThumbH, 2f * scale, + palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), + palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); + } + + private static String normalizeDescription(String description) { + if (description == null || description.isBlank()) { + return ClickGuiI18n.tr("clickgui.settings.module.description_missing", "Module description is missing."); + } + return description; + } + + private static int brightenRgb(int color, int delta) { + int a = (color >>> 24) & 0xFF; + int r = Math.min(255, ((color >>> 16) & 0xFF) + delta); + int g = Math.min(255, ((color >>> 8) & 0xFF) + delta); + int b = Math.min(255, (color & 0xFF) + delta); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static Parallax computeParallax(float mx, float my, float x, float y, float w, float h, float hover) { + float ease = smooth(Math.max(0f, Math.min(1f, hover))); + if (ease <= 0.001f || w <= 0f || h <= 0f) return Parallax.NONE; + float nx = clamp((mx - x) / w * 2f - 1f, -1f, 1f); + float ny = clamp((my - y) / h * 2f - 1f, -1f, 1f); + return new Parallax(-nx * PARALLAX_MAX_ANGLE * ease, ny * PARALLAX_MAX_ANGLE * ease, + 1f + PARALLAX_SCALE_BOOST * ease, nx * PARALLAX_CONTENT_SHIFT * ease, ny * PARALLAX_CONTENT_SHIFT * ease, true); + } + + private static RenderWarpStack.Scope pushParallax(Parallax p, float x, float y, float w, float h) { + if (p == null || !p.active()) return Renderer2D.pushWarp(null); + return Renderer2D.pushPerspectiveWarp(x, y, w, h, p.yawDeg(), p.pitchDeg(), 0f, PARALLAX_DEPTH, PARALLAX_PERSPECTIVE, p.scale()); + } + + private static float smooth(float t) { + return t * t * (3f - 2f * t); + } + + private static float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardState.java new file mode 100644 index 0000000..e990277 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/card/ModuleCardState.java @@ -0,0 +1,72 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.StatusRender; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +final class ModuleCardState { + final List hits = new ArrayList<>(); + final Map stateAnimById = new HashMap<>(); + final Map hoverAnimById = new HashMap<>(); + final Map statusRenderById = new HashMap<>(); + + float scroll; + float smoothedScroll; + boolean scrollbarVisible; + boolean draggingScrollbar; + float scrollbarX; + float scrollbarY; + float scrollbarW; + float scrollbarH; + float scrollbarThumbY; + float scrollbarThumbH; + float scrollbarMaxScroll; + float scrollbarDragOffset; + + void resetScroll() { + scroll = 0f; + smoothedScroll = 0f; + draggingScrollbar = false; + } +} + +record CardHit(String id, + float x, + float y, + float w, + float h, + float bindX, + float bindY, + float bindW, + float bindH, + float statusX, + float statusY, + float statusW, + float statusH, + boolean hasSettings, + boolean toggleable) { + String getId() { + return id; + } + + boolean statusHit(float mx, float my) { + if (!toggleable) return false; + return mx >= statusX && mx <= statusX + statusW + && my >= statusY && my <= statusY + statusH; + } + + boolean bindHit(float mx, float my) { + return mx >= bindX && mx <= bindX + bindW + && my >= bindY && my <= bindY + bindH; + } +} + +record Parallax(float yawDeg, float pitchDeg, float scale, float shiftX, float shiftY, boolean active) { + static final Parallax NONE = new Parallax(0f, 0f, 1f, 0f, 0f, false); +} + +record BindRect(float x, float y, float w, float h, float hitX, float hitY, float hitW, float hitH) { +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java index 00f9ab5..5bc58f8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java @@ -1,30 +1,33 @@ package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.config.i18n.MainConfig; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; import dev.loki.lovisual.features.gui.clickgui.util.tooltip.SettingTooltip; import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; import dev.loki.lovisual.render.helpers.util.SystemCursor; +import net.minecraft.client.Minecraft; -/** - * Отрисовка панели настроек: матовое хром-окно, кнопка-крестик, - * HUD-пилюля режимов, скроллбар, тултип строки. Чистый draw — состояние - * (геометрия/анимации) читает из {@link SettingsPanelComponent}. - */ final class SettingsPanelChrome { - private static final float DROPDOWN_RADIUS = 10.0f; + private final SettingsPanelState state; + private final SettingsPanelLayout layout; - private SettingsPanelChrome() {} + SettingsPanelChrome(SettingsPanelState state, SettingsPanelLayout layout) { + this.state = state; + this.layout = layout; + } - static boolean inside(float mx, float my, float x, float y, float w, float h) { + boolean inside(float mx, float my, float x, float y, float w, float h) { return mx >= x && mx <= x + w && my >= y && my <= y + h; } - static int mixColor(int from, int to, float t) { + int mixColor(int from, int to, float t) { t = Math.max(0f, Math.min(1f, t)); int a = (int) (((from >>> 24) & 0xFF) * (1 - t) + ((to >>> 24) & 0xFF) * t); int r = (int) (((from >>> 16) & 0xFF) * (1 - t) + ((to >>> 16) & 0xFF) * t); @@ -33,141 +36,152 @@ final class SettingsPanelChrome { return (a << 24) | (r << 16) | (g << 8) | b; } - static void renderSettingTooltip(SettingsPanelComponent p, float mx, float my, float panelScale) { - for (SettingsPanelRows.SettingHit hit : p.rows.hits()) { + void renderMatteChrome(String header, float scale, int bgTl, int bgTr, int bgBr, int bgBl, + int stroke, int line, int text, int muted, SettingsGuiPalette palette) { + if (state.panelW <= 0.5f || state.panelH <= 0.5f || state.openAnim <= 0.001f) return; + float radius = layout.dropdownRadius * scale; + float headerBottom = state.panelY + layout.dropdownHeaderH * scale; + int shadow = LayoutRender2D.alpha(palette.panelShadow(), 0.72f * state.openAnim); + int blurTint = LayoutRender2D.alpha(palette.panelBlurTint(), state.openAnim); + int topWashA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.16f), state.openAnim); + int topWashB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.18f), state.openAnim); + int bodyA = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTl, palette.menuWindowBgLeft(), 0.18f), state.openAnim); + int bodyB = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTr, palette.menuWindowBgRight(), 0.18f), state.openAnim); + int bodyC = LayoutRender2D.alpha(SettingsGuiPalette.darken(bgBr, 0.06f), state.openAnim); + int accent = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.18f), 0.55f * state.openAnim); + int accentSoft = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.panelBgRight(), 0.42f), 0.22f * state.openAnim); + int strokeTop = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.moduleDividerEnd(), 0.22f), state.openAnim); + int strokeBottom = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.panelBgRight(), 0.22f), 0.82f * state.openAnim); + LayoutRender2D.roundedSoftShadow(state.panelX - 1.4f * scale, state.panelY + 1.2f * scale, state.panelW + 2.8f * scale, state.panelH + 2.4f * scale, radius + 1.6f * scale, 9.5f * scale, 0.028f, shadow); + ClickGuiRenderer.drawBlur(state.panelX, state.panelY, state.panelW, state.panelH, radius, blurTint, (150f / 255f) * state.openAnim); + LayoutRender2D.roundedQuad(state.panelX, state.panelY, state.panelW, state.panelH, radius, bodyA, bodyB, bodyC, bgBl); + LayoutRender2D.roundedStrokeQuad(state.panelX, state.panelY, state.panelW, state.panelH, radius, 0.58f * scale, strokeTop, LayoutRender2D.alpha(palette.moduleDividerEnd(), state.openAnim * 0.72f), strokeBottom, LayoutRender2D.alpha(strokeTop, 0.82f)); + LayoutRender2D.roundedQuad(state.panelX + 1.0f * scale, state.panelY + 1.0f * scale, state.panelW - 2.0f * scale, layout.dropdownHeaderH * scale - 1.0f * scale, Math.max(0f, radius - 1.0f * scale), topWashA, topWashB, LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashB, 0.18f), 0.64f), LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashA, 0.14f), 0.64f)); + LayoutRender2D.rectQuad(state.panelX + 8.0f * scale, headerBottom, state.panelW - 16.0f * scale, Math.max(0.45f, 0.55f * scale), LayoutRender2D.alpha(line, 0.55f), line, LayoutRender2D.alpha(line, 0.28f), LayoutRender2D.alpha(line, 0.28f)); + LayoutRender2D.rectQuad(state.panelX + 8.0f * scale, headerBottom + 0.6f * scale, state.panelW - 16.0f * scale, Math.max(0.45f, 0.65f * scale), accent, accentSoft, 0x00000000, 0x00000000); + float markW = 2.4f * scale; + float markH = 9.2f * scale; + LayoutRender2D.roundedQuad(state.panelX + 8.0f * scale, state.panelY + 7.2f * scale, markW, markH, markW * 0.5f, accent, accentSoft, accentSoft, accent); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), header, state.panelX + 14.0f * scale, state.panelY + 7.0f * scale, 8.0f * scale, text, false); + renderCloseButton(scale, text, muted, palette); + } + + void renderCloseButton(float scale, int text, int muted, SettingsGuiPalette palette) { + int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.16f + state.closeHoverAnim * 0.26f); + int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.18f + state.closeHoverAnim * 0.28f); + int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelBgRight(), 0.06f), palette.menuCategoryHoverRight(), 0.12f + state.closeHoverAnim * 0.20f); + int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.26f), state.openAnim * (0.72f + state.closeHoverAnim * 0.22f)); + int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.22f), state.openAnim * (0.78f + state.closeHoverAnim * 0.20f)); + float radius = state.closeH * 0.50f; + ClickGuiRenderer.drawBlur(state.closeX, state.closeY, state.closeW, state.closeH, radius, palette.panelBlurTint(), (72f / 255f) * state.openAnim); + LayoutRender2D.roundedSoftShadow(state.closeX, state.closeY + 0.4f * scale, state.closeW, state.closeH, radius, 4.0f * scale, 0.018f + state.closeHoverAnim * 0.018f, LayoutRender2D.alpha(palette.menuShadow(), state.openAnim * (0.38f + state.closeHoverAnim * 0.36f))); + LayoutRender2D.roundedQuad(state.closeX, state.closeY, state.closeW, state.closeH, radius, LayoutRender2D.alpha(bgA, state.openAnim), LayoutRender2D.alpha(bgB, state.openAnim), LayoutRender2D.alpha(bgC, state.openAnim), LayoutRender2D.alpha(bgA, state.openAnim)); + LayoutRender2D.roundedStrokeQuad(state.closeX, state.closeY, state.closeW, state.closeH, radius, 0.42f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.74f), LayoutRender2D.alpha(strokeA, 0.78f)); + float icon = 6.8f * scale; + int iconColor = LayoutRender2D.alpha(SettingsGuiPalette.mix(muted, text, 0.42f + state.closeHoverAnim * 0.48f), state.openAnim); + Renderer2D.COLOR.svg("x", state.closeX + (state.closeW - icon) * 0.5f, state.closeY + (state.closeH - icon) * 0.5f, icon, icon, SvgRenderOptions.overrideColor(iconColor)); + if (state.closeHoverAnim > 0.08f) SystemCursor.set(SystemCursor.CursorType.HAND); + } + + void renderHudModePill(float scale, int text, int muted, SettingsGuiPalette palette) { + layout.updateHudModePillBounds(scale); + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + float segmentW = state.pillW * 0.5f; + boolean enabledHover = inside(mx, my, state.pillX, state.pillY, segmentW, state.pillH); + boolean onlyHover = inside(mx, my, state.pillX + segmentW, state.pillY, segmentW, state.pillH); + int baseA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.32f), state.openAnim); + int baseB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.40f), state.openAnim); + int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.32f), state.openAnim); + int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.20f), state.openAnim); + ClickGuiRenderer.drawBlur(state.pillX, state.pillY, state.pillW, state.pillH, 5.5f * scale, palette.panelBlurTint(), (105f / 255f) * state.openAnim); + LayoutRender2D.roundedQuad(state.pillX, state.pillY, state.pillW, state.pillH, 5.5f * scale, baseA, baseB, SettingsGuiPalette.darken(baseB, 0.05f), baseA); + LayoutRender2D.roundedStrokeQuad(state.pillX, state.pillY, state.pillW, state.pillH, 5.5f * scale, 0.45f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.82f), LayoutRender2D.alpha(strokeA, 0.88f)); + float innerPad = 1.3f * scale; + float activeW = segmentW - innerPad * 2f; + float activeH = state.pillH - innerPad * 2f; + float pillT = AnimationUtility.easeInOutCubic(state.pillActiveAnim); + float activeX = state.pillX + innerPad + (segmentW * pillT); + int activeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.18f), state.openAnim); + int activeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.24f), state.openAnim); + int activeC = LayoutRender2D.alpha(SettingsGuiPalette.darken(activeB, 0.10f), state.openAnim); + LayoutRender2D.roundedSoftShadow(activeX, state.pillY + innerPad, activeW, activeH, 4.4f * scale, 4.0f * scale, 0.018f, LayoutRender2D.alpha(palette.menuShadow(), state.openAnim * 0.55f)); + LayoutRender2D.roundedQuad(activeX, state.pillY + innerPad, activeW, activeH, 4.4f * scale, activeA, activeB, activeC, activeA); + LayoutRender2D.roundedStrokeQuad(activeX, state.pillY + innerPad, activeW, activeH, 4.4f * scale, 0.28f * scale, LayoutRender2D.alpha(palette.moduleDividerStart(), state.openAnim * 0.86f), LayoutRender2D.alpha(palette.moduleDividerEnd(), state.openAnim * 0.92f), LayoutRender2D.alpha(palette.moduleDividerEnd(), state.openAnim * 0.72f), LayoutRender2D.alpha(palette.moduleDividerStart(), state.openAnim * 0.78f)); + float sepX = state.pillX + segmentW; + LayoutRender2D.roundedQuad(sepX - 0.42f * scale, state.pillY + 4.0f * scale, 0.84f * scale, state.pillH - 8.0f * scale, 0.42f * scale, LayoutRender2D.alpha(palette.moduleDividerStart(), 0.35f * state.openAnim), LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.50f * state.openAnim), LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.42f * state.openAnim), LayoutRender2D.alpha(palette.moduleDividerStart(), 0.28f * state.openAnim)); + float ty = state.pillY + (state.pillH - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), 6.2f * scale)) * 0.5f; + int enabledColor = mixColor(mixColor(text, muted, pillT), text, enabledHover ? 0.10f : 0f); + int onlyColor = mixColor(mixColor(muted, text, pillT), text, onlyHover ? 0.10f : 0f); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Enabled", state.pillX + 8.0f * scale, ty, 6.2f * scale, enabledColor, false); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Only this", state.pillX + segmentW + 8.0f * scale, ty, 6.2f * scale, onlyColor, false); + } + + void renderScrollbar(float scale, SettingsGuiPalette palette) { + if (!state.limitedHeight || state.maxScroll <= 0f) return; + float trackW = 2.5f * scale; + float trackX = state.panelX + state.panelW - 5f * scale; + float trackY = state.contentY + 5f * scale; + float viewableH = Math.max(1f, state.contentH); + float trackH = Math.max(8f * scale, viewableH - 10f * scale); + float rowsH = Math.max(1f, layout.totalRowsHeight()); + float handleH = Math.min(trackH, Math.max(20f * scale, (viewableH / rowsH) * trackH)); + float ratio = state.maxScroll <= 0f ? 0f : (-state.smoothedScroll / state.maxScroll); + float handleY = trackY + (trackH - handleH) * ratio; + int trTl = LayoutRender2D.alpha(palette.panelScrollTrackA(), state.openAnim); + int trTr = LayoutRender2D.alpha(palette.panelScrollTrackB(), state.openAnim); + int hdTl = LayoutRender2D.alpha(palette.panelScrollHandleA(), state.openAnim); + int hdTr = LayoutRender2D.alpha(palette.panelScrollHandleB(), state.openAnim); + LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, 1.25f * scale, trTl, trTr, trTr, trTl); + LayoutRender2D.roundedQuad(trackX, handleY, trackW, handleH, 1.25f * scale, hdTl, hdTr, hdTr, hdTl); + } + + void renderSettingTooltip(float mx, float my, float panelScale, SettingsGuiPalette palette) { + for (SettingHit hit : state.hits) { if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { - SettingTooltip.render(hit.setting(), mx, my, hit.y() + hit.h(), panelScale, SettingsGuiPalette.current()); + SettingTooltip.render(hit.setting(), mx, my, hit.y() + hit.h(), panelScale, palette); return; } } } - static void renderMatteChrome(SettingsPanelComponent p, String header, float scale, int bgTl, int bgTr, - int bgBr, int bgBl, int stroke, int line, int text, int muted) { - if (p.panelW <= 0.5f || p.panelH <= 0.5f || p.openAnim <= 0.001f) return; - SettingsGuiPalette palette = SettingsGuiPalette.current(); - - float radius = DROPDOWN_RADIUS * scale; - float headerBottom = p.panelY + SettingsPanelComponent.DROPDOWN_HEADER_H * scale; - int shadow = LayoutRender2D.alpha(palette.panelShadow(), 0.72f * p.openAnim); - int blurTint = LayoutRender2D.alpha(palette.panelBlurTint(), p.openAnim); - int topWashA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.16f), p.openAnim); - int topWashB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.18f), p.openAnim); - int bodyA = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTl, palette.menuWindowBgLeft(), 0.18f), p.openAnim); - int bodyB = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTr, palette.menuWindowBgRight(), 0.18f), p.openAnim); - int bodyC = LayoutRender2D.alpha(SettingsGuiPalette.darken(bgBr, 0.06f), p.openAnim); - int accent = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.18f), 0.55f * p.openAnim); - int accentSoft = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.panelBgRight(), 0.42f), 0.22f * p.openAnim); - int strokeTop = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.moduleDividerEnd(), 0.22f), p.openAnim); - int strokeBottom = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.panelBgRight(), 0.22f), 0.82f * p.openAnim); - - LayoutRender2D.roundedSoftShadow(p.panelX - 1.4f * scale, p.panelY + 1.2f * scale, p.panelW + 2.8f * scale, p.panelH + 2.4f * scale, radius + 1.6f * scale, 9.5f * scale, 0.028f, shadow); - ClickGuiRenderer.drawBlur(p.panelX, p.panelY, p.panelW, p.panelH, radius, blurTint, (150f / 255f) * p.openAnim); - LayoutRender2D.roundedQuad(p.panelX, p.panelY, p.panelW, p.panelH, radius, bodyA, bodyB, bodyC, bgBl); - LayoutRender2D.roundedStrokeQuad(p.panelX, p.panelY, p.panelW, p.panelH, radius, 0.58f * scale, strokeTop, LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.72f), strokeBottom, LayoutRender2D.alpha(strokeTop, 0.82f)); - - LayoutRender2D.roundedQuad(p.panelX + 1.0f * scale, p.panelY + 1.0f * scale, p.panelW - 2.0f * scale, SettingsPanelComponent.DROPDOWN_HEADER_H * scale - 1.0f * scale, Math.max(0f, radius - 1.0f * scale), topWashA, topWashB, LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashB, 0.18f), 0.64f), LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashA, 0.14f), 0.64f)); - LayoutRender2D.rectQuad(p.panelX + 8.0f * scale, headerBottom, p.panelW - 16.0f * scale, Math.max(0.45f, 0.55f * scale), LayoutRender2D.alpha(line, 0.55f), line, LayoutRender2D.alpha(line, 0.28f), LayoutRender2D.alpha(line, 0.28f)); - LayoutRender2D.rectQuad(p.panelX + 8.0f * scale, headerBottom + 0.6f * scale, p.panelW - 16.0f * scale, Math.max(0.45f, 0.65f * scale), accent, accentSoft, 0x00000000, 0x00000000); - - float markW = 2.4f * scale; - float markH = 9.2f * scale; - LayoutRender2D.roundedQuad(p.panelX + 8.0f * scale, p.panelY + 7.2f * scale, markW, markH, markW * 0.5f, accent, accentSoft, accentSoft, accent); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), header, p.panelX + 14.0f * scale, p.panelY + 7.0f * scale, 8.0f * scale, text, false); - - renderCloseButton(p, scale, text, muted, palette); + void renderHudEditorHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { + Minecraft mc = Minecraft.getInstance(); + MainConfig config = MainConfig.get(); + if (!state.hudContext || !config.isClickGuiHudEditorHintsEnabled()) return; + float areaX = 0f; float areaY = 0f; + float areaW = Math.max(1f, fallbackX + fallbackW); + float areaH = Math.max(1f, fallbackY + fallbackH); + if (mc != null && mc.getWindow() != null) { + int screenW = mc.getWindow().getWidth(); + int screenH = mc.getWindow().getHeight(); + areaW = Math.max(1f, HudScale.virtualWidth(screenW, screenH)); + areaH = Math.max(1f, HudScale.virtualHeight(screenW, screenH)); + } + ClickGuiHintOverlay.renderBottomLeft(areaX, areaY, areaW, areaH, scale, alpha, + ClickGuiI18n.tr("clickgui.hints.hud.alt", "Alt - drag without widget linking"), + ClickGuiI18n.tr("clickgui.hints.hud.close", "Esc - close editor"), + ClickGuiI18n.tr("clickgui.hints.hud.hide", "Alt+H - hide hints")); } - private static void renderCloseButton(SettingsPanelComponent p, float scale, int text, int muted, SettingsGuiPalette palette) { - int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.16f + p.closeHoverAnim * 0.26f); - int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.18f + p.closeHoverAnim * 0.28f); - int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelBgRight(), 0.06f), palette.menuCategoryHoverRight(), 0.12f + p.closeHoverAnim * 0.20f); - int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.26f), p.openAnim * (0.72f + p.closeHoverAnim * 0.22f)); - int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.22f), p.openAnim * (0.78f + p.closeHoverAnim * 0.20f)); - float radius = p.closeH * 0.50f; - - ClickGuiRenderer.drawBlur(p.closeX, p.closeY, p.closeW, p.closeH, radius, palette.panelBlurTint(), (72f / 255f) * p.openAnim); - LayoutRender2D.roundedSoftShadow(p.closeX, p.closeY + 0.4f * scale, p.closeW, p.closeH, radius, 4.0f * scale, 0.018f + p.closeHoverAnim * 0.018f, LayoutRender2D.alpha(palette.menuShadow(), p.openAnim * (0.38f + p.closeHoverAnim * 0.36f))); - LayoutRender2D.roundedQuad(p.closeX, p.closeY, p.closeW, p.closeH, radius, LayoutRender2D.alpha(bgA, p.openAnim), LayoutRender2D.alpha(bgB, p.openAnim), LayoutRender2D.alpha(bgC, p.openAnim), LayoutRender2D.alpha(bgA, p.openAnim)); - LayoutRender2D.roundedStrokeQuad(p.closeX, p.closeY, p.closeW, p.closeH, radius, 0.42f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.74f), LayoutRender2D.alpha(strokeA, 0.78f)); - - float icon = 6.8f * scale; - int iconColor = LayoutRender2D.alpha(SettingsGuiPalette.mix(muted, text, 0.42f + p.closeHoverAnim * 0.48f), p.openAnim); - Renderer2D.COLOR.svg("x", p.closeX + (p.closeW - icon) * 0.5f, p.closeY + (p.closeH - icon) * 0.5f, icon, icon, SvgRenderOptions.overrideColor(iconColor)); - if (p.closeHoverAnim > 0.08f) SystemCursor.set(SystemCursor.CursorType.HAND); - } - - static void updateHudModePillBounds(SettingsPanelComponent p, float scale) { - p.pillX = p.panelX + 7f * scale; - p.pillY = p.panelY + SettingsPanelComponent.DROPDOWN_HEADER_H * scale + 4f * scale; - p.pillW = p.panelW - 14f * scale; - p.pillH = 16f * scale; - } - - static void renderHudModePill(SettingsPanelComponent p, float scale, int text, int muted, SettingsGuiPalette palette) { - updateHudModePillBounds(p, scale); - - float pillActiveAnim = p.hudPreview.activeAnim(); - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - float segmentW = p.pillW * 0.5f; - boolean enabledHover = inside(mx, my, p.pillX, p.pillY, segmentW, p.pillH); - boolean onlyHover = inside(mx, my, p.pillX + segmentW, p.pillY, segmentW, p.pillH); - int baseA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.32f), p.openAnim); - int baseB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.40f), p.openAnim); - int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.32f), p.openAnim); - int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.20f), p.openAnim); - - ClickGuiRenderer.drawBlur(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, palette.panelBlurTint(), (105f / 255f) * p.openAnim); - LayoutRender2D.roundedQuad(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, baseA, baseB, SettingsGuiPalette.darken(baseB, 0.05f), baseA); - LayoutRender2D.roundedStrokeQuad(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, 0.45f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.82f), LayoutRender2D.alpha(strokeA, 0.88f)); - - float innerPad = 1.3f * scale; - float activeW = segmentW - innerPad * 2f; - float activeH = p.pillH - innerPad * 2f; - float pillT = AnimationUtility.easeInOutCubic(pillActiveAnim); - float activeX = p.pillX + innerPad + (segmentW * pillT); - int activeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.18f), p.openAnim); - int activeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.24f), p.openAnim); - int activeC = LayoutRender2D.alpha(SettingsGuiPalette.darken(activeB, 0.10f), p.openAnim); - LayoutRender2D.roundedSoftShadow(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, 4.0f * scale, 0.018f, LayoutRender2D.alpha(palette.menuShadow(), p.openAnim * 0.55f)); - LayoutRender2D.roundedQuad(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, activeA, activeB, activeC, activeA); - LayoutRender2D.roundedStrokeQuad(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, 0.28f * scale, LayoutRender2D.alpha(palette.moduleDividerStart(), p.openAnim * 0.86f), LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.92f), LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.72f), LayoutRender2D.alpha(palette.moduleDividerStart(), p.openAnim * 0.78f)); - - float sepX = p.pillX + segmentW; - LayoutRender2D.roundedQuad(sepX - 0.42f * scale, p.pillY + 4.0f * scale, 0.84f * scale, p.pillH - 8.0f * scale, 0.42f * scale, - LayoutRender2D.alpha(palette.moduleDividerStart(), 0.35f * p.openAnim), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.50f * p.openAnim), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.42f * p.openAnim), - LayoutRender2D.alpha(palette.moduleDividerStart(), 0.28f * p.openAnim)); - - float ty = p.pillY + (p.pillH - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), 6.2f * scale)) * 0.5f; - int enabledColor = mixColor(mixColor(text, muted, pillT), text, enabledHover ? 0.10f : 0f); - int onlyColor = mixColor(mixColor(muted, text, pillT), text, onlyHover ? 0.10f : 0f); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Enabled", p.pillX + 8.0f * scale, ty, 6.2f * scale, enabledColor, false); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Only this", p.pillX + segmentW + 8.0f * scale, ty, 6.2f * scale, onlyColor, false); - } - - static void renderScrollbar(SettingsPanelComponent p, float scale, SettingsGuiPalette palette) { - if (!p.limitedHeight || p.maxScroll <= 0f) return; - - float trackW = 2.5f * scale; - float trackX = p.panelX + p.panelW - 5f * scale; - float trackY = p.contentY + 5f * scale; - float viewableH = Math.max(1f, p.contentH); - float trackH = Math.max(8f * scale, viewableH - 10f * scale); - - float rowsH = Math.max(1f, p.rows.totalHeight()); - float handleH = Math.min(trackH, Math.max(20f * scale, (viewableH / rowsH) * trackH)); - float ratio = p.maxScroll <= 0f ? 0f : (-p.smoothedScroll / p.maxScroll); - float handleY = trackY + (trackH - handleH) * ratio; - - int trTl = LayoutRender2D.alpha(palette.panelScrollTrackA(), p.openAnim); - int trTr = LayoutRender2D.alpha(palette.panelScrollTrackB(), p.openAnim); - int hdTl = LayoutRender2D.alpha(palette.panelScrollHandleA(), p.openAnim); - int hdTr = LayoutRender2D.alpha(palette.panelScrollHandleB(), p.openAnim); - - LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, 1.25f * scale, trTl, trTr, trTr, trTl); - LayoutRender2D.roundedQuad(trackX, handleY, trackW, handleH, 1.25f * scale, hdTl, hdTr, hdTr, hdTl); + void renderSettingsPanelHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { + Minecraft mc = Minecraft.getInstance(); + MainConfig config = MainConfig.get(); + if (state.hudContext || !state.isActive() || !config.isClickGuiHintsEnabled()) return; + float areaX = 0f; float areaY = 0f; + float areaW = Math.max(1f, fallbackX + fallbackW); + float areaH = Math.max(1f, fallbackY + fallbackH); + if (mc != null && mc.getWindow() != null) { + int screenW = mc.getWindow().getWidth(); + int screenH = mc.getWindow().getHeight(); + areaW = Math.max(1f, HudScale.virtualWidth(screenW, screenH)); + areaH = Math.max(1f, HudScale.virtualHeight(screenW, screenH)); + } + ClickGuiHintOverlay.renderBottomLeft(areaX, areaY, areaW, areaH, scale, alpha, + ClickGuiI18n.tr("clickgui.hints.settings_panel.change", "LMB - change focused setting"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.drag", "Drag header - move this panel"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.scroll", "Wheel - scroll settings"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.back", "Esc - back to category"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.hide", "Alt+H - hide hints")); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java deleted file mode 100644 index c895c9e..0000000 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; - -import net.minecraft.client.Minecraft; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; -import dev.loki.lovisual.render.engine.math.HudScale; - -/** - * Подсказки внизу экрана для панели настроек: вариант HUD-редактора и - * вариант обычной панели. Общая логика расчёта области — одна. - */ -final class SettingsPanelHints { - - private SettingsPanelHints() {} - - static void renderHudEditor(float fallbackX, float fallbackY, float fallbackW, float fallbackH, - float scale, float alpha) { - ClickGuiHintOverlay.renderBottomLeft( - 0f, 0f, areaWidth(fallbackX, fallbackW), areaHeight(fallbackY, fallbackH), - scale, - alpha, - ClickGuiI18n.tr("clickgui.hints.hud.alt", "Alt - drag without widget linking"), - ClickGuiI18n.tr("clickgui.hints.hud.close", "Esc - close editor"), - ClickGuiI18n.tr("clickgui.hints.hud.hide", "Alt+H - hide hints") - ); - } - - static void renderSettingsPanel(float fallbackX, float fallbackY, float fallbackW, float fallbackH, - float scale, float alpha) { - ClickGuiHintOverlay.renderBottomLeft( - 0f, 0f, areaWidth(fallbackX, fallbackW), areaHeight(fallbackY, fallbackH), - scale, - alpha, - ClickGuiI18n.tr("clickgui.hints.settings_panel.change", "LMB - change focused setting"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.drag", "Drag header - move this panel"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.scroll", "Wheel - scroll settings"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.back", "Esc - back to category"), - ClickGuiI18n.tr("clickgui.hints.settings_panel.hide", "Alt+H - hide hints") - ); - } - - private static float areaWidth(float fallbackX, float fallbackW) { - float areaW = Math.max(1f, fallbackX + fallbackW); - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.getWindow() != null) { - areaW = Math.max(1f, HudScale.virtualWidth(mc.getWindow().getWidth(), mc.getWindow().getHeight())); - } - return areaW; - } - - private static float areaHeight(float fallbackY, float fallbackH) { - float areaH = Math.max(1f, fallbackY + fallbackH); - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.getWindow() != null) { - areaH = Math.max(1f, HudScale.virtualHeight(mc.getWindow().getWidth(), mc.getWindow().getHeight())); - } - return areaH; - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java deleted file mode 100644 index 4e2e302..0000000 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java +++ /dev/null @@ -1,78 +0,0 @@ -package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; - -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.panel.SettingsPanelHudPreviewImpl; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; - -/** - * Режим превью HUD в панели настроек: ALL_ENABLED / ONLY_CURRENT, - * анимация активного сегмента пилюли и применение режима к реестру - * перетаскиваемых элементов. - */ -final class SettingsPanelHudPreview { - - enum Mode { - ALL_ENABLED, - ONLY_CURRENT - } - - private Mode mode = Mode.ONLY_CURRENT; - private float activeAnim = 1f; - - Mode mode() { - return mode; - } - - boolean isOnlyCurrent() { - return mode == Mode.ONLY_CURRENT; - } - - float activeAnim() { - return activeAnim; - } - - void tick(float dt) { - float pillTarget = mode == Mode.ALL_ENABLED ? 0f : 1f; - activeAnim = AnimationUtility.approach(activeAnim, pillTarget, dt, 14f); - activeAnim = AnimationUtility.snap(activeAnim, pillTarget, 0.002f); - } - - void setModeByPointer(String targetId, float pillX, float pillW, float mx) { - float half = pillW * 0.5f; - mode = (mx < pillX + half) ? Mode.ALL_ENABLED : Mode.ONLY_CURRENT; - apply(targetId); - } - - void resetToOnlyCurrent(String targetId) { - mode = Mode.ONLY_CURRENT; - activeAnim = 1f; - apply(targetId); - } - - void apply(String targetId) { - DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); - if (mode == Mode.ONLY_CURRENT) { - if (target != null) { - target.setPreviewEnabled(true); - } - DraggableHudElementRegistry.setEditorWidgetId(targetId); - DraggableHudElementRegistry.setForceVisible(true); - } else { - if (target != null) { - target.setPreviewEnabled(false); - } - DraggableHudElementRegistry.setEditorWidgetId(null); - DraggableHudElementRegistry.setForceVisible(true); - } - } - - static void reset(String targetId) { - DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); - if (target != null) { - target.setPreviewEnabled(false); - } - DraggableHudElementRegistry.setEditorWidgetId(null); - DraggableHudElementRegistry.setForceVisible(false); - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelLayout.java new file mode 100644 index 0000000..32ae0ca --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelLayout.java @@ -0,0 +1,70 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; + +final class SettingsPanelLayout { + final float dropdownPanelW = 115.0f; + final float dropdownPanelH = 240.0f; + final float dropdownHeaderH = 24.0f; + final float dropdownSeparatorH = 4.0f; + final float dropdownRadius = 10.0f; + + private final SettingsPanelState state; + + SettingsPanelLayout(SettingsPanelState state) { + this.state = state; + } + + void buildRows(float menuScale) { + state.rows.clear(); + for (Setting setting : state.settings) { + float rowScale = settingScale(setting, menuScale); + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, rowScale)) { + float anim = setting.updateVisibilityAnim(); + boolean targetVisible = setting.isVisibilityTargetVisible(); + if (!targetVisible && anim <= 0.01f) continue; + float h = setting.getHeight() * anim; + state.rows.add(new SettingRow(setting, anim, h, 0f, rowScale)); + } + } + } + + float totalRowsHeight() { + float out = 0f; + for (SettingRow row : state.rows) out += row.height() + row.gap(); + return out; + } + + float panelScale(float menuScale) { + float base = Math.max(0.25f, menuScale); + if (state.renderSurface != SettingRenderSurface.MODULES) return base; + return AnimationUtility.clamp(base * 1.5f, 2.55f, 3.0f); + } + + float settingScale(float menuScale) { + return Math.max(0.25f, menuScale); + } + + float settingScale(Setting setting, float menuScale) { + return settingScale(menuScale); + } + + RenderWarpStack.Scope pushLifecycleWarp(float progress) { + if (progress >= 0.999f || state.dragging) return Renderer2D.pushWarp(null); + return Renderer2D.pushPerspectiveWarp( + state.panelX, state.panelY, state.panelW, state.panelH, + 0.0f, 0.0f, 0.0f, 3.6f, 0.80f, 0.92f + 0.08f * progress); + } + + void updateHudModePillBounds(float scale) { + state.pillX = state.panelX + 7f * scale; + state.pillY = state.panelY + dropdownHeaderH * scale + 4f * scale; + state.pillW = state.panelW - 14f * scale; + state.pillH = 16f * scale; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelModels.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelModels.java new file mode 100644 index 0000000..eb1e357 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelModels.java @@ -0,0 +1,14 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; + +enum HudPreviewMode { + ALL_ENABLED, + ONLY_CURRENT +} + +record SettingRow(Setting setting, float anim, float height, float gap, float scale) { +} + +record SettingHit(Setting setting, float x, float y, float w, float h, float scale) { +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelState.java new file mode 100644 index 0000000..dcfc0f5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelState.java @@ -0,0 +1,128 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; + +import java.util.ArrayList; +import java.util.List; + +final class SettingsPanelState { + final List settings = new ArrayList<>(); + final List rows = new ArrayList<>(); + final List hits = new ArrayList<>(); + final SettingRenderSurface renderSurface; + String targetId = ""; + String title = "Settings"; + boolean hudContext = false; + boolean editorContext = false; + boolean openTarget = false; + float openAnim = 0f; + float panelHeight = 120f; + float scroll = 0f; + float smoothedScroll = 0f; + float closeHoverAnim = 0f; + boolean dragging = false; + float dragOffsetX = 0f; + float dragOffsetY = 0f; + boolean manualPos = false; + float manualX = 0f; + float manualY = 0f; + boolean limitedHeight = false; + float hudExtraTop = 0f; + float panelX; + float panelY; + float panelW; + float panelH; + float closeX; + float closeY; + float closeW; + float closeH; + float contentX; + float contentY; + float contentW; + float contentH; + float maxScroll; + float contentPadLeft; + float contentPadRight; + float pillX; + float pillY; + float pillW; + float pillH; + float pillActiveAnim = 1f; + HudPreviewMode hudPreviewMode = HudPreviewMode.ONLY_CURRENT; + float lastSettingScale = 1.0f; + + SettingsPanelState(SettingRenderSurface surface) { + this.renderSurface = surface == null ? SettingRenderSurface.SETTINGS : surface; + } + + void open(String id, String title, List source, boolean hudCtx) { + if (id == null) return; + resetHudPreview(); + this.targetId = id; + this.title = title == null || title.isBlank() ? id : title; + this.settings.clear(); + if (source != null) this.settings.addAll(source); + this.hudContext = hudCtx; + this.editorContext = false; + this.openTarget = true; + this.scroll = 0f; + this.smoothedScroll = 0f; + this.closeHoverAnim = 0f; + this.dragging = false; + this.rows.clear(); + this.hits.clear(); + if (hudCtx) { + this.manualPos = false; + this.hudPreviewMode = HudPreviewMode.ONLY_CURRENT; + this.pillActiveAnim = 1f; + applyHudPreviewMode(); + } + } + + void openEditor(String id, String title, List source) { + open(id, title, source, false); + this.editorContext = true; + this.manualPos = false; + } + + void close() { + openTarget = false; + dragging = false; + } + + boolean isActive() { + return openTarget || openAnim > 0.001f; + } + + boolean shouldMaskMenu() { + return (hudContext || editorContext) && isActive(); + } + + boolean blocksMenuInput() { + return (hudContext || editorContext) && (openTarget || openAnim > 0.35f); + } + + void applyHudPreviewMode() { + if (!hudContext) return; + DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); + if (hudPreviewMode == HudPreviewMode.ONLY_CURRENT) { + if (target != null) target.setPreviewEnabled(true); + DraggableHudElementRegistry.setEditorWidgetId(targetId); + DraggableHudElementRegistry.setForceVisible(true); + } else { + if (target != null) target.setPreviewEnabled(false); + DraggableHudElementRegistry.setEditorWidgetId(null); + DraggableHudElementRegistry.setForceVisible(true); + } + } + + void resetHudPreview() { + DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); + if (target != null) target.setPreviewEnabled(false); + DraggableHudElementRegistry.setEditorWidgetId(null); + DraggableHudElementRegistry.setForceVisible(false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/input/SettingsPanelInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/input/SettingsPanelInput.java new file mode 100644 index 0000000..b93264e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/input/SettingsPanelInput.java @@ -0,0 +1,134 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; +import org.lwjgl.glfw.GLFW; + +final class SettingsPanelInput { + private final SettingsPanelState state; + private final SettingsPanelLayout layout; + + SettingsPanelInput(SettingsPanelState state, SettingsPanelLayout layout) { + this.state = state; + this.layout = layout; + } + + boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + boolean isHintToggle(int keyCode, int modifiers) { + return keyCode == GLFW.GLFW_KEY_H && (modifiers & GLFW.GLFW_MOD_ALT) != 0; + } + + boolean mouseClicked(float mx, float my, int button, float scale) { + if (!state.isActive()) return false; + if (state.openAnim <= 0.01f) return false; + float panelScale = layout.panelScale(scale); + if (button == 0 && inside(mx, my, state.closeX, state.closeY, state.closeW, state.closeH)) { + state.close(); + return true; + } + if (!inside(mx, my, state.panelX, state.panelY, state.panelW, state.panelH)) { + return state.hudContext || state.editorContext; + } + if (button == 0 && inside(mx, my, state.panelX, state.panelY, state.panelW, layout.dropdownHeaderH * panelScale)) { + state.dragging = true; + state.dragOffsetX = state.panelX - mx; + state.dragOffsetY = state.panelY - my; + return true; + } + if (state.hudContext && button == 0 && inside(mx, my, state.pillX, state.pillY, state.pillW, state.pillH)) { + float half = state.pillW * 0.5f; + state.hudPreviewMode = (mx < state.pillX + half) ? HudPreviewMode.ALL_ENABLED : HudPreviewMode.ONLY_CURRENT; + state.applyHudPreviewMode(); + return true; + } + if (inside(mx, my, state.contentX, state.contentY, state.contentW, state.contentH)) { + SettingHit target = null; + for (SettingHit hit : state.hits) { + if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { + target = hit; + break; + } + } + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, target == null ? layout.settingScale(panelScale) : target.scale())) { + for (Setting setting : state.settings) { + if (target != null && target.setting() == setting) continue; + setting.mouseClickedOutside(mx, my, button); + } + if (target != null) { + if (!target.setting().isAvailable()) return true; + target.setting().mouseClicked(mx, my, button); + return true; + } + } + } else { + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, layout.settingScale(panelScale))) { + for (Setting setting : state.settings) setting.mouseClickedOutside(mx, my, button); + } + } + return true; + } + + boolean mouseReleased(float mx, float my, int button) { + if (!state.isActive()) return false; + if (button == 0) state.dragging = false; + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, state.lastSettingScale)) { + for (Setting setting : state.settings) { + if (!setting.isAvailable()) continue; + setting.mouseReleased(mx, my, button); + } + } + return inside(mx, my, state.panelX, state.panelY, state.panelW, state.panelH); + } + + boolean mouseScrolled(float mx, float my, double amount) { + if (!state.isActive()) return false; + if (!state.limitedHeight) return inside(mx, my, state.panelX, state.panelY, state.panelW, state.panelH); + if (!inside(mx, my, state.panelX, state.panelY, state.panelW, state.panelH)) return state.hudContext || state.editorContext; + for (SettingHit hit : state.hits) { + if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; + if (!hit.setting().isAvailable()) return true; + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, hit.scale())) { + if (hit.setting().mouseScrolled(mx, my, amount)) return true; + } + break; + } + state.scroll += (float) (amount * 20f * Math.max(0.25f, state.lastSettingScale)); + return true; + } + + boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!state.isActive()) return false; + if (isHintToggle(keyCode, modifiers)) { + MainConfig config = MainConfig.get(); + if (state.hudContext) config.setClickGuiHudEditorHintsEnabled(!config.isClickGuiHudEditorHintsEnabled()); + else config.setClickGuiHintsEnabled(!config.isClickGuiHintsEnabled()); + return true; + } + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, state.lastSettingScale)) { + for (Setting setting : state.settings) { + if (!setting.isAvailable()) continue; + if (setting.keyPressed(keyCode, scanCode, modifiers)) return true; + } + } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + state.close(); + return true; + } + return false; + } + + boolean charTyped(char chr, int modifiers) { + if (!state.isActive()) return false; + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, state.lastSettingScale)) { + for (Setting setting : state.settings) { + if (!setting.isAvailable()) continue; + if (setting.charTyped(chr, modifiers)) return true; + } + } + return false; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/render/SettingsPanelRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/render/SettingsPanelRenderer.java new file mode 100644 index 0000000..a42107d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/render/SettingsPanelRenderer.java @@ -0,0 +1,147 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +final class SettingsPanelRenderer { + private final SettingsPanelState state; + private final SettingsPanelLayout layout; + private final SettingsPanelChrome chrome; + + SettingsPanelRenderer(SettingsPanelState state, SettingsPanelLayout layout, SettingsPanelChrome chrome) { + this.state = state; + this.layout = layout; + this.chrome = chrome; + } + + boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + int mixColor(int from, int to, float t) { + t = Math.max(0f, Math.min(1f, t)); + int a = (int) (((from >>> 24) & 0xFF) * (1 - t) + ((to >>> 24) & 0xFF) * t); + int r = (int) (((from >>> 16) & 0xFF) * (1 - t) + ((to >>> 16) & 0xFF) * t); + int g = (int) (((from >>> 8) & 0xFF) * (1 - t) + ((to >>> 8) & 0xFF) * t); + int b = (int) (((from) & 0xFF) * (1 - t) + ((to) & 0xFF) * t); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { + float dt = AnimationUtility.deltaTime(); + float openTargetValue = state.openTarget ? 1f : 0f; + state.openAnim = AnimationUtility.approach(state.openAnim, openTargetValue, dt, state.openTarget ? 12f : 13f); + state.openAnim = AnimationUtility.snap(state.openAnim, openTargetValue, 0.01f); + float pillTarget = state.hudPreviewMode == HudPreviewMode.ALL_ENABLED ? 0f : 1f; + state.pillActiveAnim = AnimationUtility.approach(state.pillActiveAnim, pillTarget, dt, 14f); + state.pillActiveAnim = AnimationUtility.snap(state.pillActiveAnim, pillTarget, 0.002f); + if (!state.openTarget && state.openAnim <= 0.001f) { + state.openAnim = 0f; + if (state.hudContext) { + state.resetHudPreview(); + state.hudContext = false; + } + state.editorContext = false; + return; + } + float panelScale = layout.panelScale(scale); + state.lastSettingScale = layout.settingScale(panelScale); + layout.buildRows(panelScale); + float baseW = layout.dropdownPanelW * panelScale; + float baseX = menuX + menuW + 24f * panelScale; + float baseY = menuY + 4f * panelScale; + if (state.manualPos) { + baseX = state.manualX; + baseY = state.manualY; + } + state.hudExtraTop = state.hudContext ? 20f * panelScale : 0f; + float targetH = AnimationUtility.clamp( + layout.totalRowsHeight() + (layout.dropdownHeaderH + layout.dropdownSeparatorH) * panelScale + 5f * panelScale + state.hudExtraTop, + 0f, layout.dropdownPanelH * panelScale); + state.panelHeight = AnimationUtility.approach(state.panelHeight, targetH, dt, 14f); + state.panelHeight = AnimationUtility.snap(state.panelHeight, targetH, 0.25f); + float distanceProgress = AnimationUtility.easeInOutCubic(state.openAnim); + float fadeProgress = AnimationUtility.easeInOutCubic(state.openAnim); + if (state.dragging) { + state.manualPos = true; + state.manualX = mx + state.dragOffsetX; + state.manualY = my + state.dragOffsetY; + state.panelX = state.manualX; + state.panelY = state.manualY; + } else { + state.panelX = baseX; + state.panelY = baseY; + } + state.panelW = baseW; + state.panelH = state.panelHeight; + state.limitedHeight = state.panelH >= (layout.dropdownPanelH * panelScale - 0.25f * panelScale); + SettingsGuiPalette palette = SettingsGuiPalette.current(); + try (RenderWarpStack.Scope lifecycleScope = layout.pushLifecycleWarp(distanceProgress)) { + int bgTl = LayoutRender2D.alpha(palette.panelBgLeft(), fadeProgress); + int bgTr = LayoutRender2D.alpha(palette.panelBgRight(), fadeProgress); + int bgBr = LayoutRender2D.alpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.08f), fadeProgress); + int bgBl = LayoutRender2D.alpha(SettingsGuiPalette.darken(palette.panelBgLeft(), 0.05f), fadeProgress); + int stroke = LayoutRender2D.alpha(palette.panelStroke(), fadeProgress); + int line = LayoutRender2D.alpha(palette.panelDivider(), 0.52f * fadeProgress); + int text = LayoutRender2D.alpha(palette.panelText(), fadeProgress); + int mutedBase = LayoutRender2D.alpha(palette.panelMuted(), fadeProgress); + String header = state.title; + state.closeW = 16f * panelScale; + state.closeH = 14f * panelScale; + state.closeX = state.panelX + state.panelW - state.closeW - 6f * panelScale; + state.closeY = state.panelY + (layout.dropdownHeaderH * panelScale - state.closeH) * 0.5f; + boolean closeHover = inside(mx, my, state.closeX, state.closeY, state.closeW, state.closeH); + state.closeHoverAnim = AnimationUtility.approach(state.closeHoverAnim, closeHover ? 1f : 0f, dt, 10f); + int muted = mixColor(mutedBase, text, state.closeHoverAnim * 0.7f); + chrome.renderMatteChrome(header, panelScale, bgTl, bgTr, bgBr, bgBl, stroke, line, text, muted, palette); + float contentStartY = state.panelY + (layout.dropdownHeaderH + layout.dropdownSeparatorH) * panelScale; + if (state.hudContext) { + chrome.renderHudModePill(panelScale, text, muted, palette); + contentStartY += state.hudExtraTop; + } + state.contentPadLeft = 6f * panelScale; + state.contentPadRight = 13f * panelScale; + float contentClipInset = 2.2f * panelScale; + state.contentX = state.panelX + state.contentPadLeft; + state.contentY = contentStartY + contentClipInset; + state.contentW = Math.max(1f, state.panelW - state.contentPadLeft - state.contentPadRight); + state.contentH = Math.max(0f, state.panelY + state.panelH - state.contentY - 7.0f * panelScale); + state.maxScroll = Math.max(0f, layout.totalRowsHeight() - state.contentH); + state.scroll = AnimationUtility.clamp(state.scroll, -state.maxScroll, 0f); + state.smoothedScroll = AnimationUtility.approach(state.smoothedScroll, state.scroll, dt, 14f); + state.smoothedScroll = AnimationUtility.snap(state.smoothedScroll, state.scroll, 0.1f); + state.hits.clear(); + float rowsReveal = fadeProgress; + boolean clipped = ScissorFunction.pushRaw(state.contentX, state.contentY, state.contentW, state.contentH); + float y = state.contentY + state.smoothedScroll; + for (SettingRow row : state.rows) { + float sh = row.height() * rowsReveal; + float gap = row.gap() * rowsReveal; + if (sh > 0.5f) { + state.hits.add(new SettingHit(row.setting(), state.contentX, y, state.contentW, sh, row.scale())); + if (y + sh >= state.contentY - 1.0f * panelScale && y <= state.contentY + state.contentH + 1.0f * panelScale) { + float slide = (1f - row.anim()) * 6f * panelScale; + boolean itemClip = ScissorFunction.pushRaw(state.contentX, y, state.contentW, sh); + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(state.renderSurface, row.scale())) { + row.setting().render(state.contentX, y + slide, state.contentW, mx, my); + } + ClickGuiRenderer.flushRenderer(); + if (itemClip) ScissorFunction.pop(); + } + } + y += sh + gap; + } + ClickGuiRenderer.flushRenderer(); + if (clipped) ScissorFunction.pop(); + chrome.renderScrollbar(panelScale, palette); + } + chrome.renderSettingTooltip(mx, my, panelScale, palette); + chrome.renderHudEditorHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); + chrome.renderSettingsPanelHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java index 938ec70..bce8fc6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.other; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java index 4070f07..e0e741d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.other; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.MenuScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java index 0c2c14d..f0e4192 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.other; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java index c50c364..6cb2df4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java @@ -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.gui.clickgui.layout.screen.settings.implement.other; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java index 322eeb2..5d252f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java @@ -1,8 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; @@ -17,7 +13,7 @@ import java.util.function.Consumer; * Facade that delegates data/scroll/render/input to helpers in {@code picker/}. */ public final class OnlineRelationPlayerPickerComponent { - final Consumer statusSink; + Consumer statusSink; final OnlinePickerData data = new OnlinePickerData(); final OnlinePickerScroll scroll = new OnlinePickerScroll(); private final OnlinePickerRenderer renderer = new OnlinePickerRenderer(); @@ -28,11 +24,11 @@ public final class OnlineRelationPlayerPickerComponent { boolean searchFocused; DefineTarget.RelationTargetMode mode = DefineTarget.RelationTargetMode.FRIEND; - Rect bounds = Rect.ZERO; - Rect searchButton = Rect.ZERO; - Rect searchField = Rect.ZERO; - Rect closeButton = Rect.ZERO; - Rect listRect = Rect.ZERO; + Rect bounds = new Rect(0f, 0f, 0f, 0f); + Rect searchButton = new Rect(0f, 0f, 0f, 0f); + Rect searchField = new Rect(0f, 0f, 0f, 0f); + Rect closeButton = new Rect(0f, 0f, 0f, 0f); + Rect listRect = new Rect(0f, 0f, 0f, 0f); float openAnim; float searchAnim; @@ -40,6 +36,10 @@ public final class OnlineRelationPlayerPickerComponent { OnlineRelationPlayerPickerComponent(Consumer statusSink) { this.statusSink = statusSink; } + OnlineRelationPlayerPickerComponent() { this.statusSink = null; } + + void setStatusSink(Consumer sink) { this.statusSink = sink; } + boolean isVisible() { return openTarget || openAnim > 0.002f; } boolean blocksMovementInput() { return openTarget && searchFocused; } boolean isOpen() { return openTarget; } @@ -68,7 +68,6 @@ public final class OnlineRelationPlayerPickerComponent { record RowHit(String name, Rect rect) {} record Rect(float x, float y, float w, float h) { - static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); boolean contains(float mx, float my) { return ClickGuiMath.insideRect(mx, my, x, y, w, h); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java index 15e810a..6fabf46 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.relations; import dev.loki.lovisual.features.account.SkinManager; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java index 0ac9da8..1225ed8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java @@ -1,661 +1,54 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations.RelationPlayerCardComponent.CardHit; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; -import dev.loki.lovisual.features.module.modules.misc.DefineTarget; -import dev.loki.lovisual.features.relations.PlayerRelations; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.util.text.mask.ChatNameUtil; -import dev.loki.lovisual.util.text.edit.ClipboardUtil; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import org.lwjgl.glfw.GLFW; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations.OnlineRelationPlayerPickerComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations.RelationPlayerCardComponent; - public final class RelationsComponent { - private static final long STATUS_DURATION_MS = 2200L; - private static final String I18N = "clickgui.settings.relations."; - - private final RelationPlayerCardComponent cardComponent = new RelationPlayerCardComponent(); - private final OnlineRelationPlayerPickerComponent onlinePicker; - private final List cardHits = new ArrayList<>(); - - private RelationTab tab = RelationTab.FRIENDS; - private ActiveField activeField = ActiveField.NONE; - private String playerInput = ""; - private String statusMessage; - private long statusUntilMs; - - private Rect friendsPill = Rect.ZERO; - private Rect enemiesPill = Rect.ZERO; - private Rect staffPill = Rect.ZERO; - private Rect playerInputRect = Rect.ZERO; - private Rect addTypedButton = Rect.ZERO; - private Rect reservedPickerButton = Rect.ZERO; - - private float scroll; - private float smoothedScroll; - private boolean scrollbarVisible; - private boolean draggingScrollbar; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; - private float listX; - private float listY; - private float listW; - private float listH; - private float modeAnim; - private float friendsHoverAnim; - private float enemiesHoverAnim; - private float staffHoverAnim; - private float addHoverAnim; - private float pickerHoverAnim; - private static boolean movementInputBlocked; + private final RelationsState state; + private final RelationsRenderer renderer; + private final RelationsInput input; public RelationsComponent() { - this.onlinePicker = new OnlineRelationPlayerPickerComponent(this::setStatus); + RelationsState createdState = new RelationsState(); + RelationsRenderer createdRenderer = new RelationsRenderer(); + RelationsInput createdInput = new RelationsInput(); + createdState.bindStatusSink(); + this.state = createdState; + this.renderer = createdRenderer; + this.input = createdInput; } public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; - activeField = ActiveField.NONE; - onlinePicker.close(); - onlinePicker.resetScroll(); - movementInputBlocked = false; + state.resetScroll(); } - public static boolean isMovementInputBlocked() { - return movementInputBlocked; + public boolean isMovementInputBlocked() { + return state.isMovementInputBlocked(); } public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { - cardHits.clear(); - SettingsGuiPalette palette = SettingsGuiPalette.current(); - movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput(); - - float areaX = menuX + 31f * scale; - float areaY = menuY + 33f * scale; - float areaW = menuW - 42f * scale; - float areaH = menuH - 39f * scale; - - renderToolbar(areaX, areaY, areaW, mx, my, scale, palette); - - float y = areaY + 24f * scale; - renderStatus(areaX, y, areaW, scale, palette); - y += 13f * scale; - - listX = areaX; - listY = y; - listW = areaW; - listH = Math.max(1f, areaY + areaH - y); - if (onlinePicker.isVisible()) { - onlinePicker.render(listX, listY, listW, listH, mx, my, scale, palette); - return; - } - - listX = areaX; - listY = y; - listW = areaW; - listH = Math.max(1f, areaY + areaH - y); - renderCards(listX, listY, listW, listH, filteredEntries(tab.entries()), mx, my, scale, palette); + renderer.render(state, menuX, menuY, menuW, menuH, mx, my, scale); } public boolean mousePressedScrollbar(float mx, float my, int button) { - if (onlinePicker.isVisible()) return onlinePicker.mousePressedScrollbar(mx, my, button); - if (button != 0 || !isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; + return input.mousePressedScrollbar(state, mx, my, button); } public void mouseReleased(int button) { - onlinePicker.mouseReleased(button); - if (button == 0) draggingScrollbar = false; + input.mouseReleased(state, button); } public void scroll(float mx, float my, double amount) { - if (onlinePicker.isVisible()) { - onlinePicker.scroll(mx, my, amount); - return; - } - if (!ClickGuiMath.insideRect(mx, my, listX, listY, listW, listH)) return; - scroll += (float) (amount * 22f); + input.scroll(state, mx, my, amount); } public boolean click(float mx, float my, int button) { - if (button != 0) return false; - - if (friendsPill.contains(mx, my)) return switchTab(RelationTab.FRIENDS); - if (enemiesPill.contains(mx, my)) return switchTab(RelationTab.ENEMIES); - if (staffPill.contains(mx, my)) return switchTab(RelationTab.STAFF); - - if (playerInputRect.contains(mx, my)) { - if (onlinePicker.isOpen()) { - activeField = ActiveField.NONE; - onlinePicker.focusSearch(); - } else { - activeField = ActiveField.PLAYER; - } - movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput(); - clearStatus(); - return true; - } - if (addTypedButton.contains(mx, my)) { - if (onlinePicker.isOpen()) { - onlinePicker.clearSearch(); - onlinePicker.focusSearch(); - movementInputBlocked = onlinePicker.blocksMovementInput(); - return true; - } - commitPlayerInput(); - return true; - } - if (reservedPickerButton.contains(mx, my)) { - activeField = ActiveField.NONE; - onlinePicker.toggle(tab.pickerMode()); - movementInputBlocked = onlinePicker.blocksMovementInput(); - clearStatus(); - return true; - } - if (onlinePicker.isVisible() && onlinePicker.click(mx, my, button)) { - movementInputBlocked = onlinePicker.blocksMovementInput(); - return true; - } - if (onlinePicker.isVisible()) { - return false; - } - - for (CardEntryHit entryHit : cardHits) { - CardHit hit = entryHit.hit(); - if (inside(hit.delete(), mx, my)) { - tab.remove(entryHit.name()); - setStatus(tr("status.removed", "Removed: %s", entryHit.name())); - return true; - } - if (ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { - return true; - } - } - - activeField = ActiveField.NONE; - return false; + return input.click(state, mx, my, button); } public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (onlinePicker.keyPressed(keyCode, scanCode, modifiers)) { - movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput(); - return true; - } - if (activeField == ActiveField.NONE) return false; - boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - activeField = ActiveField.NONE; - movementInputBlocked = false; - return true; - } - if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - commitPlayerInput(); - return true; - } - if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { - setFieldText(activeField, dropLast(fieldText(activeField))); - return true; - } - if (ctrl && keyCode == GLFW.GLFW_KEY_V) { - appendToField(activeField, ClipboardUtil.get()); - return true; - } - return true; + return input.keyPressed(state, keyCode, scanCode, modifiers); } public boolean charTyped(char chr, int modifiers) { - if (onlinePicker.charTyped(chr, modifiers)) { - movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput(); - return true; - } - if (activeField == ActiveField.NONE) return false; - if (chr >= 32 && chr != 127) { - appendToField(activeField, String.valueOf(chr)); - } - return true; - } - - private void renderToolbar(float x, float y, float w, float mx, float my, float scale, SettingsGuiPalette palette) { - float pillW = 165f * scale; - float pillH = 15f * scale; - float targetMode = tab.ordinal(); - float dt = AnimationUtility.deltaTime(); - modeAnim = AnimationUtility.approach(modeAnim, targetMode, dt, 12f); - modeAnim = AnimationUtility.snap(modeAnim, targetMode, 0.01f); - - friendsPill = new Rect(x, y, pillW / 3f, pillH); - enemiesPill = new Rect(x + pillW / 3f, y, pillW / 3f, pillH); - staffPill = new Rect(x + pillW * 2f / 3f, y, pillW / 3f, pillH); - friendsHoverAnim = updateHover(friendsHoverAnim, friendsPill.contains(mx, my), dt); - enemiesHoverAnim = updateHover(enemiesHoverAnim, enemiesPill.contains(mx, my), dt); - staffHoverAnim = updateHover(staffHoverAnim, staffPill.contains(mx, my), dt); - - ClickGuiRenderer.drawBlur(x, y, pillW, pillH, 4f * scale, palette.panelBlurTint(), 145f / 255f); - LayoutRender2D.roundedQuad(x, y, pillW, pillH, 3.5f * scale, - SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f), - SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f), - SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f), - SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f)); - LayoutRender2D.roundedStrokeQuad(x, y, pillW, pillH, 3.5f * scale, 0.5f * scale, - palette.panelStroke(), palette.moduleDividerEnd(), palette.moduleDividerEnd(), palette.panelStroke()); - - float pad = 1.2f * scale; - float segment = (pillW - pad * 2f) / 3f; - float activeX = x + pad + segment * modeAnim; - LayoutRender2D.roundedQuad(activeX, y + pad, segment, pillH - pad * 2f, 3f * scale, - SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f), - SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.22f), - SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillActive(), 0.16f), palette.menuCategorySelectedRight(), 0.18f), - SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f)); - - drawToolbarSeparator(x + pad + segment, y, pillH, scale, palette); - drawToolbarSeparator(x + pad + segment * 2f, y, pillH, scale, palette); - - drawSegment(friendsPill, tr("tab.friends", "Friends"), tab == RelationTab.FRIENDS, friendsHoverAnim, scale, palette); - drawSegment(enemiesPill, tr("tab.enemies", "Enemies"), tab == RelationTab.ENEMIES, enemiesHoverAnim, scale, palette); - drawSegment(staffPill, tr("tab.staff", "Staff"), tab == RelationTab.STAFF, staffHoverAnim, scale, palette); - - float fieldX = x + pillW + 8f * scale; - float picker = 18f * scale; - float action = 18f * scale; - float gap = 4f * scale; - boolean onlineMode = onlinePicker.isOpen(); - float fieldW = Math.max(80f * scale, w - (fieldX - x) - picker - action - gap * 2f); - playerInputRect = new Rect(fieldX, y, fieldW, pillH); - addTypedButton = new Rect(fieldX + fieldW + gap, y, action, pillH); - reservedPickerButton = new Rect(addTypedButton.x() + action + gap, y, picker, pillH); - - if (onlineMode) { - String query = onlinePicker.searchText(); - drawInput(playerInputRect, query, tr("placeholder.search_online", "Search online"), onlinePicker.isSearchFocused(), mx, my, scale, palette); - addHoverAnim = updateHover(addHoverAnim, addTypedButton.contains(mx, my), dt); - drawIconButton(addTypedButton, "x", addHoverAnim, query == null || query.isBlank(), scale, palette); - } else { - drawInput(playerInputRect, playerInput, tr("placeholder.nick", "Nick"), activeField == ActiveField.PLAYER, mx, my, scale, palette); - addHoverAnim = updateHover(addHoverAnim, addTypedButton.contains(mx, my), dt); - drawIconButton(addTypedButton, "check", addHoverAnim, false, scale, palette); - } - pickerHoverAnim = updateHover(pickerHoverAnim, reservedPickerButton.contains(mx, my) || onlinePicker.isVisible(), dt); - drawIconButton(reservedPickerButton, onlineMode ? "x" : "user-plus", pickerHoverAnim, false, scale, palette); - } - - private void renderCards(float x, float y, float w, float h, List entries, float mx, float my, float scale, SettingsGuiPalette palette) { - float gap = 6f * scale; - float reservedScrollbarW = 9f * scale; - float cardAreaW = Math.max(1f, w - reservedScrollbarW); - int columns = cardAreaW > 230f * scale ? 2 : 1; - float cardW = (cardAreaW - gap * (columns - 1)) / columns; - float cardH = 36f * scale; - float stepY = cardH + gap; - int rows = (entries.size() + columns - 1) / columns; - float contentH = rows <= 0 ? 0f : rows * stepY - gap; - float maxScroll = Math.max(0f, contentH - h); - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - if (draggingScrollbar) scrollToMouse(ClickGuiRenderer.getMouseY()); - - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); - - if (entries.isEmpty()) { - String empty = ClickGuiSearch.isActive() && !ClickGuiSearch.getText().isBlank() - ? tr("empty.no_matches", "No matching players.") - : tr("empty.none_added", "No players added."); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), empty, x + 4f * scale, y + 4f * scale, 7.4f * scale, palette.panelMuted(), false); - return; - } - - boolean clipped = ScissorFunction.pushRaw(x, y, w, h); - for (int i = 0; i < entries.size(); i++) { - int col = i % columns; - int row = i / columns; - float cx = x + col * (cardW + gap); - float cy = y + row * stepY + smoothedScroll; - if (cy + cardH < y || cy > y + h) continue; - String name = entries.get(i); - CardHit hit = cardComponent.render(name, cx, cy, cardW, cardH, mx, my, scale, palette); - cardHits.add(new CardEntryHit(name, hit)); - } - if (clipped) ScissorFunction.pop(); - renderScrollbar(x, y, w, h, maxScroll, scale, palette); - } - - private void drawSegment(Rect rect, String label, boolean active, float hover, float scale, SettingsGuiPalette palette) { - float size = 7.5f * scale; - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), label, size); - float th = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), size); - int color = active ? palette.panelText() : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.20f * hover); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + (rect.h() - th) * 0.5f, size, color, false); - } - - private void drawToolbarSeparator(float x, float y, float h, float scale, SettingsGuiPalette palette) { - float backingW = Math.max(1.25f * scale, 1.05f); - float backingH = h - 4.4f * scale; - float backingY = y + (h - backingH) * 0.5f; - int backingTop = SettingsGuiPalette.withAlpha(palette.panelBgLeft(), 98); - int backingBottom = SettingsGuiPalette.withAlpha(palette.panelBgRight(), 78); - LayoutRender2D.roundedQuad( - x - backingW * 0.5f, - backingY, - backingW, - backingH, - backingW * 0.5f, - backingTop, - backingTop, - backingBottom, - backingBottom - ); - - float lineW = Math.max(0.5f * scale, 0.75f); - float lineH = h - 6.0f * scale; - float lineY = y + (h - lineH) * 0.5f; - int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerStart(), palette.menuCategorySelectedLeft(), 0.34f), 146); - int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.40f), 210); - int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.panelText(), 0.24f), 136); - LayoutRender2D.roundedQuad( - x - lineW * 0.5f, - lineY, - lineW, - lineH, - lineW * 0.5f, - top, - mid, - bottom, - top - ); - } - - private void drawInput(Rect rect, String value, String placeholder, boolean active, float mx, float my, float scale, SettingsGuiPalette palette) { - boolean hover = rect.contains(mx, my); - int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), hover || active ? 0.20f : 0.06f); - int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), hover || active ? 0.18f : 0.06f); - LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, bgA, bgB, bgB, bgA); - LayoutRender2D.roundedStrokeQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, 0.45f * scale, - active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerStart(), 0.75f), - active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.75f), - active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.65f), - active ? palette.moduleDividerStart() : LayoutRender2D.alpha(palette.moduleDividerStart(), 0.75f)); - - boolean empty = value == null || value.isEmpty(); - String raw = empty ? (active ? "" : placeholder) : value; - int color = empty && !active ? palette.panelMuted() : palette.panelText(); - float size = 6.8f * scale; - String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), raw, size, rect.w() - 8f * scale); - float textY = rect.y() + (rect.h() - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), size)) * 0.5f; - if (!text.isEmpty()) { - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, rect.x() + 4f * scale, textY, size, color, false); - } - if (active && (System.currentTimeMillis() / 500L) % 2L == 0L) { - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), text, size); - LayoutRender2D.rect(rect.x() + Math.min(tw + 5.5f * scale, rect.w() - 3f * scale), rect.y() + 3f * scale, 0.6f * scale, rect.h() - 6f * scale, palette.panelText()); - } - } - - private void drawIconButton(Rect rect, String icon, float hover, boolean disabled, float scale, SettingsGuiPalette palette) { - int bgA = disabled - ? LayoutRender2D.alpha(palette.panelPillBase(), 0.42f) - : SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.12f + hover * 0.22f); - int bgB = disabled - ? LayoutRender2D.alpha(palette.panelPillBase(), 0.32f) - : SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.12f + hover * 0.24f); - LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, bgA, bgB, bgB, bgA); - int iconColor = disabled ? LayoutRender2D.alpha(palette.panelMuted(), 0.58f) : palette.menuCategoryText(); - float iconSize = Math.min(rect.w(), rect.h()) - 7f * scale; - Renderer2D.COLOR.svg(icon, rect.x() + (rect.w() - iconSize) * 0.5f, rect.y() + (rect.h() - iconSize) * 0.5f, - iconSize, iconSize, SvgRenderOptions.overrideColor(iconColor)); - } - - private boolean switchTab(RelationTab next) { - if (tab != next) { - tab = next; - activeField = ActiveField.NONE; - resetScroll(); - if (onlinePicker.isVisible()) { - onlinePicker.open(tab.pickerMode()); - } - clearStatus(); - } - return true; - } - - private void commitPlayerInput() { - String cleaned = ChatNameUtil.normalizeNickCandidate(playerInput); - if (!ChatNameUtil.isNickLike(cleaned)) { - setStatus(tr("status.invalid_nick", "Invalid nick")); - return; - } - if (tab.add(cleaned)) { - playerInput = ""; - activeField = ActiveField.NONE; - movementInputBlocked = false; - setStatus(tr("status.added", "Added: %s", cleaned)); - } else { - setStatus(tr("status.already_exists", "Already exists: %s", cleaned)); - } - } - - private List filteredEntries(List entries) { - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) return entries; - String q = ClickGuiSearch.getText().toLowerCase(Locale.ROOT); - List out = new ArrayList<>(); - for (String entry : entries) { - if (entry != null && entry.toLowerCase(Locale.ROOT).contains(q)) out.add(entry); - } - return out; - } - - private static List sorted(Set src) { - List out = new ArrayList<>(); - if (src != null) { - for (String value : src) { - if (value != null && !value.isBlank()) out.add(value); - } - } - out.sort(String.CASE_INSENSITIVE_ORDER); - return out; - } - - private static boolean inside(RelationPlayerCardComponent.ActionHit hit, float mx, float my) { - return hit != null && ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h()); - } - - private void appendToField(ActiveField field, String raw) { - if (raw == null || raw.isEmpty()) return; - String current = fieldText(field); - int max = 32; - String next = current + raw; - if (next.length() > max) next = next.substring(0, max); - setFieldText(field, next); - } - - private String fieldText(ActiveField field) { - return switch (field) { - case PLAYER -> playerInput; - default -> ""; - }; - } - - private void setFieldText(ActiveField field, String value) { - String next = value == null ? "" : value; - switch (field) { - case PLAYER -> playerInput = next; - default -> { - } - } - } - - private static String dropLast(String text) { - if (text == null || text.isEmpty()) return ""; - return text.substring(0, text.length() - 1); - } - - private float updateHover(float current, boolean hovered, float dt) { - return AnimationUtility.snap(AnimationUtility.approach(current, hovered ? 1f : 0f, dt, 12f), hovered ? 1f : 0f, 0.01f); - } - - private void renderStatus(float x, float y, float w, float scale, SettingsGuiPalette palette) { - if (statusMessage == null || statusMessage.isBlank()) return; - if (System.currentTimeMillis() > statusUntilMs) { - clearStatus(); - return; - } - float size = 6.8f * scale; - String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), statusMessage, size, w - 8f * scale); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x + 4f * scale, y + 2.6f * scale, size, palette.panelMuted(), false); - } - - private void setStatus(String message) { - statusMessage = message; - statusUntilMs = System.currentTimeMillis() + STATUS_DURATION_MS; - } - - private static String tr(String key, String fallback, Object... args) { - return ClickGuiI18n.tr(I18N + key, fallback, args); - } - - private void clearStatus() { - statusMessage = null; - statusUntilMs = 0L; - } - - private void renderScrollbar(float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette) { - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - if (!scrollbarVisible) return; - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - if (isScrollbarHovered(mx, my) || draggingScrollbar) SystemCursor.set(SystemCursor.CursorType.SCROLL); - - LayoutRender2D.roundedQuad(scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); - LayoutRender2D.roundedQuad(scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); - } - - private void updateScrollbarMetrics(float x, float y, float w, float h, float maxScroll, float scale) { - scrollbarVisible = maxScroll > 0.5f; - scrollbarMaxScroll = Math.max(0f, maxScroll); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - scrollbarW = 3f * scale; - scrollbarX = x + w - scrollbarW - 2f * scale; - scrollbarY = y + 3f * scale; - scrollbarH = Math.max(1f, h - 6f * scale); - scrollbarThumbH = Math.max(18f * scale, scrollbarH * (scrollbarH / (scrollbarMaxScroll + scrollbarH))); - float scrollRatio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(scrollRatio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private enum ActiveField { - NONE, - PLAYER - } - - private enum RelationTab { - FRIENDS, - ENEMIES, - STAFF; - - private List entries() { - PlayerRelations rel = PlayerRelations.get(); - return switch (this) { - case FRIENDS -> sorted(rel.getFriends()); - case ENEMIES -> sorted(rel.getEnemies()); - case STAFF -> sorted(rel.getStaff()); - }; - } - - private boolean add(String name) { - PlayerRelations rel = PlayerRelations.get(); - boolean changed = switch (this) { - case FRIENDS -> rel.addFriend(name); - case ENEMIES -> rel.addEnemy(name); - case STAFF -> rel.addStaff(name); - }; - if (changed) rel.save(); - return changed; - } - - private boolean remove(String name) { - PlayerRelations rel = PlayerRelations.get(); - boolean changed = switch (this) { - case FRIENDS -> rel.removeFriend(name); - case ENEMIES -> rel.removeEnemy(name); - case STAFF -> rel.removeStaff(name); - }; - if (changed) rel.save(); - return changed; - } - - private DefineTarget.RelationTargetMode pickerMode() { - return switch (this) { - case FRIENDS -> DefineTarget.RelationTargetMode.FRIEND; - case ENEMIES -> DefineTarget.RelationTargetMode.ENEMY; - case STAFF -> DefineTarget.RelationTargetMode.STAFF; - }; - } - } - - private record CardEntryHit(String name, CardHit hit) { - } - - private record Rect(float x, float y, float w, float h) { - static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); - - boolean contains(float mx, float my) { - return ClickGuiMath.insideRect(mx, my, x, y, w, h); - } + return input.charTyped(state, chr, modifiers); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java index b202d44..8b5e2ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java @@ -1,8 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java index 987b04b..73c8bf2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java @@ -1,8 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java index 09224b9..29bb982 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java @@ -1,8 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; @@ -68,8 +64,8 @@ final class OnlinePickerRenderer { ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), subtitle, x, y + 13f * scale, 6.15f * scale, fade(palette.panelMuted(), opacity), false); float btn = 17f * scale; host.closeButton = new OnlineRelationPlayerPickerComponent.Rect(x + w - btn, y, btn, btn); - host.searchButton = OnlineRelationPlayerPickerComponent.Rect.ZERO; - host.searchField = OnlineRelationPlayerPickerComponent.Rect.ZERO; + host.searchButton = new OnlineRelationPlayerPickerComponent.Rect(0f, 0f, 0f, 0f); + host.searchField = new OnlineRelationPlayerPickerComponent.Rect(0f, 0f, 0f, 0f); drawSmallIconButton(host.closeButton, "x", false, host.closeButton.contains(mx, my), scale, palette, opacity); } private void renderRows(OnlineRelationPlayerPickerComponent host, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java index 6bb15f6..6a9d274 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java @@ -1,8 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsInput.java new file mode 100644 index 0000000..771e91d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsInput.java @@ -0,0 +1,179 @@ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.util.text.edit.ClipboardUtil; +import dev.loki.lovisual.util.text.mask.ChatNameUtil; +import org.lwjgl.glfw.GLFW; + +final class RelationsInput { + final RelationsScroll scroll = new RelationsScroll(); + + boolean mousePressedScrollbar(RelationsState state, float mx, float my, int button) { + if (state.onlinePicker.isVisible()) return state.onlinePicker.mousePressedScrollbar(mx, my, button); + return scroll.press(state, mx, my, button); + } + + void mouseReleased(RelationsState state, int button) { + state.onlinePicker.mouseReleased(button); + if (button == 0) state.draggingScrollbar = false; + } + + void scroll(RelationsState state, float mx, float my, double amount) { + if (state.onlinePicker.isVisible()) { + state.onlinePicker.scroll(mx, my, amount); + return; + } + if (!ClickGuiMath.insideRect(mx, my, state.listX, state.listY, state.listW, state.listH)) return; + state.scroll += (float) (amount * 22f); + } + + boolean click(RelationsState state, float mx, float my, int button) { + if (button != 0) return false; + if (state.friendsPill.contains(mx, my)) return switchTab(state, RelationsState.RelationTab.FRIENDS); + if (state.enemiesPill.contains(mx, my)) return switchTab(state, RelationsState.RelationTab.ENEMIES); + if (state.staffPill.contains(mx, my)) return switchTab(state, RelationsState.RelationTab.STAFF); + if (state.playerInputRect.contains(mx, my)) { + if (state.onlinePicker.isOpen()) { + state.activeField = RelationsState.ActiveField.NONE; + state.onlinePicker.focusSearch(); + } else { + state.activeField = RelationsState.ActiveField.PLAYER; + } + state.updateMovementBlocked(); + state.clearStatus(); + return true; + } + if (state.addTypedButton.contains(mx, my)) { + if (state.onlinePicker.isOpen()) { + state.onlinePicker.clearSearch(); + state.onlinePicker.focusSearch(); + state.updateMovementBlocked(); + return true; + } + commitPlayerInput(state); + return true; + } + if (state.reservedPickerButton.contains(mx, my)) { + state.activeField = RelationsState.ActiveField.NONE; + state.onlinePicker.toggle(state.tab.pickerMode()); + state.updateMovementBlocked(); + state.clearStatus(); + return true; + } + if (state.onlinePicker.isVisible() && state.onlinePicker.click(mx, my, button)) { + state.updateMovementBlocked(); + return true; + } + if (state.onlinePicker.isVisible()) return false; + for (RelationsState.CardEntryHit entryHit : state.cardHits) { + RelationPlayerCardComponent.CardHit hit = entryHit.hit(); + if (inside(hit.delete(), mx, my)) { + state.tab.remove(entryHit.name()); + state.setStatus(state.tr("status.removed", "Removed: %s", entryHit.name())); + return true; + } + if (ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) return true; + } + state.activeField = RelationsState.ActiveField.NONE; + return false; + } + + boolean keyPressed(RelationsState state, int keyCode, int scanCode, int modifiers) { + if (state.onlinePicker.keyPressed(keyCode, scanCode, modifiers)) { + state.updateMovementBlocked(); + return true; + } + if (state.activeField == RelationsState.ActiveField.NONE) return false; + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + state.activeField = RelationsState.ActiveField.NONE; + state.movementInputBlocked = false; + return true; + } + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + commitPlayerInput(state); + return true; + } + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + setFieldText(state, state.activeField, dropLast(fieldText(state, state.activeField))); + return true; + } + if (ctrl && keyCode == GLFW.GLFW_KEY_V) { + appendToField(state, state.activeField, ClipboardUtil.get()); + return true; + } + return true; + } + + boolean charTyped(RelationsState state, char chr, int modifiers) { + if (state.onlinePicker.charTyped(chr, modifiers)) { + state.updateMovementBlocked(); + return true; + } + if (state.activeField == RelationsState.ActiveField.NONE) return false; + if (chr >= 32 && chr != 127) appendToField(state, state.activeField, String.valueOf(chr)); + return true; + } + + boolean switchTab(RelationsState state, RelationsState.RelationTab next) { + if (state.tab != next) { + state.tab = next; + state.activeField = RelationsState.ActiveField.NONE; + state.resetScroll(); + if (state.onlinePicker.isVisible()) state.onlinePicker.open(state.tab.pickerMode()); + state.clearStatus(); + } + return true; + } + + void commitPlayerInput(RelationsState state) { + String cleaned = ChatNameUtil.normalizeNickCandidate(state.playerInput); + if (!ChatNameUtil.isNickLike(cleaned)) { + state.setStatus(state.tr("status.invalid_nick", "Invalid nick")); + return; + } + if (state.tab.add(cleaned)) { + state.playerInput = ""; + state.activeField = RelationsState.ActiveField.NONE; + state.movementInputBlocked = false; + state.setStatus(state.tr("status.added", "Added: %s", cleaned)); + } else { + state.setStatus(state.tr("status.already_exists", "Already exists: %s", cleaned)); + } + } + + String fieldText(RelationsState state, RelationsState.ActiveField field) { + return switch (field) { + case PLAYER -> state.playerInput; + default -> ""; + }; + } + + void setFieldText(RelationsState state, RelationsState.ActiveField field, String value) { + String next = value == null ? "" : value; + switch (field) { + case PLAYER -> state.playerInput = next; + default -> { + } + } + } + + void appendToField(RelationsState state, RelationsState.ActiveField field, String raw) { + if (raw == null || raw.isEmpty()) return; + String current = fieldText(state, field); + int max = 32; + String next = current + raw; + if (next.length() > max) next = next.substring(0, max); + setFieldText(state, field, next); + } + + String dropLast(String text) { + if (text == null || text.isEmpty()) return ""; + return text.substring(0, text.length() - 1); + } + + boolean inside(RelationPlayerCardComponent.ActionHit hit, float mx, float my) { + return hit != null && ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsRenderer.java new file mode 100644 index 0000000..f1c58c3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsRenderer.java @@ -0,0 +1,191 @@ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import java.util.List; + +final class RelationsRenderer { + final RelationsScroll scroll = new RelationsScroll(); + + void render(RelationsState state, float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { + state.cardHits.clear(); + SettingsGuiPalette palette = SettingsGuiPalette.current(); + state.updateMovementBlocked(); + float areaX = menuX + 31f * scale; + float areaY = menuY + 33f * scale; + float areaW = menuW - 42f * scale; + float areaH = menuH - 39f * scale; + renderToolbar(state, areaX, areaY, areaW, mx, my, scale, palette); + float y = areaY + 24f * scale; + renderStatus(state, areaX, y, areaW, scale, palette); + y += 13f * scale; + state.listX = areaX; + state.listY = y; + state.listW = areaW; + state.listH = Math.max(1f, areaY + areaH - y); + if (state.onlinePicker.isVisible()) { + state.onlinePicker.render(state.listX, state.listY, state.listW, state.listH, mx, my, scale, palette); + return; + } + renderCards(state, state.listX, state.listY, state.listW, state.listH, state.filteredEntries(state.tab.entries()), mx, my, scale, palette); + } + + void renderToolbar(RelationsState state, float x, float y, float w, float mx, float my, float scale, SettingsGuiPalette palette) { + float pillW = 165f * scale; + float pillH = 15f * scale; + float targetMode = state.tab.ordinal(); + float dt = AnimationUtility.deltaTime(); + state.modeAnim = AnimationUtility.approach(state.modeAnim, targetMode, dt, 12f); + state.modeAnim = AnimationUtility.snap(state.modeAnim, targetMode, 0.01f); + state.friendsPill = new RelationsState.Rect(x, y, pillW / 3f, pillH); + state.enemiesPill = new RelationsState.Rect(x + pillW / 3f, y, pillW / 3f, pillH); + state.staffPill = new RelationsState.Rect(x + pillW * 2f / 3f, y, pillW / 3f, pillH); + state.friendsHoverAnim = updateHover(state.friendsHoverAnim, state.friendsPill.contains(mx, my), dt); + state.enemiesHoverAnim = updateHover(state.enemiesHoverAnim, state.enemiesPill.contains(mx, my), dt); + state.staffHoverAnim = updateHover(state.staffHoverAnim, state.staffPill.contains(mx, my), dt); + ClickGuiRenderer.drawBlur(x, y, pillW, pillH, 4f * scale, palette.panelBlurTint(), 145f / 255f); + LayoutRender2D.roundedQuad(x, y, pillW, pillH, 3.5f * scale, SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f), SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f), SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f), SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f)); + LayoutRender2D.roundedStrokeQuad(x, y, pillW, pillH, 3.5f * scale, 0.5f * scale, palette.panelStroke(), palette.moduleDividerEnd(), palette.moduleDividerEnd(), palette.panelStroke()); + float pad = 1.2f * scale; + float segment = (pillW - pad * 2f) / 3f; + float activeX = x + pad + segment * state.modeAnim; + LayoutRender2D.roundedQuad(activeX, y + pad, segment, pillH - pad * 2f, 3f * scale, SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f), SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.22f), SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillActive(), 0.16f), palette.menuCategorySelectedRight(), 0.18f), SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f)); + drawToolbarSeparator(x + pad + segment, y, pillH, scale, palette); + drawToolbarSeparator(x + pad + segment * 2f, y, pillH, scale, palette); + drawSegment(state.friendsPill, state.tr("tab.friends", "Friends"), state.tab == RelationsState.RelationTab.FRIENDS, state.friendsHoverAnim, scale, palette); + drawSegment(state.enemiesPill, state.tr("tab.enemies", "Enemies"), state.tab == RelationsState.RelationTab.ENEMIES, state.enemiesHoverAnim, scale, palette); + drawSegment(state.staffPill, state.tr("tab.staff", "Staff"), state.tab == RelationsState.RelationTab.STAFF, state.staffHoverAnim, scale, palette); + float fieldX = x + pillW + 8f * scale; + float picker = 18f * scale; + float action = 18f * scale; + float gap = 4f * scale; + boolean onlineMode = state.onlinePicker.isOpen(); + float fieldW = Math.max(80f * scale, w - (fieldX - x) - picker - action - gap * 2f); + state.playerInputRect = new RelationsState.Rect(fieldX, y, fieldW, pillH); + state.addTypedButton = new RelationsState.Rect(fieldX + fieldW + gap, y, action, pillH); + state.reservedPickerButton = new RelationsState.Rect(state.addTypedButton.x() + action + gap, y, picker, pillH); + if (onlineMode) { + String query = state.onlinePicker.searchText(); + drawInput(state.playerInputRect, query, state.tr("placeholder.search_online", "Search online"), state.onlinePicker.isSearchFocused(), mx, my, scale, palette); + state.addHoverAnim = updateHover(state.addHoverAnim, state.addTypedButton.contains(mx, my), dt); + drawIconButton(state.addTypedButton, "x", state.addHoverAnim, query == null || query.isBlank(), scale, palette); + } else { + drawInput(state.playerInputRect, state.playerInput, state.tr("placeholder.nick", "Nick"), state.activeField == RelationsState.ActiveField.PLAYER, mx, my, scale, palette); + state.addHoverAnim = updateHover(state.addHoverAnim, state.addTypedButton.contains(mx, my), dt); + drawIconButton(state.addTypedButton, "check", state.addHoverAnim, false, scale, palette); + } + state.pickerHoverAnim = updateHover(state.pickerHoverAnim, state.reservedPickerButton.contains(mx, my) || state.onlinePicker.isVisible(), dt); + drawIconButton(state.reservedPickerButton, onlineMode ? "x" : "user-plus", state.pickerHoverAnim, false, scale, palette); + } + + void renderCards(RelationsState state, float x, float y, float w, float h, List entries, float mx, float my, float scale, SettingsGuiPalette palette) { + float gap = 6f * scale; + float reservedScrollbarW = 9f * scale; + float cardAreaW = Math.max(1f, w - reservedScrollbarW); + int columns = cardAreaW > 230f * scale ? 2 : 1; + float cardW = (cardAreaW - gap * (columns - 1)) / columns; + float cardH = 36f * scale; + float stepY = cardH + gap; + int rows = (entries.size() + columns - 1) / columns; + float contentH = rows <= 0 ? 0f : rows * stepY - gap; + float maxScroll = Math.max(0f, contentH - h); + scroll.updateMetrics(state, x, y, w, h, maxScroll, scale); + if (state.draggingScrollbar) scroll.scrollToMouse(state, ClickGuiRenderer.getMouseY()); + state.scroll = ClickGuiMath.clamp(state.scroll, -maxScroll, 0f); + state.smoothedScroll = AnimationUtility.approach(state.smoothedScroll, state.scroll, state.draggingScrollbar ? 0.55f : 0.2f); + state.smoothedScroll = AnimationUtility.snap(state.smoothedScroll, state.scroll, state.draggingScrollbar ? 0.01f : 0.05f); + if (entries.isEmpty()) { + String empty = ClickGuiSearch.isActive() && !ClickGuiSearch.getText().isBlank() ? state.tr("empty.no_matches", "No matching players.") : state.tr("empty.none_added", "No players added."); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), empty, x + 4f * scale, y + 4f * scale, 7.4f * scale, palette.panelMuted(), false); + return; + } + boolean clipped = ScissorFunction.pushRaw(x, y, w, h); + for (int i = 0; i < entries.size(); i++) { + int col = i % columns; + int row = i / columns; + float cx = x + col * (cardW + gap); + float cy = y + row * stepY + state.smoothedScroll; + if (cy + cardH < y || cy > y + h) continue; + String name = entries.get(i); + RelationPlayerCardComponent.CardHit hit = state.cardComponent.render(name, cx, cy, cardW, cardH, mx, my, scale, palette); + state.cardHits.add(new RelationsState.CardEntryHit(name, hit)); + } + if (clipped) ScissorFunction.pop(); + scroll.renderScrollbar(state, x, y, w, h, maxScroll, scale, palette); + } + + void drawSegment(RelationsState.Rect rect, String label, boolean active, float hover, float scale, SettingsGuiPalette palette) { + float size = 7.5f * scale; + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), label, size); + float th = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), size); + int color = active ? palette.panelText() : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.20f * hover); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + (rect.h() - th) * 0.5f, size, color, false); + } + + void drawToolbarSeparator(float x, float y, float h, float scale, SettingsGuiPalette palette) { + float backingW = Math.max(1.25f * scale, 1.05f); + float backingH = h - 4.4f * scale; + float backingY = y + (h - backingH) * 0.5f; + int backingTop = SettingsGuiPalette.withAlpha(palette.panelBgLeft(), 98); + int backingBottom = SettingsGuiPalette.withAlpha(palette.panelBgRight(), 78); + LayoutRender2D.roundedQuad(x - backingW * 0.5f, backingY, backingW, backingH, backingW * 0.5f, backingTop, backingTop, backingBottom, backingBottom); + float lineW = Math.max(0.5f * scale, 0.75f); + float lineH = h - 6.0f * scale; + float lineY = y + (h - lineH) * 0.5f; + int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerStart(), palette.menuCategorySelectedLeft(), 0.34f), 146); + int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.40f), 210); + int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.panelText(), 0.24f), 136); + LayoutRender2D.roundedQuad(x - lineW * 0.5f, lineY, lineW, lineH, lineW * 0.5f, top, mid, bottom, top); + } + + void drawInput(RelationsState.Rect rect, String value, String placeholder, boolean active, float mx, float my, float scale, SettingsGuiPalette palette) { + boolean hover = rect.contains(mx, my); + int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), hover || active ? 0.20f : 0.06f); + int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), hover || active ? 0.18f : 0.06f); + LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, bgA, bgB, bgB, bgA); + LayoutRender2D.roundedStrokeQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, 0.45f * scale, active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerStart(), 0.75f), active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.75f), active ? palette.moduleDividerEnd() : LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.65f), active ? palette.moduleDividerStart() : LayoutRender2D.alpha(palette.moduleDividerStart(), 0.75f)); + boolean empty = value == null || value.isEmpty(); + String raw = empty ? (active ? "" : placeholder) : value; + int color = empty && !active ? palette.panelMuted() : palette.panelText(); + float size = 6.8f * scale; + String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), raw, size, rect.w() - 8f * scale); + float textY = rect.y() + (rect.h() - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterRegular(), size)) * 0.5f; + if (!text.isEmpty()) ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, rect.x() + 4f * scale, textY, size, color, false); + if (active && (System.currentTimeMillis() / 500L) % 2L == 0L) { + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), text, size); + LayoutRender2D.rect(rect.x() + Math.min(tw + 5.5f * scale, rect.w() - 3f * scale), rect.y() + 3f * scale, 0.6f * scale, rect.h() - 6f * scale, palette.panelText()); + } + } + + void drawIconButton(RelationsState.Rect rect, String icon, float hover, boolean disabled, float scale, SettingsGuiPalette palette) { + int bgA = disabled ? LayoutRender2D.alpha(palette.panelPillBase(), 0.42f) : SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.12f + hover * 0.22f); + int bgB = disabled ? LayoutRender2D.alpha(palette.panelPillBase(), 0.32f) : SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.12f + hover * 0.24f); + LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * scale, bgA, bgB, bgB, bgA); + int iconColor = disabled ? LayoutRender2D.alpha(palette.panelMuted(), 0.58f) : palette.menuCategoryText(); + float iconSize = Math.min(rect.w(), rect.h()) - 7f * scale; + Renderer2D.COLOR.svg(icon, rect.x() + (rect.w() - iconSize) * 0.5f, rect.y() + (rect.h() - iconSize) * 0.5f, iconSize, iconSize, SvgRenderOptions.overrideColor(iconColor)); + } + + void renderStatus(RelationsState state, float x, float y, float w, float scale, SettingsGuiPalette palette) { + if (state.statusMessage == null || state.statusMessage.isBlank()) return; + if (System.currentTimeMillis() > state.statusUntilMs) { + state.clearStatus(); + return; + } + float size = 6.8f * scale; + String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), state.statusMessage, size, w - 8f * scale); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x + 4f * scale, y + 2.6f * scale, size, palette.panelMuted(), false); + } + + float updateHover(float current, boolean hovered, float dt) { + return AnimationUtility.snap(AnimationUtility.approach(current, hovered ? 1f : 0f, dt, 12f), hovered ? 1f : 0f, 0.01f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsScroll.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsScroll.java new file mode 100644 index 0000000..68e9922 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsScroll.java @@ -0,0 +1,61 @@ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +final class RelationsScroll { + void renderScrollbar(RelationsState state, float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette) { + updateMetrics(state, x, y, w, h, maxScroll, scale); + if (!state.scrollbarVisible) return; + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + if (isHovered(state, mx, my) || state.draggingScrollbar) SystemCursor.set(SystemCursor.CursorType.SCROLL); + LayoutRender2D.roundedQuad(state.scrollbarX, state.scrollbarY, state.scrollbarW, state.scrollbarH, 2f * scale, palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); + LayoutRender2D.roundedQuad(state.scrollbarX, state.scrollbarThumbY, state.scrollbarW, state.scrollbarThumbH, 2f * scale, palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); + } + + void updateMetrics(RelationsState state, float x, float y, float w, float h, float maxScroll, float scale) { + state.scrollbarVisible = maxScroll > 0.5f; + state.scrollbarMaxScroll = Math.max(0f, maxScroll); + if (!state.scrollbarVisible) { + state.draggingScrollbar = false; + state.scrollbarX = state.scrollbarY = state.scrollbarW = state.scrollbarH = state.scrollbarThumbY = state.scrollbarThumbH = 0f; + return; + } + state.scrollbarW = 3f * scale; + state.scrollbarX = x + w - state.scrollbarW - 2f * scale; + state.scrollbarY = y + 3f * scale; + state.scrollbarH = Math.max(1f, h - 6f * scale); + state.scrollbarThumbH = Math.max(18f * scale, state.scrollbarH * (state.scrollbarH / (state.scrollbarMaxScroll + state.scrollbarH))); + float ratio = state.scrollbarMaxScroll <= 0f ? 0f : (-state.smoothedScroll / state.scrollbarMaxScroll); + state.scrollbarThumbY = state.scrollbarY + (state.scrollbarH - state.scrollbarThumbH) * AnimationUtility.clamp(ratio, 0f, 1f); + } + + boolean isHovered(RelationsState state, float mx, float my) { + if (!state.scrollbarVisible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, state.scrollbarX - pad, state.scrollbarY - pad, state.scrollbarW + pad * 2f, state.scrollbarH + pad * 2f); + } + + void scrollToMouse(RelationsState state, float my) { + if (!state.scrollbarVisible || state.scrollbarMaxScroll <= 0f) return; + float span = state.scrollbarH - state.scrollbarThumbH; + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - state.scrollbarDragOffset, state.scrollbarY, state.scrollbarY + span); + float ratio = (thumbTop - state.scrollbarY) / span; + state.scroll = -state.scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); + } + + boolean press(RelationsState state, float mx, float my, int button) { + if (button != 0 || !isHovered(state, mx, my)) return false; + state.draggingScrollbar = true; + state.scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, state.scrollbarX, state.scrollbarThumbY, state.scrollbarW, state.scrollbarThumbH) ? my - state.scrollbarThumbY : state.scrollbarThumbH * 0.5f; + scrollToMouse(state, my); + return true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsState.java new file mode 100644 index 0000000..a9daf84 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/relations/RelationsState.java @@ -0,0 +1,175 @@ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.module.modules.misc.DefineTarget; +import dev.loki.lovisual.features.relations.PlayerRelations; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +final class RelationsState { + final RelationPlayerCardComponent cardComponent = new RelationPlayerCardComponent(); + final OnlineRelationPlayerPickerComponent onlinePicker; + final List cardHits = new ArrayList<>(); + + RelationTab tab = RelationTab.FRIENDS; + ActiveField activeField = ActiveField.NONE; + String playerInput = ""; + String statusMessage; + long statusUntilMs; + + Rect friendsPill = new Rect(0f, 0f, 0f, 0f); + Rect enemiesPill = new Rect(0f, 0f, 0f, 0f); + Rect staffPill = new Rect(0f, 0f, 0f, 0f); + Rect playerInputRect = new Rect(0f, 0f, 0f, 0f); + Rect addTypedButton = new Rect(0f, 0f, 0f, 0f); + Rect reservedPickerButton = new Rect(0f, 0f, 0f, 0f); + + float scroll; + float smoothedScroll; + boolean scrollbarVisible; + boolean draggingScrollbar; + float scrollbarX; + float scrollbarY; + float scrollbarW; + float scrollbarH; + float scrollbarThumbY; + float scrollbarThumbH; + float scrollbarMaxScroll; + float scrollbarDragOffset; + float listX; + float listY; + float listW; + float listH; + float modeAnim; + float friendsHoverAnim; + float enemiesHoverAnim; + float staffHoverAnim; + float addHoverAnim; + float pickerHoverAnim; + boolean movementInputBlocked; + + final long STATUS_DURATION_MS = 2200L; + final String I18N = "clickgui.settings.relations."; + + RelationsState() { + onlinePicker = new OnlineRelationPlayerPickerComponent(); + } + + void bindStatusSink() { + onlinePicker.setStatusSink(this::setStatus); + } + + void resetScroll() { + scroll = 0f; + smoothedScroll = 0f; + draggingScrollbar = false; + activeField = ActiveField.NONE; + onlinePicker.close(); + onlinePicker.resetScroll(); + movementInputBlocked = false; + } + + boolean isMovementInputBlocked() { + return movementInputBlocked; + } + + void updateMovementBlocked() { + movementInputBlocked = activeField != ActiveField.NONE || onlinePicker.blocksMovementInput(); + } + + void setStatus(String message) { + statusMessage = message; + statusUntilMs = System.currentTimeMillis() + STATUS_DURATION_MS; + } + + void clearStatus() { + statusMessage = null; + statusUntilMs = 0L; + } + + String tr(String key, String fallback, Object... args) { + return ClickGuiI18n.tr(I18N + key, fallback, args); + } + + java.util.List filteredEntries(java.util.List entries) { + if (!dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch.isActive() || dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch.getText().isBlank()) return entries; + String q = dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch.getText().toLowerCase(java.util.Locale.ROOT); + java.util.List out = new java.util.ArrayList<>(); + for (String entry : entries) { + if (entry != null && entry.toLowerCase(java.util.Locale.ROOT).contains(q)) out.add(entry); + } + return out; + } + + enum ActiveField { + NONE, + PLAYER + } + + enum RelationTab { + FRIENDS, + ENEMIES, + STAFF; + + List entries() { + PlayerRelations rel = PlayerRelations.get(); + return switch (this) { + case FRIENDS -> sorted(rel.getFriends()); + case ENEMIES -> sorted(rel.getEnemies()); + case STAFF -> sorted(rel.getStaff()); + }; + } + + boolean add(String name) { + PlayerRelations rel = PlayerRelations.get(); + boolean changed = switch (this) { + case FRIENDS -> rel.addFriend(name); + case ENEMIES -> rel.addEnemy(name); + case STAFF -> rel.addStaff(name); + }; + if (changed) rel.save(); + return changed; + } + + boolean remove(String name) { + PlayerRelations rel = PlayerRelations.get(); + boolean changed = switch (this) { + case FRIENDS -> rel.removeFriend(name); + case ENEMIES -> rel.removeEnemy(name); + case STAFF -> rel.removeStaff(name); + }; + if (changed) rel.save(); + return changed; + } + + DefineTarget.RelationTargetMode pickerMode() { + return switch (this) { + case FRIENDS -> DefineTarget.RelationTargetMode.FRIEND; + case ENEMIES -> DefineTarget.RelationTargetMode.ENEMY; + case STAFF -> DefineTarget.RelationTargetMode.STAFF; + }; + } + + List sorted(Set src) { + List out = new ArrayList<>(); + if (src != null) { + for (String value : src) { + if (value != null && !value.isBlank()) out.add(value); + } + } + out.sort(String.CASE_INSENSITIVE_ORDER); + return out; + } + } + + record CardEntryHit(String name, RelationPlayerCardComponent.CardHit hit) {} + + record Rect(float x, float y, float w, float h) { + boolean contains(float mx, float my) { + return ClickGuiMath.insideRect(mx, my, x, y, w, h); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeCardRenderer.java new file mode 100644 index 0000000..f55e72d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeCardRenderer.java @@ -0,0 +1,199 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme; + +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Card drawing for {@link ThemeComponent}: theme picker card, delete-button, + * "add theme" placeholder and the little gradient-chip strip that previews the + * palette. State (per-swatch hover animation) is threaded in via the caller so + * the class itself remains stateless. + */ +final class ThemeCardRenderer { + + /** Text of the "+ new theme" placeholder card. */ + static final String ADD_CARD_LABEL = "Добавить тему"; + /** Number of preview chips drawn under each card title. */ + static final int SWATCH_COUNT = 12; + private static final float CARD_RADIUS = 5f; + private static final float SELECTED_EPSILON = 0.001f; + + /** Reused across frames so {@link #drawSwatches} avoids three per-card allocations; single-threaded render, always refilled. */ + private static final Themes.GradientSpec[] GRADIENTS = new Themes.GradientSpec[SWATCH_COUNT]; + private static final int[] CHIP_START = new int[SWATCH_COUNT]; + private static final int[] CHIP_END = new int[SWATCH_COUNT]; + + private ThemeCardRenderer() { } + + /** Theme entries matching the current search box (or all of them). */ + static List filteredEntries() { + List all = Theme.themes(); + if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) return all; + List out = new ArrayList<>(); + for (Themes.ThemeEntry entry : all) { + if (ClickGuiSearch.matches(entry.name()) || ClickGuiSearch.matches(entry.getId())) out.add(entry); + } + return out; + } + + /** Whether the "+ new theme" card matches the current search filter. */ + static boolean shouldShowAddCard() { + if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) return true; + return ClickGuiSearch.matches(ADD_CARD_LABEL); + } + + /** Delete-button geometry, shared between the drawer and the facade's hit rects. */ + static float deleteButtonSize(float scale) { return 12f * scale; } + static float deleteButtonX(float x, float w, float scale) { return x + w - 16f * scale; } + static float deleteButtonY(float y, float scale) { return y + 2.2f * scale; } + + /** Paint a single theme card, including its selection ring and swatch strip. */ + static void drawThemeCard(Themes.ThemeEntry entry, + float x, float y, float w, float h, + float mx, float my, float scale, + SettingsGuiPalette palette, + float selectedAnim, + Map swatchHoverByKey) { + Themes.Theme t = entry.theme(); + float selected = AnimationUtility.easeInOutCubic(selectedAnim); + if (selected > SELECTED_EPSILON) { + LayoutRender2D.roundedSoftShadow(x - 0.8f * scale, y - 0.8f * scale, w + 1.6f * scale, h + 1.6f * scale, + 5.8f * scale, (5.5f + 2.5f * selected) * scale, 0.025f * selected, + SettingsGuiPalette.withAlpha(t.accent(), Math.round(72f * selected))); + } + ClickGuiRenderer.drawBlur(x, y, w, h, CARD_RADIUS * scale, 0xFF000000, 200f / 255f); + LayoutRender2D.roundedQuad(x, y, w, h, CARD_RADIUS * scale, + palette.moduleCardTop(), palette.moduleCardTopStrong(), + palette.moduleCardBottom(), palette.moduleCardBottomStrong()); + if (selected > SELECTED_EPSILON) drawSelectedOverlay(t, x, y, w, h, scale, selected); + LayoutRender2D.rectQuad(x, y + 15.5f * scale, w, 0.5f * scale, + SettingsGuiPalette.mix(palette.moduleDividerStart(), SettingsGuiPalette.withAlpha(t.accent(), 210), selected * 0.78f), + SettingsGuiPalette.mix(palette.moduleDividerEnd(), SettingsGuiPalette.withAlpha(t.accentSoft(), 190), selected * 0.72f), + SettingsGuiPalette.mix(palette.moduleDividerEnd(), SettingsGuiPalette.withAlpha(t.accentSoft(), 145), selected * 0.58f), + SettingsGuiPalette.mix(palette.moduleDividerStart(), SettingsGuiPalette.withAlpha(t.accent(), 175), selected * 0.68f)); + + boolean custom = !entry.builtin(); + float titleSize = 9.2f * scale; + String title = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), entry.name(), titleSize, w - (custom ? 40f : 12f) * scale); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), title, x + 6f * scale, y + 3.8f * scale, titleSize, + SettingsGuiPalette.mix(0xFFFFFFFF, SettingsGuiPalette.withAlpha(t.accent(), 255), selected * 0.24f), false); + if (custom) { + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), title, titleSize); + float iconX = Math.min(x + w - 28f * scale, x + 7f * scale + tw + 3f * scale); + Renderer2D.COLOR.svg("paintbrush", iconX, y + 5.3f * scale, 7f * scale, 7f * scale, + SvgRenderOptions.overrideColor(SettingsGuiPalette.withAlpha(t.accent(), 230))); + drawCardDeleteButton(x, y, w, mx, my, scale, palette); + } + float padX = 6f * scale; + float chipsY = y + 20f * scale; + float chipsH = Math.max(2f * scale, y + h - 5f * scale - chipsY); + drawSwatches(entry, t, x + padX, chipsY, w - padX * 2f, chipsH, scale, mx, my, swatchHoverByKey); + } + + /** Rounded fill + stroke overlay tinted by the theme accent, faded in via {@code selected}. */ + private static void drawSelectedOverlay(Themes.Theme t, float x, float y, float w, float h, float scale, float selected) { + LayoutRender2D.roundedQuad(x, y, w, h, CARD_RADIUS * scale, + SettingsGuiPalette.withAlpha(t.accent(), Math.round(82f * selected)), + SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(48f * selected)), + SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(24f * selected)), + SettingsGuiPalette.withAlpha(t.accent(), Math.round(38f * selected))); + LayoutRender2D.roundedStrokeQuad(x + 0.35f * scale, y + 0.35f * scale, w - 0.7f * scale, h - 0.7f * scale, + 4.7f * scale, (0.55f + 0.35f * selected) * scale, + SettingsGuiPalette.withAlpha(t.accent(), Math.round(235f * selected)), + SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(190f * selected)), + SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(115f * selected)), + SettingsGuiPalette.withAlpha(t.accent(), Math.round(180f * selected))); + } + + /** Row of 12 tiny chips previewing the theme's key colours, with hover scale animation. */ + private static void drawSwatches(Themes.ThemeEntry entry, Themes.Theme t, + float cx, float cy, float cw, float ch, + float scale, float mx, float my, + Map swatchHoverByKey) { + fillSwatchPalette(entry, t); + int columns = SWATCH_COUNT; + float minGap = scale; + float chipSize = Math.max(6f * scale, Math.min(Math.min(10.2f * scale, ch), (cw - minGap * (columns - 1)) / columns)); + float chipGap = Math.max(minGap, (cw - chipSize * columns) / (columns - 1)); + float hoverBoost = 0.75f * scale; + float yCenterOffset = (ch - chipSize) * 0.5f; + for (int i = 0; i < SWATCH_COUNT; i++) { + float sx = cx + (i % columns) * (chipSize + chipGap); + float sy = cy + yCenterOffset + (i / columns) * (chipSize + chipGap); + boolean hover = ClickGuiMath.insideRect(mx, my, sx, sy, chipSize, chipSize); + String key = entry.getId() + "#" + i; + float anim = AnimationUtility.approach(swatchHoverByKey.getOrDefault(key, 0f), hover ? 1f : 0f, 0.20f); + anim = AnimationUtility.snap(anim, hover ? 1f : 0f, 0.01f); + swatchHoverByKey.put(key, anim); + float size = chipSize + hoverBoost * anim; + float pad = (size - chipSize) * 0.5f; + drawThemeGradientRect(sx - pad, sy - pad, size, size, 2.3f * scale, GRADIENTS[i], CHIP_START[i], CHIP_END[i]); + } + } + + /** Re-fill the class-level chip buffers from the given theme; called once per visible card. */ + private static void fillSwatchPalette(Themes.ThemeEntry entry, Themes.Theme t) { + Arrays.fill(GRADIENTS, null); + GRADIENTS[0] = entry.windowGradient(); GRADIENTS[1] = entry.cardGradient(); + CHIP_START[0] = t.windowBg(); CHIP_START[1] = t.cardEnabled(); CHIP_START[2] = t.windowHeader(); + CHIP_START[3] = t.surface(); CHIP_START[4] = t.surfaceHover(); CHIP_START[5] = t.accent(); + CHIP_START[6] = t.accentSoft(); CHIP_START[7] = t.windowStroke(); CHIP_START[8] = t.strokeSoft(); + CHIP_START[9] = t.textPrimary(); CHIP_START[10] = t.textMuted(); CHIP_START[11] = t.windowBg(); + CHIP_END[0] = t.windowHeader(); CHIP_END[1] = t.accentSoft(); CHIP_END[2] = t.windowStroke(); + CHIP_END[3] = t.surfaceHover(); CHIP_END[4] = t.windowHeader(); CHIP_END[5] = t.accentSoft(); + CHIP_END[6] = t.strokeSoft(); CHIP_END[7] = t.strokeSoft(); CHIP_END[8] = t.textMuted(); + CHIP_END[9] = t.textMuted(); CHIP_END[10] = t.surface(); CHIP_END[11] = t.accentSoft(); + } + + /** Rounded rect filled with either the theme's gradient or a two-stop fallback. */ + private static void drawThemeGradientRect(float x, float y, float w, float h, float radius, + Themes.GradientSpec gradient, int fallbackStart, int fallbackEnd) { + boolean useGradient = gradient != null && gradient.enabled(); + int start = SettingsGuiPalette.withAlpha(useGradient ? gradient.start() : fallbackStart, 220); + int end = SettingsGuiPalette.withAlpha(useGradient ? gradient.end() : fallbackEnd, 220); + ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, radius, start, end, useGradient ? gradient.angleDeg() : 0f); + } + + /** Small red "trash" affordance shown only on custom theme cards. */ + private static void drawCardDeleteButton(float x, float y, float w, float mx, float my, float scale, SettingsGuiPalette palette) { + float size = deleteButtonSize(scale); + float bx = deleteButtonX(x, w, scale); + float by = deleteButtonY(y, scale); + boolean hover = ClickGuiMath.insideRect(mx, my, bx, by, size, size); + int bg = hover ? SettingsGuiPalette.withAlpha(0xFFE5484D, 64) : SettingsGuiPalette.withAlpha(palette.moduleCardBottomStrong(), 90); + int stroke = hover ? SettingsGuiPalette.withAlpha(0xFFFF6B6F, 160) : SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 80); + LayoutRender2D.roundedQuad(bx, by, size, size, 3.5f * scale, bg, bg, bg, bg); + LayoutRender2D.roundedStroke(bx, by, size, size, 3.5f * scale, 0.45f * scale, stroke); + Renderer2D.COLOR.svg("trash-2", bx + 2.4f * scale, by + 2.3f * scale, 7.2f * scale, 7.2f * scale, + SvgRenderOptions.overrideColor(SettingsGuiPalette.withAlpha(hover ? 0xFFFFC4C7 : 0xFFFFFFFF, hover ? 245 : 160))); + } + + /** Dashed-looking placeholder card that opens the "create theme" flow when clicked. */ + static void drawAddCard(float x, float y, float w, float h, float scale, SettingsGuiPalette palette) { + ClickGuiRenderer.drawBlur(x, y, w, h, CARD_RADIUS * scale, 0xFF000000, 200f / 255f); + LayoutRender2D.roundedQuad(x, y, w, h, CARD_RADIUS * scale, + palette.moduleCardTop(), palette.moduleCardTopStrong(), + palette.moduleCardBottom(), palette.moduleCardBottomStrong()); + float plusSize = 11f * scale; + float plusW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), "+", plusSize); + float plusH = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), plusSize); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "+", x + (w - plusW) * 0.5f, y + 5.5f * scale, plusSize, palette.panelText(), false); + float titleSize = 7.6f * scale; + String title = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), ADD_CARD_LABEL, titleSize, w - 12f * scale); + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), title, titleSize); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), title, x + (w - tw) * 0.5f, y + h - plusH - 10f * scale, titleSize, palette.moduleDescriptionText(), false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java index 2fd31b3..0221b7d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java @@ -1,24 +1,11 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.theme.Theme; import dev.loki.lovisual.features.theme.Themes; import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; import java.util.ArrayList; import java.util.HashMap; @@ -27,96 +14,47 @@ import java.util.List; import java.util.Map; import java.util.Set; +/** + * Settings screen component that lists every installed theme as a card. Owns the + * hit rectangles used for click routing, the per-card selection animation and a + * shared {@link ThemeScrollState}; card visuals themselves live in + * {@link ThemeCardRenderer}. + */ public final class ThemeComponent { - private static final String ADD_CARD_LABEL = "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u0435\u043c\u0443"; private final List hits = new ArrayList<>(); private final Map swatchHoverByKey = new HashMap<>(); private final Map selectedAnimByTheme = new HashMap<>(); private final Set liveThemeIds = new HashSet<>(); - private float scroll; - private float smoothedScroll; - private boolean scrollbarVisible; - private boolean draggingScrollbar; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; - - private static void drawThemeGradientRect(float x, - float y, - float w, - float h, - float radius, - Themes.GradientSpec gradient, - int fallbackStart, - int fallbackEnd) { - if (gradient != null && gradient.enabled()) { - int start = SettingsGuiPalette.withAlpha(gradient.start(), 220); - int end = SettingsGuiPalette.withAlpha(gradient.end(), 220); - ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, radius, start, end, gradient.angleDeg()); - return; - } - int start = SettingsGuiPalette.withAlpha(fallbackStart, 220); - int end = SettingsGuiPalette.withAlpha(fallbackEnd, 220); - ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, radius, start, end, 0f); - } - - private static List filteredEntries() { - List all = Theme.themes(); - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) return all; - List out = new ArrayList<>(); - for (Themes.ThemeEntry entry : all) { - if (ClickGuiSearch.matches(entry.name()) || ClickGuiSearch.matches(entry.getId())) { - out.add(entry); - } - } - return out; - } - - private static boolean shouldShowAddCard() { - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) return true; - return ClickGuiSearch.matches(ADD_CARD_LABEL); - } + private final ThemeScrollState scrollState = new ThemeScrollState(); + /** Reset both raw and smoothed scroll (called when the panel is reopened). */ public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; + scrollState.reset(); } + /** Wheel-scroll handler; ignored unless the cursor is inside the card area. */ public void scroll(float mx, float my, float menuX, float menuY, float menuW, float menuH, double amount, float scale) { float x = menuX + 31f * scale; float y = menuY + 33f * scale; float w = menuW - 42f * scale; float h = menuH - 39f * scale; if (!ClickGuiMath.insideRect(mx, my, x, y, w, h)) return; - scroll += (float) (amount * 22f); + scrollState.addDelta((float) (amount * 22f)); } + /** Primary-button press routed to the scrollbar thumb. Returns true if consumed. */ public boolean mousePressedScrollbar(float mx, float my, int button) { - if (button != 0) return false; - if (!isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; + return scrollState.press(mx, my, button); } public void mouseReleased(int button) { - if (button == 0) { - draggingScrollbar = false; - } + scrollState.release(button); } + /** Translate a click on a previously-recorded hit rect into a {@link ThemeAction}. */ public ThemeAction click(float mx, float my, int button) { - if (isScrollbarHovered(mx, my)) return null; + if (scrollState.hovered(mx, my)) return null; if (button != 0 && button != 1) return null; for (Hit hit : hits) { if (!ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; @@ -129,13 +67,14 @@ public final class ThemeComponent { return null; } + /** Layout + paint the theme grid for one frame. */ public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { hits.clear(); SettingsGuiPalette palette = SettingsGuiPalette.current(); updateSelectedAnimations(Theme.themes()); - List entries = filteredEntries(); - boolean includeAddCard = shouldShowAddCard(); + List entries = ThemeCardRenderer.filteredEntries(); + boolean includeAddCard = ThemeCardRenderer.shouldShowAddCard(); int cardsTotal = entries.size() + (includeAddCard ? 1 : 0); if (cardsTotal <= 0) return; @@ -153,55 +92,46 @@ public final class ThemeComponent { int rows = (cardsTotal + columns - 1) / columns; float contentH = rows * stepY - gap; float maxScroll = Math.max(0f, contentH - areaH); - updateScrollbarMetrics(menuX, menuY, menuH, areaH, contentH, maxScroll, scale); - if (draggingScrollbar) { - scrollToMouse(my); - } - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); + scrollState.updateMetrics(menuX, menuY, menuH, areaH, contentH, maxScroll, scale); + if (scrollState.dragging()) scrollState.scrollToMouse(my); + scrollState.smooth(); + scrollState.updateMetrics(menuX, menuY, menuH, areaH, contentH, maxScroll, scale); + float smoothedScroll = scrollState.smoothed(); + boolean clipped = ScissorFunction.pushRaw(areaX, areaY, areaW, areaH); - int idx = 0; for (Themes.ThemeEntry entry : entries) { - int col = idx % columns; - int row = idx / columns; - float x = areaX + col * (cardW + gap); - float y = areaY + row * stepY + smoothedScroll; - if (y + cardH < areaY || y > areaY + areaH) { - idx++; - continue; - } + float x = areaX + (idx % columns) * (cardW + gap); + float y = areaY + (idx / columns) * stepY + smoothedScroll; + idx++; + if (y + cardH < areaY || y > areaY + areaH) continue; boolean custom = !entry.builtin(); - drawThemeCard(entry, x, y, cardW, cardH, mx, my, scale, palette, selectedAnim(entry.getId())); + ThemeCardRenderer.drawThemeCard(entry, x, y, cardW, cardH, mx, my, scale, palette, selectedAnim(entry.getId()), swatchHoverByKey); if (custom) { - float deleteSize = deleteButtonSize(scale); - hits.add(new Hit(deleteButtonX(x, cardW, scale), deleteButtonY(y, scale), deleteSize, deleteSize, entry.getId(), false, true, true)); + float ds = ThemeCardRenderer.deleteButtonSize(scale); + hits.add(new Hit(ThemeCardRenderer.deleteButtonX(x, cardW, scale), ThemeCardRenderer.deleteButtonY(y, scale), ds, ds, entry.getId(), false, true, true)); } hits.add(new Hit(x, y, cardW, cardH, entry.getId(), false, custom, false)); - idx++; } if (includeAddCard) { - int col = idx % columns; - int row = idx / columns; - float x = areaX + col * (cardW + gap); - float y = areaY + row * stepY + smoothedScroll; + float x = areaX + (idx % columns) * (cardW + gap); + float y = areaY + (idx / columns) * stepY + smoothedScroll; if (!(y + cardH < areaY || y > areaY + areaH)) { - drawAddCard(x, y, cardW, cardH, scale, palette); + ThemeCardRenderer.drawAddCard(x, y, cardW, cardH, scale, palette); hits.add(new Hit(x, y, cardW, cardH, null, true, false, false)); } } if (clipped) ScissorFunction.pop(); - renderScrollbar(menuX, menuY, menuH, areaH, contentH, maxScroll, scale, palette); + scrollState.render(scale, palette); } + /** Approach / snap every theme's selection value towards 0 or 1 and drop stale ids. */ private void updateSelectedAnimations(List entries) { String currentId = Theme.currentId(); float dt = AnimationUtility.deltaTime(); liveThemeIds.clear(); - for (Themes.ThemeEntry entry : entries) { String id = entry.getId(); liveThemeIds.add(id); @@ -211,348 +141,20 @@ public final class ThemeComponent { value = AnimationUtility.snap(value, target, 0.002f); selectedAnimByTheme.put(id, value); } - selectedAnimByTheme.keySet().removeIf(id -> !liveThemeIds.contains(id)); + // Chip keys look like "#"; drop the ones whose owning theme vanished + // so renamed/deleted cards cannot leave entries accumulating in the map forever. + swatchHoverByKey.keySet().removeIf(key -> { + int hash = key.lastIndexOf('#'); + return hash < 0 || !liveThemeIds.contains(key.substring(0, hash)); + }); } private float selectedAnim(String themeId) { return selectedAnimByTheme.getOrDefault(themeId, themeId.equals(Theme.currentId()) ? 1f : 0f); } - private void drawThemeCard(Themes.ThemeEntry entry, - float x, - float y, - float w, - float h, - float mx, - float my, - float scale, - SettingsGuiPalette palette, - float selectedAnim) { - Themes.Theme t = entry.theme(); - float selected = AnimationUtility.easeInOutCubic(selectedAnim); - int selectedAccent = SettingsGuiPalette.withAlpha(t.accent(), Math.round(82f * selected)); - int selectedAccentSoft = SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(48f * selected)); - - if (selected > 0.001f) { - LayoutRender2D.roundedSoftShadow( - x - 0.8f * scale, - y - 0.8f * scale, - w + 1.6f * scale, - h + 1.6f * scale, - 5.8f * scale, - (5.5f + 2.5f * selected) * scale, - 0.025f * selected, - SettingsGuiPalette.withAlpha(t.accent(), Math.round(72f * selected)) - ); - } - - ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 200f / 255f); - LayoutRender2D.roundedQuad( - x, - y, - w, - h, - 5f * scale, - palette.moduleCardTop(), - palette.moduleCardTopStrong(), - palette.moduleCardBottom(), - palette.moduleCardBottomStrong() - ); - if (selected > 0.001f) { - LayoutRender2D.roundedQuad( - x, - y, - w, - h, - 5f * scale, - selectedAccent, - selectedAccentSoft, - SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(24f * selected)), - SettingsGuiPalette.withAlpha(t.accent(), Math.round(38f * selected)) - ); - LayoutRender2D.roundedStrokeQuad( - x + 0.35f * scale, - y + 0.35f * scale, - w - 0.7f * scale, - h - 0.7f * scale, - 4.7f * scale, - (0.55f + 0.35f * selected) * scale, - SettingsGuiPalette.withAlpha(t.accent(), Math.round(235f * selected)), - SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(190f * selected)), - SettingsGuiPalette.withAlpha(t.accentSoft(), Math.round(115f * selected)), - SettingsGuiPalette.withAlpha(t.accent(), Math.round(180f * selected)) - ); - - } - LayoutRender2D.rectQuad( - x, - y + 15.5f * scale, - w, - 0.5f * scale, - SettingsGuiPalette.mix(palette.moduleDividerStart(), SettingsGuiPalette.withAlpha(t.accent(), 210), selected * 0.78f), - SettingsGuiPalette.mix(palette.moduleDividerEnd(), SettingsGuiPalette.withAlpha(t.accentSoft(), 190), selected * 0.72f), - SettingsGuiPalette.mix(palette.moduleDividerEnd(), SettingsGuiPalette.withAlpha(t.accentSoft(), 145), selected * 0.58f), - SettingsGuiPalette.mix(palette.moduleDividerStart(), SettingsGuiPalette.withAlpha(t.accent(), 175), selected * 0.68f) - ); - - boolean custom = !entry.builtin(); - float titleSize = 9.2f * scale; - float titleReserve = custom ? 40f * scale : 12f * scale; - String title = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), entry.name(), titleSize, w - titleReserve); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterMedium(), - title, - x + 6f * scale, - y + 3.8f * scale, - titleSize, - SettingsGuiPalette.mix(0xFFFFFFFF, SettingsGuiPalette.withAlpha(t.accent(), 255), selected * 0.24f), - false - ); - if (custom) { - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), title, titleSize); - float iconSize = 7f * scale; - float iconX = Math.min(x + w - 28f * scale, x + 7f * scale + tw + 3f * scale); - Renderer2D.COLOR.svg("paintbrush", iconX, y + 5.3f * scale, iconSize, iconSize, SvgRenderOptions.overrideColor(SettingsGuiPalette.withAlpha(t.accent(), 230))); - drawCardDeleteButton(x, y, w, mx, my, scale, palette); - } - - float swatchPadX = 6f * scale; - float chipsX = x + swatchPadX; - float chipsY = y + 20f * scale; - float chipsW = w - swatchPadX * 2f; - float chipsBottom = y + h - 5f * scale; - float chipsH = Math.max(2f * scale, chipsBottom - chipsY); - - Themes.GradientSpec[] gradients = new Themes.GradientSpec[]{ - entry.windowGradient(), - entry.cardGradient(), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - }; - int[] colorA = new int[]{ - t.windowBg(), - t.cardEnabled(), - t.windowHeader(), - t.surface(), - t.surfaceHover(), - t.accent(), - t.accentSoft(), - t.windowStroke(), - t.strokeSoft(), - t.textPrimary(), - t.textMuted(), - t.windowBg() - }; - int[] colorB = new int[]{ - t.windowHeader(), - t.accentSoft(), - t.windowStroke(), - t.surfaceHover(), - t.windowHeader(), - t.accentSoft(), - t.strokeSoft(), - t.strokeSoft(), - t.textMuted(), - t.textMuted(), - t.surface(), - t.accentSoft() - }; - - int columns = colorA.length; - float chipSize = Math.min(10.2f * scale, chipsH); - float minGap = scale; - if (columns > 1) { - float maxChipFromWidth = (chipsW - minGap * (columns - 1)) / columns; - chipSize = Math.min(chipSize, maxChipFromWidth); - } - chipSize = Math.max(6f * scale, chipSize); - float chipGap = columns <= 1 ? 0f : Math.max(minGap, (chipsW - chipSize * columns) / (columns - 1)); - float hoverBoost = 0.75f * scale; - float yCenterOffset = (chipsH - chipSize) * 0.5f; - - for (int i = 0; i < colorA.length; i++) { - int col = i % columns; - int row = i / columns; - float cx = chipsX + col * (chipSize + chipGap); - float cy = chipsY + yCenterOffset + row * (chipSize + chipGap); - boolean hover = ClickGuiMath.insideRect(mx, my, cx, cy, chipSize, chipSize); - String key = entry.getId() + "#" + i; - float anim = swatchHoverByKey.getOrDefault(key, 0f); - anim = AnimationUtility.approach(anim, hover ? 1f : 0f, 0.20f); - anim = AnimationUtility.snap(anim, hover ? 1f : 0f, 0.01f); - swatchHoverByKey.put(key, anim); - - float size = chipSize + hoverBoost * anim; - float ox = cx - (size - chipSize) * 0.5f; - float oy = cy - (size - chipSize) * 0.5f; - - drawThemeGradientRect( - ox, - oy, - size, - size, - 2.3f * scale, - gradients[i], - colorA[i], - colorB[i] - ); - - // TODO: Show swatch tooltip on hover (name/value), once UX spec is provided. - } - } - - private static float deleteButtonSize(float scale) { - return 12f * scale; - } - - private static float deleteButtonX(float x, float w, float scale) { - return x + w - 16f * scale; - } - - private static float deleteButtonY(float y, float scale) { - return y + 2.2f * scale; - } - - private void drawCardDeleteButton(float x, - float y, - float w, - float mx, - float my, - float scale, - SettingsGuiPalette palette) { - float size = deleteButtonSize(scale); - float bx = deleteButtonX(x, w, scale); - float by = deleteButtonY(y, scale); - boolean hover = ClickGuiMath.insideRect(mx, my, bx, by, size, size); - int bg = hover - ? SettingsGuiPalette.withAlpha(0xFFE5484D, 64) - : SettingsGuiPalette.withAlpha(palette.moduleCardBottomStrong(), 90); - int stroke = hover - ? SettingsGuiPalette.withAlpha(0xFFFF6B6F, 160) - : SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 80); - LayoutRender2D.roundedQuad(bx, by, size, size, 3.5f * scale, bg, bg, bg, bg); - LayoutRender2D.roundedStroke(bx, by, size, size, 3.5f * scale, 0.45f * scale, stroke); - Renderer2D.COLOR.svg( - "trash-2", - bx + 2.4f * scale, - by + 2.3f * scale, - 7.2f * scale, - 7.2f * scale, - SvgRenderOptions.overrideColor(SettingsGuiPalette.withAlpha(hover ? 0xFFFFC4C7 : 0xFFFFFFFF, hover ? 245 : 160)) - ); - } - - private void drawAddCard(float x, float y, float w, float h, float scale, SettingsGuiPalette palette) { - ClickGuiRenderer.drawBlur(x, y, w, h, 5f * scale, 0xFF000000, 200f / 255f); - LayoutRender2D.roundedQuad( - x, - y, - w, - h, - 5f * scale, - palette.moduleCardTop(), - palette.moduleCardTopStrong(), - palette.moduleCardBottom(), - palette.moduleCardBottomStrong() - ); - - float plusSize = 11f * scale; - float plusW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), "+", plusSize); - float plusH = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), plusSize); - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterMedium(), - "+", - x + (w - plusW) * 0.5f, - y + 5.5f * scale, - plusSize, - palette.panelText(), - false - ); - - float titleSize = 7.6f * scale; - String title = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), ADD_CARD_LABEL, titleSize, w - 12f * scale); - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), title, titleSize); - float ty = y + h - plusH - 10f * scale; - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - title, - x + (w - tw) * 0.5f, - ty, - titleSize, - palette.moduleDescriptionText(), - false - ); - } - - private void renderScrollbar(float menuX, float menuY, float menuH, float areaH, float contentH, float maxScroll, float scale, SettingsGuiPalette palette) { - updateScrollbarMetrics(menuX, menuY, menuH, areaH, contentH, maxScroll, scale); - if (!scrollbarVisible) return; - - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - boolean hover = isScrollbarHovered(mx, my); - if (hover || draggingScrollbar) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - - LayoutRender2D.roundedQuad( - scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - palette.moduleScrollTrackA(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackA() - ); - LayoutRender2D.roundedQuad( - scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - palette.moduleScrollHandleA(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleA() - ); - } - - private void updateScrollbarMetrics(float menuX, float menuY, float menuH, float areaH, float contentH, float maxScroll, float scale) { - scrollbarVisible = maxScroll > 0f && contentH > areaH + 0.5f; - scrollbarMaxScroll = Math.max(0f, maxScroll); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - - scrollbarW = 4f * scale; - scrollbarX = menuX + 400f * scale - 35f * scale - scrollbarW + 50f * scale; - scrollbarY = menuY + 33f * scale; - scrollbarH = menuH - 58f * scale; - scrollbarThumbH = Math.max(20f * scale, scrollbarH * (areaH / contentH)); - float ratio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - + /** Kinds of interaction the caller can respond to. */ public enum ThemeActionType { SELECT, ADD, @@ -560,9 +162,11 @@ public final class ThemeComponent { DELETE } + /** Result of a click on one of the recorded hit rects. */ public record ThemeAction(ThemeActionType type, String themeId) { } + /** Screen rectangle registered by {@link #render} for the next {@link #click} call. */ private record Hit(float x, float y, float w, float h, String themeId, boolean addCard, boolean custom, boolean deleteButton) { } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java index e0bd23f..3f542ed 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java @@ -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.gui.clickgui.layout.screen.settings.implement.theme; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeScrollState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeScrollState.java new file mode 100644 index 0000000..82e4668 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeScrollState.java @@ -0,0 +1,123 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +/** + * Scroll + scrollbar state for {@link ThemeComponent}: raw and smoothed offsets, + * thumb geometry, drag handling and the on-screen track/thumb painting. Kept + * separate so the component facade stays focused on card layout and hit routing. + */ +final class ThemeScrollState { + + private float scroll; + private float smoothedScroll; + private boolean visible; + private boolean dragging; + private float x; + private float y; + private float w; + private float h; + private float thumbY; + private float thumbH; + private float maxScroll; + private float dragOffset; + + /** Reset both raw and smoothed scroll plus the drag flag. */ + void reset() { + scroll = 0f; + smoothedScroll = 0f; + dragging = false; + } + + /** Add a wheel delta expressed in pixels. */ + void addDelta(float amount) { + scroll += amount; + } + + /** Begin a thumb drag if the cursor is over the scrollbar. Returns true if consumed. */ + boolean press(float mx, float my, int button) { + if (button != 0) return false; + if (!hovered(mx, my)) return false; + dragging = true; + dragOffset = ClickGuiMath.insideRect(mx, my, x, thumbY, w, thumbH) ? my - thumbY : thumbH * 0.5f; + scrollToMouse(my); + return true; + } + + /** End a thumb drag on primary-button release. */ + void release(int button) { + if (button == 0) dragging = false; + } + + /** Recompute thumb geometry from the current layout; call after {@link #smooth()}. */ + void updateMetrics(float menuX, float menuY, float menuH, float areaH, float contentH, float maxScroll, float scale) { + visible = maxScroll > 0f && contentH > areaH + 0.5f; + this.maxScroll = Math.max(0f, maxScroll); + if (!visible) { + dragging = false; + x = y = w = h = thumbY = thumbH = 0f; + return; + } + w = 4f * scale; + x = menuX + 400f * scale - 35f * scale - w + 50f * scale; + y = menuY + 33f * scale; + h = menuH - 58f * scale; + thumbH = Math.max(20f * scale, h * (areaH / contentH)); + float ratio = this.maxScroll <= 0f ? 0f : (-smoothedScroll / this.maxScroll); + thumbY = y + (h - thumbH) * AnimationUtility.clamp(ratio, 0f, 1f); + } + + /** Clamp the raw offset and animate the smoothed value towards it. */ + void smooth() { + scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); + smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, dragging ? 0.55f : 0.2f); + smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, dragging ? 0.01f : 0.05f); + } + + /** Whether the pointer is close enough to the scrollbar strip to interact with it. */ + boolean hovered(float mx, float my) { + if (!visible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, x - pad, y - pad, w + pad * 2f, h + pad * 2f); + } + + /** Jump the raw scroll so the thumb sits under {@code my}. */ + void scrollToMouse(float my) { + if (!visible || maxScroll <= 0f) return; + float span = h - thumbH; + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - dragOffset, y, y + span); + scroll = -maxScroll * AnimationUtility.clamp((thumbTop - y) / span, 0f, 1f); + } + + /** Paint the track + thumb (and toggle the scroll cursor while hovering). */ + void render(float scale, SettingsGuiPalette palette) { + if (!visible) return; + if (hovered(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY()) || dragging) { + SystemCursor.set(SystemCursor.CursorType.SCROLL); + } + LayoutRender2D.roundedQuad(x, y, w, h, 2f * scale, + palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), + palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); + LayoutRender2D.roundedQuad(x, thumbY, w, thumbH, 2f * scale, + palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), + palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); + } + + float smoothed() { + return smoothedScroll; + } + + boolean dragging() { + return dragging; + } + + boolean isVisible() { + return visible; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenInput.java new file mode 100644 index 0000000..754d969 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenInput.java @@ -0,0 +1,141 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeComponent; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.theme.EditableClickGuiTheme; +import dev.loki.lovisual.features.theme.Theme; +import org.lwjgl.glfw.GLFW; +import java.nio.file.Path; +import java.util.List; + +final class MenuScreenInput { + boolean isHintToggle(int keyCode, int modifiers) { + return keyCode == GLFW.GLFW_KEY_H && (modifiers & GLFW.GLFW_MOD_ALT) != 0; + } + boolean mouseClicked(MenuScreenState state, float mx, float my, int button) { + if (!state.isInteractive() || (button != 0 && button != 1)) return false; + if (state.activeThemeEditor != null && state.themeEditorPreview.mouseClicked(state.activeThemeEditor, mx, my, button)) { + state.settingsPanel.close(); + state.activeThemeEditor = null; + state.themeEditorPreview.reset(); + return true; + } + if (state.settingsPanel.mouseClicked(mx, my, button, state.s)) return true; + if (state.settingsPanel.blocksMenuInput()) return true; + if (state.searchComponent.click(state.searchX, state.searchY, 80f * state.s, 15f * state.s, mx, my, button)) return true; + MenuScreen.Category hit = state.categoryContainer.click(state.categoryX, state.categoryY, mx, my, state.s); + if (hit != null) { + if (hit != state.category) { + state.category = hit; + state.moduleComponent.resetScroll(); + state.themeComponent.resetScroll(); + state.mainSettingsComponent.resetScroll(); + state.relationsComponent.resetScroll(); + state.configProfilesComponent.resetScroll(); + } + return true; + } + if (state.category == MenuScreen.Category.THEMES) { + if (state.themeComponent.mousePressedScrollbar(mx, my, button)) return true; + ThemeComponent.ThemeAction action = state.themeComponent.click(mx, my, button); + if (action == null) return false; + handleThemeAction(state, action); + return true; + } + if (state.category == MenuScreen.Category.CONFIGS) { + if (state.configProfilesComponent.mousePressedScrollbar(mx, my, button)) return true; + return state.configProfilesComponent.click(mx, my, button); + } + if (state.category == MenuScreen.Category.MAIN_SETTINGS) { + if (state.mainSettingsComponent.mousePressedScrollbar(mx, my, button)) return true; + return state.mainSettingsComponent.mouseClicked(mx, my, button); + } + if (state.category == MenuScreen.Category.RELATIONS) { + if (state.relationsComponent.mousePressedScrollbar(mx, my, button)) return true; + return state.relationsComponent.click(mx, my, button); + } + if (state.moduleComponent.mousePressedScrollbar(mx, my, button)) return true; + ModuleComponent.CardAction action = state.moduleComponent.click(mx, my, button); + if (action == null) return false; + if (action.toggleable() && (action.statusClick() || action.leftClick())) toggleEntry(action.getId()); + if (action.rightClick()) openSettings(state, action.getId()); + return true; + } + void mouseReleased(MenuScreenState state, float mx, float my, int button) { + if (!state.isInteractive()) return; + state.settingsPanel.mouseReleased(mx, my, button); + state.themeComponent.mouseReleased(button); + state.mainSettingsComponent.mouseReleased(mx, my, button); + state.relationsComponent.mouseReleased(button); + state.configProfilesComponent.mouseReleased(button); + state.moduleComponent.mouseReleased(button); + } + void mouseScrolled(MenuScreenState state, float mx, float my, double delta) { + if (!state.isInteractive()) return; + if (state.settingsPanel.mouseScrolled(mx, my, delta)) return; + if (state.settingsPanel.blocksMenuInput()) return; + if (state.category == MenuScreen.Category.THEMES) { + state.themeComponent.scroll(mx, my, state.menuX, state.menuY, state.menuW, state.menuH, delta, state.s); + return; + } + if (state.category == MenuScreen.Category.MAIN_SETTINGS) { state.mainSettingsComponent.scroll(mx, my, delta); return; } + if (state.category == MenuScreen.Category.RELATIONS) { state.relationsComponent.scroll(mx, my, delta); return; } + if (state.category == MenuScreen.Category.CONFIGS) { + state.configProfilesComponent.scroll(mx, my, state.menuX, state.menuY, state.menuW, state.menuH, delta, state.s); + return; + } + state.moduleComponent.scroll(mx, my, state.menuX, state.menuY, state.menuW, state.menuH, delta, state.s); + } + boolean keyPressed(MenuScreenState state, int keyCode, int scanCode, int modifiers) { + if (!state.isInteractive()) return false; + if (state.settingsPanel.isActive() && state.settingsPanel.keyPressed(keyCode, scanCode, modifiers)) return true; + if (isHintToggle(keyCode, modifiers)) { + MainConfig config = MainConfig.get(); + config.setClickGuiHintsEnabled(!config.isClickGuiHintsEnabled()); + return true; + } + if (state.category == MenuScreen.Category.MAIN_SETTINGS && state.mainSettingsComponent.keyPressed(keyCode, scanCode, modifiers)) return true; + if (state.category == MenuScreen.Category.CONFIGS && state.configProfilesComponent.keyPressed(keyCode, scanCode, modifiers)) return true; + if (state.category == MenuScreen.Category.RELATIONS && state.relationsComponent.keyPressed(keyCode, scanCode, modifiers)) return true; + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + if (ctrl && keyCode == GLFW.GLFW_KEY_F) { ClickGuiSearch.setActive(true); return true; } + if (ClickGuiSearch.isActive()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { ClickGuiSearch.deactivate(); return true; } + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { ClickGuiSearch.backspace(); return true; } + return true; + } + return false; + } + boolean charTyped(MenuScreenState state, char chr, int modifiers) { + if (!state.isInteractive()) return false; + if (state.category == MenuScreen.Category.MAIN_SETTINGS && state.mainSettingsComponent.charTyped(chr, modifiers)) return true; + if (state.category == MenuScreen.Category.CONFIGS && state.configProfilesComponent.charTyped(chr, modifiers)) return true; + if (state.category == MenuScreen.Category.RELATIONS && state.relationsComponent.charTyped(chr, modifiers)) return true; + if (ClickGuiSearch.isActive()) { ClickGuiSearch.append(chr); return true; } + return state.settingsPanel.charTyped(chr, modifiers); + } + boolean onFilesDrop(MenuScreenState state, List paths) { + if (!state.isInteractive() || state.category != MenuScreen.Category.CONFIGS) return false; + return state.configProfilesComponent.importDroppedFiles(paths); + } + private void handleThemeAction(MenuScreenState state, ThemeComponent.ThemeAction action) { + if (action.type() == ThemeComponent.ThemeActionType.SELECT && action.themeId() != null) { Theme.setTheme(action.themeId()); return; } + if (action.type() == ThemeComponent.ThemeActionType.ADD) { openThemeEditor(state, EditableClickGuiTheme.createFromCurrent()); return; } + if (action.type() == ThemeComponent.ThemeActionType.EDIT && action.themeId() != null) { openThemeEditor(state, EditableClickGuiTheme.existing(action.themeId())); return; } + if (action.type() == ThemeComponent.ThemeActionType.DELETE && action.themeId() != null) Theme.deleteCustomTheme(action.themeId()); + } + private void toggleEntry(String id) { MenuSettingsResolver.toggleEntry(id); } + private void openSettings(MenuScreenState state, String id) { + MenuSettingsResolver.ResolvedSettings resolved = MenuSettingsResolver.resolveSettings(id); + if (resolved == null || resolved.settings().isEmpty()) return; + state.settingsPanel.open(resolved.getId(), resolved.title(), resolved.settings(), resolved.hudContext()); + } + void openThemeEditor(MenuScreenState state, EditableClickGuiTheme editor) { + if (editor == null) return; + state.activeThemeEditor = editor; + state.themeEditorPreview.reset(); + state.settingsPanel.openEditor("theme:" + editor.id(), editor.title(), editor.buildSettings()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenLayout.java new file mode 100644 index 0000000..17e0dd9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenLayout.java @@ -0,0 +1,40 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings; + +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; + +final class MenuScreenLayout { + + void layout(MenuScreenState state, float areaX, float areaY, float areaW, float areaH) { + state.menuW = state.menuWConst * state.s; + state.menuH = state.menuHConst * state.s; + state.menuX = areaX + (areaW - state.menuW) * 0.5f; + state.menuY = areaY + (areaH - state.menuH) * 0.5f; + state.shellX = state.menuX - 20f * state.s; + state.shellY = state.menuY; + state.shellW = state.menuW + 40f * state.s; + state.shellH = state.menuH; + state.categoryX = state.menuX - 20f * state.s; + state.categoryY = state.menuY + 40f * state.s; + state.searchX = state.menuX + 330f * state.s; + state.searchY = state.menuY + 7.5f * state.s; + } + + RenderWarpStack.Scope pushLifecycleWarp(MenuScreenState state, float progress) { + if (progress >= 0.999f) { + return Renderer2D.pushWarp(null); + } + return Renderer2D.pushPerspectiveWarp( + state.shellX, + state.shellY, + state.shellW, + state.shellH, + 0.0f, + 0.0f, + 0.0f, + 3.6f, + 0.80f, + 0.90f + 0.10f * progress + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenRenderer.java new file mode 100644 index 0000000..9695229 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenRenderer.java @@ -0,0 +1,141 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import java.util.List; + +final class MenuScreenRenderer { + private final MenuScreenLayout layout; + MenuScreenRenderer(MenuScreenLayout layout) { this.layout = layout; } + void render(MenuScreenState state, float areaX, float areaY, float areaW, float areaH, float mx, float my) { + SettingsGuiPalette palette = SettingsGuiPalette.current(); + float dt = AnimationUtility.deltaTime(); + state.screenAnim = AnimationUtility.approach(state.screenAnim, state.openTarget ? 1.0f : 0.0f, dt, state.openTarget ? 8.5f : 7.2f); + state.screenAnim = AnimationUtility.snap(state.screenAnim, state.openTarget ? 1.0f : 0.0f, 0.002f); + if (state.screenAnim <= 0.001f && !state.openTarget) return; + layout.layout(state, areaX, areaY, areaW, areaH); + boolean hudPanelActive = state.settingsPanel.shouldMaskMenu(); + float maskTarget = hudPanelActive ? 1f : 0f; + state.menuMaskAnim = AnimationUtility.approach(state.menuMaskAnim, maskTarget, dt, 10f); + state.menuMaskAnim = AnimationUtility.snap(state.menuMaskAnim, maskTarget, 0.01f); + boolean renderMain = !hudPanelActive; + if (state.activeThemeEditor != null && !state.settingsPanel.isActive()) { + state.activeThemeEditor = null; + state.themeEditorPreview.reset(); + } + float distanceProgress = state.easeOutQuint(state.screenAnim); + float fadeProgress = state.smootherStep(state.screenAnim); + float previousAlpha = ClickGuiRenderer.setRenderAlphaMultiplier(fadeProgress); + Renderer2D.COLOR.setAlpha(fadeProgress); + try (RenderWarpStack.Scope ignored = layout.pushLifecycleWarp(state, distanceProgress)) { + if (renderMain) { + renderPanelShadow(state, palette); + state.backgroundComponent.render(state.shellX, state.shellY, state.shellW, state.shellH, state.s); + renderShellChrome(state, palette); + state.categoryContainer.render(state.categoryX, state.categoryY, mx, my, state.category, state.s); + state.searchComponent.render(state.searchX, state.searchY, 80f * state.s, 15f * state.s); + renderCategoryContent(state, mx, my); + } + if (renderMain && state.menuMaskAnim > 0.01f) { + int c0 = LayoutRender2D.alpha(palette.menuMaskLeft(), state.menuMaskAnim * fadeProgress); + int c1 = LayoutRender2D.alpha(palette.menuMaskRight(), state.menuMaskAnim * fadeProgress); + LayoutRender2D.roundedQuad(state.shellX, state.shellY, state.shellW, state.shellH, 8f * state.s, c0, c1, c1, c0); + } + if (state.activeThemeEditor != null && state.settingsPanel.isActive()) { + state.themeEditorPreview.render(state.activeThemeEditor, state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); + } + state.settingsPanel.render(state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); + } finally { + Renderer2D.COLOR.setAlpha(1.0f); + ClickGuiRenderer.restoreRenderAlphaMultiplier(previousAlpha); + } + if (renderMain && !state.settingsPanel.isActive()) renderContextHints(state, areaX, areaY, areaW, areaH, fadeProgress); + } + private void renderCategoryContent(MenuScreenState state, float mx, float my) { + if (state.category == MenuScreen.Category.THEMES) { state.themeComponent.render(state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); return; } + if (state.category == MenuScreen.Category.MAIN_SETTINGS) { state.mainSettingsComponent.render(state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); return; } + if (state.category == MenuScreen.Category.RELATIONS) { state.relationsComponent.render(state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); return; } + if (state.category == MenuScreen.Category.CONFIGS) { state.configProfilesComponent.render(state.menuX, state.menuY, state.menuW, state.menuH, mx, my, state.s); return; } + List entries = state.visibleEntries(MenuSettingsResolver.buildCards(state.category)); + state.moduleComponent.render(state.menuX, state.menuY, state.menuW, state.menuH, entries, state.s); + } + private void renderPanelShadow(MenuScreenState state, SettingsGuiPalette palette) { + LayoutRender2D.roundedSoftShadow(state.shellX, state.shellY, state.shellW, state.shellH, 8f * state.s, 8f * state.s, 0.018f, palette.menuShadow()); + } + void renderShellChrome(MenuScreenState state, SettingsGuiPalette palette) { + float vLineX = state.shellX + 42.5f * state.s; + float hLineY = state.shellY + 28f * state.s; + float lineThickness = 0.5f * state.s; + float vLineH = state.shellH; + float hLineW = state.shellW - 43f * state.s; + float hSegW = 56f * state.s; + float vSegH = 52f * state.s; + float hTravel = Math.max(1f, hLineW - hSegW); + float vTravel = Math.max(1f, vLineH - vSegH); + float dt = AnimationUtility.deltaTime(); + LayoutRender2D.rectQuad(vLineX, state.shellY, lineThickness, vLineH, palette.menuLineLow(), palette.menuLineMid(), palette.menuLineLow(), palette.menuLineStrong()); + LayoutRender2D.rectQuad(state.shellX + 43f * state.s, hLineY, hLineW, lineThickness, palette.menuLineStrong(), palette.menuLineLow(), palette.menuLineStrong(), palette.menuLineLow()); + state.menuLineHSweep = AnimationUtility.clamp(state.menuLineHSweep + state.menuLineHDir * 120f * dt, 0f, hTravel); + if (state.menuLineHSweep >= hTravel) state.menuLineHDir = -1f; else if (state.menuLineHSweep <= 0f) state.menuLineHDir = 1f; + state.menuLineVSweep = AnimationUtility.clamp(state.menuLineVSweep + state.menuLineVDir * 95f * dt, 0f, vTravel); + if (state.menuLineVSweep >= vTravel) state.menuLineVDir = -1f; else if (state.menuLineVSweep <= 0f) state.menuLineVDir = 1f; + state.menuLinePulsePhase += 4.1f * dt; + float pulse = 0.65f + 0.35f * (float) Math.sin(state.menuLinePulsePhase); + float hSegX = state.shellX + 43f * state.s + state.menuLineHSweep; + int hA0 = LayoutRender2D.alpha(palette.menuSweepStart(), pulse); + int hA1 = LayoutRender2D.alpha(palette.menuSweepEnd(), pulse); + LayoutRender2D.rectQuad(hSegX, hLineY, hSegW, lineThickness, hA0, hA1, hA1, hA0); + float vSegY = state.shellY + state.menuLineVSweep; + int vA0 = LayoutRender2D.alpha(palette.menuSweepStart(), pulse * 0.86f); + int vA1 = LayoutRender2D.alpha(palette.menuSweepEnd(), pulse * 0.93f); + LayoutRender2D.rectQuad(vLineX, vSegY, lineThickness, vSegH, vA0, vA1, vA0, vA1); + renderHeaderTitle(state, palette); + } + private void renderHeaderTitle(MenuScreenState state, SettingsGuiPalette palette) { + float baseX = state.shellX + 50f * state.s; + float baseY = state.shellY + 13.5f * state.s; + float iconSize = 11.5f * state.s; + float textSize = 11.5f * state.s; + if (state.category.iconToken()) { + TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); + ClickGuiRenderer.drawText(icons, state.category.token(), baseX, baseY, iconSize, palette.menuHeaderText(), false); + float iconW = ClickGuiRenderer.textWidth(icons, state.category.token(), iconSize); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), " " + state.category.title(), baseX + iconW + 1.5f * state.s, baseY, textSize, palette.menuHeaderText(), false); + return; + } + if (state.category.svgIcon()) { + Renderer2D.COLOR.svg(state.category.token(), baseX, baseY + 0.5f * state.s, iconSize, iconSize, SvgRenderOptions.overrideColor(palette.menuHeaderText())); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), " " + state.category.title(), baseX + iconSize + 1.5f * state.s, baseY, textSize, palette.menuHeaderText(), false); + return; + } + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), "| " + state.category.title(), baseX, baseY, textSize, palette.menuHeaderText(), false); + } + private void renderContextHints(MenuScreenState state, float areaX, float areaY, float areaW, float areaH, float alpha) { + MainConfig config = MainConfig.get(); + if (!config.isClickGuiHintsEnabled()) return; + String primary; String secondary; String tertiary; + switch (state.category) { + case HUD -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.hud.toggle", "LMB - toggle HUD element"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.hud.settings", "RMB - open element settings"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.hud.drag", "Open settings - edit widget position"); } + case UI -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.ui.toggle", "LMB - toggle UI element"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.ui.settings", "RMB - open element settings"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.ui.search", "Ctrl+F - search cards"); } + case THEMES -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.themes.select", "LMB - select theme"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.themes.edit", "Edit - open theme editor"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.themes.scroll", "Wheel - scroll themes"); } + case MAIN_SETTINGS -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.main.select", "LMB - change setting"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.main.category", "Left list - switch setting group"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.main.scroll", "Wheel - scroll settings"); } + case RELATIONS -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.relations.mode", "Top pills - choose relation list"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.relations.add", "User-plus icon - pick online player"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.relations.search", "Search - filter names"); } + case CONFIGS -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.configs.select", "LMB - select config"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.configs.rename", "Click name - rename config"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.configs.enter", "Enter/Esc - confirm or cancel text edit"); } + default -> { primary = ClickGuiI18n.tr("clickgui.hints.settings.generic.select", "LMB - select"); secondary = ClickGuiI18n.tr("clickgui.hints.settings.generic.scroll", "Wheel - scroll"); tertiary = ClickGuiI18n.tr("clickgui.hints.settings.generic.search", "Ctrl+F - search"); } + } + ClickGuiHintOverlay.renderBottomLeft(areaX, areaY, areaW, areaH, state.s, alpha, primary, secondary, tertiary, + ClickGuiI18n.tr("clickgui.hints.settings.close", "Esc - close ClickGUI"), + ClickGuiI18n.tr("clickgui.hints.settings.hide", "Alt+H - hide hints")); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenState.java new file mode 100644 index 0000000..eb1ce38 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/menu/MenuScreenState.java @@ -0,0 +1,97 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigProfilesComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.main.MainSettingsComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.SettingsPanelComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.BackgroundComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.CategoryContainerComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.other.SearchComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations.RelationsComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeEditorPreviewComponent; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.theme.EditableClickGuiTheme; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import java.util.ArrayList; +import java.util.List; + +final class MenuScreenState { + final float inputReadyProgress = 0.72f; + final float s = 2f; + final float menuWConst = 400f; + final float menuHConst = 250f; + + final BackgroundComponent backgroundComponent = new BackgroundComponent(); + final CategoryContainerComponent categoryContainer = new CategoryContainerComponent(); + final SearchComponent searchComponent = new SearchComponent(); + final ModuleComponent moduleComponent = new ModuleComponent(); + final ThemeComponent themeComponent = new ThemeComponent(); + final ThemeEditorPreviewComponent themeEditorPreview = new ThemeEditorPreviewComponent(); + final MainSettingsComponent mainSettingsComponent = new MainSettingsComponent(); + final RelationsComponent relationsComponent = new RelationsComponent(); + final ConfigProfilesComponent configProfilesComponent = new ConfigProfilesComponent(); + final SettingsPanelComponent settingsPanel = new SettingsPanelComponent(); + + MenuScreen.Category category = MenuScreen.Category.HUD; + float menuX; + float menuY; + float menuW; + float menuH; + float shellX; + float shellY; + float shellW; + float shellH; + float categoryX; + float categoryY; + float searchX; + float searchY; + float menuLinePulsePhase; + float menuLineHSweep; + float menuLineVSweep; + float menuLineHDir = 1f; + float menuLineVDir = 1f; + float menuMaskAnim; + float screenAnim; + boolean openTarget; + EditableClickGuiTheme activeThemeEditor; + + boolean isInteractive() { + return openTarget && screenAnim >= inputReadyProgress; + } + + boolean isVisible() { + return openTarget || screenAnim > 0.001f; + } + + float easeOutCubic(float t) { + t = AnimationUtility.clamp(t, 0.0f, 1.0f); + float inv = 1.0f - t; + return 1.0f - inv * inv * inv; + } + + float easeOutQuint(float t) { + t = AnimationUtility.clamp(t, 0.0f, 1.0f); + float inv = 1.0f - t; + return 1.0f - inv * inv * inv * inv * inv; + } + + float smootherStep(float t) { + t = AnimationUtility.clamp(t, 0.0f, 1.0f); + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); + } + + List visibleEntries(List src) { + if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isEmpty()) { + return src; + } + List out = new ArrayList<>(); + for (ModuleComponent.CardEntry e : src) { + if (ClickGuiSearch.matches(e.title(), e.searchAliases())) { + out.add(e); + } + } + return out; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java index f06bccd..2239d8c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java @@ -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.gui.clickgui.layout.screen.settings.render; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java index 59ddee0..38ff3cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java @@ -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.gui.clickgui.picker; import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerCatalog; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java index 05984b3..d1510cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java @@ -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.gui.clickgui.picker.catalog; import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java index 89d0e28..f642940 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java @@ -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.gui.clickgui.picker.catalog; import net.minecraft.client.resources.language.I18n; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java index 20fcf1b..c487431 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java @@ -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.gui.clickgui.picker.catalog; import net.minecraft.world.item.ItemStack; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java index f5f8cc0..3dc38c2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java @@ -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.gui.clickgui.picker.screen; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java index afafb66..26328f0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java @@ -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.gui.clickgui.picker; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java index 7106028..d662045 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java @@ -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.gui.clickgui.picker; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java index 12c2bbf..a15c4ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java @@ -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.gui.clickgui.picker; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java index dbe2ac5..e3e9d65 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java @@ -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.gui.clickgui.picker; import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerEntryData; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java index b8777da..67b0e26 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java @@ -1,22 +1,24 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.render; +import dev.loki.lovisual.features.gui.clickgui.ClickGuiModel.*; +import dev.loki.lovisual.features.gui.clickgui.editor.ClickGuiEditorManager; import dev.loki.lovisual.features.gui.clickgui.editor.ClickGuiTextEditorState; +import dev.loki.lovisual.features.gui.clickgui.font.ClickGuiFonts; +import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiInputHandler; +import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiKeyBindManager; +import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiKeyboardHandler; +import dev.loki.lovisual.features.gui.clickgui.picker.ClickGuiPickerIconManager; import dev.loki.lovisual.features.gui.clickgui.picker.ClickGuiPickerState; +import dev.loki.lovisual.features.gui.clickgui.picker.screen.ClickGuiPickerScreen; import dev.loki.lovisual.features.gui.clickgui.protocol.CombatProtocolHeuristicsEditorState; +import dev.loki.lovisual.features.gui.clickgui.screen.*; import dev.loki.lovisual.features.gui.clickgui.sections.ModulesSection; import dev.loki.lovisual.features.gui.clickgui.sections.SettingsSection; import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.ProtocolHeuristicsSetting; import dev.loki.lovisual.features.gui.clickgui.settings.owner.TextEditorOwner; import dev.loki.lovisual.features.gui.clickgui.settings.text.TextSetting; import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; - +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiTabState; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -25,587 +27,174 @@ import net.minecraft.world.item.ItemStack; import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; -import dev.loki.lovisual.features.gui.clickgui.editor.ClickGuiEditorManager; -import dev.loki.lovisual.features.gui.clickgui.font.ClickGuiFonts; -import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiInputHandler; -import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiKeyBindManager; -import dev.loki.lovisual.features.gui.clickgui.input.ClickGuiKeyboardHandler; -import dev.loki.lovisual.features.gui.clickgui.picker.ClickGuiPickerIconManager; -import dev.loki.lovisual.features.gui.clickgui.picker.screen.ClickGuiPickerScreen; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiDrawHelpers; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderEngine; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiTextBatch; -import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiLifecycle; -import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiScreen; -import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiScreenManager; -import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiTopLayer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiTabState; -import dev.loki.lovisual.features.gui.clickgui.ClickGuiModel.PickerIcon; -import dev.loki.lovisual.features.gui.clickgui.ClickGuiModel.TextBatchItem; -import dev.loki.lovisual.features.gui.clickgui.ClickGuiModel.TextHeightKey; -import dev.loki.lovisual.features.gui.clickgui.ClickGuiModel.TextMeasureKey; +/** + * Static facade for the ClickGui: owns shared mutable state (renderer handle, mouse + * position, lifecycle/tab animation, editor / picker slots, text-measure caches) and + * one-line forwarders to render engine, draw helpers, text batch, editor / picker / + * screen managers, lifecycle, input, top layer, fonts and tab state. + */ public enum ClickGuiRenderer { ; - public static final ModulesSection MODULES_SECTION = new ModulesSection(); - public static final SettingsSection SETTINGS_SECTION = new SettingsSection(); - public static final String MODULES_TAB_ID = "lovisual:modules"; - public static final String SETTINGS_TAB_ID = "lovisual:settings"; - - public static final float TAB_HEIGHT = 34f; - - public static final float TAB_GAP = 0f; - - public static final float TAB_PAD_X = 18f; - - public static final float TAB_FONT = 17f; - - public static final float TAB_MIN_W = 96f; - - public static float[] tabX = new float[0]; - - public static float[] tabW = new float[0]; - - public static final float LIFECYCLE_OPEN_SPEED = 9.5f; - - public static final float LIFECYCLE_CLOSE_SPEED = 8.0f; - - public static final float INPUT_READY_PROGRESS = 0.72f; - - public static final Set pendingBindKeys = new LinkedHashSet<>(); - - public static final List pickerIcons = new ArrayList<>(); - + public static final float TAB_HEIGHT = 34f, TAB_GAP = 0f, TAB_PAD_X = 18f, TAB_FONT = 17f, TAB_MIN_W = 96f; + public static final float LIFECYCLE_OPEN_SPEED = 9.5f, LIFECYCLE_CLOSE_SPEED = 8.0f, INPUT_READY_PROGRESS = 0.72f; + static final int TEXT_WIDTH_CACHE_LIMIT = 4096, TEXT_HEIGHT_CACHE_LIMIT = 256; static final ArrayList TEXT_BATCH = new ArrayList<>(); - static final int TEXT_WIDTH_CACHE_LIMIT = 4096; - static final int TEXT_HEIGHT_CACHE_LIMIT = 256; static final Map TEXT_WIDTH_CACHE = new LinkedHashMap<>(512, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > TEXT_WIDTH_CACHE_LIMIT; - } - }; + @Override protected boolean removeEldestEntry(Map.Entry e) { return size() > TEXT_WIDTH_CACHE_LIMIT; } }; static final Map TEXT_HEIGHT_CACHE = new LinkedHashMap<>(64, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > TEXT_HEIGHT_CACHE_LIMIT; - } - }; - public static boolean waitingForKey = false; + @Override protected boolean removeEldestEntry(Map.Entry e) { return size() > TEXT_HEIGHT_CACHE_LIMIT; } }; + public static float[] tabX = new float[0], tabW = new float[0]; + public static final Set pendingBindKeys = new LinkedHashSet<>(); + public static final List pickerIcons = new ArrayList<>(); + public static boolean waitingForKey = false, cursorShown = false, init = false; + public static boolean tabAnimInit = false, closingForExit = false, finishingClose = false, pickerIconScissorActive = false; public static Object keyBindOwner = null; - static int colorClipboard = 0xFFFFFFFF; - public static boolean init = false; - - public static float mouseX, mouseY; - - public static boolean cursorShown = false; - - public static int fbWidth, fbHeight; - - public static TextRenderer interRegular; - - public static TextRenderer interMedium; - - public static TextRenderer onestMedium; - - public static TextRenderer onestBold; - - public static TextRenderer monsterratRegular; - + public static TextRenderer interRegular, interMedium, onestMedium, onestBold, monsterratRegular; public static String activeTabId = MODULES_TAB_ID; - - public static float tabBarX, tabBarY, tabBarW, tabBarH; - - public static float tabTargetX, tabTargetW; - - public static float tabAnimX, tabAnimW; - - public static boolean tabAnimInit = false; - - public static long lastTabAnimNs = System.nanoTime(); - - public static float lifecycleAnim = 0.0f; - - public static boolean closingForExit = false; - - public static boolean finishingClose = false; - - public static long lastLifecycleAnimNs = System.nanoTime(); - - public static float modulesAreaX, modulesAreaY, modulesAreaW, modulesAreaH; - - public static CompletableFuture pendingSaveTask; - public static ClickGuiTextEditorState textEditor = null; - public static ClickGuiPickerState picker = null; - public static CombatProtocolHeuristicsEditorState protocolHeuristicsEditor = null; - public static TextSetting inlineTextOwner = null; - public static StringBuilder inlineTextBuffer = null; - - public static float inlineTextX, inlineTextY, inlineTextW, inlineTextH; - - public static boolean pickerIconScissorActive = false; - - public static float pickerIconScissorX, pickerIconScissorY, pickerIconScissorW, pickerIconScissorH; - - public static Renderer2D renderer; - - static float renderAlphaMultiplier = 1.0f; public static ClickGuiScreen mainScreen; - public static ClickGuiPickerScreen pickerScreen; - + public static Renderer2D renderer; + public static CompletableFuture pendingSaveTask; + public static float mouseX, mouseY; + public static int fbWidth, fbHeight; + public static long lastTabAnimNs = System.nanoTime(), lastLifecycleAnimNs = System.nanoTime(); + public static float lifecycleAnim = 0.0f, tabBarX, tabBarY, tabBarW, tabBarH, tabTargetX, tabTargetW, tabAnimX, tabAnimW; + public static float modulesAreaX, modulesAreaY, modulesAreaW, modulesAreaH, inlineTextX, inlineTextY, inlineTextW, inlineTextH; + public static float pickerIconScissorX, pickerIconScissorY, pickerIconScissorW, pickerIconScissorH; + static int colorClipboard = 0xFFFFFFFF; + static float renderAlphaMultiplier = 1.0f; static boolean textBatchActive; - // ========================= - // API for MIXIN MOUSE - // ========================= - public static void onMouseMove(double x, double y) { - ClickGuiInputHandler.onMouseMove(x, y); - } - - public static void onMouseButton(double x, double y, int button, boolean pressed) { - ClickGuiInputHandler.onMouseButton(x, y, button, pressed); - } - - public static void onMouseScroll(double x, double y, double delta) { - ClickGuiInputHandler.onMouseScroll(x, y, delta); - } - - public static void onMouseMoveScaled(double x, double y) { - ClickGuiInputHandler.onMouseMoveScaled(x, y); - } - - public static void onMouseButtonScaled(double x, double y, int button, boolean pressed) { - ClickGuiInputHandler.onMouseButtonScaled(x, y, button, pressed); - } - - public static void onMouseScrollScaled(double x, double y, double delta) { - ClickGuiInputHandler.onMouseScrollScaled(x, y, delta); - } - - public static void onEditorMouseMoveScaled(double x, double y) { - ClickGuiInputHandler.onEditorMouseMoveScaled(x, y); - } - - public static void onEditorMouseButtonScaled(double x, double y, int button, boolean pressed) { - ClickGuiInputHandler.onEditorMouseButtonScaled(x, y, button, pressed); - } - - public static void onEditorMouseScrollScaled(double x, double y, double delta) { - ClickGuiInputHandler.onEditorMouseScrollScaled(x, y, delta); - } - - public static boolean onEditorCharTyped(char c) { - return ClickGuiKeyboardHandler.onEditorCharTyped(c); - } - - public static boolean onEditorKey(int key, int scancode, int action, int mods) { - return ClickGuiKeyboardHandler.onEditorKey(key, scancode, action, mods); - } - - public static void closeGuiEditor() { - ClickGuiKeyboardHandler.closeGuiEditor(); - } - - public static boolean onCharTyped(char c) { - return ClickGuiKeyboardHandler.onCharTyped(c); - } - - public static boolean onFilesDrop(List paths) { - return ClickGuiKeyboardHandler.onFilesDrop(paths); - } - - public static boolean onKey(int key, int scancode, int action, int mods) { - return ClickGuiKeyboardHandler.onKey(key, scancode, action, mods); - } - - // ========================= - // INIT - // ========================= - public static void init() { - ClickGuiLifecycle.init(); - } - - public static void beginOpenAnimation() { - ClickGuiLifecycle.beginOpenAnimation(); - } - - public static void beginCloseAnimation() { - ClickGuiLifecycle.beginCloseAnimation(); - } - - // ========================= - // CURSOR OPEN/CLOSE - // ========================= - public static boolean isLifecycleActive() { - return ClickGuiLifecycle.isLifecycleActive(); - } - - public static boolean isClosingForExit() { - return ClickGuiLifecycle.isClosingForExit(); - } - - public static void finishCloseImmediately() { - ClickGuiLifecycle.finishCloseImmediately(); - } - - public static void onClose() { - ClickGuiLifecycle.onClose(); - } - - public static void releaseCursorForGameplay() { - ClickGuiLifecycle.releaseCursorForGameplay(); - } - - public static void ensureCursorShown() { - ClickGuiLifecycle.ensureCursorShown(); - } - - public static void renderEngine(Renderer2D rendererIn, TextRenderer fallback, GuiGraphicsExtractor ctx, float tickDelta) { - ClickGuiRenderEngine.renderEngine(rendererIn, fallback, ctx, tickDelta); - } - - public static boolean shouldRenderAsTopLayer() { - return ClickGuiTopLayer.shouldRenderAsTopLayer(); - } - - public static void renderTopLayer(GuiGraphicsExtractor ctx, float tickDelta) { - ClickGuiTopLayer.renderTopLayer(ctx, tickDelta); - } - - public static void renderEditor(Renderer2D rendererIn, TextRenderer fallback, GuiGraphicsExtractor ctx, float tickDelta) { - ClickGuiTopLayer.renderEditor(rendererIn, fallback, ctx, tickDelta); - } - - public static void renderModulesPreview(float x, float y, float w, float h) { - ClickGuiTopLayer.renderModulesPreview(x, y, w, h); - } - - public static ClickGuiIslandState islandState() { - return ClickGuiTabState.islandState(); - } - - public static TextRenderer getInterRegular() { - return ClickGuiFonts.getInterRegular(); - } - - public static Renderer2D currentRenderer() { - return ClickGuiFonts.currentRenderer(); - } - - public static TextRenderer getInterMedium() { - return ClickGuiFonts.getInterMedium(); - } - - public static TextRenderer getIosevkaRegular() { - return ClickGuiFonts.getIosevkaRegular(); - } - - public static TextRenderer getSfMedium() { - return ClickGuiFonts.getSfMedium(); - } - - public static TextRenderer getSfProDisplaySemibold() { - return ClickGuiFonts.getSfProDisplaySemibold(); - } - - public static TextRenderer getMonsterratRegular() { - return ClickGuiFonts.getMonsterratRegular(); - } - - public static int framebufferWidth() { - return ClickGuiRenderEngine.framebufferWidth(); - } - - public static int framebufferHeight() { - return ClickGuiRenderEngine.framebufferHeight(); - } - - public static float getAnimAlpha() { - return ClickGuiRenderEngine.getAnimAlpha(); - } - - public static float getMouseX() { - return ClickGuiRenderEngine.getMouseX(); - } - - public static float getMouseY() { - return ClickGuiRenderEngine.getMouseY(); - } - - public static int mixColor(int from, int to, float t) { - return ClickGuiDrawHelpers.mixColor(from, to, t); - } - - public static int scaleAlpha(int color, float factor) { - return ClickGuiDrawHelpers.scaleAlpha(color, factor); - } - - public static int withAlpha(int color, int alpha) { - return ClickGuiDrawHelpers.withAlpha(color, alpha); - } - - public static float setRenderAlphaMultiplier(float alpha) { - return ClickGuiDrawHelpers.setRenderAlphaMultiplier(alpha); - } - - public static float getRenderAlphaMultiplier() { - return ClickGuiDrawHelpers.getRenderAlphaMultiplier(); - } - - public static void restoreRenderAlphaMultiplier(float previous) { - ClickGuiDrawHelpers.restoreRenderAlphaMultiplier(previous); - } - - public static float scaleForSize(float size) { - return ClickGuiTextBatch.scaleForSize(size); - } - - public static float textWidth(TextRenderer tr, String text, float size) { - return ClickGuiTextBatch.textWidth(tr, text, size); - } - - public static float textHeight(TextRenderer tr, float size) { - return ClickGuiTextBatch.textHeight(tr, size); - } - - public static void beginTextBatch() { - ClickGuiTextBatch.beginTextBatch(); - } - - public static void endTextBatch() { - ClickGuiTextBatch.endTextBatch(); - } - - public static void drawText(TextRenderer tr, String text, float x, float y, float size, int argb, boolean shadow) { - ClickGuiTextBatch.drawText(tr, text, x, y, size, argb, shadow); - } - - public static String fitText(TextRenderer tr, String text, float size, float maxWidth) { - return ClickGuiTextBatch.fitText(tr, text, size, maxWidth); - } - - public static List wrapText(TextRenderer tr, String text, float size, float maxWidth, int maxLines) { - return ClickGuiTextBatch.wrapText(tr, text, size, maxWidth, maxLines); - } - - public static void drawRoundedRect(float x, float y, float w, float h, float radius, int argb) { - ClickGuiDrawHelpers.drawRoundedRect(x, y, w, h, radius, argb); - } - - public static void drawRoundedRectStroke(float x, float y, float w, float h, float radius, float thickness, int argb) { - ClickGuiDrawHelpers.drawRoundedRectStroke(x, y, w, h, radius, thickness, argb); - } - - public static void drawRoundedRectGlow(float x, float y, float w, float h, - float radius, float glow, int argb) { - ClickGuiDrawHelpers.drawRoundedRectGlow(x, y, w, h, radius, glow, argb); - } - - public static void drawRoundedRectShadow(float x, float y, float w, float h, - float radius, float softness, float spread, int argb) { - ClickGuiDrawHelpers.drawRoundedRectShadow(x, y, w, h, radius, softness, spread, argb); - } - - public static void drawRoundedRectGradient(float x, float y, float w, float h, float radius, - int startArgb, int endArgb, float angleDeg) { - ClickGuiDrawHelpers.drawRoundedRectGradient(x, y, w, h, radius, startArgb, endArgb, angleDeg); - } - - public static void drawRoundedRectStrokeGradient(float x, float y, float w, float h, float radius, float thickness, - int startArgb, int endArgb, float angleDeg) { - ClickGuiDrawHelpers.drawRoundedRectStrokeGradient(x, y, w, h, radius, thickness, startArgb, endArgb, angleDeg); - } - - public static void drawRoundedRectCorners(float x, float y, float w, float h, - float radiusTL, float radiusTR, float radiusBR, float radiusBL, - int argb) { - ClickGuiDrawHelpers.drawRoundedRectCorners(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL, argb); - } - - public static void drawRect(float x, float y, float w, float h, int argb) { - ClickGuiDrawHelpers.drawRect(x, y, w, h, argb); - } - - public static void flushRenderer() { - ClickGuiDrawHelpers.flushRenderer(); - } - - public static void drawGradientRect(float x, float y, float w, float h, - int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { - ClickGuiDrawHelpers.drawGradientRect(x, y, w, h, cTopLeft, cTopRight, cBottomRight, cBottomLeft); - } - - public static void drawCircle(float cx, float cy, float r, int argb) { - ClickGuiDrawHelpers.drawCircle(cx, cy, r, argb); - } - - public static void drawLine(float x1, float y1, float x2, float y2, int argb) { - ClickGuiDrawHelpers.drawLine(x1, y1, x2, y2, argb); - } - - public static void drawBlur(float x, float y, float w, float h, float radius, int argb, float alpha) { - ClickGuiDrawHelpers.drawBlur(x, y, w, h, radius, argb, alpha); - } - - public static void drawGlass(float x, float y, float w, float h, float radius, float alpha) { - ClickGuiDrawHelpers.drawGlass(x, y, w, h, radius, alpha); - } - - public static float clickGuiBlurQuality() { - return ClickGuiDrawHelpers.clickGuiBlurQuality(); - } - - public static float clickGuiBlurDensityScale() { - return ClickGuiDrawHelpers.clickGuiBlurDensityScale(); - } - - public static float clickGuiGlassBlurScale() { - return ClickGuiDrawHelpers.clickGuiGlassBlurScale(); - } - - public static int getColorClipboard() { - return ClickGuiDrawHelpers.getColorClipboard(); - } - - public static void setColorClipboard(int argb) { - ClickGuiDrawHelpers.setColorClipboard(argb); - } - - public static void beginKeyBind(Object owner) { - ClickGuiKeyBindManager.beginKeyBind(owner); - } - - public static void finishKeyBind() { - ClickGuiKeyBindManager.finishKeyBind(); - } - - public static String getPendingBindDisplay() { - return ClickGuiKeyBindManager.getPendingBindDisplay(); - } - - public static void openTextEditor(TextEditorOwner owner, String title, String seedText, float anchorX, float anchorY, float anchorW) { - ClickGuiEditorManager.openTextEditor(owner, title, seedText, anchorX, anchorY, anchorW); - } - - public static void closeTextEditor(boolean save) { - ClickGuiEditorManager.closeTextEditor(save); - } - - public static void renderTextEditor(int fbw, int fbh) { - ClickGuiEditorManager.renderTextEditor(fbw, fbh); - } - - public static void toggleInlineText(TextSetting owner) { - ClickGuiEditorManager.toggleInlineText(owner); - } - - public static boolean isInlineTextActive() { - return ClickGuiEditorManager.isInlineTextActive(); - } - - public static boolean isInlineTextOwner(TextSetting owner) { - return ClickGuiEditorManager.isInlineTextOwner(owner); - } - - public static String getInlineText(TextSetting owner) { - return ClickGuiEditorManager.getInlineText(owner); - } - - public static void updateInlineTextBounds(TextSetting owner, float x, float y, float w, float h) { - ClickGuiEditorManager.updateInlineTextBounds(owner, x, y, w, h); - } - - public static boolean isTextEditorActive() { - return ClickGuiEditorManager.isTextEditorActive(); - } - - public static boolean isPickerActive() { - return ClickGuiEditorManager.isPickerActive(); - } - - public static boolean isBlockingModuleKeybinds() { - return ClickGuiEditorManager.isBlockingModuleKeybinds(); - } - - public static void processPickerIcons(GuiGraphicsExtractor ctx) { - ClickGuiPickerIconManager.processPickerIcons(ctx); - } - - public static void beginPickerIconScissor(float x, float y, float w, float h) { - ClickGuiPickerIconManager.beginPickerIconScissor(x, y, w, h); - } - - public static void endPickerIconScissor() { - ClickGuiPickerIconManager.endPickerIconScissor(); - } - - public static void queuePickerIcon(ItemStack stack, float x, float y, float scale) { - ClickGuiPickerIconManager.queuePickerIcon(stack, x, y, scale); - } - - public static void queuePickerIcon(ItemStack stack, - float x, - float y, - float scale, - float clipX, - float clipY, - float clipW, - float clipH) { - ClickGuiPickerIconManager.queuePickerIcon(stack, x, y, scale, clipX, clipY, clipW, clipH); - } - - public static void openClickGuiScreen() { - ClickGuiScreenManager.openClickGuiScreen(); - } - - public static void captureMainScreen() { - ClickGuiScreenManager.captureMainScreen(); - } - - public static ClickGuiScreen ensureMainScreen() { - return ClickGuiScreenManager.ensureMainScreen(); - } - - public static void clearScreens() { - ClickGuiScreenManager.clearScreens(); - } - - public static void openPicker(TextListSetting owner, String title, TextListSetting.PickerMode mode) { - ClickGuiScreenManager.openPicker(owner, title, mode); - } - - public static void openProtocolHeuristicsEditor(ProtocolHeuristicsSetting owner, String title) { - ClickGuiScreenManager.openProtocolHeuristicsEditor(owner, title); - } - - public static void closeProtocolHeuristicsEditor() { - ClickGuiScreenManager.closeProtocolHeuristicsEditor(); - } - - public static void closePickerScreen() { - ClickGuiScreenManager.closePickerScreen(); - } - - public record ClickGuiIslandState(float tabBarX, - float tabBarY, - float tabBarW, - float tabBarH, - float activeX, - float activeW, - float lifecycle, - String activeLabel, - int tabCount, - boolean pickerActive) { - } - + // Mouse / keyboard / editor input (mixin hooks) + public static void onMouseMove(double x, double y) { ClickGuiInputHandler.onMouseMove(x, y); } + public static void onMouseButton(double x, double y, int button, boolean pressed) { ClickGuiInputHandler.onMouseButton(x, y, button, pressed); } + public static void onMouseScroll(double x, double y, double delta) { ClickGuiInputHandler.onMouseScroll(x, y, delta); } + public static void onMouseMoveScaled(double x, double y) { ClickGuiInputHandler.onMouseMoveScaled(x, y); } + public static void onMouseButtonScaled(double x, double y, int b, boolean p) { ClickGuiInputHandler.onMouseButtonScaled(x, y, b, p); } + public static void onMouseScrollScaled(double x, double y, double d) { ClickGuiInputHandler.onMouseScrollScaled(x, y, d); } + public static void onEditorMouseMoveScaled(double x, double y) { ClickGuiInputHandler.onEditorMouseMoveScaled(x, y); } + public static void onEditorMouseButtonScaled(double x, double y, int b, boolean p) { ClickGuiInputHandler.onEditorMouseButtonScaled(x, y, b, p); } + public static void onEditorMouseScrollScaled(double x, double y, double d) { ClickGuiInputHandler.onEditorMouseScrollScaled(x, y, d); } + public static boolean onEditorCharTyped(char c) { return ClickGuiKeyboardHandler.onEditorCharTyped(c); } + public static boolean onEditorKey(int k, int s, int a, int m) { return ClickGuiKeyboardHandler.onEditorKey(k, s, a, m); } + public static void closeGuiEditor() { ClickGuiKeyboardHandler.closeGuiEditor(); } + public static boolean onCharTyped(char c) { return ClickGuiKeyboardHandler.onCharTyped(c); } + public static boolean onFilesDrop(List paths) { return ClickGuiKeyboardHandler.onFilesDrop(paths); } + public static boolean onKey(int k, int s, int a, int m) { return ClickGuiKeyboardHandler.onKey(k, s, a, m); } + + // Init / lifecycle + public static void init() { ClickGuiLifecycle.init(); } + public static void beginOpenAnimation() { ClickGuiLifecycle.beginOpenAnimation(); } + public static void beginCloseAnimation() { ClickGuiLifecycle.beginCloseAnimation(); } + public static boolean isLifecycleActive() { return ClickGuiLifecycle.isLifecycleActive(); } + public static boolean isClosingForExit() { return ClickGuiLifecycle.isClosingForExit(); } + public static void finishCloseImmediately() { ClickGuiLifecycle.finishCloseImmediately(); } + public static void onClose() { ClickGuiLifecycle.onClose(); } + public static void releaseCursorForGameplay() { ClickGuiLifecycle.releaseCursorForGameplay(); } + public static void ensureCursorShown() { ClickGuiLifecycle.ensureCursorShown(); } + + // Render entry / top layer / island + public static void renderEngine(Renderer2D r, TextRenderer f, GuiGraphicsExtractor ctx, float dt) { ClickGuiRenderEngine.renderEngine(r, f, ctx, dt); } + public static boolean shouldRenderAsTopLayer() { return ClickGuiTopLayer.shouldRenderAsTopLayer(); } + public static void renderTopLayer(GuiGraphicsExtractor ctx, float tickDelta) { ClickGuiTopLayer.renderTopLayer(ctx, tickDelta); } + public static void renderEditor(Renderer2D r, TextRenderer f, GuiGraphicsExtractor ctx, float dt) { ClickGuiTopLayer.renderEditor(r, f, ctx, dt); } + public static void renderModulesPreview(float x, float y, float w, float h) { ClickGuiTopLayer.renderModulesPreview(x, y, w, h); } + public static ClickGuiIslandState islandState() { return ClickGuiTabState.islandState(); } + public static int framebufferWidth() { return ClickGuiRenderEngine.framebufferWidth(); } + public static int framebufferHeight() { return ClickGuiRenderEngine.framebufferHeight(); } + public static float getAnimAlpha() { return ClickGuiRenderEngine.getAnimAlpha(); } + public static float getMouseX() { return ClickGuiRenderEngine.getMouseX(); } + public static float getMouseY() { return ClickGuiRenderEngine.getMouseY(); } + + // Fonts / renderer handle + public static TextRenderer getInterRegular() { return ClickGuiFonts.getInterRegular(); } + public static TextRenderer getInterMedium() { return ClickGuiFonts.getInterMedium(); } + public static TextRenderer getIosevkaRegular() { return ClickGuiFonts.getIosevkaRegular(); } + public static TextRenderer getSfMedium() { return ClickGuiFonts.getSfMedium(); } + public static TextRenderer getSfProDisplaySemibold() { return ClickGuiFonts.getSfProDisplaySemibold(); } + public static TextRenderer getMonsterratRegular() { return ClickGuiFonts.getMonsterratRegular(); } + public static Renderer2D currentRenderer() { return ClickGuiFonts.currentRenderer(); } + + // Color helpers + public static int mixColor(int from, int to, float t) { return ClickGuiDrawHelpers.mixColor(from, to, t); } + public static int scaleAlpha(int color, float factor) { return ClickGuiDrawHelpers.scaleAlpha(color, factor); } + public static int withAlpha(int color, int alpha) { return ClickGuiDrawHelpers.withAlpha(color, alpha); } + public static float setRenderAlphaMultiplier(float a) { return ClickGuiDrawHelpers.setRenderAlphaMultiplier(a); } + public static float getRenderAlphaMultiplier() { return ClickGuiDrawHelpers.getRenderAlphaMultiplier(); } + public static void restoreRenderAlphaMultiplier(float previous) { ClickGuiDrawHelpers.restoreRenderAlphaMultiplier(previous); } + public static int getColorClipboard() { return ClickGuiDrawHelpers.getColorClipboard(); } + public static void setColorClipboard(int argb) { ClickGuiDrawHelpers.setColorClipboard(argb); } + + // Text batch + public static float scaleForSize(float size) { return ClickGuiTextBatch.scaleForSize(size); } + public static float textWidth(TextRenderer tr, String text, float size) { return ClickGuiTextBatch.textWidth(tr, text, size); } + public static float textHeight(TextRenderer tr, float size) { return ClickGuiTextBatch.textHeight(tr, size); } + public static void beginTextBatch() { ClickGuiTextBatch.beginTextBatch(); } + public static void endTextBatch() { ClickGuiTextBatch.endTextBatch(); } + public static void drawText(TextRenderer tr, String text, float x, float y, float size, int argb, boolean shadow) { ClickGuiTextBatch.drawText(tr, text, x, y, size, argb, shadow); } + public static String fitText(TextRenderer tr, String text, float size, float maxWidth) { return ClickGuiTextBatch.fitText(tr, text, size, maxWidth); } + public static List wrapText(TextRenderer tr, String text, float size, float maxW, int maxLines) { return ClickGuiTextBatch.wrapText(tr, text, size, maxW, maxLines); } + + // Shape primitives + public static void drawRoundedRect(float x, float y, float w, float h, float radius, int argb) { ClickGuiDrawHelpers.drawRoundedRect(x, y, w, h, radius, argb); } + public static void drawRoundedRectStroke(float x, float y, float w, float h, float r, float t, int argb) { ClickGuiDrawHelpers.drawRoundedRectStroke(x, y, w, h, r, t, argb); } + public static void drawRoundedRectGlow(float x, float y, float w, float h, float r, float g, int argb) { ClickGuiDrawHelpers.drawRoundedRectGlow(x, y, w, h, r, g, argb); } + public static void drawRoundedRectShadow(float x, float y, float w, float h, float r, float so, float sp, int argb) { ClickGuiDrawHelpers.drawRoundedRectShadow(x, y, w, h, r, so, sp, argb); } + public static void drawRoundedRectGradient(float x, float y, float w, float h, float r, int sa, int ea, float ad) { ClickGuiDrawHelpers.drawRoundedRectGradient(x, y, w, h, r, sa, ea, ad); } + public static void drawRoundedRectStrokeGradient(float x, float y, float w, float h, float r, float t, int sa, int ea, float ad) { ClickGuiDrawHelpers.drawRoundedRectStrokeGradient(x, y, w, h, r, t, sa, ea, ad); } + public static void drawRoundedRectCorners(float x, float y, float w, float h, float tl, float tr, float br, float bl, int argb) { ClickGuiDrawHelpers.drawRoundedRectCorners(x, y, w, h, tl, tr, br, bl, argb); } + public static void drawRect(float x, float y, float w, float h, int argb) { ClickGuiDrawHelpers.drawRect(x, y, w, h, argb); } + public static void flushRenderer() { ClickGuiDrawHelpers.flushRenderer(); } + public static void drawGradientRect(float x, float y, float w, float h, int tl, int tr, int br, int bl) { ClickGuiDrawHelpers.drawGradientRect(x, y, w, h, tl, tr, br, bl); } + public static void drawCircle(float cx, float cy, float r, int argb) { ClickGuiDrawHelpers.drawCircle(cx, cy, r, argb); } + public static void drawLine(float x1, float y1, float x2, float y2, int argb) { ClickGuiDrawHelpers.drawLine(x1, y1, x2, y2, argb); } + public static void drawBlur(float x, float y, float w, float h, float r, int argb, float a) { ClickGuiDrawHelpers.drawBlur(x, y, w, h, r, argb, a); } + public static void drawGlass(float x, float y, float w, float h, float r, float a) { ClickGuiDrawHelpers.drawGlass(x, y, w, h, r, a); } + public static float clickGuiBlurQuality() { return ClickGuiDrawHelpers.clickGuiBlurQuality(); } + public static float clickGuiBlurDensityScale() { return ClickGuiDrawHelpers.clickGuiBlurDensityScale(); } + public static float clickGuiGlassBlurScale() { return ClickGuiDrawHelpers.clickGuiGlassBlurScale(); } + + // Keybind / editor / picker / screens + public static void beginKeyBind(Object owner) { ClickGuiKeyBindManager.beginKeyBind(owner); } + public static void finishKeyBind() { ClickGuiKeyBindManager.finishKeyBind(); } + public static String getPendingBindDisplay() { return ClickGuiKeyBindManager.getPendingBindDisplay(); } + public static void openTextEditor(TextEditorOwner o, String title, String seed, float ax, float ay, float aw) { ClickGuiEditorManager.openTextEditor(o, title, seed, ax, ay, aw); } + public static void closeTextEditor(boolean save) { ClickGuiEditorManager.closeTextEditor(save); } + public static void renderTextEditor(int fbw, int fbh) { ClickGuiEditorManager.renderTextEditor(fbw, fbh); } + public static void toggleInlineText(TextSetting owner) { ClickGuiEditorManager.toggleInlineText(owner); } + public static boolean isInlineTextActive() { return ClickGuiEditorManager.isInlineTextActive(); } + public static boolean isInlineTextOwner(TextSetting owner) { return ClickGuiEditorManager.isInlineTextOwner(owner); } + public static String getInlineText(TextSetting owner) { return ClickGuiEditorManager.getInlineText(owner); } + public static void updateInlineTextBounds(TextSetting o, float x, float y, float w, float h) { ClickGuiEditorManager.updateInlineTextBounds(o, x, y, w, h); } + public static boolean isTextEditorActive() { return ClickGuiEditorManager.isTextEditorActive(); } + public static boolean isPickerActive() { return ClickGuiEditorManager.isPickerActive(); } + public static boolean isBlockingModuleKeybinds() { return ClickGuiEditorManager.isBlockingModuleKeybinds(); } + public static void processPickerIcons(GuiGraphicsExtractor ctx) { ClickGuiPickerIconManager.processPickerIcons(ctx); } + public static void beginPickerIconScissor(float x, float y, float w, float h) { ClickGuiPickerIconManager.beginPickerIconScissor(x, y, w, h); } + public static void endPickerIconScissor() { ClickGuiPickerIconManager.endPickerIconScissor(); } + public static void queuePickerIcon(ItemStack s, float x, float y, float sc) { ClickGuiPickerIconManager.queuePickerIcon(s, x, y, sc); } + public static void queuePickerIcon(ItemStack s, float x, float y, float sc, float cx, float cy, float cw, float ch) { ClickGuiPickerIconManager.queuePickerIcon(s, x, y, sc, cx, cy, cw, ch); } + public static void openClickGuiScreen() { ClickGuiScreenManager.openClickGuiScreen(); } + public static void captureMainScreen() { ClickGuiScreenManager.captureMainScreen(); } + public static ClickGuiScreen ensureMainScreen() { return ClickGuiScreenManager.ensureMainScreen(); } + public static void clearScreens() { ClickGuiScreenManager.clearScreens(); } + public static void openPicker(TextListSetting o, String title, TextListSetting.PickerMode mode) { ClickGuiScreenManager.openPicker(o, title, mode); } + public static void openProtocolHeuristicsEditor(ProtocolHeuristicsSetting o, String title) { ClickGuiScreenManager.openProtocolHeuristicsEditor(o, title); } + public static void closeProtocolHeuristicsEditor() { ClickGuiScreenManager.closeProtocolHeuristicsEditor(); } + public static void closePickerScreen() { ClickGuiScreenManager.closePickerScreen(); } + + /** Snapshot of the tab-bar island geometry + lifecycle state, consumed by preview widgets. */ + public record ClickGuiIslandState(float tabBarX, float tabBarY, float tabBarW, float tabBarH, + float activeX, float activeW, float lifecycle, + String activeLabel, int tabCount, boolean pickerActive) { } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java index f1da248..45611e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java @@ -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.gui.clickgui.screen; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java index 65d0a9e..3e51383 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java @@ -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.gui.clickgui.sections; import java.nio.file.Path; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java index 3632383..c9dda6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java @@ -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.gui.clickgui.sections; import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.ModulesMenuScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java index cde839d..148927a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java @@ -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.gui.clickgui.sections; import org.lwjgl.glfw.GLFW; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java index e53a41e..90c9937 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java @@ -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.gui.clickgui.sections.settings; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java index 74b00c4..02799dd 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java @@ -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.gui.clickgui.settings.bind; import dev.loki.lovisual.config.definition.SettingOwner; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java index e53b909..04f0155 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java @@ -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.gui.clickgui.settings.bind; import dev.loki.lovisual.config.values.mode.BindMode; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java index d25d6e8..f27029a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java @@ -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.gui.clickgui.settings.bind; import dev.loki.lovisual.config.definition.SettingOwner; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java index e4e00a7..c58eb45 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java @@ -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.gui.clickgui.settings.bind; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java index 4df7dc6..dc1342f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java @@ -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.gui.clickgui.settings.bool; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java index ffe5a00..27ed40a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java @@ -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.gui.clickgui.settings.bool; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java index f855564..f9cf987 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java @@ -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.gui.clickgui.settings.color; import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRendererBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java index a2ce353..8a07488 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java @@ -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.gui.clickgui.settings.color; import java.awt.Color; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java index 1922782..8254959 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java @@ -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.gui.clickgui.settings.color; import dev.loki.lovisual.config.values.color.RGBColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java index c166d59..509c0f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java @@ -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.gui.clickgui.settings.color; import dev.loki.lovisual.config.definition.ConfigObject; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java index bbe995f..ed2cbf7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java @@ -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.gui.clickgui.settings.color.hex; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java index 6d56c6d..d5b6c8f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java @@ -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.gui.clickgui.settings.color.hex; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java index b8f4a93..8732b8b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java @@ -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.gui.clickgui.settings.color.preset; import com.google.gson.*; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java index c36b597..09080b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java @@ -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.gui.clickgui.settings.color.preset; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java index a01670d..ff579eb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java @@ -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.gui.clickgui.settings.color.preset; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java index ba5eab0..0715355 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java @@ -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.gui.clickgui.settings.color.render; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java index c8c4605..cc01b7d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java @@ -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.gui.clickgui.settings.color.render; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java index cb12f7f..67abf8f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java @@ -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.gui.clickgui.settings.color.render; import dev.loki.lovisual.config.values.color.ColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java index c453d0b..396a1af 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java @@ -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.gui.clickgui.settings.color.render; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRuleEditor.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRuleEditor.java new file mode 100644 index 0000000..c2dba66 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRuleEditor.java @@ -0,0 +1,173 @@ +package dev.loki.lovisual.features.gui.clickgui.settings.cooldown; + +import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.clickgui.settings.bool.BooleanSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.mode.ModeSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.slider.SliderSetting; + +import java.util.ArrayList; +import java.util.List; + +/** + * Bundles the seven per-item controls (enabled / seconds / uses / window / scope / + * trigger / block mode) shown inside the picker detail panel for a focused item. + * The editor owns its {@code ConfigValue} mirrors so text edits and slider drags + * commit back into {@link ItemCooldownRulesValue} only after the user's action. + */ +final class CooldownRuleEditor { + + final String itemId; + final BooleanValue enabledValue = new BooleanValue("cooldown_rule_enabled", false); + final NumberValue secondsValue = new NumberValue<>("cooldown_rule_seconds", 0, 0, 600); + final NumberValue usesValue = new NumberValue<>("cooldown_rule_uses", 1, 1, 10); + final NumberValue windowValue = new NumberValue<>("cooldown_rule_window", 0, 0, 300); + final ModeValue scopeValue = new ModeValue("cooldown_rule_scope", + ItemCooldownRulesValue.Scope.PVP_GRACE.getId(), "always", "pvp_only", "pvp_grace"); + final ModeValue triggerValue = new ModeValue("cooldown_rule_trigger", + ItemCooldownRulesValue.Trigger.CONSUME_FINISH.getId(), "interact_accept", "consume_finish", "totem_pop"); + final ModeValue blockValue = new ModeValue("cooldown_rule_block", + ItemCooldownRulesValue.UseBlockMode.NONE.getId(), "none", "cooldown", "window", "any"); + + final List settings = new ArrayList<>(); + + private final CooldownRulesSetting owner; + private final RuleBooleanSetting enabledSetting; + private final RuleSliderSetting secondsSetting; + private final RuleSliderSetting usesSetting; + private final RuleSliderSetting windowSetting; + + CooldownRuleEditor(CooldownRulesSetting owner, String itemId) { + this.owner = owner; + this.itemId = itemId; + enabledSetting = new RuleBooleanSetting("Enabled", enabledValue, this); + secondsSetting = new RuleSliderSetting("Time (s)", secondsValue, this); + usesSetting = new RuleSliderSetting("Uses", usesValue, this); + windowSetting = new RuleSliderSetting("Window (s)", windowValue, this); + settings.add(enabledSetting); + settings.add(secondsSetting); + settings.add(usesSetting); + settings.add(windowSetting); + settings.add(new ModeSetting("Scope", scopeValue, this::commit)); + settings.add(new ModeSetting("Trigger", triggerValue, this::commit)); + settings.add(new ModeSetting("Block", blockValue, this::commit)); + } + + /** Look up a {@link ItemCooldownRulesValue.Scope} by its persisted id or enum name; falls back to PVP_GRACE. */ + static ItemCooldownRulesValue.Scope scopeById(String id) { + if (id != null) { + for (ItemCooldownRulesValue.Scope scope : ItemCooldownRulesValue.Scope.values()) { + if (scope.getId().equals(id) || scope.name().equalsIgnoreCase(id)) return scope; + } + } + return ItemCooldownRulesValue.Scope.PVP_GRACE; + } + + /** Look up a {@link ItemCooldownRulesValue.Trigger} by id or name; falls back to CONSUME_FINISH. */ + static ItemCooldownRulesValue.Trigger triggerById(String id) { + if (id != null) { + for (ItemCooldownRulesValue.Trigger trigger : ItemCooldownRulesValue.Trigger.values()) { + if (trigger.getId().equals(id) || trigger.name().equalsIgnoreCase(id)) return trigger; + } + } + return ItemCooldownRulesValue.Trigger.CONSUME_FINISH; + } + + /** Look up a {@link ItemCooldownRulesValue.UseBlockMode} by id or name; falls back to NONE. */ + static ItemCooldownRulesValue.UseBlockMode blockById(String id) { + if (id != null) { + for (ItemCooldownRulesValue.UseBlockMode mode : ItemCooldownRulesValue.UseBlockMode.values()) { + if (mode.getId().equals(id) || mode.name().equalsIgnoreCase(id)) return mode; + } + } + return ItemCooldownRulesValue.UseBlockMode.NONE; + } + + /** Pull the persisted rule (if any) into the seven editor values so the UI mirrors storage. */ + void syncFromRule() { + ItemCooldownRulesValue rules = owner.rules(); + if (rules == null) return; + ItemCooldownRulesValue.Rule rule = rules.getRule(itemId); + if (rule == null) return; + enabledValue.set(rule.enabled()); + secondsValue.set(rule.seconds()); + usesValue.set(rule.uses()); + windowValue.set(rule.windowSeconds()); + scopeValue.set(rule.scope().getId()); + triggerValue.set(rule.trigger().getId()); + blockValue.set(rule.blockMode().getId()); + } + + /** True while any of the three sliders is mid-drag; used to skip the read-back sync for that frame. */ + boolean isDraggingSlider() { + return secondsSetting.ui().dragging || usesSetting.ui().dragging || windowSetting.ui().dragging; + } + + /** Normalise the current editor values and push them back into the underlying rules store. */ + void commit() { + ItemCooldownRulesValue rules = owner.rules(); + if (rules == null || !rules.containsRule(itemId)) return; + ItemCooldownRulesValue.Rule current = rules.getRule(itemId); + if (current == null) current = rules.getDefaultRule(itemId); + + ItemCooldownRulesValue.Rule next = new ItemCooldownRulesValue.Rule( + enabledValue.get(), + secondsValue.get(), + usesValue.get(), + windowValue.get(), + scopeById(scopeValue.get()), + triggerById(triggerValue.get()), + blockById(blockValue.get()) + ).normalized(); + + rules.putRule(itemId, next); + syncFromRule(); + owner.syncBridge(); + owner.saveParentConfig(); + } + + /** Boolean toggle that commits the whole editor after a single click. */ + private static final class RuleBooleanSetting extends BooleanSetting { + private final CooldownRuleEditor editor; + + RuleBooleanSetting(String name, BooleanValue value, CooldownRuleEditor editor) { + super(name, value); + this.editor = editor; + } + + @Override + public void mouseClicked(double mx, double my, int button) { + boolean before = get(); + super.mouseClicked(mx, my, button); + if (before != get()) editor.commit(); + } + } + + /** Integer slider that commits the whole editor after value changes or a completed drag. */ + private static final class RuleSliderSetting extends SliderSetting { + private final CooldownRuleEditor editor; + + RuleSliderSetting(String name, NumberValue value, CooldownRuleEditor editor) { + super(name, value); + this.editor = editor; + } + + @Override + public void mouseClicked(double mx, double my, int button) { + int before = value().get(); + super.mouseClicked(mx, my, button); + if (before != value().get()) editor.commit(); + } + + @Override + public void mouseReleased(double mx, double my, int button) { + boolean wasDragging = ui().dragging; + int before = value().get(); + super.mouseReleased(mx, my, button); + if (wasDragging || before != value().get()) editor.commit(); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesDetailRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesDetailRenderer.java new file mode 100644 index 0000000..504ced0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesDetailRenderer.java @@ -0,0 +1,198 @@ +package dev.loki.lovisual.features.gui.clickgui.settings.cooldown; + +import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.CooldownRulesSetting.Action; +import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.CooldownRulesSetting.ActionHit; +import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.CooldownRulesSetting.SettingHit; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.skin.UnifiedSettingsSkin; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.ArrayList; +import java.util.List; + +/** + * Draws the right-hand "detail" panel of {@link CooldownRulesSetting}: item header + * (icon + name + Add/Remove chip), the vertical editor body and the scrollbar. Also + * owns the click hit lists consumed by {@link #mouseClicked} and the scroll offset + * adjusted by {@link #mouseScrolled}. + */ +final class CooldownRulesDetailRenderer { + + static final float WIDTH = 380f; + private static final float PANEL_PAD = 10f; + private static final float HEADER_H = 112f; + private static final float SCROLL_STEP = 18f; + private static final float SCROLLBAR_W = 3.25f; + + private final CooldownRulesSetting owner; + private final List actionHits = new ArrayList<>(); + private final List settingHits = new ArrayList<>(); + private float scroll; + private float maxScroll; + + CooldownRulesDetailRenderer(CooldownRulesSetting owner) { this.owner = owner; } + + /** Reset the panel's scroll offset back to the top (called when the focus changes). */ + void resetScroll() { scroll = 0f; } + + /** Repaint the detail panel for one frame; records click hits consumed by {@link #mouseClicked}. */ + void render(float x, float y, float w, float h, String itemId, float mx, float my) { + UnifiedSettingsSkin.syncTheme(); + actionHits.clear(); + settingHits.clear(); + ItemCooldownRulesValue rules = owner.rules(); + ClickGuiRenderer.drawRoundedRect(x, y, w, h, 8f, UnifiedSettingsSkin.SURFACE_SOFT); + ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 8f, 0.8f, UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 70)); + ClickGuiRenderer.drawText(UnifiedSettingsSkin.fontSemibold(), "Cooldown Rule", + x + PANEL_PAD, y + 13f, 20f, UnifiedSettingsSkin.TEXT_PRIMARY, false); + ClickGuiRenderer.drawRoundedRect(x + PANEL_PAD, y + 39f, w - PANEL_PAD * 2f, 1f, 0.5f, + UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 48)); + boolean hasItem = itemId != null && !itemId.isBlank() && rules != null; + boolean hasRule = hasItem && rules.containsRule(itemId); + if (hasItem) { + ItemStack stack = toStack(itemId); + drawItemHeader(x, y, w, itemId, stack.isEmpty() ? itemId : stack.getHoverName().getString(), stack, hasRule, mx, my); + } + if (!hasItem) { + scroll = 0f; maxScroll = 0f; + drawHint(x, y + h * 0.5f - 14f, w, "Select an item on the left.", "A grid click only focuses it; rule creation is explicit."); + } else if (!hasRule) { + scroll = 0f; maxScroll = 0f; + drawHint(x, y + HEADER_H + 14f, w, "No cooldown rule for this item.", "Press Add to create one, then configure it below."); + } else { + drawEditorBody(x, y, w, h, owner.editorFor(itemId), mx, my); + } + } + + /** Route a left click through the recorded action / setting hit rects. */ + boolean mouseClicked(double mx, double my, int button) { + if (button != 0) return false; + for (ActionHit hit : new ArrayList<>(actionHits)) { + if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; + owner.handleAction(hit.action(), hit.itemId()); + return true; + } + for (SettingHit hit : new ArrayList<>(settingHits)) { + if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; + hit.setting().mouseClicked(mx, my, button); + return true; + } + return false; + } + + /** Forward a release to every editor setting so sliders can end their drag state. */ + void mouseReleased(double mx, double my, int button) { + for (SettingHit hit : new ArrayList<>(settingHits)) hit.setting().mouseReleased(mx, my, button); + } + /** Adjust the vertical scroll; returns true to signal the wheel event was consumed. */ + boolean mouseScrolled(double mx, double my, double delta) { + if (maxScroll > 0.5f) scroll = Math.max(0f, Math.min(scroll - (float) (delta * SCROLL_STEP), maxScroll)); + return true; + } + private void drawEditorBody(float x, float y, float w, float h, CooldownRuleEditor editor, float mx, float my) { + if (!editor.isDraggingSlider()) editor.syncFromRule(); + List settings = editor.settings; + float contentX = x + PANEL_PAD; + float contentY = y + HEADER_H; + float contentH = Math.max(1f, h - HEADER_H - PANEL_PAD); + float total = computeSettingsContentHeight(settings); + maxScroll = Math.max(0f, total - contentH); + scroll = Math.max(0f, Math.min(scroll, maxScroll)); + float contentW = w - PANEL_PAD * 2f - (maxScroll > 0.5f ? SCROLLBAR_W + 5f : 0f); + boolean clipped = ScissorFunction.pushRaw(contentX - 1f, contentY - 1f, contentW + 2f, contentH + 2f); + float cy = contentY - scroll; + for (Setting setting : settings) { + float rowH = setting.getHeight(); + if (cy + rowH >= contentY - 2f && cy <= contentY + contentH + 2f) { + setting.render(contentX, cy, contentW, mx, my); + settingHits.add(new SettingHit(setting, contentX, cy, contentW, rowH)); + } + cy += rowH + 2f; + } + if (clipped) ScissorFunction.pop(); + drawScrollbar(x, contentY, w, contentH); + } + + private void drawItemHeader(float x, float y, float w, String itemId, String name, ItemStack stack, boolean hasRule, float mx, float my) { + TextRenderer titleFont = UnifiedSettingsSkin.fontSemibold(); + TextRenderer font = UnifiedSettingsSkin.fontRegular(); + float iconBox = 46f, iconX = x + PANEL_PAD, iconY = y + 50f; + ClickGuiRenderer.drawRoundedRect(iconX, iconY, iconBox, iconBox, 7f, UnifiedSettingsSkin.SURFACE); + ClickGuiRenderer.drawRoundedRectStroke(iconX, iconY, iconBox, iconBox, 7f, 0.75f, UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 75)); + if (!stack.isEmpty()) { + float scale = 1.85f; + float px = 16f * scale; + ClickGuiRenderer.queuePickerIcon(stack, iconX + (iconBox - px) * 0.5f, iconY + (iconBox - px) * 0.5f, scale, iconX, iconY, iconBox, iconBox); + } + float textX = iconX + iconBox + 11f; + float buttonW = hasRule ? 76f : 68f, buttonH = 22f; + float buttonX = x + w - PANEL_PAD - buttonW, buttonY = y + 62f; + ClickGuiRenderer.drawText(titleFont, fit(titleFont, name, 15f, buttonX - textX - 10f), textX, y + 53f, 15f, UnifiedSettingsSkin.TEXT_PRIMARY, false); + ClickGuiRenderer.drawText(font, fit(font, itemId, 11.5f, buttonX - textX - 10f), textX, y + 73f, 11.5f, UnifiedSettingsSkin.TEXT_MUTED, false); + chip(buttonX, buttonY, buttonW, buttonH, hasRule ? "Remove" : "Add", inside(mx, my, buttonX, buttonY, buttonW, buttonH), hasRule); + actionHits.add(new ActionHit(hasRule ? Action.REMOVE_RULE : Action.ADD_RULE, itemId, buttonX, buttonY, buttonW, buttonH)); + } + + private void drawHint(float x, float top, float w, String line1, String line2) { + TextRenderer font = UnifiedSettingsSkin.fontRegular(); + float lineH = ClickGuiRenderer.textHeight(font, 12f) + 4f; + ClickGuiRenderer.drawText(font, line1, x + PANEL_PAD, top, 12f, UnifiedSettingsSkin.TEXT_MUTED, false); + ClickGuiRenderer.drawText(font, fit(font, line2, 12f, w - PANEL_PAD * 2f), x + PANEL_PAD, top + lineH, 12f, UnifiedSettingsSkin.TEXT_MUTED, false); + } + + private void chip(float x, float y, float w, float h, String text, boolean hover, boolean destructive) { + int bg = destructive ? UnifiedSettingsSkin.withAlpha(0xFFFF5555, 32) : UnifiedSettingsSkin.ACCENT_SOFT; + int stroke = destructive ? UnifiedSettingsSkin.withAlpha(0xFFFF7777, 120) : UnifiedSettingsSkin.ACCENT; + if (hover) bg = UnifiedSettingsSkin.mix(bg, UnifiedSettingsSkin.SURFACE_HOVER, 0.45f); + ClickGuiRenderer.drawRoundedRect(x, y, w, h, 4f, bg); + ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 4f, 0.6f, stroke); + TextRenderer font = UnifiedSettingsSkin.fontRegular(); + float tw = ClickGuiRenderer.textWidth(font, text, 10.5f), th = ClickGuiRenderer.textHeight(font, 10.5f); + ClickGuiRenderer.drawText(font, text, x + (w - tw) * 0.5f, y + (h - th) * 0.5f, 10.5f, UnifiedSettingsSkin.TEXT_PRIMARY, false); + } + + private void drawScrollbar(float panelX, float contentY, float panelW, float contentH) { + if (maxScroll <= 0.5f) return; + float trackX = panelX + panelW - PANEL_PAD - SCROLLBAR_W, trackY = contentY + 1f; + float trackH = Math.max(1f, contentH - 2f); + ClickGuiRenderer.drawRoundedRect(trackX, trackY, SCROLLBAR_W, trackH, SCROLLBAR_W * 0.5f, + UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 38)); + float thumbH = Math.max(18f, trackH * (contentH / (contentH + maxScroll))); + float thumbY = trackY + Math.max(1f, trackH - thumbH) * (scroll / Math.max(1f, maxScroll)); + ClickGuiRenderer.drawRoundedRect(trackX, thumbY, SCROLLBAR_W, thumbH, SCROLLBAR_W * 0.5f, + UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 120)); + } + + private static float computeSettingsContentHeight(List settings) { + float h = 0f; + for (int i = 0; i < settings.size(); i++) h += settings.get(i).getHeight() + (i + 1 < settings.size() ? 2f : 0f); + return h; + } + + private static ItemStack toStack(String itemId) { + try { + Identifier id = Identifier.tryParse(itemId); + if (id == null) return ItemStack.EMPTY; + Item item = BuiltInRegistries.ITEM.getValue(id); + return item == Items.AIR ? ItemStack.EMPTY : item.getDefaultInstance(); + } catch (Throwable ignored) { + return ItemStack.EMPTY; + } + } + + private static String fit(TextRenderer font, String text, float size, float width) { + return ClickGuiRenderer.fitText(font, text == null ? "" : text, size, Math.max(1f, width)); + } + + private static boolean inside(double mx, double my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java index 9ec1fb3..25c0592 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java @@ -1,36 +1,17 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.settings.cooldown; import dev.loki.lovisual.config.values.collection.ConfigValue; import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; import dev.loki.lovisual.config.values.collection.SetValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.Identifier; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.features.gui.clickgui.settings.owner.PickerDetailOwner; +import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; import dev.loki.lovisual.util.wav.util.ClickGuiSounds; -import java.util.*; -import dev.loki.lovisual.features.gui.clickgui.settings.bool.BooleanSetting; -import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; -import dev.loki.lovisual.features.gui.clickgui.settings.mode.ModeSetting; -import dev.loki.lovisual.features.gui.clickgui.settings.owner.PickerDetailOwner; -import dev.loki.lovisual.features.gui.clickgui.settings.skin.UnifiedSettingsSkin; -import dev.loki.lovisual.features.gui.clickgui.settings.slider.SliderSetting; -import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; /** * Picker-backed editor for per-item cooldown rules. @@ -41,22 +22,15 @@ import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSettin */ public final class CooldownRulesSetting extends TextListSetting implements PickerDetailOwner { - private static final float DETAIL_W = 190f * 2f; - private static final float PANEL_PAD = 10f; - private static final float HEADER_H = 112f; - private static final float DETAIL_SCROLL_STEP = 18f; - private static final float DETAIL_SCROLLBAR_W = 3.25f; - private final ItemCooldownRulesValue rules; - private final Ui ui = new Ui(); - private final Map editors = new HashMap<>(); + private final Map editors = new HashMap<>(); + private final CooldownRulesDetailRenderer detail; private String focusedItemId; - private float detailScroll; - private float detailMaxScroll; public CooldownRulesSetting(String name, ItemCooldownRulesValue rules) { super(name, bridgeValue(rules), TextListSetting.PickerMode.ITEMS); this.rules = rules; + this.detail = new CooldownRulesDetailRenderer(this); syncBridge(); } @@ -65,61 +39,6 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke rules != null ? rules.getItemIds() : Set.of()); } - private static float computeSettingsContentHeight(List settings) { - if (settings == null || settings.isEmpty()) return 0f; - float h = 0f; - for (int i = 0; i < settings.size(); i++) { - h += settings.get(i).getHeight(); - if (i + 1 < settings.size()) h += 2f; - } - return h; - } - - private static Item resolveItem(String itemId) { - try { - Identifier id = Identifier.tryParse(itemId); - if (id == null) return Items.AIR; - return BuiltInRegistries.ITEM.getValue(id); - } catch (Throwable ignored) { - return Items.AIR; - } - } - - private static String fit(TextRenderer font, String text, float size, float width) { - return ClickGuiRenderer.fitText(font, text == null ? "" : text, size, Math.max(1f, width)); - } - - private static boolean inside(double mx, double my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static ItemCooldownRulesValue.Scope scopeById(String id) { - if (id != null) { - for (ItemCooldownRulesValue.Scope scope : ItemCooldownRulesValue.Scope.values()) { - if (scope.getId().equals(id) || scope.name().equalsIgnoreCase(id)) return scope; - } - } - return ItemCooldownRulesValue.Scope.PVP_GRACE; - } - - private static ItemCooldownRulesValue.Trigger triggerById(String id) { - if (id != null) { - for (ItemCooldownRulesValue.Trigger trigger : ItemCooldownRulesValue.Trigger.values()) { - if (trigger.getId().equals(id) || trigger.name().equalsIgnoreCase(id)) return trigger; - } - } - return ItemCooldownRulesValue.Trigger.CONSUME_FINISH; - } - - private static ItemCooldownRulesValue.UseBlockMode blockById(String id) { - if (id != null) { - for (ItemCooldownRulesValue.UseBlockMode mode : ItemCooldownRulesValue.UseBlockMode.values()) { - if (mode.getId().equals(id) || mode.name().equalsIgnoreCase(id)) return mode; - } - } - return ItemCooldownRulesValue.UseBlockMode.NONE; - } - @Override public ConfigValue getConfigValue() { return rules; @@ -145,7 +64,6 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke @Override public void setValueSet(Set next) { if (rules == null) return; - Set normalized = new LinkedHashSet<>(); if (next != null) { for (String raw : next) { @@ -153,22 +71,14 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke if (id != null && !id.isBlank()) normalized.add(id); } } - for (String existing : new ArrayList<>(rules.getItemIds())) { if (!normalized.contains(existing)) { rules.removeRule(existing); editors.remove(existing); } } - - for (String id : normalized) { - rules.ensureRule(id); - } - - if (focusedItemId != null && !normalized.contains(focusedItemId) && normalized.isEmpty()) { - focusedItemId = null; - } - + for (String id : normalized) rules.ensureRule(id); + if (focusedItemId != null && !normalized.contains(focusedItemId) && normalized.isEmpty()) focusedItemId = null; syncBridge(); saveParentConfig(); } @@ -186,7 +96,7 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke @Override public float pickerDetailWidth() { - return DETAIL_W; + return CooldownRulesDetailRenderer.WIDTH; } @Override @@ -198,252 +108,48 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke public void onPickerFocusChanged(String id) { String normalized = normalizeEntry(id); if (normalized == null || normalized.isBlank()) return; - if (!normalized.equals(focusedItemId)) { - detailScroll = 0f; - } + if (!normalized.equals(focusedItemId)) detail.resetScroll(); focusedItemId = normalized; } @Override public void renderPickerDetails(float x, float y, float w, float h, String focusedId, float mouseX, float mouseY) { - UnifiedSettingsSkin.syncTheme(); - ui.actionHits.clear(); - ui.settingHits.clear(); - - String itemId = resolveFocusedId(focusedId); - drawPanel(x, y, w, h, itemId, mouseX, mouseY); + detail.render(x, y, w, h, resolveFocusedId(focusedId), mouseX, mouseY); } @Override public boolean mouseClickedPickerDetails(float mouseX, float mouseY, int button) { - if (button != 0) return false; - - for (ActionHit hit : new ArrayList<>(ui.actionHits)) { - if (!inside(mouseX, mouseY, hit.x, hit.y, hit.w, hit.h)) continue; - handleAction(hit.action, hit.itemId); - return true; - } - - for (SettingHit hit : new ArrayList<>(ui.settingHits)) { - if (!inside(mouseX, mouseY, hit.x, hit.y, hit.w, hit.h)) continue; - hit.setting.mouseClicked(mouseX, mouseY, button); - return true; - } - - return false; + return detail.mouseClicked(mouseX, mouseY, button); } @Override public void mouseReleasedPickerDetails(float mouseX, float mouseY, int button) { - for (SettingHit hit : new ArrayList<>(ui.settingHits)) { - hit.setting.mouseReleased(mouseX, mouseY, button); - } + detail.mouseReleased(mouseX, mouseY, button); } @Override public boolean mouseScrolledPickerDetails(float mouseX, float mouseY, double delta) { - if (detailMaxScroll <= 0.5f) return true; - detailScroll -= (float) (delta * DETAIL_SCROLL_STEP); - clampDetailScroll(); - return true; + return detail.mouseScrolled(mouseX, mouseY, delta); } - private void drawPanel(float x, float y, float w, float h, String itemId, float mx, float my) { - ClickGuiRenderer.drawRoundedRect(x, y, w, h, 8f, UnifiedSettingsSkin.SURFACE_SOFT); - ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 8f, 0.8f, UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 70)); + /** Actions routed from the header Add/Remove chip. */ + public enum Action { ADD_RULE, REMOVE_RULE } - TextRenderer titleFont = UnifiedSettingsSkin.fontSemibold(); - float titleSize = 20f; - ClickGuiRenderer.drawText(titleFont, "Cooldown Rule", x + PANEL_PAD, y + 13f, titleSize, UnifiedSettingsSkin.TEXT_PRIMARY, false); - float dividerY = y + 39f; - ClickGuiRenderer.drawRoundedRect(x + PANEL_PAD, dividerY, w - PANEL_PAD * 2f, 1f, 0.5f, UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 48)); + /** Screen rect of a chip button in the detail panel. */ + record ActionHit(Action action, String itemId, float x, float y, float w, float h) { } - if (itemId == null || itemId.isBlank() || rules == null) { - detailScroll = 0f; - detailMaxScroll = 0f; - drawEmptyDetails(x, y, w, h); - return; - } + /** Screen rect of a nested editor setting inside the detail panel body. */ + record SettingHit(dev.loki.lovisual.features.gui.clickgui.settings.core.Setting setting, float x, float y, float w, float h) { } - Item item = resolveItem(itemId); - ItemStack stack = item == Items.AIR ? ItemStack.EMPTY : item.getDefaultInstance(); - String name = stack.isEmpty() ? itemId : stack.getHoverName().getString(); - boolean hasRule = rules.containsRule(itemId); - - drawItemHeader(x, y, w, itemId, name, stack, hasRule, mx, my); - - if (!hasRule) { - detailScroll = 0f; - detailMaxScroll = 0f; - drawMissingRuleHint(x, y, w, h); - return; - } - - RuleEditor editor = editorFor(itemId); - if (!editor.isDraggingSlider()) { - editor.syncFromRule(); - } - - float contentX = x + PANEL_PAD; - float contentY = y + HEADER_H; - float contentH = Math.max(1f, h - HEADER_H - PANEL_PAD); - float totalContentH = computeSettingsContentHeight(editor.settings); - detailMaxScroll = Math.max(0f, totalContentH - contentH); - clampDetailScroll(); - - float scrollbarReserve = detailMaxScroll > 0.5f ? DETAIL_SCROLLBAR_W + 5f : 0f; - float contentW = w - PANEL_PAD * 2f - scrollbarReserve; - boolean clipped = ScissorFunction.pushRaw(contentX - 1f, contentY - 1f, contentW + 2f, contentH + 2f); - - float cy = contentY - detailScroll; - for (Setting setting : editor.settings) { - float rowH = setting.getHeight(); - if (cy + rowH >= contentY - 2f && cy <= contentY + contentH + 2f) { - setting.render(contentX, cy, contentW, mx, my); - ui.settingHits.add(new SettingHit(setting, contentX, cy, contentW, rowH)); - } - cy += rowH + 2f; - } - - if (clipped) ScissorFunction.pop(); - drawDetailScrollbar(x, contentY, w, contentH); + /** Lazily create / fetch the per-item editor; keeps slider drag state alive across frames. */ + CooldownRuleEditor editorFor(String itemId) { + return editors.computeIfAbsent(itemId, id -> new CooldownRuleEditor(this, id)); } - private void drawItemHeader(float x, - float y, - float w, - String itemId, - String name, - ItemStack stack, - boolean hasRule, - float mx, - float my) { - TextRenderer titleFont = UnifiedSettingsSkin.fontSemibold(); - TextRenderer font = UnifiedSettingsSkin.fontRegular(); - float small = 11.5f; + ItemCooldownRulesValue rules() { return rules; } - float iconBox = 46f; - float iconX = x + PANEL_PAD; - float iconY = y + 50f; - ClickGuiRenderer.drawRoundedRect(iconX, iconY, iconBox, iconBox, 7f, UnifiedSettingsSkin.SURFACE); - ClickGuiRenderer.drawRoundedRectStroke(iconX, iconY, iconBox, iconBox, 7f, 0.75f, UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 75)); - if (!stack.isEmpty()) { - float scale = 1.85f; - float px = 16f * scale; - ClickGuiRenderer.queuePickerIcon(stack, iconX + (iconBox - px) * 0.5f, iconY + (iconBox - px) * 0.5f, scale, iconX, iconY, iconBox, iconBox); - } - - float textX = iconX + iconBox + 11f; - float buttonW = hasRule ? 76f : 68f; - float buttonH = 22f; - float buttonX = x + w - PANEL_PAD - buttonW; - float buttonY = y + 62f; - ClickGuiRenderer.drawText(titleFont, fit(titleFont, name, 15f, buttonX - textX - 10f), textX, y + 53f, 15f, UnifiedSettingsSkin.TEXT_PRIMARY, false); - ClickGuiRenderer.drawText(font, fit(font, itemId, small, buttonX - textX - 10f), textX, y + 73f, small, UnifiedSettingsSkin.TEXT_MUTED, false); - - Action action = hasRule ? Action.REMOVE_RULE : Action.ADD_RULE; - String label = hasRule ? "Remove" : "Add"; - chip(buttonX, buttonY, buttonW, buttonH, label, inside(mx, my, buttonX, buttonY, buttonW, buttonH), hasRule); - ui.actionHits.add(new ActionHit(action, itemId, buttonX, buttonY, buttonW, buttonH)); - } - - private void drawEmptyDetails(float x, float y, float w, float h) { - TextRenderer font = UnifiedSettingsSkin.fontRegular(); - String line1 = "Select an item on the left."; - String line2 = "A grid click only focuses it; rule creation is explicit."; - float size = 12f; - float lineH = ClickGuiRenderer.textHeight(font, size) + 4f; - float top = y + h * 0.5f - lineH; - ClickGuiRenderer.drawText(font, line1, x + PANEL_PAD, top, size, UnifiedSettingsSkin.TEXT_MUTED, false); - ClickGuiRenderer.drawText(font, fit(font, line2, size, w - PANEL_PAD * 2f), x + PANEL_PAD, top + lineH, size, UnifiedSettingsSkin.TEXT_MUTED, false); - } - - private void drawMissingRuleHint(float x, float y, float w, float h) { - TextRenderer font = UnifiedSettingsSkin.fontRegular(); - String line1 = "No cooldown rule for this item."; - String line2 = "Press Add to create one, then configure it below."; - float size = 12f; - float lineH = ClickGuiRenderer.textHeight(font, size) + 4f; - float top = y + HEADER_H + 14f; - ClickGuiRenderer.drawText(font, line1, x + PANEL_PAD, top, size, UnifiedSettingsSkin.TEXT_MUTED, false); - ClickGuiRenderer.drawText(font, fit(font, line2, size, w - PANEL_PAD * 2f), x + PANEL_PAD, top + lineH, size, UnifiedSettingsSkin.TEXT_MUTED, false); - } - - private void chip(float x, float y, float w, float h, String text, boolean hover, boolean destructive) { - int bg = destructive ? UnifiedSettingsSkin.withAlpha(0xFFFF5555, 32) : UnifiedSettingsSkin.ACCENT_SOFT; - int stroke = destructive ? UnifiedSettingsSkin.withAlpha(0xFFFF7777, 120) : UnifiedSettingsSkin.ACCENT; - if (hover) bg = UnifiedSettingsSkin.mix(bg, UnifiedSettingsSkin.SURFACE_HOVER, 0.45f); - ClickGuiRenderer.drawRoundedRect(x, y, w, h, 4f, bg); - ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 4f, 0.6f, stroke); - TextRenderer font = UnifiedSettingsSkin.fontRegular(); - float size = 10.5f; - float tw = ClickGuiRenderer.textWidth(font, text, size); - float th = ClickGuiRenderer.textHeight(font, size); - ClickGuiRenderer.drawText(font, text, x + (w - tw) * 0.5f, y + (h - th) * 0.5f, size, UnifiedSettingsSkin.TEXT_PRIMARY, false); - } - - private void drawDetailScrollbar(float panelX, float contentY, float panelW, float contentH) { - if (detailMaxScroll <= 0.5f) return; - - float trackX = panelX + panelW - PANEL_PAD - DETAIL_SCROLLBAR_W; - float trackY = contentY + 1f; - float trackH = Math.max(1f, contentH - 2f); - ClickGuiRenderer.drawRoundedRect( - trackX, - trackY, - DETAIL_SCROLLBAR_W, - trackH, - DETAIL_SCROLLBAR_W * 0.5f, - UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 38) - ); - - float ratio = contentH / (contentH + detailMaxScroll); - float thumbH = Math.max(18f, trackH * ratio); - float thumbRange = Math.max(1f, trackH - thumbH); - float thumbY = trackY + thumbRange * (detailScroll / Math.max(1f, detailMaxScroll)); - ClickGuiRenderer.drawRoundedRect( - trackX, - thumbY, - DETAIL_SCROLLBAR_W, - thumbH, - DETAIL_SCROLLBAR_W * 0.5f, - UnifiedSettingsSkin.withAlpha(UnifiedSettingsSkin.TEXT_MUTED, 120) - ); - } - - private void clampDetailScroll() { - detailScroll = Math.max(0f, Math.min(detailScroll, detailMaxScroll)); - } - - private void handleAction(Action action, String itemId) { - if (rules == null || itemId == null || itemId.isBlank()) return; - switch (action) { - case ADD_RULE -> { - rules.ensureRule(itemId); - focusedItemId = itemId; - detailScroll = 0f; - editorFor(itemId).syncFromRule(); - syncBridge(); - saveParentConfig(); - ClickGuiSounds.changeMode(); - } - case REMOVE_RULE -> { - rules.removeRule(itemId); - editors.remove(itemId); - focusedItemId = itemId; - detailScroll = 0f; - syncBridge(); - saveParentConfig(); - ClickGuiSounds.changeMode(); - } - } - } - - private RuleEditor editorFor(String itemId) { - return editors.computeIfAbsent(itemId, RuleEditor::new); - } - - private String resolveFocusedId(String focusedId) { + /** Resolve which item id to show in the detail panel, falling back to the first selected item. */ + String resolveFocusedId(String focusedId) { String normalized = normalizeEntry(focusedId); if (normalized != null && !normalized.isBlank()) { focusedItemId = normalized; @@ -456,148 +162,37 @@ public final class CooldownRulesSetting extends TextListSetting implements Picke return focusedItemId; } - private void syncBridge() { - ConfigValue bridge = super.getConfigValue(); - if (bridge instanceof SetValue set) { - set.set(getValueSet()); + /** Apply an ADD / REMOVE chip click to the underlying rules store and refresh the picker bridge. */ + void handleAction(Action action, String itemId) { + if (rules == null || itemId == null || itemId.isBlank()) return; + switch (action) { + case ADD_RULE -> { + rules.ensureRule(itemId); + focusedItemId = itemId; + detail.resetScroll(); + editorFor(itemId).syncFromRule(); + syncBridge(); + saveParentConfig(); + ClickGuiSounds.changeMode(); + } + case REMOVE_RULE -> { + rules.removeRule(itemId); + editors.remove(itemId); + focusedItemId = itemId; + detail.resetScroll(); + syncBridge(); + saveParentConfig(); + ClickGuiSounds.changeMode(); + } } } - private void saveParentConfig() { + /** Mirror the current rules' item-id set into the parent {@link TextListSetting} picker value. */ + void syncBridge() { + if (super.getConfigValue() instanceof SetValue set) set.set(getValueSet()); + } + + void saveParentConfig() { if (getParent() != null) getParent().saveConfig(); } - - private enum Action { - ADD_RULE, - REMOVE_RULE - } - - private record ActionHit(Action action, String itemId, float x, float y, float w, float h) { - } - - private record SettingHit(Setting setting, float x, float y, float w, float h) { - } - - private static final class RuleBooleanSetting extends BooleanSetting { - private final RuleEditor editor; - - RuleBooleanSetting(String name, BooleanValue value, RuleEditor editor) { - super(name, value); - this.editor = editor; - } - - @Override - public void mouseClicked(double mx, double my, int button) { - boolean before = get(); - super.mouseClicked(mx, my, button); - if (before != get()) editor.commit(); - } - } - - private static final class RuleSliderSetting extends SliderSetting { - private final RuleEditor editor; - - RuleSliderSetting(String name, NumberValue value, RuleEditor editor) { - super(name, value); - this.editor = editor; - } - - @Override - public void mouseClicked(double mx, double my, int button) { - int before = value().get(); - super.mouseClicked(mx, my, button); - if (before != value().get()) editor.commit(); - } - - @Override - public void mouseReleased(double mx, double my, int button) { - boolean wasDragging = ui().dragging; - int before = value().get(); - super.mouseReleased(mx, my, button); - if (wasDragging || before != value().get()) editor.commit(); - } - } - - private static final class Ui { - final List actionHits = new ArrayList<>(); - final List settingHits = new ArrayList<>(); - } - - private final class RuleEditor { - final String itemId; - final BooleanValue enabledValue = new BooleanValue("cooldown_rule_enabled", false); - final NumberValue secondsValue = new NumberValue<>("cooldown_rule_seconds", 0, 0, 600); - final NumberValue usesValue = new NumberValue<>("cooldown_rule_uses", 1, 1, 10); - final NumberValue windowValue = new NumberValue<>("cooldown_rule_window", 0, 0, 300); - final ModeValue scopeValue = new ModeValue("cooldown_rule_scope", ItemCooldownRulesValue.Scope.PVP_GRACE.getId(), "always", "pvp_only", "pvp_grace"); - final ModeValue triggerValue = new ModeValue("cooldown_rule_trigger", ItemCooldownRulesValue.Trigger.CONSUME_FINISH.getId(), "interact_accept", "consume_finish", "totem_pop"); - final ModeValue blockValue = new ModeValue("cooldown_rule_block", ItemCooldownRulesValue.UseBlockMode.NONE.getId(), "none", "cooldown", "window", "any"); - - final List settings = new ArrayList<>(); - - final RuleBooleanSetting enabledSetting; - final RuleSliderSetting secondsSetting; - final RuleSliderSetting usesSetting; - final RuleSliderSetting windowSetting; - final ModeSetting scopeSetting; - final ModeSetting triggerSetting; - final ModeSetting blockSetting; - - RuleEditor(String itemId) { - this.itemId = itemId; - enabledSetting = new RuleBooleanSetting("Enabled", enabledValue, this); - secondsSetting = new RuleSliderSetting("Time (s)", secondsValue, this); - usesSetting = new RuleSliderSetting("Uses", usesValue, this); - windowSetting = new RuleSliderSetting("Window (s)", windowValue, this); - scopeSetting = new ModeSetting("Scope", scopeValue, this::commit); - triggerSetting = new ModeSetting("Trigger", triggerValue, this::commit); - blockSetting = new ModeSetting("Block", blockValue, this::commit); - - settings.add(enabledSetting); - settings.add(secondsSetting); - settings.add(usesSetting); - settings.add(windowSetting); - settings.add(scopeSetting); - settings.add(triggerSetting); - settings.add(blockSetting); - } - - void syncFromRule() { - if (rules == null) return; - ItemCooldownRulesValue.Rule rule = rules.getRule(itemId); - if (rule == null) return; - enabledValue.set(rule.enabled()); - secondsValue.set(rule.seconds()); - usesValue.set(rule.uses()); - windowValue.set(rule.windowSeconds()); - scopeValue.set(rule.scope().getId()); - triggerValue.set(rule.trigger().getId()); - blockValue.set(rule.blockMode().getId()); - } - - boolean isDraggingSlider() { - return secondsSetting.ui().dragging || usesSetting.ui().dragging || windowSetting.ui().dragging; - } - - void commit() { - if (rules == null || !rules.containsRule(itemId)) return; - ItemCooldownRulesValue.Rule current = rules.getRule(itemId); - if (current == null) current = rules.getDefaultRule(itemId); - - ItemCooldownRulesValue.Rule next = new ItemCooldownRulesValue.Rule( - enabledValue.get(), - secondsValue.get(), - usesValue.get(), - windowValue.get(), - scopeById(scopeValue.get()), - triggerById(triggerValue.get()), - blockById(blockValue.get()) - ).normalized(); - - rules.putRule(itemId, next); - syncFromRule(); - syncBridge(); - saveParentConfig(); - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java index 8ea2a7a..5eabbc3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java @@ -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.gui.clickgui.settings.cooldown; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java index 4310b73..939fab4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java @@ -1,365 +1,87 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.clickgui.settings.core; -import net.minecraft.client.resources.language.I18n; -import dev.loki.lovisual.config.i18n.ConfigLang; import dev.loki.lovisual.config.definition.SettingOwner; import dev.loki.lovisual.config.values.collection.ConfigValue; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.function.Supplier; +/** + * Base class for every ClickGUI setting widget. Owns the underlying {@link ConfigValue}, the + * show/hide visibility animation and the input/render contract subclasses implement. All i18n + * resolution and translation-key derivation is delegated to {@link SettingLocalization} (per + * instance) and {@link SettingI18n} (process-wide); this class keeps only the stable API used + * by the settings panels and by the concrete setting types. + */ public abstract class Setting { protected static final float LABEL_PAD = 6f; - private static final ThreadLocal MISSING_REPORTER = new ThreadLocal<>(); - private static boolean LANG_READY = false; protected final ConfigValue value; private final String name; - private final Map cachedOptionDisplayNames = new HashMap<>(); - private SettingOwner parent; + private final SettingLocalization localization; private Supplier visibility = () -> true; private Supplier unavailableReason = () -> null; private boolean lastVisible = true; private float visibilityAnim = 1f; private long lastAnimTime = System.nanoTime(); - private String cachedDisplayName; - private String cachedTranslationKey; - private String cachedDescription; - private String cachedDescriptionBase; - private SettingOwner cachedTranslationOwner; - private final List commonI18nKeys = new ArrayList<>(); - private boolean i18nNameEnabled = true; - private boolean i18nOptionsEnabled = true; public Setting(String name, ConfigValue value) { this.name = name; this.value = value; + this.localization = new SettingLocalization(this); } public Setting(String name) { this.name = name; this.value = null; + this.localization = new SettingLocalization(this); } protected static float labelX(float x) { return x + LABEL_PAD; } - private static String resolveFirstTranslation(java.util.List keys, boolean logIfMissing) { - if (keys == null || keys.isEmpty()) return null; + /** Reset the language gate and probe readiness; call before a preflight pass. */ + public static boolean prepareI18nPreflightPass() { return SettingI18n.preparePreflight(); } - if (!ensureLangReady()) { - return null; - } - - String firstLoggable = null; - - for (String key : keys) { - if (key == null || key.isBlank()) continue; - - boolean loggable = key.startsWith("setting."); - if (loggable && firstLoggable == null) { - firstLoggable = key; - } - - String translated = I18n.get(key); - if (!translated.equals(key)) { - return translated; - } - } - - if (logIfMissing && firstLoggable != null) { - MissingI18nReporter reporter = MISSING_REPORTER.get(); - if (reporter != null) { - reporter.missing(keys); - } - } - - return null; - } - - private static boolean ensureLangReady() { - if (LANG_READY) return true; - if (!I18n.get("setting.module.bind").equals("setting.module.bind")) { - LANG_READY = true; - } - return LANG_READY; - } - - public static boolean prepareI18nPreflightPass() { - LANG_READY = false; - return ensureLangReady(); - } - - public static AutoCloseable captureMissingI18n(MissingI18nReporter reporter) { - MissingI18nReporter previous = MISSING_REPORTER.get(); - if (reporter == null) { - MISSING_REPORTER.remove(); - } else { - MISSING_REPORTER.set(reporter); - } - return () -> { - if (previous == null) { - MISSING_REPORTER.remove(); - } else { - MISSING_REPORTER.set(previous); - } - }; - } + /** Capture missing translation keys reported during the returned handle's lifetime. */ + public static AutoCloseable captureMissingI18n(MissingI18nReporter reporter) { return SettingI18n.captureMissing(reporter); } // --- UI --- - public String getName() { - return name; - } + public String getName() { return name; } - public String getDisplayName() { - if (LANG_READY && cachedDisplayName != null) return cachedDisplayName; + public String getDisplayName() { return localization.getDisplayName(); } - if (i18nNameEnabled) { - String translated = resolveFirstTranslation(getTranslationKeys(), true); - if (translated != null) { - if (LANG_READY) cachedDisplayName = translated; - return translated; - } - } + public String getOptionDisplayName(String optionId) { return localization.getOptionDisplayName(optionId); } - if (value != null) { - String fallback = ConfigLang.getName(value); - if (fallback != null && !fallback.isBlank() && !fallback.equals(value.getName())) { - if (LANG_READY) cachedDisplayName = fallback; - return fallback; - } - } + /** Hover description from {@code .desc}; null when no key exists. */ + public String getDescription() { return localization.getDescription(); } - if (LANG_READY) cachedDisplayName = name; - return name; - } + public SettingOwner getParent() { return localization.getParent(); } - public String getOptionDisplayName(String optionId) { - if (optionId == null || optionId.isBlank()) return optionId; + public void setParent(SettingOwner owner) { localization.setParent(owner); } - if (!i18nOptionsEnabled) { - return optionId; - } + public String getTranslationKey() { return localization.getTranslationKey(); } - if (LANG_READY) { - String cached = cachedOptionDisplayNames.get(optionId); - if (cached != null) return cached; - } + public List getTranslationKeys() { return localization.getTranslationKeys(); } - java.util.List optionKeys = optionTranslationKeys(optionId); + public void setCommonI18nKey(String key) { localization.setCommonI18nKey(key); } - String translated = resolveFirstTranslation(optionKeys, true); - String resolved = translated != null ? translated : optionId; + public void setCommonI18nKeys(Iterable keys) { localization.setCommonI18nKeys(keys); } - if (LANG_READY) { - cachedOptionDisplayNames.put(optionId, resolved); - } + public void setI18nEnabled(boolean name, boolean options) { localization.setI18nEnabled(name, options); } - return resolved; - } + /** Trigger i18n lookup so missing keys are logged early. */ + public void preflightI18n() { localization.preflight(); } - /** - * Hover description from {@code .desc} (e.g. - * setting.cosmeticdisplay.next_cosmetic.desc); null when no key exists — - * never falls back to the raw key, unlike names. - */ - public String getDescription() { - String local = getTranslationKey(); - if (local == null || local.isBlank()) return null; - if (LANG_READY && cachedDescription != null && local.equals(cachedDescriptionBase)) { - return cachedDescription; - } - String translated = resolveFirstTranslation(java.util.List.of(local + ".desc"), false); - if (LANG_READY && translated != null) { - cachedDescription = translated; - cachedDescriptionBase = local; - } - return translated; - } - - public SettingOwner getParent() { - return parent; - } - - public void setParent(SettingOwner owner) { - if (parent != owner) { - parent = owner; - invalidateNameCaches(); - return; - } - parent = owner; - } - - public String getTranslationKey() { - if (parent == null) return null; - if (cachedTranslationKey != null && cachedTranslationOwner == parent) { - return cachedTranslationKey; - } - String prefix = parent.getTranslationKeyPrefix(); - String id = getTranslationId(); - if (prefix == null || id == null || prefix.isBlank() || id.isBlank()) return null; - cachedTranslationOwner = parent; - cachedTranslationKey = prefix + "." + id; - return cachedTranslationKey; - } - - public java.util.List getTranslationKeys() { - java.util.List keys = new java.util.ArrayList<>(3); - - String local = getTranslationKey(); - if (local != null && !local.isBlank()) { - keys.add(local); - } - - for (String commonI18nKey : commonI18nKeys) { - if (commonI18nKey != null && !commonI18nKey.isBlank()) { - keys.add("setting.common." + commonI18nKey); - } - } - - String id = getTranslationId(); - if (id != null && !id.isBlank()) { - keys.add("setting.common." + id); - } - - return keys; - } - - public void setCommonI18nKey(String key) { - ArrayList keys = new ArrayList<>(); - addCommonI18nKey(keys, key); - setCommonI18nKeys(keys); - } - - public void setCommonI18nKeys(Iterable keys) { - ArrayList normalized = new ArrayList<>(); - if (keys != null) { - for (String key : keys) { - addCommonI18nKey(normalized, key); - } - } - if (!commonI18nKeys.equals(normalized)) { - commonI18nKeys.clear(); - commonI18nKeys.addAll(normalized); - invalidateNameCaches(); - } - } - - private static void addCommonI18nKey(ArrayList keys, String key) { - if (key == null) return; - String normalized = key.trim(); - if (!normalized.isBlank() && !keys.contains(normalized)) { - keys.add(normalized); - } - } - - public void setI18nEnabled(boolean name, boolean options) { - if (i18nNameEnabled != name || i18nOptionsEnabled != options) { - i18nNameEnabled = name; - i18nOptionsEnabled = options; - invalidateNameCaches(); - } - } - - /** - * Trigger i18n lookup so missing keys are logged early. - */ - public void preflightI18n() { - if (!i18nNameEnabled) return; - resolveFirstTranslation(getTranslationKeys(), true); - } - - protected void preflightOptionI18n(String optionId) { - if (!i18nOptionsEnabled) return; - if (optionId == null || optionId.isBlank()) return; - - resolveFirstTranslation(optionTranslationKeys(optionId), true); - } - - private java.util.List optionTranslationKeys(String optionId) { - ArrayList optionKeys = new ArrayList<>(); - if (optionId == null || optionId.isBlank()) return optionKeys; - ArrayList variants = optionIdVariants(optionId); - for (String base : getTranslationKeys()) { - if (base == null || base.isBlank()) continue; - for (String variant : variants) { - addOptionKey(optionKeys, base, variant); - } - } - return optionKeys; - } - - private static ArrayList optionIdVariants(String optionId) { - ArrayList variants = new ArrayList<>(); - addVariant(variants, optionId); - - String lower = optionId.toLowerCase(java.util.Locale.ROOT); - addVariant(variants, lower); - addNormalizedOptionVariants(variants, lower); - addVariant(variants, optionId.toUpperCase(java.util.Locale.ROOT)); - - if (!lower.isEmpty()) { - addVariant(variants, Character.toUpperCase(lower.charAt(0)) + lower.substring(1)); - } - - return variants; - } - - private static void addNormalizedOptionVariants(ArrayList variants, String lower) { - if (lower == null || lower.isBlank()) return; - String normalized = lower.trim() - .replace('-', '_') - .replace(' ', '_'); - while (normalized.contains("__")) { - normalized = normalized.replace("__", "_"); - } - normalized = stripEdgeUnderscores(normalized); - addVariant(variants, normalized); - - String compact = normalized.replace("_", ""); - addVariant(variants, compact); - } - - private static String stripEdgeUnderscores(String value) { - int start = 0; - int end = value.length(); - while (start < end && value.charAt(start) == '_') start++; - while (end > start && value.charAt(end - 1) == '_') end--; - return start == 0 && end == value.length() ? value : value.substring(start, end); - } - - private static void addVariant(ArrayList variants, String value) { - if (value != null && !value.isBlank() && !variants.contains(value)) { - variants.add(value); - } - } - - private static void addOptionKey(ArrayList keys, String base, String optionId) { - String key = base + ".option." + optionId; - if (!keys.contains(key)) { - keys.add(key); - } - } + protected void preflightOptionI18n(String optionId) { localization.preflightOption(optionId); } public Setting unavailableReason(Supplier supplier) { this.unavailableReason = supplier == null ? () -> null : supplier; return this; } - public boolean isAvailable() { - return getUnavailableReason().isEmpty(); - } + public boolean isAvailable() { return getUnavailableReason().isEmpty(); } public String getUnavailableReason() { try { @@ -374,65 +96,34 @@ public abstract class Setting { public abstract void mouseClicked(double mx, double my, int button); - public void mouseReleased(double mx, double my, int button) { - } + public void mouseReleased(double mx, double my, int button) { } - /** - * Called by parent panels when a click happens outside this setting hitbox. - */ - public void mouseClickedOutside(double mx, double my, int button) { - } + /** Called by parent panels when a click happens outside this setting hitbox. */ + public void mouseClickedOutside(double mx, double my, int button) { } - public boolean mouseScrolled(double mx, double my, double amount) { - return false; - } + public boolean mouseScrolled(double mx, double my, double amount) { return false; } - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - return false; - } + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { return false; } - public boolean charTyped(char chr, int modifiers) { - return false; - } + public boolean charTyped(char chr, int modifiers) { return false; } public abstract float getHeight(); - /** - * Уникальный ID (для сериализации) - */ - public String getId() { - return name.toLowerCase().replace(" ", "_"); - } + /** Unique id used for serialization. */ + public String getId() { return name.toLowerCase().replace(" ", "_"); } - protected String getTranslationId() { - return getId(); - } + protected String getTranslationId() { return getId(); } - // ====================================================== - // Сериализация (через ConfigValue) - // ====================================================== - - public Object save() { - if (value != null) - return value.toJson(); - return null; - } + public Object save() { return value != null ? value.toJson() : null; } public void load(Object o) { - if (value != null) - value.fromJson(o); + if (value != null) value.fromJson(o); } - /** - * Underlying config value (used for persistence). - */ - public ConfigValue getConfigValue() { - return value; - } + /** Underlying config value (used for persistence). */ + public ConfigValue getConfigValue() { return value; } - // ====================================================== - // Visibility - // ====================================================== + // --- Visibility --- public Setting visibleWhen(Supplier supplier) { this.visibility = supplier == null ? () -> true : supplier; return this; @@ -446,9 +137,7 @@ public abstract class Setting { } } - /** - * Smooth visibility progress 0..1 for animating show/hide. - */ + /** Smooth visibility progress 0..1 for animating show/hide. */ public float updateVisibilityAnim() { boolean vis = isVisible(); long now = System.nanoTime(); @@ -456,50 +145,26 @@ public abstract class Setting { lastAnimTime = now; if (dt <= 0f) dt = 0.0001f; dt = Math.min(dt, 0.05f); // avoid huge jumps on long pauses - - if (vis && !lastVisible) { - visibilityAnim = 0f; // start animating from 0 when becoming visible - } - + if (vis && !lastVisible) visibilityAnim = 0f; // start animating from 0 when becoming visible float target = vis ? 1f : 0f; float speed = 10f; // higher = faster float step = Math.min(0.35f, dt * speed); // cap so it never jumps in a single frame visibilityAnim += (target - visibilityAnim) * step; - - // clamp if (visibilityAnim < 0.001f) visibilityAnim = 0f; if (visibilityAnim > 0.999f) visibilityAnim = 1f; - lastVisible = vis; return visibilityAnim; } - public float getVisibilityAnim() { - return visibilityAnim; - } + public float getVisibilityAnim() { return visibilityAnim; } - public boolean isVisibilityTargetVisible() { - return lastVisible; - } + public boolean isVisibilityTargetVisible() { return lastVisible; } - /** - * Height scaled by visibility animation, used for layout. - */ - public float getAnimatedHeight() { - return getHeight() * visibilityAnim; - } - - private void invalidateNameCaches() { - cachedDisplayName = null; - cachedTranslationKey = null; - cachedDescription = null; - cachedDescriptionBase = null; - cachedTranslationOwner = null; - cachedOptionDisplayNames.clear(); - } + /** Height scaled by visibility animation, used for layout. */ + public float getAnimatedHeight() { return getHeight() * visibilityAnim; } @FunctionalInterface public interface MissingI18nReporter { - void missing(java.util.List keyChain); + void missing(List keyChain); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java index b3ab6dd..9d06a5b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java @@ -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.gui.clickgui.settings.core; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingI18n.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingI18n.java new file mode 100644 index 0000000..1920aeb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingI18n.java @@ -0,0 +1,119 @@ +package dev.loki.lovisual.features.gui.clickgui.settings.core; + +import net.minecraft.client.resources.language.I18n; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Static i18n resolution and translation-key generation for {@link Setting}: language-readiness + * gate, first-hit translation lookup, missing-key capture and the option-id variant expansion + * used to build candidate keys. Holds only process-wide state; per-instance caches live in + * {@link SettingLocalization}. Extracted so the setting base class stays under the file-size cap. + */ +final class SettingI18n { + + private static final ThreadLocal MISSING_REPORTER = new ThreadLocal<>(); + private static boolean langReady = false; + + private SettingI18n() { } + + static boolean langReady() { return langReady; } + + /** Reset the language-readiness gate and re-run the probe; used before a preflight pass. */ + static boolean preparePreflight() { + langReady = false; + return ensureLangReady(); + } + + /** Install a missing-key reporter for the current thread; the returned handle restores the prior one. */ + static AutoCloseable captureMissing(Setting.MissingI18nReporter reporter) { + Setting.MissingI18nReporter previous = MISSING_REPORTER.get(); + if (reporter == null) MISSING_REPORTER.remove(); + else MISSING_REPORTER.set(reporter); + return () -> { + if (previous == null) MISSING_REPORTER.remove(); + else MISSING_REPORTER.set(previous); + }; + } + + /** Return the first key that resolves to a translation (distinct from the key), else report it as missing. */ + static String resolveFirstTranslation(List keys, boolean logIfMissing) { + if (keys == null || keys.isEmpty()) return null; + if (!ensureLangReady()) return null; + String firstLoggable = null; + for (String key : keys) { + if (key == null || key.isBlank()) continue; + if (key.startsWith("setting.") && firstLoggable == null) firstLoggable = key; + String translated = I18n.get(key); + if (!translated.equals(key)) return translated; + } + if (logIfMissing && firstLoggable != null) { + Setting.MissingI18nReporter reporter = MISSING_REPORTER.get(); + if (reporter != null) reporter.missing(keys); + } + return null; + } + + /** Candidate option keys: {@code .option.} for every base key and option-id variant. */ + static List optionKeys(List baseKeys, String optionId) { + ArrayList keys = new ArrayList<>(); + if (optionId == null || optionId.isBlank()) return keys; + ArrayList variants = optionIdVariants(optionId); + for (String base : baseKeys) { + if (base == null || base.isBlank()) continue; + for (String variant : variants) addOptionKey(keys, base, variant); + } + return keys; + } + + static void addCommonI18nKey(ArrayList keys, String key) { + if (key == null) return; + String normalized = key.trim(); + if (!normalized.isBlank() && !keys.contains(normalized)) keys.add(normalized); + } + + private static boolean ensureLangReady() { + if (langReady) return true; + if (!I18n.get("setting.module.bind").equals("setting.module.bind")) langReady = true; + return langReady; + } + + private static ArrayList optionIdVariants(String optionId) { + ArrayList variants = new ArrayList<>(); + addVariant(variants, optionId); + String lower = optionId.toLowerCase(Locale.ROOT); + addVariant(variants, lower); + addNormalizedOptionVariants(variants, lower); + addVariant(variants, optionId.toUpperCase(Locale.ROOT)); + if (!lower.isEmpty()) addVariant(variants, Character.toUpperCase(lower.charAt(0)) + lower.substring(1)); + return variants; + } + + private static void addNormalizedOptionVariants(ArrayList variants, String lower) { + if (lower == null || lower.isBlank()) return; + String normalized = lower.trim().replace('-', '_').replace(' ', '_'); + while (normalized.contains("__")) normalized = normalized.replace("__", "_"); + normalized = stripEdgeUnderscores(normalized); + addVariant(variants, normalized); + addVariant(variants, normalized.replace("_", "")); + } + + private static String stripEdgeUnderscores(String value) { + int start = 0; + int end = value.length(); + while (start < end && value.charAt(start) == '_') start++; + while (end > start && value.charAt(end - 1) == '_') end--; + return start == 0 && end == value.length() ? value : value.substring(start, end); + } + + private static void addVariant(ArrayList variants, String value) { + if (value != null && !value.isBlank() && !variants.contains(value)) variants.add(value); + } + + private static void addOptionKey(ArrayList keys, String base, String optionId) { + String key = base + ".option." + optionId; + if (!keys.contains(key)) keys.add(key); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingLocalization.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingLocalization.java new file mode 100644 index 0000000..ae3b5a6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/impl/SettingLocalization.java @@ -0,0 +1,164 @@ +package dev.loki.lovisual.features.gui.clickgui.settings.core; + +import dev.loki.lovisual.config.i18n.ConfigLang; +import dev.loki.lovisual.config.definition.SettingOwner; +import dev.loki.lovisual.config.values.collection.ConfigValue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Per-setting localization state: translation-key derivation, resolved display-name / + * option-name / description caches, common-i18n-key registration and the i18n on/off flags. + * Owned by a single {@link Setting} and backed by the static helpers in {@link SettingI18n}. + * Extracted so the setting base class stays under the file-size cap. + */ +final class SettingLocalization { + + private final Setting owner; + private final Map cachedOptionDisplayNames = new HashMap<>(); + private final List commonI18nKeys = new ArrayList<>(); + private SettingOwner parent; + private boolean nameEnabled = true; + private boolean optionsEnabled = true; + private String cachedDisplayName; + private String cachedTranslationKey; + private String cachedDescription; + private String cachedDescriptionBase; + private SettingOwner cachedTranslationOwner; + + SettingLocalization(Setting owner) { + this.owner = owner; + } + + SettingOwner getParent() { return parent; } + + void setParent(SettingOwner ownerRef) { + if (parent != ownerRef) { + parent = ownerRef; + invalidateNameCaches(); + return; + } + parent = ownerRef; + } + + /** {@code .} or null when either part is missing. */ + String getTranslationKey() { + if (parent == null) return null; + if (cachedTranslationKey != null && cachedTranslationOwner == parent) return cachedTranslationKey; + String prefix = parent.getTranslationKeyPrefix(); + String id = owner.getTranslationId(); + if (prefix == null || id == null || prefix.isBlank() || id.isBlank()) return null; + cachedTranslationOwner = parent; + cachedTranslationKey = prefix + "." + id; + return cachedTranslationKey; + } + + /** Ordered candidate keys: local translation key, then registered common keys, then id-based. */ + List getTranslationKeys() { + List keys = new ArrayList<>(3); + String local = getTranslationKey(); + if (local != null && !local.isBlank()) keys.add(local); + for (String commonI18nKey : commonI18nKeys) { + if (commonI18nKey != null && !commonI18nKey.isBlank()) keys.add("setting.common." + commonI18nKey); + } + String id = owner.getTranslationId(); + if (id != null && !id.isBlank()) keys.add("setting.common." + id); + return keys; + } + + String getDisplayName() { + boolean ready = SettingI18n.langReady(); + if (ready && cachedDisplayName != null) return cachedDisplayName; + if (nameEnabled) { + String translated = SettingI18n.resolveFirstTranslation(getTranslationKeys(), true); + if (translated != null) { + if (ready) cachedDisplayName = translated; + return translated; + } + } + ConfigValue value = owner.getConfigValue(); + if (value != null) { + String fallback = ConfigLang.getName(value); + if (fallback != null && !fallback.isBlank() && !fallback.equals(value.getName())) { + if (ready) cachedDisplayName = fallback; + return fallback; + } + } + if (ready) cachedDisplayName = owner.getName(); + return owner.getName(); + } + + String getOptionDisplayName(String optionId) { + if (optionId == null || optionId.isBlank()) return optionId; + if (!optionsEnabled) return optionId; + boolean ready = SettingI18n.langReady(); + if (ready) { + String cached = cachedOptionDisplayNames.get(optionId); + if (cached != null) return cached; + } + String translated = SettingI18n.resolveFirstTranslation(SettingI18n.optionKeys(getTranslationKeys(), optionId), true); + String resolved = translated != null ? translated : optionId; + if (ready) cachedOptionDisplayNames.put(optionId, resolved); + return resolved; + } + + /** Hover description from {@code .desc}; null when no key exists. */ + String getDescription() { + boolean ready = SettingI18n.langReady(); + String local = getTranslationKey(); + if (local == null || local.isBlank()) return null; + if (ready && cachedDescription != null && local.equals(cachedDescriptionBase)) return cachedDescription; + String translated = SettingI18n.resolveFirstTranslation(List.of(local + ".desc"), false); + if (ready && translated != null) { + cachedDescription = translated; + cachedDescriptionBase = local; + } + return translated; + } + + void setCommonI18nKey(String key) { + ArrayList keys = new ArrayList<>(); + SettingI18n.addCommonI18nKey(keys, key); + setCommonI18nKeys(keys); + } + + void setCommonI18nKeys(Iterable keys) { + ArrayList normalized = new ArrayList<>(); + if (keys != null) for (String key : keys) SettingI18n.addCommonI18nKey(normalized, key); + if (!commonI18nKeys.equals(normalized)) { + commonI18nKeys.clear(); + commonI18nKeys.addAll(normalized); + invalidateNameCaches(); + } + } + + void setI18nEnabled(boolean name, boolean options) { + if (nameEnabled != name || optionsEnabled != options) { + nameEnabled = name; + optionsEnabled = options; + invalidateNameCaches(); + } + } + + void preflight() { + if (nameEnabled) SettingI18n.resolveFirstTranslation(getTranslationKeys(), true); + } + + void preflightOption(String optionId) { + if (optionsEnabled && optionId != null && !optionId.isBlank()) { + SettingI18n.resolveFirstTranslation(SettingI18n.optionKeys(getTranslationKeys(), optionId), true); + } + } + + private void invalidateNameCaches() { + cachedDisplayName = null; + cachedTranslationKey = null; + cachedDescription = null; + cachedDescriptionBase = null; + cachedTranslationOwner = null; + cachedOptionDisplayNames.clear(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java index 76e7970..56501d0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java @@ -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.gui.clickgui.settings.group; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java index fc32bb4..c75f1b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java @@ -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.gui.clickgui.settings.group; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java index b75a8e1..3e088f5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java @@ -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.gui.clickgui.settings.i18n; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java index 5c571a9..7fe0ee7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java @@ -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.gui.clickgui.settings.i18n; import dev.loki.lovisual.config.io.ConfigSerializer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java index ab18594..70cd58d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java @@ -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.gui.clickgui.settings.i18n; import dev.loki.lovisual.events.Events; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java index 0c83c42..49e7133 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java @@ -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.gui.clickgui.settings.mode; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java index 1bea6c0..304529d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java @@ -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.gui.clickgui.settings.mode; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java index 901fb60..36f1217 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java @@ -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.gui.clickgui.settings.owner; /** diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java index 2f3e214..e9ab730 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java @@ -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.gui.clickgui.settings.owner; public interface TextEditorOwner { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java index fe358d6..d3c8f0a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java @@ -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.gui.clickgui.settings.render; import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java index ba01d94..46d21eb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java @@ -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.gui.clickgui.settings.render; public enum SettingRenderSurface { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java index 76a98be..4a16305 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java @@ -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.gui.clickgui.settings.render; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java index 6fbfcd8..1894d40 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java @@ -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.gui.clickgui.settings.render; import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSettingRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java index 7c8c7a2..76927a0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java @@ -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.gui.clickgui.settings.skin; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java index 714da52..cabb8e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java @@ -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.gui.clickgui.settings.skin; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java index 49c99e4..3ee1ddc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java @@ -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.gui.clickgui.settings.skin; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java index a0708c1..6052cd0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java @@ -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.gui.clickgui.settings.slider; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java index d53ebe7..24d7893 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java @@ -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.gui.clickgui.settings.slider; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java index 165a685..15d426d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java @@ -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.gui.clickgui.settings.slider; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java index b4b55e0..1380969 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java @@ -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.gui.clickgui.settings.slider; import dev.loki.lovisual.features.gui.clickgui.settings.slider.SliderSetting; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java index d624dbd..da6744b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java @@ -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.gui.clickgui.settings.text; import dev.loki.lovisual.config.values.primitive.StringValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java index e78c9a9..f885778 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java @@ -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.gui.clickgui.settings.text; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java index 298746e..0b45ef3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java @@ -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.gui.clickgui.settings.text; import dev.loki.lovisual.render.engine.text.render.TextRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java index 70749b3..79ca5b7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java @@ -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.gui.clickgui.settings.text.list; import dev.loki.lovisual.config.values.collection.ConfigValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java index 8415a00..b3e6762 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java @@ -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.gui.clickgui.settings.text.list; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java index 0ab7e98..d3f9b23 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java @@ -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.gui.clickgui.settings.text.list; import dev.loki.lovisual.render.engine.text.render.TextRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java index 460a013..9f10f6d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java @@ -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.gui.clickgui.settings.util; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java index 01a0c57..5a77a5a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java @@ -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.gui.clickgui.settings.util; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java index 35d9af3..04554b6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java @@ -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.gui.clickgui.tabs; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java index 6945140..db791b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java @@ -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.gui.clickgui.util; import net.minecraft.locale.Language; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java index cb15da3..7e44eb3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java @@ -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.gui.clickgui.util; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java index 16b92bd..d3fa772 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java @@ -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.gui.clickgui.util; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java index ad1b0cc..75d9b7b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java @@ -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.gui.clickgui.util.tooltip; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java index 298dbc1..37ad9c0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java @@ -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.gui.clickgui.util.tooltip; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java index e2c7219..8aa84f6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java @@ -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.gui.hud.anchor; public enum HudAnchorX { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java index 1f5cc4c..cc1a39c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java @@ -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.gui.hud.anchor; public enum HudAnchorY { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java index 14f2200..d16d300 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java @@ -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.gui.hud.anchor; public enum HudRenderSpace { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java index fd90309..642ebb2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java @@ -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.gui.hud.anchor; public enum WidgetAnchorSide { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java index 8673e4f..096d87a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java @@ -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.gui.hud.base; import dev.loki.lovisual.features.gui.hud.base.BaseHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java index 5b9214a..ea0bbc0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java @@ -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.gui.hud.base; import dev.loki.lovisual.config.definition.ConfigNameProvider; @@ -25,37 +18,32 @@ import dev.loki.lovisual.config.values.primitive.BooleanMapValue; import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; -import dev.loki.lovisual.config.values.collection.ConfigValue; -import dev.loki.lovisual.config.values.collection.SetValue; -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.gui.GuiGraphicsExtractor; import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import net.minecraft.client.resources.language.I18n; - -import java.util.*; -import java.util.function.Supplier; import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace; import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Common base for every HUD element: owns identity, the enabled flag, screen bounds, the + * settings-definition lifecycle and the protected config-value DSL used by subclasses inside + * {@link #defineSettings(List)}. Def storage lives in {@link HudSettingRegistry} and id/title + * naming in {@link HudElementNamer}; this class exposes only the stable subclass-facing API. + */ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProvider, SettingOwner, SettingDefProvider { private final String id; private final String title; private final BooleanValue enabled; - private final List settingDefs = new ArrayList<>(); - private final Map, SettingDef> settingDefsByValue = new IdentityHashMap<>(); + private final HudSettingRegistry settings = new HudSettingRegistry(); protected float x; protected float y; protected float width; @@ -65,10 +53,7 @@ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProv protected BaseHudElement() { HudElementInfo info = getClass().getAnnotation(HudElementInfo.class); - if (info == null) { - throw new IllegalStateException(getClass().getName() + " must be annotated with @HudElementInfo"); - } - + if (info == null) throw new IllegalStateException(getClass().getName() + " must be annotated with @HudElementInfo"); this.id = info.id(); this.title = info.displayName(); this.enabled = new BooleanValue("enabled", info.enabledByDefault()); @@ -80,368 +65,54 @@ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProv this.enabled = new BooleanValue("enabled", defaultEnabled); } - private static String stripOwnerPrefix(String value, String owner) { - if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value; + /** Populate this element's settings; called once, lazily, via {@link #ensureSettingsInitialized()}. */ + protected void defineSettings(List defs) { } - String normalizedOwner = normalizePrefix(owner); - String lowerValue = value.toLowerCase(Locale.ROOT); - if (!lowerValue.startsWith(normalizedOwner)) return value; + /** Hook invoked after the persisted config has been loaded. */ + protected void onLoaded() { } - String tail = value.substring(normalizedOwner.length()); - if (tail.isEmpty()) return value; - - char first = tail.charAt(0); - if (first == '_' || first == '-' || first == '.') { - tail = tail.substring(1); - } else if (!Character.isUpperCase(first)) { - return value; - } - - return camelToSnake(tail); - } - - private static String normalizePrefix(String value) { - StringBuilder out = new StringBuilder(value.length()); - for (int i = 0; i < value.length(); i++) { - char c = Character.toLowerCase(value.charAt(i)); - if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { - out.append(c); - } - } - return out.toString(); - } - - private static String camelToSnake(String value) { - if (value == null || value.isEmpty()) return value; - - StringBuilder out = new StringBuilder(value.length() + 4); - char previous = 0; - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (c == '-' || c == '.' || c == ' ') { - c = '_'; - } - if (c == '_') { - if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') { - out.append('_'); - } - previous = '_'; - continue; - } - if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) { - out.append('_'); - } - out.append(Character.toLowerCase(c)); - previous = c; - } - return out.toString(); - } - - protected void defineSettings(List defs) { - } - - protected void onLoaded() { - } - - protected void collectExtraConfigValues(Map> map) { - } - - protected final BooleanValue bool(String configName, boolean def) { - return bool(configName, settingIdFromConfigName(configName), def); - } - - protected final BooleanValue bool(String configName, String settingId, boolean def) { - BooleanValue value = new BooleanValue(configName, def); - declare(value, SettingDef.bool(settingId, value)); - return value; - } - - protected final NumberValue num(String configName, int def, int min, int max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, int def, int min, int max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; - } - - protected final NumberValue num(String configName, float def, float min, float max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, float def, float min, float max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; - } - - protected final NumberValue num(String configName, double def, double min, double max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, double def, double min, double max) { - NumberValue value = new NumberValue<>(configName, def, min, max); - declare(value, SettingDef.number(settingId, value)); - return value; - } - - protected final ModeValue mode(String configName, String def, String... options) { - return mode(configName, settingIdFromConfigName(configName), def, options); - } - - protected final ModeValue mode(String configName, String settingId, String def, String... options) { - ModeValue value = new ModeValue(configName, def, options); - declare(value, SettingDef.mode(settingId, value)); - return value; - } - - protected final > EnumValue enumSetting(String configName, E def) { - return enumSetting(configName, settingIdFromConfigName(configName), def); - } - - protected final > EnumValue enumSetting(String configName, String settingId, E def) { - Class enumClass = def.getDeclaringClass(); - EnumValue value = new EnumValue<>(configName, def, enumClass); - declare(value, SettingDef.mode(settingId, value)); - return value; - } - - protected final > EnumValue enumSetting(String configName, E def, Class enumClass) { - return enumSetting(configName, settingIdFromConfigName(configName), def, enumClass); - } - - protected final > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass) { - EnumValue value = new EnumValue<>(configName, def, enumClass); - declare(value, SettingDef.mode(settingId, value)); - return value; - } + /** Hook to contribute extra (non-def) config values to the serialization map. */ + protected void collectExtraConfigValues(Map> map) { } + // ---- protected config-value DSL (thin delegators to HudSettingRegistry) ------------------ + protected final BooleanValue bool(String c, boolean def) { return bool(c, settingIdFromConfigName(c), def); } + protected final BooleanValue bool(String c, String settingId, boolean def) { return settings.bool(c, settingId, def); } + protected final NumberValue num(String c, int def, int min, int max) { return num(c, settingIdFromConfigName(c), def, min, max); } + protected final NumberValue num(String c, String settingId, int def, int min, int max) { return settings.numInt(c, settingId, def, min, max); } + protected final NumberValue num(String c, float def, float min, float max) { return num(c, settingIdFromConfigName(c), def, min, max); } + protected final NumberValue num(String c, String settingId, float def, float min, float max) { return settings.numFloat(c, settingId, def, min, max); } + protected final NumberValue num(String c, double def, double min, double max) { return num(c, settingIdFromConfigName(c), def, min, max); } + protected final NumberValue num(String c, String settingId, double def, double min, double max) { return settings.numDouble(c, settingId, def, min, max); } + protected final ModeValue mode(String c, String def, String... options) { return mode(c, settingIdFromConfigName(c), def, options); } + protected final ModeValue mode(String c, String settingId, String def, String... options) { return settings.mode(c, settingId, def, options); } + protected final > EnumValue enumSetting(String c, E def) { return enumSetting(c, settingIdFromConfigName(c), def); } + protected final > EnumValue enumSetting(String c, String settingId, E def) { return settings.enumSetting(c, settingId, def); } + protected final > EnumValue enumSetting(String c, E def, Class enumClass) { return enumSetting(c, settingIdFromConfigName(c), def, enumClass); } + protected final > EnumValue enumSetting(String c, String settingId, E def, Class enumClass) { return settings.enumSetting(c, settingId, def, enumClass); } @SafeVarargs - protected final > EnumValue enumSetting(String configName, E def, E... options) { - return enumSetting(configName, settingIdFromConfigName(configName), def, options); - } - + protected final > EnumValue enumSetting(String c, E def, E... options) { return enumSetting(c, settingIdFromConfigName(c), def, options); } @SafeVarargs - protected final > EnumValue enumSetting(String configName, String settingId, E def, E... options) { - EnumValue value = new EnumValue<>(configName, def, options); - declare(value, SettingDef.mode(settingId, value)); - return value; - } - - protected final RGBAColorValue color(String configName, String defHex) { - return color(configName, settingIdFromConfigName(configName), defHex); - } - - protected final RGBAColorValue color(String configName, String settingId, String defHex) { - RGBAColorValue value = new RGBAColorValue(configName, defHex); - declare(value, SettingDef.color(settingId, value)); - return value; - } - - protected final RGBColorValue colorNoAlpha(String configName, String defHex) { - return colorNoAlpha(configName, settingIdFromConfigName(configName), defHex); - } - - protected final RGBColorValue colorNoAlpha(String configName, String settingId, String defHex) { - RGBColorValue value = new RGBColorValue(configName, defHex); - declare(value, SettingDef.colorNoAlpha(settingId, value)); - return value; - } - - protected final SetValue textList(String configName) { - return textList(configName, settingIdFromConfigName(configName), TextListSetting.PickerMode.ALL); - } - - protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { - SetValue value = new SetValue(configName); - declare(value, SettingDef.textList(settingId, value, pickerMode)); - return value; - } - - protected final KeyBindValue bind(String configName, String def, BindMode mode) { - return bind(configName, settingIdFromConfigName(configName), def, mode); - } - - protected final KeyBindValue bind(String configName, String settingId, String def, BindMode mode) { - KeyBindValue value = new KeyBindValue(configName, def); - declare(value, SettingDef.bind(settingId, value, mode)); - return value; - } - - protected final BooleanMapValue group(String configName, String settingId, Map defaults) { - BooleanMapValue value = new BooleanMapValue(configName, defaults); - declare(value, SettingDef.group(settingId, value)); - return value; - } - - protected final > V visibleWhen(V value, Supplier supplier) { - SettingDef def = settingDefsByValue.get(value); - if (def != null) { - def.visibleWhen(supplier); - } - return value; - } - - protected final > V common(V value, String commonI18nKey) { - SettingDef def = settingDefsByValue.get(value); - if (def != null && commonI18nKey != null && !commonI18nKey.isBlank()) { - def.common(commonI18nKey); - } - return value; - } + protected final > EnumValue enumSetting(String c, String settingId, E def, E... options) { return settings.enumSetting(c, settingId, def, options); } + protected final RGBAColorValue color(String c, String defHex) { return color(c, settingIdFromConfigName(c), defHex); } + protected final RGBAColorValue color(String c, String settingId, String defHex) { return settings.color(c, settingId, defHex); } + protected final RGBColorValue colorNoAlpha(String c, String defHex) { return colorNoAlpha(c, settingIdFromConfigName(c), defHex); } + protected final RGBColorValue colorNoAlpha(String c, String settingId, String defHex) { return settings.colorNoAlpha(c, settingId, defHex); } + protected final SetValue textList(String c) { return textList(c, settingIdFromConfigName(c), TextListSetting.PickerMode.ALL); } + protected final SetValue textList(String c, String settingId, TextListSetting.PickerMode pickerMode) { return settings.textList(c, settingId, pickerMode); } + protected final KeyBindValue bind(String c, String def, BindMode mode) { return bind(c, settingIdFromConfigName(c), def, mode); } + protected final KeyBindValue bind(String c, String settingId, String def, BindMode mode) { return settings.bind(c, settingId, def, mode); } + protected final BooleanMapValue group(String c, String settingId, Map defaults) { return settings.group(c, settingId, defaults); } + protected final > V visibleWhen(V value, Supplier supplier) { return settings.visibleWhen(value, supplier); } + protected final > V common(V value, String commonI18nKey) { return settings.common(value, commonI18nKey); } + protected final SettingDef settingDef(ConfigValue value) { return settings.settingDef(value); } + protected final SettingDef enabledSettingDef() { return SettingDef.bool(enabled); } + /** Register a pre-built value/def pair directly (bypasses the typed builders). */ protected final > V declareSetting(V value, SettingDef def) { - declare(value, def); + settings.declare(value, def); return value; } - protected final SettingDef settingDef(ConfigValue value) { - return settingDefsByValue.get(value); - } - - protected final SettingDef enabledSettingDef() { - return SettingDef.bool(enabled); - } - - public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { - if (event == null) return; - event.font("Inter", FontInfo.Type.Regular) - .font("Inter", FontInfo.Type.Bold) - .font("InterMedium", FontInfo.Type.Regular) - .font("OnestMedium", FontInfo.Type.Regular) - .font("Onest", FontInfo.Type.Regular) - .font("Onest", FontInfo.Type.Bold) - .font("OnestMedium", FontInfo.Type.Regular) - .font("OnestBold", FontInfo.Type.Regular) - .font("Iosevka", FontInfo.Type.Regular) - .font("Iosevka", FontInfo.Type.Bold) - .font("Comfortaa", FontInfo.Type.Regular) - .font("Icons", FontInfo.Type.Regular) - .font("IconsNur", FontInfo.Type.Regular) - .font("WeatherIcons", FontInfo.Type.Regular) - .font("MediaPlayer", FontInfo.Type.Regular); - } - - public void onTick() { - } - - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - } - - public void renderEngineForeground(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - } - - public boolean usesEngineRenderer() { - return false; - } - - public boolean isMouseOverInteractive(float mx, float my) { - return false; - } - - public boolean onMouseClicked(float mx, float my, int button) { - return false; - } - - public HudPhase getHudPhase() { - return HudPhase.AFTER_SUBTITLES; - } - - public HudRenderSpace getRenderSpace() { - return HudRenderSpace.UNSCALED_LOGICAL; - } - - public int getRenderOrder() { - return 0; - } - - public final void postInit() { - ensureSettingsInitialized(); - ConfigSerializer.load(this); - onLoaded(); - } - - public final String getId() { - return id; - } - - public final String getTitle() { - return translateTitle(title, getTranslationKeyPrefix(), id); - } - - private static String translateTitle(String fallback, String prefix, String id) { - List keys = new ArrayList<>(3); - if (prefix != null && prefix.startsWith("setting.draggable.")) { - keys.add("hud.draggable." + id); - keys.add("hud.draggable." + id + ".title"); - } else if (prefix != null && prefix.startsWith("setting.hud_element.")) { - keys.add("hud.static." + id); - keys.add("hud.static." + id + ".title"); - } - keys.add("hud." + id); - for (String key : keys) { - if (key == null || key.isBlank()) continue; - String translated = I18n.get(key); - if (!translated.equals(key)) { - return translated; - } - } - return fallback == null ? "" : fallback; - } - - public boolean isEnabled() { - return enabled.get() || previewEnabled; - } - - public void setEnabled(boolean value) { - enabled.set(value); - ConfigSerializer.requestSave(this); - } - - public void setRuntimeEnabledTransient(boolean value) { - enabled.set(value); - } - - public void setPreviewEnabled(boolean value) { - previewEnabled = value; - } - - public BooleanValue enabledValue() { - return enabled; - } - - public float getX() { - return x; - } - - public float getY() { - return y; - } - - public float getWidth() { - return width; - } - - public float getHeight() { - return height; - } - - public void moveTo(float x, float y) { - this.x = x; - this.y = y; - } - protected final void setBounds(float x, float y, float width, float height) { this.x = x; this.y = y; @@ -449,24 +120,68 @@ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProv this.height = height; } + private String settingIdFromConfigName(String configName) { + return HudElementNamer.settingId(configName, getId(), getClass().getSimpleName()); + } + + private void ensureSettingsInitialized() { + if (settingsInitialized) return; + settingsInitialized = true; + defineSettings(settings.defs()); + SettingDef.applyI18nAnnotations(this, settings.defs()); + } + + // ---- lifecycle + render hooks (overridable defaults) -------------------------------------- + public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { HudPrewarm.standard(event); } + public void onTick() { } + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { } + public void renderEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { } + public boolean usesEngineRenderer() { return false; } + public boolean isMouseOverInteractive(float mx, float my) { return false; } + public boolean onMouseClicked(float mx, float my, int button) { return false; } + public HudPhase getHudPhase() { return HudPhase.AFTER_SUBTITLES; } + public HudRenderSpace getRenderSpace() { return HudRenderSpace.UNSCALED_LOGICAL; } + public int getRenderOrder() { return 0; } + + // ---- identity, enabled state + geometry ---------------------------------------------------- + public final void postInit() { + ensureSettingsInitialized(); + ConfigSerializer.load(this); + onLoaded(); + } + + public final String getId() { return id; } + public final String getTitle() { return HudElementNamer.translateTitle(title, getTranslationKeyPrefix(), id); } + public boolean isEnabled() { return enabled.get() || previewEnabled; } + public void setRuntimeEnabledTransient(boolean value) { enabled.set(value); } + public void setPreviewEnabled(boolean value) { previewEnabled = value; } + public BooleanValue enabledValue() { return enabled; } + public float getX() { return x; } + public float getY() { return y; } + public float getWidth() { return width; } + public float getHeight() { return height; } + public void setEnabled(boolean value) { + enabled.set(value); + ConfigSerializer.requestSave(this); + } + public void moveTo(float x, float y) { + this.x = x; + this.y = y; + } public boolean contains(float mx, float my) { return mx >= x && mx <= x + width && my >= y && my <= y + height; } + // ---- config-object contract --------------------------------------------------------------- @Override - public final String name() { - return id; - } - + public final String name() { return id; } @Override - public final void saveConfig() { - ConfigSerializer.requestSave(this); - } + public final void saveConfig() { ConfigSerializer.requestSave(this); } @Override public final List getSettingDefs() { ensureSettingsInitialized(); - return new ArrayList<>(settingDefs); + return new ArrayList<>(settings.defs()); } @Override @@ -475,32 +190,10 @@ public abstract class BaseHudElement implements JsonConfigObject, ConfigNameProv Map> map = new LinkedHashMap<>(); map.put(enabled.getName(), enabled); collectExtraConfigValues(map); - for (SettingDef def : settingDefs) { + for (SettingDef def : settings.defs()) { ConfigValue v = def.value(); - if (v != null) { - map.put(v.getName(), v); - } + if (v != null) map.put(v.getName(), v); } return new ArrayList<>(map.values()); } - - private void ensureSettingsInitialized() { - if (settingsInitialized) return; - settingsInitialized = true; - defineSettings(settingDefs); - SettingDef.applyI18nAnnotations(this, settingDefs); - } - - private void declare(ConfigValue value, SettingDef def) { - settingDefs.add(def); - settingDefsByValue.put(value, def); - } - - private String settingIdFromConfigName(String configName) { - if (configName == null || configName.isBlank()) return configName; - - String tail = stripOwnerPrefix(configName, getId()); - tail = stripOwnerPrefix(tail, getClass().getSimpleName()); - return tail; - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java index f340016..f2708fb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java @@ -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.gui.hud.base; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java index e2d529a..b6fe024 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java @@ -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.gui.hud.base; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudElementNamer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudElementNamer.java new file mode 100644 index 0000000..5aa4ff4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudElementNamer.java @@ -0,0 +1,88 @@ +package dev.loki.lovisual.features.gui.hud.base; + +import net.minecraft.client.resources.language.I18n; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Naming and localization helpers for {@link BaseHudElement}: derive stable setting ids from + * config names by stripping the element/class owner prefix, and resolve a translated HUD title + * from the resource-pack i18n keys. Extracted so the base class stays under the file-size cap. + */ +final class HudElementNamer { + + private HudElementNamer() { } + + /** Strip the element-id and simple-class prefixes from a config name to yield the setting id. */ + static String settingId(String configName, String ownerId, String ownerClass) { + if (configName == null || configName.isBlank()) return configName; + String tail = stripOwnerPrefix(configName, ownerId); + tail = stripOwnerPrefix(tail, ownerClass); + return tail; + } + + /** Resolve a translated HUD title from candidate i18n keys, falling back to the raw title. */ + static String translateTitle(String fallback, String prefix, String id) { + List keys = new ArrayList<>(3); + if (prefix != null && prefix.startsWith("setting.draggable.")) { + keys.add("hud.draggable." + id); + keys.add("hud.draggable." + id + ".title"); + } else if (prefix != null && prefix.startsWith("setting.hud_element.")) { + keys.add("hud.static." + id); + keys.add("hud.static." + id + ".title"); + } + keys.add("hud." + id); + for (String key : keys) { + if (key == null || key.isBlank()) continue; + String translated = I18n.get(key); + if (!translated.equals(key)) return translated; + } + return fallback == null ? "" : fallback; + } + + private static String stripOwnerPrefix(String value, String owner) { + if (value == null || value.isBlank() || owner == null || owner.isBlank()) return value; + String normalizedOwner = normalizePrefix(owner); + String lowerValue = value.toLowerCase(Locale.ROOT); + if (!lowerValue.startsWith(normalizedOwner)) return value; + String tail = value.substring(normalizedOwner.length()); + if (tail.isEmpty()) return value; + char first = tail.charAt(0); + if (first == '_' || first == '-' || first == '.') { + tail = tail.substring(1); + } else if (!Character.isUpperCase(first)) { + return value; + } + return camelToSnake(tail); + } + + private static String normalizePrefix(String value) { + StringBuilder out = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = Character.toLowerCase(value.charAt(i)); + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) out.append(c); + } + return out.toString(); + } + + private static String camelToSnake(String value) { + if (value == null || value.isEmpty()) return value; + StringBuilder out = new StringBuilder(value.length() + 4); + char previous = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '-' || c == '.' || c == ' ') c = '_'; + if (c == '_') { + if (!out.isEmpty() && out.charAt(out.length() - 1) != '_') out.append('_'); + previous = '_'; + continue; + } + if (Character.isUpperCase(c) && i > 0 && previous != '_' && !Character.isUpperCase(previous)) out.append('_'); + out.append(Character.toLowerCase(c)); + previous = c; + } + return out.toString(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudPrewarm.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudPrewarm.java new file mode 100644 index 0000000..126d261 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudPrewarm.java @@ -0,0 +1,26 @@ +package dev.loki.lovisual.features.gui.hud.base; + +import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; + +/** + * Registers the common font set every HUD element may draw with, so the render pipeline can + * pre-warm them before the first frame. Used by {@link BaseHudElement#collectRenderPrewarm}. + */ +final class HudPrewarm { + + private HudPrewarm() { } + + /** Queue the standard HUD font atlas set (Inter / Onest / Iosevka / icon fonts) for pre-warm. */ + static void standard(RenderPrewarmCollectEvent event) { + if (event == null) return; + event.font("Inter", FontInfo.Type.Regular).font("Inter", FontInfo.Type.Bold) + .font("InterMedium", FontInfo.Type.Regular).font("OnestMedium", FontInfo.Type.Regular) + .font("Onest", FontInfo.Type.Regular).font("Onest", FontInfo.Type.Bold) + .font("OnestMedium", FontInfo.Type.Regular).font("OnestBold", FontInfo.Type.Regular) + .font("Iosevka", FontInfo.Type.Regular).font("Iosevka", FontInfo.Type.Bold) + .font("Comfortaa", FontInfo.Type.Regular).font("Icons", FontInfo.Type.Regular) + .font("IconsNur", FontInfo.Type.Regular).font("WeatherIcons", FontInfo.Type.Regular) + .font("MediaPlayer", FontInfo.Type.Regular); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudSettingRegistry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudSettingRegistry.java new file mode 100644 index 0000000..adc6aac --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/impl/HudSettingRegistry.java @@ -0,0 +1,135 @@ +package dev.loki.lovisual.features.gui.hud.base; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.config.values.collection.SetValue; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Owns the ordered list of {@link SettingDef}s and the value→def index for a + * {@link BaseHudElement}, and provides the concrete config-value builders behind the base + * class' protected settings DSL. Each builder receives an already-resolved setting id. + * Extracted so the base class stays under the file-size cap. + */ +final class HudSettingRegistry { + + private final List defs = new ArrayList<>(); + private final Map, SettingDef> byValue = new IdentityHashMap<>(); + + List defs() { + return defs; + } + + BooleanValue bool(String configName, String settingId, boolean def) { + BooleanValue value = new BooleanValue(configName, def); + declare(value, SettingDef.bool(settingId, value)); + return value; + } + + NumberValue numInt(String configName, String settingId, int def, int min, int max) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare(value, SettingDef.number(settingId, value)); + return value; + } + + NumberValue numFloat(String configName, String settingId, float def, float min, float max) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare(value, SettingDef.number(settingId, value)); + return value; + } + + NumberValue numDouble(String configName, String settingId, double def, double min, double max) { + NumberValue value = new NumberValue<>(configName, def, min, max); + declare(value, SettingDef.number(settingId, value)); + return value; + } + + ModeValue mode(String configName, String settingId, String def, String... options) { + ModeValue value = new ModeValue(configName, def, options); + declare(value, SettingDef.mode(settingId, value)); + return value; + } + + > EnumValue enumSetting(String configName, String settingId, E def) { + return enumSetting(configName, settingId, def, def.getDeclaringClass()); + } + + > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass) { + EnumValue value = new EnumValue<>(configName, def, enumClass); + declare(value, SettingDef.mode(settingId, value)); + return value; + } + + @SafeVarargs + final > EnumValue enumSetting(String configName, String settingId, E def, E... options) { + EnumValue value = new EnumValue<>(configName, def, options); + declare(value, SettingDef.mode(settingId, value)); + return value; + } + + RGBAColorValue color(String configName, String settingId, String defHex) { + RGBAColorValue value = new RGBAColorValue(configName, defHex); + declare(value, SettingDef.color(settingId, value)); + return value; + } + + RGBColorValue colorNoAlpha(String configName, String settingId, String defHex) { + RGBColorValue value = new RGBColorValue(configName, defHex); + declare(value, SettingDef.colorNoAlpha(settingId, value)); + return value; + } + + SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { + SetValue value = new SetValue(configName); + declare(value, SettingDef.textList(settingId, value, pickerMode)); + return value; + } + + KeyBindValue bind(String configName, String settingId, String def, BindMode mode) { + KeyBindValue value = new KeyBindValue(configName, def); + declare(value, SettingDef.bind(settingId, value, mode)); + return value; + } + + BooleanMapValue group(String configName, String settingId, Map defaults) { + BooleanMapValue value = new BooleanMapValue(configName, defaults); + declare(value, SettingDef.group(settingId, value)); + return value; + } + + > V visibleWhen(V value, Supplier supplier) { + SettingDef def = byValue.get(value); + if (def != null) def.visibleWhen(supplier); + return value; + } + + > V common(V value, String commonI18nKey) { + SettingDef def = byValue.get(value); + if (def != null && commonI18nKey != null && !commonI18nKey.isBlank()) def.common(commonI18nKey); + return value; + } + + SettingDef settingDef(ConfigValue value) { + return byValue.get(value); + } + + void declare(ConfigValue value, SettingDef def) { + defs.add(def); + byValue.put(value, def); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java index e27d0bd..b75f07f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java @@ -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.gui.hud.config; import dev.loki.lovisual.config.definition.ConfigNameProvider; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java index c36ce31..1dd3b43 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java @@ -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.gui.hud.config; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java index 31f40aa..ff19953 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java @@ -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.gui.hud.draggable; import dev.loki.lovisual.config.values.collection.ConfigValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java index aea710d..c0b37de 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java @@ -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.gui.hud.draggable; import dev.loki.lovisual.features.gui.hud.draggable.registry.RegistryState; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java index 798f9c5..154484d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java @@ -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.gui.hud.draggable; import dev.loki.lovisual.config.values.collection.ConfigValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java index d360939..12a3567 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java @@ -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.gui.hud.draggable.impl; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java index 573b44d..a500400 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java @@ -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.gui.hud.draggable.impl; import dev.loki.lovisual.config.values.mode.ModeValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java index 838c539..c9b6929 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java @@ -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.gui.hud.draggable.impl; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java index 50a12d3..f453058 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java @@ -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.gui.hud.draggable.impl; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java index d6fef3d..d3b4583 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java index 6117953..4d0c416 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java index 3871366..d1dae4a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java index a8fc5eb..31baeb1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java @@ -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.gui.hud.draggable.impl.combat; final class InventoryConstants { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java index 59ec78c..f9faec1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java index 4734966..c233c77 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java index 4c89186..0268a1c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java @@ -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.gui.hud.draggable.impl.combat; import net.minecraft.world.item.ItemStack; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java index f9c3b66..5de6c0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java index 45a3cb2..6ccbfec 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java index 831bdd4..e42cd08 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.render.engine.text.font.Fonts; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java index 83ee815..a23e073 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java index dd1f47b..ec5cedb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java index 3a22f7f..9b19346 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java @@ -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.gui.hud.draggable.impl.combat; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java index 0360514..c8a7c5a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java @@ -1,66 +1,44 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; import com.mojang.blaze3d.platform.Window; import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; 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.draggable.impl.hudnotifier.ToastGeometry.MessengerSide; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastPosition; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.PreviewToast; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.Toast; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.ToastDraw; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.ToastVisual; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastStyle; -import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.runtime.HudNotifierRuntime; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleManager; +import dev.loki.lovisual.features.module.phase.HudPhase; import dev.loki.lovisual.features.module.phase.ModuleStateListener; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.screen.ClientScreen; -import dev.loki.lovisual.util.wav.engine.CustomSoundEngine; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.resources.Identifier; -import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastCardRenderer; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry; +/** + * Public facade for the toast-based HUD notification module. + *

    + * Owns the six {@link dev.loki.lovisual.config.values.ConfigValue}s, the static + * push / render API and the {@link DraggableHudElement} lifecycle hooks. The mutable + * toast queue lives in {@link HudNotifierRuntime} (subpackage) and every draw path + * is delegated to {@link HudNotifierPainter}. + */ @HudElementRegister(order = 80) public final class HudNotifier extends DraggableHudElement implements ModuleStateListener { private static final HudNotifier INSTANCE = new HudNotifier(); - private static final int MAX_TOASTS = 6; - private static final float BASE_MARGIN = 9f; - private static final float BASE_GAP = 3f; - private static final float BASE_BOTTOM_OFFSET = 160f; - private static final float LAYOUT_SMOOTH_SPEED = 14.0f; - private static final float LAYOUT_SNAP_EPS = 0.12f; + static final float BASE_MARGIN = 9f; + static final float BASE_GAP = 3f; + static final float BASE_BOTTOM_OFFSET = 160f; private final NumberValue durationMs = new NumberValue<>("duration_ms", 2000, 500, 6000); private final NumberValue fadeMs = new NumberValue<>("fade_ms", 250, 0, 1500); @@ -69,11 +47,8 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat private final ModeValue soundMode = new ModeValue("sound_mode", "2", "1", "2", "3"); private final NumberValue soundVolume = new NumberValue<>("sound_volume", 0.47, 0.05, 1.0); - private final List toasts = new ArrayList<>(); - private final RenderColor colorTmp = new RenderColor(0xFFFFFFFF); - private boolean stackLayoutReady; - private float stackWidthAnim; - private float stackHeightAnim; + private final HudNotifierRuntime runtime = new HudNotifierRuntime(this); + private final HudNotifierPainter painter = new HudNotifierPainter(this, runtime); private HudNotifier() { super("hud_notifier", "HudNotifier", true); @@ -81,44 +56,35 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } public static void pushMessage(String text) { - INSTANCE.enqueue(null, text, null, null); + INSTANCE.runtime.enqueue(null, text, null, null); } public static void pushMessage(String text, NotifyType type) { - INSTANCE.enqueue(null, text, null, type); + INSTANCE.runtime.enqueue(null, text, null, type); } public static void pushState(String text, boolean enabled) { - INSTANCE.enqueue(null, text, enabled, null); + INSTANCE.runtime.enqueue(null, text, enabled, null); } public static void pushOrUpdateMessage(String key, String text) { - INSTANCE.enqueue(key, text, null, null); + INSTANCE.runtime.enqueue(key, text, null, null); } public static void pushOrUpdateMessage(String key, String text, NotifyType type) { - INSTANCE.enqueue(key, text, null, type); + INSTANCE.runtime.enqueue(key, text, null, type); } public static void clearMessage(String key) { - INSTANCE.clearKey(key); + INSTANCE.runtime.clearKey(key); } - public static void renderEngine(HudPhase phase, - Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta) { - INSTANCE.renderEngineInternal(renderer, textRenderer); + public static void renderEngine(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + INSTANCE.painter.renderEngine(renderer, textRenderer); } - public static void renderPreview(Renderer2D renderer, - TextRenderer textRenderer, - float x, - float y, - float w, - float h) { - INSTANCE.renderPreviewInternal(renderer, textRenderer, x, y, w, h); + public static void renderPreview(Renderer2D renderer, TextRenderer textRenderer, float x, float y, float w, float h) { + INSTANCE.painter.renderPreview(renderer, textRenderer, x, y, w, h); } @Override @@ -135,11 +101,8 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat public void applyDefaultPosition(int screenW, int screenH) { HudGlobalConfig hud = HudGlobalConfig.get(); float scale = HudScale.scale(screenW, screenH); - if (hud != null) { - scale *= hud.getFontSize() / 18f; - } + if (hud != null) scale *= hud.getFontSize() / 18f; scale *= getScale(); - float margin = BASE_MARGIN * scale; float width = 170f * scale; float height = ToastGeometry.BASE_BOX_HEIGHT * scale; @@ -159,344 +122,47 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - renderEngineInternal(renderer, textRenderer); - } - - private void renderEngineInternal(Renderer2D renderer, - TextRenderer textRenderer) { - Minecraft mc = Minecraft.getInstance(); - - if (mc == null || mc.level == null || mc.player == null) return; - - HudGlobalConfig hud = HudGlobalConfig.get(); - if (hud == null) return; - - boolean preview = DraggableHudElementRegistry.isForceVisible() || ClientScreen.current() instanceof ChatScreen; - if (!enabledValue().get() && !preview) return; - if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER && !preview) return; - - int duration = Math.max(1, getDurationMs()); - int fade = Math.max(0, getFadeMs()); - long now = System.currentTimeMillis(); - - pruneExpiredToasts(now, duration); - - boolean hasRealToasts = !toasts.isEmpty(); - boolean sample = preview && !hasRealToasts; - List source = sample ? previewToasts(now) : toasts; - - if (source.isEmpty()) { - resetStackLayout(); - setBounds(x, y, 0f, 0f); - return; - } - - Window window = mc.getWindow(); - int fbw = window.getWidth(); - int fbh = window.getHeight(); - int screenW = Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh))); - - float baseScale = HudScale.scale(screenW, screenH) * (hud.getFontSize() / 18f) * getScale(); - float gap = BASE_GAP * baseScale; - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer lineRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, fallback); - TextRenderer iconRenderer = Fonts.renderer("Icons", FontInfo.Type.Regular, fallback); - if (lineRenderer == null) lineRenderer = fallback; - if (iconRenderer == null) iconRenderer = fallback; - float lineScale = baseScale * 0.95f; - float iconScale = baseScale * 1.08f; - - List draws = new ArrayList<>(); - float maxW = 0f; - float totalH = 0f; - - for (int i = source.size() - 1; i >= 0; i--) { - Toast toast = source.get(i); - float anim = sample ? 1.0f : toast.anim(now, duration, fade); - if (anim <= 0.001f) continue; - - ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); - float remaining = sample ? 0.45f : toast.remaining(now, duration); - draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f)); - maxW = Math.max(maxW, metrics.boxW); - totalH += metrics.boxH * anim; - if (i > 0) { - totalH += gap * anim; - } - } - - if (draws.isEmpty()) { - resetStackLayout(); - setBounds(x, y, 0f, 0f); - return; - } - - LayoutSize layout = resolveStackLayout(maxW, totalH, sample); - float layoutW = Math.max(maxW, layout.width()); - float layoutH = Math.max(totalH, layout.height()); - - MessengerSide side = getSide(); - float cursorY = y; - for (int i = 0; i < draws.size(); i++) { - ToastDraw draw = draws.get(i); - ToastMetrics metrics = draw.metrics; - float targetX = switch (side) { - case LEFT -> x; - case TOP, BOTTOM -> x + (layoutW - metrics.boxW) * 0.5f; - default -> x + (layoutW - metrics.boxW); - }; - float targetY = cursorY; - ToastPosition pos = resolveToastPosition(draw.toast, targetX, targetY, sample, baseScale); - ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); - draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, pos.x() + slide.x(), pos.y() + slide.y())); - cursorY += (metrics.boxH + gap) * draw.anim; - } - - setBounds(x, y, layoutW, Math.max(1f, layoutH)); - - ToastGeometry.drawGlassBlurBatch(draws, baseScale); - for (ToastDraw draw : draws) { - ToastCardRenderer.drawToastCard( - renderer, - colorTmp, - draw.toast, - lineRenderer, - iconRenderer, - draw.x, - draw.y, - draw.metrics, - draw.remaining, - draw.anim, - baseScale - ); - } - } - - private void pruneExpiredToasts(long now, int duration) { - Iterator it = toasts.iterator(); - while (it.hasNext()) { - Toast t = it.next(); - if (t.isExpired(now, duration)) { - it.remove(); - } - } - } - - private LayoutSize resolveStackLayout(float targetW, float targetH, boolean sample) { - if (sample) { - resetStackLayout(); - return new LayoutSize(targetW, targetH); - } - - if (!stackLayoutReady) { - stackWidthAnim = targetW; - stackHeightAnim = targetH; - stackLayoutReady = true; - return new LayoutSize(targetW, targetH); - } - - float dt = AnimationUtility.deltaTime(); - stackWidthAnim = AnimationUtility.approach(stackWidthAnim, targetW, dt, LAYOUT_SMOOTH_SPEED); - stackHeightAnim = AnimationUtility.approach(stackHeightAnim, targetH, dt, LAYOUT_SMOOTH_SPEED); - stackWidthAnim = AnimationUtility.snap(stackWidthAnim, targetW, LAYOUT_SNAP_EPS); - stackHeightAnim = AnimationUtility.snap(stackHeightAnim, targetH, LAYOUT_SNAP_EPS); - return new LayoutSize(stackWidthAnim, stackHeightAnim); - } - - private ToastPosition resolveToastPosition(Toast toast, float targetX, float targetY, boolean sample, float baseScale) { - if (toast == null || sample) { - return new ToastPosition(targetX, targetY); - } - - if (!toast.layoutReady) { - toast.layoutX = targetX; - toast.layoutY = targetY; - toast.layoutReady = true; - return new ToastPosition(targetX, targetY); - } - - float dt = AnimationUtility.deltaTime(); - float eps = Math.max(0.05f, LAYOUT_SNAP_EPS * baseScale); - toast.layoutX = AnimationUtility.approach(toast.layoutX, targetX, dt, LAYOUT_SMOOTH_SPEED); - toast.layoutY = AnimationUtility.approach(toast.layoutY, targetY, dt, LAYOUT_SMOOTH_SPEED); - toast.layoutX = AnimationUtility.snap(toast.layoutX, targetX, eps); - toast.layoutY = AnimationUtility.snap(toast.layoutY, targetY, eps); - return new ToastPosition(toast.layoutX, toast.layoutY); - } - - private void resetStackLayout() { - stackLayoutReady = false; - stackWidthAnim = 0f; - stackHeightAnim = 0f; - } - - private List previewToasts(long now) { - return List.of(previewToast(now)); - } - - private Toast previewToast(long now) { - PreviewToast preview = ToastModel.previewExample(now); - long start = ToastModel.previewStart(now); - return new Toast(ToastModel.PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled()); - } - - private void renderPreviewInternal(Renderer2D renderer, - TextRenderer textRenderer, - float originX, - float originY, - float width, - float height) { - if (renderer == null) return; - HudGlobalConfig hud = HudGlobalConfig.get(); - if (hud == null) return; - if (width <= 2f || height <= 2f) return; - - int screenW = Math.max(1, Math.round(width)); - int screenH = Math.max(1, Math.round(height)); - - float baseScale = HudScale.scale(screenW, screenH) * (hud.getFontSize() / 18f) * getScale(); - float margin = BASE_MARGIN * baseScale; - float gap = BASE_GAP * baseScale; - - MessengerSide side = getSide(); - float cursorY = margin; - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer lineRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, fallback); - TextRenderer iconRenderer = Fonts.renderer("Icons", FontInfo.Type.Regular, fallback); - if (lineRenderer == null) lineRenderer = fallback; - if (iconRenderer == null) iconRenderer = fallback; - float lineScale = baseScale * 0.95f; - float iconScale = baseScale * 1.08f; - - long now = System.currentTimeMillis(); - int duration = Math.max(1, getDurationMs()); - int fade = Math.max(0, getFadeMs()); - pruneExpiredToasts(now, duration); - - boolean sampleMode = toasts.isEmpty(); - List source = sampleMode ? previewToasts(now) : toasts; - - List draws = new ArrayList<>(); - float maxW = 0f; - for (int i = source.size() - 1; i >= 0; i--) { - Toast toast = source.get(i); - float anim = sampleMode ? 1.0f : toast.anim(now, duration, fade); - if (anim <= 0.001f) continue; - - ToastMetrics metrics = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); - float remaining = sampleMode ? 0.45f : toast.remaining(now, duration); - maxW = Math.max(maxW, metrics.boxW); - draws.add(new ToastDraw(toast, metrics, remaining, anim, 0f, 0f)); - } - - for (int i = 0; i < draws.size(); i++) { - ToastDraw draw = draws.get(i); - ToastMetrics metrics = draw.metrics; - float targetX = switch (side) { - case LEFT -> originX + margin; - case TOP, BOTTOM -> originX + (screenW - metrics.boxW) * 0.5f; - default -> originX + screenW - margin - metrics.boxW; - }; - float targetY = originY + cursorY; - ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); - draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, targetX + slide.x(), targetY + slide.y())); - cursorY += (metrics.boxH + gap) * draw.anim; - } - - ToastGeometry.drawGlassBlurBatch(draws, baseScale); - for (ToastDraw draw : draws) { - ToastCardRenderer.drawToastCard( - renderer, - colorTmp, - draw.toast, - lineRenderer, - iconRenderer, - draw.x, - draw.y, - draw.metrics, - draw.remaining, - draw.anim, - baseScale - ); - } + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + painter.renderEngine(renderer, textRenderer); } @Override public void onModuleStateChanged(String name, boolean enabled) { Module module = ModuleManager.get(name); // HudNotifier module-state info logging disabled; keep only warnings/errors here. - if (module == null) { - // HudNotifier module-state info logging disabled; keep only warnings/errors here. - return; - } - if (!module.isShownInModuleList()) { - // HudNotifier module-state info logging disabled; keep only warnings/errors here. - return; - } - enqueueModuleState(module.getDisplayName(), enabled); + if (module == null) return; + if (!module.isShownInModuleList()) return; + runtime.enqueueModuleState(module.getDisplayName(), enabled); } - private void enqueueModuleState(String text, boolean enabled) { - enqueueInternal(null, text, enabled, enabled ? NotifyType.YES : NotifyType.NO, ToastVisual.MODULE_TOGGLE); + @Override + public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { + super.collectRenderPrewarm(event); + event.svg("toggle-left") + .svg("toggle-right") + .svg("toggle-track") + .svg("toggle-thumb"); } - private void enqueue(String key, String text, Boolean enabled, NotifyType forcedType) { - NotifyType type = forcedType != null ? forcedType : ToastStyle.resolveType(text, enabled); - enqueueInternal(key, text, enabled, type, ToastVisual.STANDARD); + /** Package-private bounds passthrough so {@link HudNotifierPainter} can update the module hitbox. */ + void applyBounds(float nx, float ny, float nw, float nh) { + setBounds(nx, ny, nw, nh); } - private void enqueueInternal(String key, String text, Boolean enabled, NotifyType type, ToastVisual visual) { - if (text == null || text.isBlank()) return; - - HudGlobalConfig hud = HudGlobalConfig.get(); - if (hud == null) return; - if (!enabledValue().get()) return; - if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER) return; - - String line = ToastStyle.resolveLine(text, enabled); - long now = System.currentTimeMillis(); - - if (key != null) { - for (Toast toast : toasts) { - if (key.equals(toast.key)) { - toast.line = line; - toast.type = type; - toast.visual = visual == null ? ToastVisual.STANDARD : visual; - toast.stateEnabled = enabled; - toast.startMs = now; - return; - } - } - } - - toasts.add(new Toast(key, line, type, now, visual, enabled)); - if (toasts.size() > MAX_TOASTS) { - toasts.remove(0); - } - - if (enabled != null) { - playSound(enabled); - } - } - - private int getDurationMs() { + int getDurationMs() { return durationMs.get(); } - private int getFadeMs() { + int getFadeMs() { return fadeMs.get(); } - private MessengerSide getSide() { + float getScale() { + return scaleValue.get().floatValue(); + } + + /** Resolve which edge of the screen the toast stack slides from, based on the current module position. */ + MessengerSide getSide() { Minecraft mc = Minecraft.getInstance(); Window window = mc == null ? null : mc.getWindow(); if (window == null) return MessengerSide.RIGHT; @@ -511,57 +177,24 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat return cx < screenW * 0.50f ? MessengerSide.LEFT : MessengerSide.RIGHT; } - private float getScale() { - return scaleValue.get().floatValue(); + /** Public accessors used by {@link HudNotifierRuntime} (in a sibling subpackage). */ + public boolean isSoundEnabled() { + return soundEnabled.get(); } - private float getSoundVolume() { + public ModeValue getSoundMode() { + return soundMode; + } + + public float getSoundVolume() { return soundVolume.get().floatValue(); } - private void playSound(boolean enabled) { - if (ModuleManager.isToggleSoundSuppressed()) return; - if (!soundEnabled.get()) return; - Identifier snd = resolveSound(enabled, soundMode.get()); - if (snd == null) return; - double vol = Math.min(1.0, getSoundVolume()); - CustomSoundEngine.get().play(snd, vol, 1.0, false, true); - } - - private void clearKey(String key) { - if (key == null) return; - toasts.removeIf(t -> key.equals(t.key)); - } - - private Identifier resolveSound(boolean enabled, String mode) { - String base = switch (mode) { - case "2" -> "enable/enable2"; - case "3" -> "enable/enable3"; - default -> "enable/enable1"; - }; - if (!enabled) { - base = base.replace("enable/", "disable/").replace("enable", "disable"); - } - return Identifier.fromNamespaceAndPath("lovisual", "sounds/" + base + ".wav"); - } - + /** Notification severity, resolved from the raw text or forced by the caller. */ public enum NotifyType { YES, NO, WARN, INFO } - - private record LayoutSize(float width, float height) { - } - - @Override - public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { - super.collectRenderPrewarm(event); - event.svg("toggle-left") - .svg("toggle-right") - .svg("toggle-track") - .svg("toggle-thumb"); - } - } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifierPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifierPainter.java new file mode 100644 index 0000000..9020aba --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifierPainter.java @@ -0,0 +1,185 @@ +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier; + +import com.mojang.blaze3d.platform.Window; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.MessengerSide; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastMetrics; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.ToastGeometry.ToastPosition; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.Toast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.ToastDraw; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.runtime.HudNotifierRuntime; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ChatScreen; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders the HudNotifier toast stack, both in-game and inside the drag-preview panel. + *

    + * Also owns the smooth stack-size animation and the per-toast layout lerp so cards + * glide to their target slot instead of snapping. Reads the live queue from + * {@link HudNotifierRuntime}. + */ +final class HudNotifierPainter { + + private static final float LAYOUT_SMOOTH_SPEED = 14.0f; + private static final float LAYOUT_SNAP_EPS = 0.12f; + + private final HudNotifier owner; + private final HudNotifierRuntime runtime; + private final RenderColor colorTmp = new RenderColor(0xFFFFFFFF); + private boolean stackLayoutReady; + private float stackWidthAnim, stackHeightAnim; + private TextRenderer lineRenderer, iconRenderer; + private float baseScale, lineScale, iconScale; + + HudNotifierPainter(HudNotifier owner, HudNotifierRuntime runtime) { + this.owner = owner; + this.runtime = runtime; + } + + /** Animated stack dimensions consumed by the drag hit-test / bounding box. */ + record LayoutSize(float width, float height) { } + + /** Draw the live toast stack anchored at the module's current screen position. */ + void renderEngine(Renderer2D renderer, TextRenderer textRenderer) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) return; + HudGlobalConfig hud = HudGlobalConfig.get(); + if (hud == null) return; + boolean preview = DraggableHudElementRegistry.isForceVisible() || ClientScreen.current() instanceof ChatScreen; + if (!owner.enabledValue().get() && !preview) return; + if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER && !preview) return; + int duration = Math.max(1, owner.getDurationMs()), fade = Math.max(0, owner.getFadeMs()); + long now = System.currentTimeMillis(); + runtime.pruneExpiredToasts(now, duration); + float x = owner.getX(), y = owner.getY(); + List source = runtime.toasts(); + boolean sample = preview && source.isEmpty(); + if (sample) source = previewToasts(now); + if (source.isEmpty()) { resetStackLayout(); owner.applyBounds(x, y, 0f, 0f); return; } + Window window = mc.getWindow(); + int fbw = window.getWidth(), fbh = window.getHeight(); + setupRenderers(textRenderer, hud, Math.max(1, Math.round(HudScale.virtualWidth(fbw, fbh))), Math.max(1, Math.round(HudScale.virtualHeight(fbw, fbh)))); + float[] mwTh = new float[2]; + List draws = buildDraws(source, sample, duration, fade, now, true, mwTh); + if (draws.isEmpty()) { resetStackLayout(); owner.applyBounds(x, y, 0f, 0f); return; } + LayoutSize layout = resolveStackLayout(mwTh[0], mwTh[1], sample); + float layoutW = Math.max(mwTh[0], layout.width()), layoutH = Math.max(mwTh[1], layout.height()); + MessengerSide side = owner.getSide(); + float gap = HudNotifier.BASE_GAP * baseScale, cursorY = y; + for (int i = 0; i < draws.size(); i++) { + ToastDraw draw = draws.get(i); + ToastMetrics metrics = draw.metrics; + float targetX = switch (side) { + case LEFT -> x; + case TOP, BOTTOM -> x + (layoutW - metrics.boxW) * 0.5f; + default -> x + (layoutW - metrics.boxW); + }; + ToastPosition pos = resolveToastPosition(draw.toast, targetX, cursorY, sample, baseScale); + ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); + draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, pos.x() + slide.x(), pos.y() + slide.y())); + cursorY += (metrics.boxH + gap) * draw.anim; + } + owner.applyBounds(x, y, layoutW, Math.max(1f, layoutH)); + drawCards(renderer, draws); + } + + /** Draw the toast stack inside the given rect used by the clickgui drag preview. */ + void renderPreview(Renderer2D renderer, TextRenderer textRenderer, float originX, float originY, float width, float height) { + if (renderer == null || width <= 2f || height <= 2f) return; + HudGlobalConfig hud = HudGlobalConfig.get(); + if (hud == null) return; + setupRenderers(textRenderer, hud, Math.max(1, Math.round(width)), Math.max(1, Math.round(height))); + int screenW = Math.max(1, Math.round(width)); + float margin = HudNotifier.BASE_MARGIN * baseScale, gap = HudNotifier.BASE_GAP * baseScale; + long now = System.currentTimeMillis(); + int duration = Math.max(1, owner.getDurationMs()), fade = Math.max(0, owner.getFadeMs()); + runtime.pruneExpiredToasts(now, duration); + boolean sampleMode = runtime.toasts().isEmpty(); + List source = sampleMode ? previewToasts(now) : runtime.toasts(); + List draws = buildDraws(source, sampleMode, duration, fade, now, false, null); + MessengerSide side = owner.getSide(); + float cursorY = margin; + for (int i = 0; i < draws.size(); i++) { + ToastDraw draw = draws.get(i); + ToastMetrics metrics = draw.metrics; + float targetX = switch (side) { + case LEFT -> originX + margin; + case TOP, BOTTOM -> originX + (screenW - metrics.boxW) * 0.5f; + default -> originX + screenW - margin - metrics.boxW; + }; + float targetY = originY + cursorY; + ToastPosition slide = ToastGeometry.toastSlideOffset(side, draw.anim, baseScale); + draws.set(i, new ToastDraw(draw.toast, metrics, draw.remaining, draw.anim, targetX + slide.x(), targetY + slide.y())); + cursorY += (metrics.boxH + gap) * draw.anim; + } + drawCards(renderer, draws); + } + + private void setupRenderers(TextRenderer textRenderer, HudGlobalConfig hud, int screenW, int screenH) { + baseScale = HudScale.scale(screenW, screenH) * (hud.getFontSize() / 18f) * owner.getScale(); + lineScale = baseScale * 0.95f; + iconScale = baseScale * 1.08f; + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + lineRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, fallback); + iconRenderer = Fonts.renderer("Icons", FontInfo.Type.Regular, fallback); + if (lineRenderer == null) lineRenderer = fallback; + if (iconRenderer == null) iconRenderer = fallback; + } + + private List buildDraws(List source, boolean sample, int duration, int fade, long now, boolean wantMeasure, float[] outMaxWTotalH) { + List draws = new ArrayList<>(); + float gap = HudNotifier.BASE_GAP * baseScale, maxW = 0f, totalH = 0f; + for (int i = source.size() - 1; i >= 0; i--) { + Toast toast = source.get(i); + float anim = sample ? 1.0f : toast.anim(now, duration, fade); + if (anim <= 0.001f) continue; + ToastMetrics m = ToastGeometry.measureToast(toast, lineRenderer, iconRenderer, lineScale, iconScale, baseScale); + float remaining = sample ? 0.45f : toast.remaining(now, duration); + draws.add(new ToastDraw(toast, m, remaining, anim, 0f, 0f)); + if (wantMeasure) { + maxW = Math.max(maxW, m.boxW); + totalH += m.boxH * anim; + if (i > 0) totalH += gap * anim; + } + } + if (outMaxWTotalH != null) { outMaxWTotalH[0] = maxW; outMaxWTotalH[1] = totalH; } + return draws; + } + + private void drawCards(Renderer2D renderer, List draws) { + ToastGeometry.drawGlassBlurBatch(draws, baseScale); + for (ToastDraw d : draws) { + ToastCardRenderer.drawToastCard(renderer, colorTmp, d.toast, lineRenderer, iconRenderer, d.x, d.y, d.metrics, d.remaining, d.anim, baseScale); + } + } + private List previewToasts(long now) { return List.of(HudNotifierRuntime.previewToast(now)); } + private LayoutSize resolveStackLayout(float targetW, float targetH, boolean sample) { + if (sample) { resetStackLayout(); return new LayoutSize(targetW, targetH); } + if (!stackLayoutReady) { stackWidthAnim = targetW; stackHeightAnim = targetH; stackLayoutReady = true; return new LayoutSize(targetW, targetH); } + float dt = AnimationUtility.deltaTime(); + stackWidthAnim = AnimationUtility.snap(AnimationUtility.approach(stackWidthAnim, targetW, dt, LAYOUT_SMOOTH_SPEED), targetW, LAYOUT_SNAP_EPS); + stackHeightAnim = AnimationUtility.snap(AnimationUtility.approach(stackHeightAnim, targetH, dt, LAYOUT_SMOOTH_SPEED), targetH, LAYOUT_SNAP_EPS); + return new LayoutSize(stackWidthAnim, stackHeightAnim); + } + private ToastPosition resolveToastPosition(Toast toast, float targetX, float targetY, boolean sample, float scale) { + if (toast == null || sample) return new ToastPosition(targetX, targetY); + if (!toast.layoutReady) { toast.layoutX = targetX; toast.layoutY = targetY; toast.layoutReady = true; return new ToastPosition(targetX, targetY); } + float dt = AnimationUtility.deltaTime(), eps = Math.max(0.05f, LAYOUT_SNAP_EPS * scale); + toast.layoutX = AnimationUtility.snap(AnimationUtility.approach(toast.layoutX, targetX, dt, LAYOUT_SMOOTH_SPEED), targetX, eps); + toast.layoutY = AnimationUtility.snap(AnimationUtility.approach(toast.layoutY, targetY, dt, LAYOUT_SMOOTH_SPEED), targetY, eps); + return new ToastPosition(toast.layoutX, toast.layoutY); + } + private void resetStackLayout() { stackLayoutReady = false; stackWidthAnim = 0f; stackHeightAnim = 0f; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java index 8f50d3c..4829783 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java @@ -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.gui.hud.draggable.impl.hudnotifier; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java index 6111b7a..ec5f6ef 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java @@ -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.gui.hud.draggable.impl.hudnotifier; import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java index 1960104..2256886 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java @@ -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.gui.hud.draggable.impl.hudnotifier.model; import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java index ec1367c..15c5ed3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java @@ -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.gui.hud.draggable.impl.hudnotifier.model; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/runtime/HudNotifierRuntime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/runtime/HudNotifierRuntime.java new file mode 100644 index 0000000..075f276 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/runtime/HudNotifierRuntime.java @@ -0,0 +1,124 @@ +package dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.runtime; + +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier.NotifyType; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.PreviewToast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.Toast; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastModel.ToastVisual; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.model.ToastStyle; +import dev.loki.lovisual.features.module.core.ModuleManager; +import dev.loki.lovisual.util.wav.engine.CustomSoundEngine; +import net.minecraft.resources.Identifier; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Owns the mutable toast queue, the enqueue / clear API and the notification sound helper. + *

    + * Called by {@link HudNotifier} (public static facade) and by the painter at the start of + * every frame (to prune expired entries and to read the live list). Kept in its own + * subpackage so the main folder stays within the four-file-per-directory limit. + */ +public final class HudNotifierRuntime { + + /** Hard cap for the visible stack; the oldest toast is dropped when exceeded. */ + public static final int MAX_TOASTS = 6; + + private final HudNotifier owner; + private final List toasts = new ArrayList<>(); + + public HudNotifierRuntime(HudNotifier owner) { + this.owner = owner; + } + + /** Live, ordered view of the toast stack. Callers must not mutate; use the enqueue / clear APIs. */ + public List toasts() { + return toasts; + } + + /** Drop every toast whose lifetime has fully elapsed (including the fade-out tail). */ + public void pruneExpiredToasts(long now, int duration) { + Iterator it = toasts.iterator(); + while (it.hasNext()) { + Toast t = it.next(); + if (t.isExpired(now, duration)) it.remove(); + } + } + + /** Push a module-toggle notification with a fixed YES / NO visual. */ + public void enqueueModuleState(String text, boolean enabled) { + enqueueInternal(null, text, enabled, enabled ? NotifyType.YES : NotifyType.NO, ToastVisual.MODULE_TOGGLE); + } + + /** Public push helper used by the HudNotifier facade: picks type / visual from the raw arguments. */ + public void enqueue(String key, String text, Boolean enabled, NotifyType forcedType) { + NotifyType type = forcedType != null ? forcedType : ToastStyle.resolveType(text, enabled); + enqueueInternal(key, text, enabled, type, ToastVisual.STANDARD); + } + + /** Remove every toast that was pushed under the given key. */ + public void clearKey(String key) { + if (key == null) return; + toasts.removeIf(t -> key.equals(t.key)); + } + + private void enqueueInternal(String key, String text, Boolean enabled, NotifyType type, ToastVisual visual) { + if (text == null || text.isBlank()) return; + HudGlobalConfig hud = HudGlobalConfig.get(); + if (hud == null) return; + if (!owner.enabledValue().get()) return; + if (hud.getHeaderMode() == HudGlobalConfig.HeaderMode.NEVER) return; + + String line = ToastStyle.resolveLine(text, enabled); + long now = System.currentTimeMillis(); + + if (key != null) { + for (Toast toast : toasts) { + if (key.equals(toast.key)) { + toast.line = line; + toast.type = type; + toast.visual = visual == null ? ToastVisual.STANDARD : visual; + toast.stateEnabled = enabled; + toast.startMs = now; + return; + } + } + } + + toasts.add(new Toast(key, line, type, now, visual, enabled)); + if (toasts.size() > MAX_TOASTS) toasts.remove(0); + if (enabled != null) playSound(enabled); + } + + private void playSound(boolean enabled) { + if (ModuleManager.isToggleSoundSuppressed()) return; + if (!owner.isSoundEnabled()) return; + ModeValue mode = owner.getSoundMode(); + Identifier snd = resolveSound(enabled, mode == null ? "2" : mode.get()); + if (snd == null) return; + double vol = Math.min(1.0, owner.getSoundVolume()); + CustomSoundEngine.get().play(snd, vol, 1.0, false, true); + } + + private static Identifier resolveSound(boolean enabled, String mode) { + String base = switch (mode) { + case "2" -> "enable/enable2"; + case "3" -> "enable/enable3"; + default -> "enable/enable1"; + }; + if (!enabled) base = base.replace("enable/", "disable/").replace("enable", "disable"); + return Identifier.fromNamespaceAndPath("lovisual", "sounds/" + base + ".wav"); + } + + /** Sample toast used by the drag-preview overlay when the real stack is empty. */ + public static Toast previewToast(long now) { + PreviewToast preview = ToastModel.previewExample(now); + long start = ToastModel.previewStart(now); + return new Toast(ToastModel.PREVIEW_KEY, preview.line(), preview.type(), start, preview.visual(), preview.enabled()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java index 7e52e03..d2fe0c2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java index 100430a..30d882e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java index 24d0a4b..b4fd522 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java index d9bf596..130e8bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java index 779d152..01bf1fd 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java index 2e59503..f475edf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java index aba6cd4..6a758af 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java index f4db563..65c850d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java index 81ab4da..6725013 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java index 5dd6994..46c91e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java index b2e3ea7..068bb29 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java index 2a92941..6447ee2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java @@ -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.gui.hud.draggable.impl.metrics; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java index e2b7734..4e5225e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.render.engine.text.font.Fonts; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java index e37e1d7..b49e33b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java index 840a81a..57cca02 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java @@ -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.gui.hud.draggable.impl.metrics; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java index 9b2c160..53f5e3f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java index 034fa92..9b84e04 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java @@ -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.gui.hud.draggable.impl.panels; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java index 9f8c8d4..25722c2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java index 6640867..cfb472a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java index 1441364..a0a755c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java index 00db470..be18f96 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.config.values.mode.BindMode; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java index d7fc24b..55f44eb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java index 6c8c581..52b8fe0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java index 32d2f2e..df89a51 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.module.core.Module; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java index 33ea1b4..6188e8b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java index e1fec64..80f653d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java @@ -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.gui.hud.draggable.impl.panels; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java index c2fcfcc..7254934 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java index 1948ba8..86d68f8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java index 7f055bc..d873908 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java index 2435548..a8da1bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java index b822778..f40ddd9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.render.engine.text.font.Fonts; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java index 12e8894..509d86d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java @@ -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.gui.hud.draggable.impl.panels; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java index 93082e3..059ccab 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java @@ -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.gui.hud.draggable.impl.scripted; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java index 65f6cdc..455f070 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java @@ -1,16 +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.features.gui.hud.draggable.impl.scripted; +import java.util.ArrayList; import java.util.LinkedHashMap; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; +import java.util.List; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; @@ -20,38 +13,29 @@ import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.Cached import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.Keybinds; -import dev.loki.lovisual.features.gui.hud.draggable.impl.status.Admins; -import dev.loki.lovisual.features.gui.hud.draggable.impl.status.Cooldowns; -import dev.loki.lovisual.features.gui.hud.draggable.impl.status.Potions; - +/** + * Scripted list HUD panel (potions / cooldowns / admins / keybinds). This facade holds the + * public data model ({@link Variant}, {@link Palette}, {@link Panel}), the shared builders + * used by the individual renderers and the bake-and-render entry point. Property-map and + * cache-signature computation live in {@code ScriptedListPanelProps} / {@code ScriptedListPanelSignature}. + */ public final class ScriptedListHudPanel { public static final Variant POTIONS = new Variant( "potions", "Potions", String.valueOf((char) 0x42), - 95.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, - 3.5f, 8.0f, 14.0f, 7.0f, 18.0f, 8.0f, 2.1f - ); + 95.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, 3.5f, 8.0f, 14.0f, 7.0f, 18.0f, 8.0f, 2.1f); public static final Variant COOLDOWNS = new Variant( "cooldowns", "Cooldowns", "C", - 110.0f, 18.0f, 4.0f, 22.0f, 22.0f, 2.5f, - 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.9f - ); + 110.0f, 18.0f, 4.0f, 22.0f, 22.0f, 2.5f, 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.9f); public static final Variant ADMINS = new Variant( "admins", "Admins", String.valueOf((char) 0x45), - 96.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, - 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f - ); + 96.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f); public static final Variant KEYBINDS = new Variant( "keybinds", "Keybinds", "M", - 102.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, - 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f, - "Icons", 1.18f - ); + 102.0f, 18.0f, 5.0f, 22.0f, 22.0f, 3.0f, 3.5f, 8.0f, 15.0f, 6.0f, 18.0f, 8.0f, 1.95f, "Icons", 1.18f); public static final float HEADER_HEIGHT = 15.5f; public static final float BODY_Y_OFFSET = 18.5f; public static final float CONTENT_START_Y = 25.0f; @@ -75,14 +59,9 @@ public final class ScriptedListHudPanel { return part; } - public static LinkedHashMap row(String key, - String iconKind, - String icon, - List> nameParts, - String rightText, - int rightColor, - int dividerColor, - float alpha) { + public static LinkedHashMap row(String key, String iconKind, String icon, + List> nameParts, + String rightText, int rightColor, int dividerColor, float alpha) { LinkedHashMap row = new LinkedHashMap<>(); row.put("key", key != null ? key : ""); row.put("iconKind", iconKind != null ? iconKind : ""); @@ -99,70 +78,27 @@ public final class ScriptedListHudPanel { return id != null ? id.toString() : ""; } - private static String string(Object value) { - return value instanceof String s ? s : ""; - } - - private static int intValue(Object value) { - return value instanceof Number n ? n.intValue() : 0; - } - - private static float floatValue(Object value) { - return value instanceof Number n ? n.floatValue() : 0.0f; - } - - private static void putPatch(LinkedHashMap> patches, - String key, - String prop, - Object value) { - LinkedHashMap patch = patches.get(key); - if (patch == null) { - patch = new LinkedHashMap<>(); - patches.put(key, patch); - } - patch.put(prop, value); - } - public static List> rows() { return new ArrayList<>(); } - public boolean render(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - Panel panel) { - if (renderer == null || panel == null || panel.width <= 0.0f || panel.height <= 0.0f) return false; + public boolean render(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, Panel panel) { + if (renderer == null || panel == null || panel.width() <= 0.0f || panel.height() <= 0.0f) return false; Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getResourceManager() == null) return false; HudScriptLayouts.pollReloadCombo(mc); - if (moduleHandle.consumeChanged()) { - runtime.reset(); - } + if (moduleHandle.consumeChanged()) runtime.reset(); UiScriptModule module = ensureModule(mc); if (module == null) return false; TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); LinkedHashMap props = panel.toProps(); - UiRuntime baked = runtime.bake( - moduleHandle, - module, - panel.variant.id, - panel.treeSignature(), - panel.dataSignature(), - panel.layoutSignature(), - panel.width, - panel.height, - fallback, - panel.x, - panel.y, - panel.width, - panel.height, - () -> props, - panel::patches - ); + UiRuntime baked = runtime.bake(moduleHandle, module, panel.variant().id(), panel.treeSignature(), + panel.dataSignature(), panel.layoutSignature(), panel.width(), panel.height(), fallback, + panel.x(), panel.y(), panel.width(), panel.height(), () -> props, panel::patches); if (baked == null) return false; baked.render(new UiRenderContext(renderer, fallback, ctx, tickDelta, UiProjectionMode.CURRENT)); return true; @@ -177,330 +113,46 @@ public final class ScriptedListHudPanel { return moduleHandle.module(); } - public record Variant(String id, - String title, - String headerIcon, - float minWidth, - float headerDividerX, - float titleIconX, - float titleTextX, - float countLabelOffset, - float countValueOffset, - float rowIconX, - float rowIconSize, - float rowDividerX, - float rowDividerH, - float rowTextX, - float rowRightPad, - float rowCenterOffset, - String headerIconFont, - float headerIconScale) { - Variant(String id, - String title, - String headerIcon, - float minWidth, - float headerDividerX, - float titleIconX, - float titleTextX, - float countLabelOffset, - float countValueOffset, - float rowIconX, - float rowIconSize, - float rowDividerX, - float rowDividerH, - float rowTextX, - float rowRightPad, - float rowCenterOffset, - String headerIconFont) { - this(id, title, headerIcon, minWidth, headerDividerX, titleIconX, titleTextX, - countLabelOffset, countValueOffset, rowIconX, rowIconSize, rowDividerX, - rowDividerH, rowTextX, rowRightPad, rowCenterOffset, headerIconFont, 1.0f); - } - - Variant(String id, - String title, - String headerIcon, - float minWidth, - float headerDividerX, - float titleIconX, - float titleTextX, - float countLabelOffset, - float countValueOffset, - float rowIconX, - float rowIconSize, - float rowDividerX, - float rowDividerH, - float rowTextX, - float rowRightPad, + /** Per-list visual metrics: header/row layout offsets, icon placement and default font settings. */ + public record Variant(String id, String title, String headerIcon, float minWidth, float headerDividerX, + float titleIconX, float titleTextX, float countLabelOffset, float countValueOffset, + float rowIconX, float rowIconSize, float rowDividerX, float rowDividerH, float rowTextX, + float rowRightPad, float rowCenterOffset, String headerIconFont, float headerIconScale) { + Variant(String id, String title, String headerIcon, float minWidth, float headerDividerX, + float titleIconX, float titleTextX, float countLabelOffset, float countValueOffset, float rowIconX, + float rowIconSize, float rowDividerX, float rowDividerH, float rowTextX, float rowRightPad, float rowCenterOffset) { - this(id, title, headerIcon, minWidth, headerDividerX, titleIconX, titleTextX, - countLabelOffset, countValueOffset, rowIconX, rowIconSize, rowDividerX, - rowDividerH, rowTextX, rowRightPad, rowCenterOffset, "IconsNur", 1.0f); + this(id, title, headerIcon, minWidth, headerDividerX, titleIconX, titleTextX, countLabelOffset, + countValueOffset, rowIconX, rowIconSize, rowDividerX, rowDividerH, rowTextX, rowRightPad, + rowCenterOffset, "IconsNur", 1.0f); } } - public record Palette(int headerLeft, - int headerRight, - int bodyLeft, - int bodyRight, - int outline, - int text, - int muted, - int counter, - int titleText, - int divider, - int blurTint) { - public LinkedHashMap toProps() { - LinkedHashMap out = new LinkedHashMap<>(); - out.put("headerLeft", hex(headerLeft)); - out.put("headerRight", hex(headerRight)); - out.put("bodyLeft", hex(bodyLeft)); - out.put("bodyRight", hex(bodyRight)); - out.put("outline", hex(outline)); - out.put("text", hex(text)); - out.put("muted", hex(muted)); - out.put("counter", hex(counter)); - out.put("titleText", hex(titleText)); - out.put("divider", hex(divider)); - out.put("blurTint", hex(blurTint)); - return out; - } + /** Panel color scheme as ARGB ints; serialized to hex strings for the scripted UI. */ + public record Palette(int headerLeft, int headerRight, int bodyLeft, int bodyRight, int outline, int text, + int muted, int counter, int titleText, int divider, int blurTint) { + public LinkedHashMap toProps() { return ScriptedListPanelProps.paletteProps(this); } } - public record Panel(Variant variant, Palette palette, float x, float y, float width, float height, float drawScale, - float baseScale, float fontScale, float headerIconHeight, float headerTextHeight, float rowTextHeight, - float countLabelWidth, float countValueWidth, - int activeCount, boolean blur, float blurAlpha, int headerIconColor, String layout, - List> rows) { - public Panel(Variant variant, - Palette palette, - float x, - float y, - float width, - float height, - float drawScale, - float baseScale, - float fontScale, - float headerIconHeight, - float headerTextHeight, - float rowTextHeight, - float countLabelWidth, - float countValueWidth, - int activeCount, - boolean blur, - float blurAlpha, - int headerIconColor, - String layout, - List> rows) { - this.variant = variant; - this.palette = palette; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.drawScale = drawScale; - this.baseScale = baseScale; - this.fontScale = fontScale; - this.headerIconHeight = headerIconHeight; - this.headerTextHeight = headerTextHeight; - this.rowTextHeight = rowTextHeight; - this.countLabelWidth = countLabelWidth; - this.countValueWidth = countValueWidth; - this.activeCount = activeCount; - this.blur = blur; - this.blurAlpha = blurAlpha; - this.headerIconColor = headerIconColor; - this.layout = layout != null ? layout : HudPanelLayoutModes.SPLIT_HEADER; - this.rows = rows != null ? rows : List.of(); + /** Immutable render request: variant + palette + geometry + text metrics + row data. */ + public record Panel(Variant variant, Palette palette, float x, float y, float width, float height, + float drawScale, float baseScale, float fontScale, float headerIconHeight, + float headerTextHeight, float rowTextHeight, float countLabelWidth, float countValueWidth, + int activeCount, boolean blur, float blurAlpha, int headerIconColor, String layout, + List> rows) { + public Panel { + layout = layout != null ? layout : HudPanelLayoutModes.SPLIT_HEADER; + rows = rows != null ? rows : List.of(); } - public LinkedHashMap toProps() { - LinkedHashMap out = new LinkedHashMap<>(); - out.put("id", variant.id); - out.put("title", variant.title); - out.put("headerIcon", variant.headerIcon); - out.put("width", width); - out.put("height", height); - out.put("drawScale", drawScale); - out.put("baseScale", baseScale); - out.put("fontScale", fontScale); - out.put("headerIconHeight", headerIconHeight); - out.put("headerTextHeight", headerTextHeight); - out.put("rowTextHeight", rowTextHeight); - out.put("countLabelWidth", countLabelWidth); - out.put("countValueWidth", countValueWidth); - out.put("activeCount", activeCount); - out.put("blur", blur); - out.put("blurAlpha", blurAlpha); - out.put("headerIconColor", hex(headerIconColor)); - out.put("layout", layout); - out.put("palette", palette.toProps()); - out.put("rows", rows.toArray()); - out.put("variant", variantProps()); - return out; - } + public LinkedHashMap toProps() { return ScriptedListPanelProps.toProps(this); } - private Map variantProps() { - LinkedHashMap out = new LinkedHashMap<>(); - out.put("minWidth", variant.minWidth); - out.put("headerDividerX", variant.headerDividerX); - out.put("titleIconX", variant.titleIconX); - out.put("titleTextX", variant.titleTextX); - out.put("countLabelOffset", variant.countLabelOffset); - out.put("countValueOffset", variant.countValueOffset); - out.put("rowIconX", variant.rowIconX); - out.put("rowIconSize", variant.rowIconSize); - out.put("rowDividerX", variant.rowDividerX); - out.put("rowDividerH", variant.rowDividerH); - out.put("rowTextX", variant.rowTextX); - out.put("rowRightPad", variant.rowRightPad); - out.put("rowCenterOffset", variant.rowCenterOffset); - out.put("headerIconFont", variant.headerIconFont); - out.put("headerIconScale", variant.headerIconScale); - return out; - } + long treeSignature() { return ScriptedListPanelSignature.treeSignature(this); } - long treeSignature() { - long h = 0xcbf29ce484222325L; - h = CachedUiScriptRuntime.mix(h, variant.id); - h = mixVariant(h); - h = mixPalette(h); - h = CachedUiScriptRuntime.mix(h, width); - h = CachedUiScriptRuntime.mix(h, height); - h = CachedUiScriptRuntime.mix(h, drawScale); - h = CachedUiScriptRuntime.mix(h, baseScale); - h = CachedUiScriptRuntime.mix(h, fontScale); - h = CachedUiScriptRuntime.mix(h, headerIconHeight); - h = CachedUiScriptRuntime.mix(h, headerTextHeight); - h = CachedUiScriptRuntime.mix(h, rowTextHeight); - h = CachedUiScriptRuntime.mix(h, countLabelWidth); - h = CachedUiScriptRuntime.mix(h, countValueWidth); - h = CachedUiScriptRuntime.mix(h, blur); - h = CachedUiScriptRuntime.mix(h, blurAlpha); - h = CachedUiScriptRuntime.mix(h, layout); - h = CachedUiScriptRuntime.mix(h, String.valueOf(Math.max(0, activeCount)).length()); - h = CachedUiScriptRuntime.mix(h, activeCount); - h = CachedUiScriptRuntime.mix(h, rows.size()); - for (LinkedHashMap row : rows) { - String key = string(row.get("key")); - h = CachedUiScriptRuntime.mix(h, key); - h = CachedUiScriptRuntime.mix(h, string(row.get("iconKind"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("rightText")).length()); - h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(row.get("alpha")) * 100.0f)); - Object partsValue = row.get("nameParts"); - Object[] parts = partsValue instanceof Object[] arr ? arr : new Object[0]; - h = CachedUiScriptRuntime.mix(h, parts.length); - for (Object partValue : parts) { - if (!(partValue instanceof Map part)) continue; - h = CachedUiScriptRuntime.mix(h, string(part.get("text")).length()); - h = CachedUiScriptRuntime.mix(h, floatValue(part.get("x"))); - } - } - return h; - } + long dataSignature() { return ScriptedListPanelSignature.dataSignature(this); } - long dataSignature() { - long h = 0xcbf29ce484222325L; - h = CachedUiScriptRuntime.mix(h, activeCount); - h = CachedUiScriptRuntime.mix(h, headerIconColor); - for (LinkedHashMap row : rows) { - h = CachedUiScriptRuntime.mix(h, string(row.get("key"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("icon"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("iconTint"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("rightText"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("rightColor"))); - h = CachedUiScriptRuntime.mix(h, string(row.get("dividerColor"))); - Object partsValue = row.get("nameParts"); - Object[] parts = partsValue instanceof Object[] arr ? arr : new Object[0]; - for (Object partValue : parts) { - if (!(partValue instanceof Map part)) continue; - h = CachedUiScriptRuntime.mix(h, string(part.get("text"))); - h = CachedUiScriptRuntime.mix(h, string(part.get("color"))); - } - } - return h; - } + long layoutSignature() { return ScriptedListPanelSignature.layoutSignature(this); } - long layoutSignature() { - long h = 0xcbf29ce484222325L; - h = CachedUiScriptRuntime.mix(h, x); - h = CachedUiScriptRuntime.mix(h, y); - h = CachedUiScriptRuntime.mix(h, width); - h = CachedUiScriptRuntime.mix(h, height); - return h; - } - - LinkedHashMap> patches() { - LinkedHashMap> patches = new LinkedHashMap<>(); - putPatch(patches, "header:count-value", "text", String.valueOf(Math.max(0, activeCount))); - putPatch(patches, "header:icon", "color", hex(headerIconColor)); - for (LinkedHashMap row : rows) { - String key = string(row.get("key")); - String rowPrefix = "row:" + key; - String iconKind = string(row.get("iconKind")); - String icon = string(row.get("icon")); - String iconTint = string(row.get("iconTint")); - if ("item".equals(iconKind)) { - putPatch(patches, rowPrefix + ":item", "item", icon); - } else if ("head".equals(iconKind) && !icon.isEmpty()) { - putPatch(patches, rowPrefix + ":head", "asset", icon); - putPatch(patches, rowPrefix + ":head", "tint", iconTint); - } else if ("texture".equals(iconKind) && !icon.isEmpty()) { - putPatch(patches, rowPrefix + ":texture", "asset", icon); - putPatch(patches, rowPrefix + ":texture", "tint", iconTint); - } else if ("svg".equals(iconKind) && !icon.isEmpty()) { - putPatch(patches, rowPrefix + ":svg", "asset", icon); - putPatch(patches, rowPrefix + ":svg", "tint", iconTint); - } - putPatch(patches, rowPrefix + ":divider", "fill", string(row.get("dividerColor"))); - putPatch(patches, rowPrefix + ":right", "text", string(row.get("rightText"))); - putPatch(patches, rowPrefix + ":right", "color", string(row.get("rightColor"))); - Object partsValue = row.get("nameParts"); - Object[] parts = partsValue instanceof Object[] arr ? arr : new Object[0]; - for (int i = 0; i < parts.length; i++) { - if (!(parts[i] instanceof Map part)) continue; - String partKey = rowPrefix + ":name:" + i; - putPatch(patches, partKey, "text", string(part.get("text"))); - putPatch(patches, partKey, "color", string(part.get("color"))); - } - } - return patches; - } - - private long mixVariant(long h) { - h = CachedUiScriptRuntime.mix(h, variant.title); - h = CachedUiScriptRuntime.mix(h, variant.headerIcon); - h = CachedUiScriptRuntime.mix(h, variant.minWidth); - h = CachedUiScriptRuntime.mix(h, variant.headerDividerX); - h = CachedUiScriptRuntime.mix(h, variant.titleIconX); - h = CachedUiScriptRuntime.mix(h, variant.titleTextX); - h = CachedUiScriptRuntime.mix(h, variant.countLabelOffset); - h = CachedUiScriptRuntime.mix(h, variant.countValueOffset); - h = CachedUiScriptRuntime.mix(h, variant.rowIconX); - h = CachedUiScriptRuntime.mix(h, variant.rowIconSize); - h = CachedUiScriptRuntime.mix(h, variant.rowDividerX); - h = CachedUiScriptRuntime.mix(h, variant.rowDividerH); - h = CachedUiScriptRuntime.mix(h, variant.rowTextX); - h = CachedUiScriptRuntime.mix(h, variant.rowRightPad); - h = CachedUiScriptRuntime.mix(h, variant.rowCenterOffset); - h = CachedUiScriptRuntime.mix(h, variant.headerIconFont); - h = CachedUiScriptRuntime.mix(h, variant.headerIconScale); - return h; - } - - private long mixPalette(long h) { - h = CachedUiScriptRuntime.mix(h, palette.headerLeft); - h = CachedUiScriptRuntime.mix(h, palette.headerRight); - h = CachedUiScriptRuntime.mix(h, palette.bodyLeft); - h = CachedUiScriptRuntime.mix(h, palette.bodyRight); - h = CachedUiScriptRuntime.mix(h, palette.outline); - h = CachedUiScriptRuntime.mix(h, palette.text); - h = CachedUiScriptRuntime.mix(h, palette.muted); - h = CachedUiScriptRuntime.mix(h, palette.counter); - h = CachedUiScriptRuntime.mix(h, palette.titleText); - h = CachedUiScriptRuntime.mix(h, palette.divider); - h = CachedUiScriptRuntime.mix(h, palette.blurTint); - return h; - } + LinkedHashMap> patches() { return ScriptedListPanelProps.patches(this); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java index 2f4f104..617f8e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java @@ -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.gui.hud.draggable.impl.scripted; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelProps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelProps.java new file mode 100644 index 0000000..6ada405 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelProps.java @@ -0,0 +1,127 @@ +package dev.loki.lovisual.features.gui.hud.draggable.impl.scripted; + +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Panel; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Palette; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Variant; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Property-map and runtime-patch builders for {@link ScriptedListHudPanel}: convert the + * immutable {@link Panel} description into the string-keyed maps consumed by the scripted + * UI layout, plus the per-element patch overlay. Extracted so the panel facade stays under + * the file-size cap. Sibling folder sharing the parent's package. + */ +final class ScriptedListPanelProps { + + private ScriptedListPanelProps() { } + + /** Full property tree for the baked UI: geometry, colors, variant metrics, palette and rows. */ + static LinkedHashMap toProps(Panel panel) { + Variant variant = panel.variant(); + LinkedHashMap out = new LinkedHashMap<>(); + out.put("id", variant.id()); + out.put("title", variant.title()); + out.put("headerIcon", variant.headerIcon()); + out.put("width", panel.width()); + out.put("height", panel.height()); + out.put("drawScale", panel.drawScale()); + out.put("baseScale", panel.baseScale()); + out.put("fontScale", panel.fontScale()); + out.put("headerIconHeight", panel.headerIconHeight()); + out.put("headerTextHeight", panel.headerTextHeight()); + out.put("rowTextHeight", panel.rowTextHeight()); + out.put("countLabelWidth", panel.countLabelWidth()); + out.put("countValueWidth", panel.countValueWidth()); + out.put("activeCount", panel.activeCount()); + out.put("blur", panel.blur()); + out.put("blurAlpha", panel.blurAlpha()); + out.put("headerIconColor", ScriptedListHudPanel.hex(panel.headerIconColor())); + out.put("layout", panel.layout()); + out.put("palette", paletteProps(panel.palette())); + out.put("rows", panel.rows().toArray()); + out.put("variant", variantProps(variant)); + return out; + } + + static Map variantProps(Variant variant) { + LinkedHashMap out = new LinkedHashMap<>(); + out.put("minWidth", variant.minWidth()); + out.put("headerDividerX", variant.headerDividerX()); + out.put("titleIconX", variant.titleIconX()); + out.put("titleTextX", variant.titleTextX()); + out.put("countLabelOffset", variant.countLabelOffset()); + out.put("countValueOffset", variant.countValueOffset()); + out.put("rowIconX", variant.rowIconX()); + out.put("rowIconSize", variant.rowIconSize()); + out.put("rowDividerX", variant.rowDividerX()); + out.put("rowDividerH", variant.rowDividerH()); + out.put("rowTextX", variant.rowTextX()); + out.put("rowRightPad", variant.rowRightPad()); + out.put("rowCenterOffset", variant.rowCenterOffset()); + out.put("headerIconFont", variant.headerIconFont()); + out.put("headerIconScale", variant.headerIconScale()); + return out; + } + + static LinkedHashMap paletteProps(Palette palette) { + LinkedHashMap out = new LinkedHashMap<>(); + out.put("headerLeft", ScriptedListHudPanel.hex(palette.headerLeft())); + out.put("headerRight", ScriptedListHudPanel.hex(palette.headerRight())); + out.put("bodyLeft", ScriptedListHudPanel.hex(palette.bodyLeft())); + out.put("bodyRight", ScriptedListHudPanel.hex(palette.bodyRight())); + out.put("outline", ScriptedListHudPanel.hex(palette.outline())); + out.put("text", ScriptedListHudPanel.hex(palette.text())); + out.put("muted", ScriptedListHudPanel.hex(palette.muted())); + out.put("counter", ScriptedListHudPanel.hex(palette.counter())); + out.put("titleText", ScriptedListHudPanel.hex(palette.titleText())); + out.put("divider", ScriptedListHudPanel.hex(palette.divider())); + out.put("blurTint", ScriptedListHudPanel.hex(palette.blurTint())); + return out; + } + + /** Per-element patch overlay applied after bake: count text, header icon color and row content. */ + static LinkedHashMap> patches(Panel panel) { + LinkedHashMap> patches = new LinkedHashMap<>(); + putPatch(patches, "header:count-value", "text", String.valueOf(Math.max(0, panel.activeCount()))); + putPatch(patches, "header:icon", "color", ScriptedListHudPanel.hex(panel.headerIconColor())); + for (LinkedHashMap row : panel.rows()) { + String key = string(row.get("key")); + String rowPrefix = "row:" + key; + String iconKind = string(row.get("iconKind")); + String icon = string(row.get("icon")); + String iconTint = string(row.get("iconTint")); + if ("item".equals(iconKind)) { + putPatch(patches, rowPrefix + ":item", "item", icon); + } else if ("head".equals(iconKind) && !icon.isEmpty()) { + putPatch(patches, rowPrefix + ":head", "asset", icon); + putPatch(patches, rowPrefix + ":head", "tint", iconTint); + } else if ("texture".equals(iconKind) && !icon.isEmpty()) { + putPatch(patches, rowPrefix + ":texture", "asset", icon); + putPatch(patches, rowPrefix + ":texture", "tint", iconTint); + } else if ("svg".equals(iconKind) && !icon.isEmpty()) { + putPatch(patches, rowPrefix + ":svg", "asset", icon); + putPatch(patches, rowPrefix + ":svg", "tint", iconTint); + } + putPatch(patches, rowPrefix + ":divider", "fill", string(row.get("dividerColor"))); + putPatch(patches, rowPrefix + ":right", "text", string(row.get("rightText"))); + putPatch(patches, rowPrefix + ":right", "color", string(row.get("rightColor"))); + Object[] parts = row.get("nameParts") instanceof Object[] arr ? arr : new Object[0]; + for (int i = 0; i < parts.length; i++) { + if (!(parts[i] instanceof Map part)) continue; + String partKey = rowPrefix + ":name:" + i; + putPatch(patches, partKey, "text", string(part.get("text"))); + putPatch(patches, partKey, "color", string(part.get("color"))); + } + } + return patches; + } + + private static void putPatch(LinkedHashMap> patches, String key, String prop, Object value) { + LinkedHashMap patch = patches.computeIfAbsent(key, ignored -> new LinkedHashMap<>()); + patch.put(prop, value); + } + + private static String string(Object value) { return value instanceof String s ? s : ""; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelSignature.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelSignature.java new file mode 100644 index 0000000..9be2c48 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/panel/ScriptedListPanelSignature.java @@ -0,0 +1,136 @@ +package dev.loki.lovisual.features.gui.hud.draggable.impl.scripted; + +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Panel; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Palette; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.Variant; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Cache-key signature computation for {@link ScriptedListHudPanel.Panel}: tree, data + * and layout signatures used by {@link CachedUiScriptRuntime} to decide when the baked + * UI must be re-generated. Extracted so the panel facade stays under the file-size cap. + * Sibling folder sharing the parent's package. + */ +final class ScriptedListPanelSignature { + + private ScriptedListPanelSignature() { } + + /** Structure signature: variant geometry, palette, sizing and per-row shape (not text). */ + static long treeSignature(Panel panel) { + long h = 0xcbf29ce484222325L; + h = mix(h, panel.variant().id()); + h = mixVariant(h, panel.variant()); + h = mixPalette(h, panel.palette()); + h = mix(h, panel.width()); + h = mix(h, panel.height()); + h = mix(h, panel.drawScale()); + h = mix(h, panel.baseScale()); + h = mix(h, panel.fontScale()); + h = mix(h, panel.headerIconHeight()); + h = mix(h, panel.headerTextHeight()); + h = mix(h, panel.rowTextHeight()); + h = mix(h, panel.countLabelWidth()); + h = mix(h, panel.countValueWidth()); + h = mix(h, panel.blur()); + h = mix(h, panel.blurAlpha()); + h = mix(h, panel.layout()); + h = mix(h, String.valueOf(Math.max(0, panel.activeCount())).length()); + h = mix(h, panel.activeCount()); + List> rows = panel.rows(); + h = mix(h, rows.size()); + for (LinkedHashMap row : rows) { + h = mix(h, string(row.get("key"))); + h = mix(h, string(row.get("iconKind"))); + h = mix(h, string(row.get("rightText")).length()); + h = mix(h, Math.round(floatValue(row.get("alpha")) * 100.0f)); + Object[] parts = parts(row.get("nameParts")); + h = mix(h, parts.length); + for (Object partValue : parts) { + if (!(partValue instanceof Map part)) continue; + h = mix(h, string(part.get("text")).length()); + h = mix(h, floatValue(part.get("x"))); + } + } + return h; + } + + /** Content signature: colors, icon assets and visible text that change without a re-bake. */ + static long dataSignature(Panel panel) { + long h = 0xcbf29ce484222325L; + h = mix(h, panel.activeCount()); + h = mix(h, panel.headerIconColor()); + for (LinkedHashMap row : panel.rows()) { + h = mix(h, string(row.get("key"))); + h = mix(h, string(row.get("icon"))); + h = mix(h, string(row.get("iconTint"))); + h = mix(h, string(row.get("rightText"))); + h = mix(h, string(row.get("rightColor"))); + h = mix(h, string(row.get("dividerColor"))); + for (Object partValue : parts(row.get("nameParts"))) { + if (!(partValue instanceof Map part)) continue; + h = mix(h, string(part.get("text"))); + h = mix(h, string(part.get("color"))); + } + } + return h; + } + + /** Placement signature: only the panel rectangle, so moving does not change tree/data keys. */ + static long layoutSignature(Panel panel) { + long h = 0xcbf29ce484222325L; + h = mix(h, panel.x()); + h = mix(h, panel.y()); + h = mix(h, panel.width()); + h = mix(h, panel.height()); + return h; + } + + private static long mixVariant(long h, Variant variant) { + h = mix(h, variant.title()); + h = mix(h, variant.headerIcon()); + h = mix(h, variant.minWidth()); + h = mix(h, variant.headerDividerX()); + h = mix(h, variant.titleIconX()); + h = mix(h, variant.titleTextX()); + h = mix(h, variant.countLabelOffset()); + h = mix(h, variant.countValueOffset()); + h = mix(h, variant.rowIconX()); + h = mix(h, variant.rowIconSize()); + h = mix(h, variant.rowDividerX()); + h = mix(h, variant.rowDividerH()); + h = mix(h, variant.rowTextX()); + h = mix(h, variant.rowRightPad()); + h = mix(h, variant.rowCenterOffset()); + h = mix(h, variant.headerIconFont()); + h = mix(h, variant.headerIconScale()); + return h; + } + + private static long mixPalette(long h, Palette palette) { + h = mix(h, palette.headerLeft()); + h = mix(h, palette.headerRight()); + h = mix(h, palette.bodyLeft()); + h = mix(h, palette.bodyRight()); + h = mix(h, palette.outline()); + h = mix(h, palette.text()); + h = mix(h, palette.muted()); + h = mix(h, palette.counter()); + h = mix(h, palette.titleText()); + h = mix(h, palette.divider()); + h = mix(h, palette.blurTint()); + return h; + } + + private static long mix(long h, String value) { return CachedUiScriptRuntime.mix(h, value); } + private static long mix(long h, float value) { return CachedUiScriptRuntime.mix(h, value); } + private static long mix(long h, int value) { return CachedUiScriptRuntime.mix(h, value); } + private static long mix(long h, boolean value) { return CachedUiScriptRuntime.mix(h, value); } + + private static Object[] parts(Object value) { return value instanceof Object[] arr ? arr : new Object[0]; } + private static String string(Object value) { return value instanceof String s ? s : ""; } + private static float floatValue(Object value) { return value instanceof Number n ? n.floatValue() : 0.0f; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java index 21c149e..d0adc74 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java index f838d31..364e44f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java index 00c1ac8..30f268f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java index 23bd6bd..bef710b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java index 6ca07a4..2d2789d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java index 95d8d45..6e24c6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java index bdb1e17..7e0a2e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java index c85af21..8902b7a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java @@ -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.gui.hud.draggable.impl.stats; final class MemoryFormat { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java index f06f0e1..4b763c7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java index c5ab96d..c812d0a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java index d57492c..391b393 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java @@ -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.gui.hud.draggable.impl.stats; final class PingFormat { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java index a49236f..aab1b0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java index cd0cfdf..cb1499b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java index b7aff72..022f61d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java @@ -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.gui.hud.draggable.impl.stats; final class TpsFormat { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java index f443b34..d4a5a44 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java index 0f4da92..d5d2861 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java @@ -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.gui.hud.draggable.impl.stats; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java index a41085b..af9397c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java index 6495943..0a492fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java index 6b9e1b7..e24095b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java index c225054..39ce524 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java index 356efff..104e860 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.relations.StaffData; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java index 63eb272..5137e1e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.relations.StaffData; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java index 468e824..1405fd0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java index f2755ab..0f28a0c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java index 0466576..a86f2f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java index 589a4af..cf3848f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java index c186f38..56458e6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java index 8da14c9..1b9a6ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java index 34d970e..03dff87 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java index 59a6cc0..4ef28d0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java index 0dbcb85..96d6563 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java index 2d0dd02..a488bd5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java @@ -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.gui.hud.draggable.impl.status; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java index 7cc877b..fb687af 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java index f2304d6..bd47f8f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java @@ -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.gui.hud.draggable.impl.status; import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java index 2967a48..66e8436 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java @@ -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.gui.hud.draggable.impl.targethud; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java index 7a167a0..77478bf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java @@ -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.gui.hud.draggable.impl.targethud; import dev.loki.lovisual.util.player.effect.tracker.StatusEffectTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java index aac3b6b..e62b640 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java @@ -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.gui.hud.draggable.impl.targethud; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java index 9df123c..6ec4494 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java @@ -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.gui.hud.draggable.impl.targethud; import dev.loki.lovisual.util.player.health.PlayerHealthResolver; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java index 60f7301..c8cd803 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java index 3cb97be..8ab6756 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java index 69207c1..9f2a8c2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java index 455d251..2bf8b0d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java index cb2d39c..b867f93 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render.parts; import static dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud.ARC_CENTER_Y; import static dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud.ARC_RADIUS; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java index ce5ffb1..996b513 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render.parts; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java index 0e70a92..49437a6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render.parts; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java index 5419a28..42f33cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java @@ -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.gui.hud.draggable.impl.targethud.render.parts; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java index c0876d1..3cf357e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java @@ -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.gui.hud.draggable.impl.triangulator; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java index ed65de0..cda650c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java @@ -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.gui.hud.draggable.impl.triangulator; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java index 7f3fe88..a37317c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java @@ -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.gui.hud.draggable.impl.triangulator; import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TrackedEye.TrackedCapture; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java index 475d58f..5e8379d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java @@ -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.gui.hud.draggable.impl.triangulator; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java index c45c884..bb5ed73 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java @@ -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.gui.hud.draggable.impl.triangulator.model; /** Tunables the tracker needs for a single tick. */ diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java index a3eae29..b1b7171 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java @@ -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.gui.hud.draggable.impl.triangulator.model; public record DisplayState(String statusText, diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java index 74563da..315e632 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java @@ -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.gui.hud.draggable.impl.triangulator.render; import dev.loki.lovisual.features.gui.hud.draggable.impl.triangulator.TriangulatorSolver.CachedThrow; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java index 000d039..a26dbbe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java @@ -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.gui.hud.draggable.impl.triangulator.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java index 1fb02ed..3e1975e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java @@ -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.gui.hud.draggable.interaction; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java index 894adb4..9068b65 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java @@ -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.gui.hud.draggable.interaction; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java index ba9f6ba..2eb6580 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java @@ -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.gui.hud.draggable.registry; import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorX; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java index f17b19e..7d95ce3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java @@ -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.gui.hud.draggable.registry; import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorX; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java index 9490119..e5c9bd3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java @@ -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.gui.hud.draggable.registry; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java index b076983..073c345 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java @@ -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.gui.hud.draggable.registry; import dev.loki.lovisual.Lifecycle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java index 50e5e93..bdba580 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java @@ -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.gui.hud.draggable.render; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java index 53279b4..3443549 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java @@ -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.gui.hud.draggable.render; import dev.loki.lovisual.Lifecycle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java index 66ff684..fed5a7c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java @@ -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.gui.hud.draggable.render; import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java index 02c5dcc..eb1fc8f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java @@ -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.gui.hud.draggable.render; import dev.loki.lovisual.Lifecycle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java index 57b9a7b..1b5872d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java @@ -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.gui.hud.nondraggable; import dev.loki.lovisual.features.gui.hud.registry.HudElementAutoLoader; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java index d608fc2..edf75f1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java @@ -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.gui.hud.nondraggable; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java index 32170f9..e4ff1ad 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java @@ -1,166 +1,23 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars; import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.values.collection.SetValue; -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.primitive.StringValue; import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.util.input.KeyManager; -import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import java.util.*; import java.util.List; +import java.util.Set; import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomXpBarRenderer; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.jump.CustomJumpBarRenderer; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.locator.panel.CustomLocatorRenderer; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; @HudElementRegister(order = 40) public final class CustomBar extends AbstractHudElement { - public static final String COLOR_THEME = "Theme"; - public static final String COLOR_CUSTOM = "Custom"; public static final CustomBar INSTANCE = new CustomBar(); - public static final float XP_BAR_WIDTH = 182f; - public static final double XP_TEXT_SCALE = 0.59; - public static final int XP_BAR_OFFSET = 2; - - public static final int XP_TEXT_OFFSET = 0; - public static final float XP_BAR_HEIGHT = 5.0f; - public static final float XP_BAR_RADIUS = 2.5f; - public static final float BAR_SOFTNESS = 1.0f; - public static final float BAR_BLUR_ALPHA = 0.72f; - public static final float BAR_GLASS_ALPHA = 0.58f; - public static final float BAR_TRACK_ALPHA = 0.54f; - public static final float BAR_FILL_ALPHA = 0.82f; - public static final float BAR_STROKE_ALPHA = 0.48f; - public static final Identifier XP_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/experience_bar_background"); - public static final Identifier XP_PROGRESS = Identifier.fromNamespaceAndPath("minecraft", "hud/experience_bar_progress"); - public static final Identifier LOCATOR_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_background"); - - public static final Identifier JUMP_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_background"); - public static final Identifier JUMP_COOLDOWN = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_cooldown"); - public static final Identifier JUMP_PROGRESS = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_progress"); - public static final float LOCATOR_BAR_HEIGHT = XP_BAR_HEIGHT; - - public static final int LOCATOR_TICK_COUNT = 6; - public static final float LOCATOR_HEAD_SIZE = 6.5f; - public static final float LOCATOR_HEAD_RADIUS = 1.6f; - public static final float LOCATOR_HEAD_GAP = 0f; - public static final float LOCATOR_HEAD_RANGE = XP_BAR_WIDTH - LOCATOR_HEAD_SIZE; - - public static final float LOCATOR_ANGLE_RANGE = 60f; - - public static final float LOCATOR_HEAD_SCALE_START = 24f; - public static final float LOCATOR_HEAD_SCALE_END = 96f; - public static final float LOCATOR_HEAD_MIN_SCALE = 0.7f; - public static final float LOCATOR_OUTLINE_DISTANCE = 64f; - public static final float LOCATOR_OUTLINE_MIN = 0.6f; - public static final float LOCATOR_OUTLINE_MAX = 1.6f; - public static final Identifier LOCATOR_ARROW_UP = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_arrow_up"); - public static final Identifier LOCATOR_ARROW_DOWN = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_arrow_down"); - public static final int LOCATOR_ARROW_W = 6; - public static final int LOCATOR_ARROW_H = 4; - public static final int LOCATOR_ARROW_GAP = 1; - public static final float LOCATOR_LABEL_SCALE = 0.285f; - public static final float LOCATOR_LABEL_PADDING_X = 3.0f; - public static final float LOCATOR_LABEL_PADDING_Y = 2.0f; - public static final float LOCATOR_LABEL_RADIUS = 4.8f; - public static final float LOCATOR_LABEL_OFFSET = 3.4f; - public static final float LOCATOR_LABEL_ANIM_SPEED = 14f; - public static final float LOCATOR_LABEL_ROW_GAP = 0.35f; - public static final float LOCATOR_LABEL_CHIP_PAD_X = 3.1f; - public static final float LOCATOR_LABEL_CHIP_PAD_Y = 1.35f; - public static final float LOCATOR_LABEL_CHIP_RADIUS = 3.6f; - public static final float LOCATOR_LABEL_ACCENT_W = 1.15f; - public static final int LOCATOR_LABEL_MAX_ROWS = 4; - - private final NumberValue overlayAlpha = - new NumberValue<>("overlay_alpha", 255, 30, 255); - private final BooleanValue xpBar = - new BooleanValue("xp_bar", true); - private final BooleanValue xpBarShowLevel = - new BooleanValue("xp_bar_show_level", true); - private final EnumValue barMode = - new EnumValue<>("bar_mode", CustomBarModel.BarMode.LEVEL, CustomBarModel.BarMode.values()); - private final ModeValue colorMode = - new ModeValue("bar_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final RGBColorValue xpBarBgColor = - new RGBColorValue("xp_bar_bg_color", "#151515"); - private final RGBColorValue xpBarFillStart = - new RGBColorValue("xp_bar_fill_start", "#4CB2FF"); - private final RGBColorValue xpBarFillEnd = - new RGBColorValue("xp_bar_fill_end", "#76F0FF"); - private final RGBColorValue xpBarTextColor = - new RGBColorValue("xp_bar_text_color", "#A0E5FF"); - private final EnumValue xpBarStyle = - new EnumValue<>("xp_bar_style", CustomBarModel.XpBarStyle.GRADIENT, CustomBarModel.XpBarStyle.values()); - public final RGBAColorValue customBgColor = - new RGBAColorValue("custom_bg_color", "#88151515"); - public final RGBAColorValue customFillColor = - new RGBAColorValue("custom_fill_color", "#CC4CB2FF"); - private final BooleanValue customFillGradient = - new BooleanValue("custom_fill_gradient", true); - public final RGBAColorValue customFillGradientColor = - new RGBAColorValue("custom_fill_gradient_color", "#CC76F0FF"); - private final NumberValue customFillGradientAngle = - new NumberValue<>("custom_fill_gradient_angle", 0.0f, 0.0f, 360.0f); - private final RGBAColorValue customTextColor = - new RGBAColorValue("custom_text_color", "#FFA0E5FF"); - private final NumberValue customTextScale = - new NumberValue<>("custom_text_scale", (float) XP_TEXT_SCALE, 0.3f, 1.2f); - private final BooleanValue locatorLabels = - new BooleanValue("locator_labels", true); - private final EnumValue locatorLabelMode = - new EnumValue<>("locator_label_mode", CustomBarModel.LocatorLabelMode.CENTER, CustomBarModel.LocatorLabelMode.values()); - private final NumberValue locatorLabelAngle = - new NumberValue<>("locator_label_angle", 6.0f, 4.0f, 45.0f); - private final BooleanValue locatorLabelDistance = - new BooleanValue("locator_label_distance", true); - private final BooleanValue locatorSearchEnabled = - new BooleanValue("locator_search_enabled", true); - private final SetValue locatorSearchPlayers = - new SetValue("locator_search_list", new java.util.LinkedHashSet<>(java.util.List.of("imhorny"))); - private final RGBColorValue locatorSearchColor = - new RGBColorValue("locator_search_color", "#FFAA00"); - private final RGBColorValue locatorDimmedColor = - new RGBColorValue("locator_dimmed_color", "#777777"); - private final BooleanValue locatorDimmedScaleOnly = - new BooleanValue("locator_dimmed_scale_only", true); - private final BooleanValue locatorMapFilterEnabled = - new BooleanValue("locator_map_filter_enabled", false); - private final StringValue locatorMapServer = - new StringValue("locator_map_server", ""); - private final StringValue locatorMapUrl = - new StringValue("locator_map_url", "example"); - private final KeyBindValue locatorTargetToggle = - new KeyBindValue("locator_target_toggle", "NONE"); - private final KeyBindValue locatorBarToggle = - new KeyBindValue("locator_bar_toggle", "U"); + private final BarSettings settings = new BarSettings(); private CustomBar() { super("vanilla_bar", "Bar", true); @@ -180,123 +37,19 @@ public final class CustomBar extends AbstractHudElement { }; } - public static boolean shouldUseJumpBar(Minecraft mc) { - return CustomJumpBarRenderer.shouldUseJumpBar(mc); - } - - public static void renderXpBarBackground(Renderer2D r2d, Minecraft mc) { - CustomXpBarRenderer.renderXpBarBackground(r2d, mc); - } - - public static void renderXpBarTexture(GuiGraphicsExtractor ctx, Minecraft mc) { - CustomXpBarRenderer.renderXpBarTexture(ctx, mc); - } - - public static void renderJumpBarBackground(Renderer2D r2d, Minecraft mc) { - CustomJumpBarRenderer.renderJumpBarBackground(r2d, mc); - } - - public static void renderJumpBarTexture(GuiGraphicsExtractor ctx, Minecraft mc) { - CustomJumpBarRenderer.renderJumpBarTexture(ctx, mc); - } - - public static void renderLocatorBackground(Renderer2D r2d, Minecraft mc) { - CustomLocatorRenderer.renderLocatorBackground(r2d, mc); - } - - public static void renderLocatorTexture(GuiGraphicsExtractor ctx, Minecraft mc) { - CustomLocatorRenderer.renderLocatorTexture(ctx, mc); - } - - public static void renderLocatorArrows(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) { - CustomLocatorRenderer.renderLocatorArrows(ctx, tickCounter, mc); - } - - public static void renderLocatorHeads(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) { - CustomLocatorRenderer.renderLocatorHeads(ctx, tickCounter, mc); - } - - public static void renderLocatorMarkers(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) { - CustomLocatorRenderer.renderLocatorMarkers(ctx, tickCounter, mc); - } - - public static void renderXpBarText(GuiGraphicsExtractor ctx, Minecraft mc) { - CustomXpBarRenderer.renderXpBarText(ctx, mc); - } - - public static void renderPreview(Renderer2D r2d, float x, float y, float scale, float progress) { - CustomXpBarRenderer.renderPreview(r2d, x, y, scale, progress); - } - - public static CustomBarModel.XpTextInfo buildPreviewText(float barX, float barY, float scale, int level) { - return CustomXpBarRenderer.buildPreviewText(barX, barY, scale, level); - } - @Override protected void defineSettings(List defs) { - defs.add(SettingDef.bool(xpBar)); - defs.add(SettingDef.bool(xpBarShowLevel).visibleWhen(xpBar::get)); - defs.add(SettingDef.mode(barMode).visibleWhen(xpBar::get)); - defs.add(SettingDef.mode(colorMode).visibleWhen(xpBar::get)); - defs.add(SettingDef.color(customBgColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(customFillColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(customFillGradientColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(customTextColor).visibleWhen(this::isCustomMode)); - - defs.add(SettingDef.bool(locatorLabels).visibleWhen(this::locatorModeEnabled)); - defs.add(SettingDef.mode(locatorLabelMode) - .visibleWhen(() -> locatorModeEnabled() && locatorLabels.get())); - defs.add(SettingDef.number(locatorLabelAngle) - .visibleWhen(() -> locatorModeEnabled() - && locatorLabels.get() - && locatorLabelModeUsesCenter())); - defs.add(SettingDef.bool(locatorLabelDistance) - .visibleWhen(() -> locatorModeEnabled() && locatorLabels.get())); - - defs.add(SettingDef.bool(locatorSearchEnabled).visibleWhen(this::locatorModeEnabled)); - defs.add(SettingDef.textList(locatorSearchPlayers) - .visibleWhen(() -> locatorModeEnabled() && locatorSearchEnabled.get())); - defs.add(SettingDef.colorNoAlpha(locatorDimmedColor) - .visibleWhen(() -> locatorModeEnabled() - && locatorSearchEnabled.get() - && !locatorSearchPlayers.get().isEmpty())); - defs.add(SettingDef.colorNoAlpha(locatorSearchColor) - .visibleWhen(() -> locatorModeEnabled() - && locatorSearchEnabled.get() - && !locatorSearchPlayers.get().isEmpty())); - defs.add(SettingDef.bool(locatorDimmedScaleOnly).visibleWhen(this::locatorModeEnabled)); - defs.add(SettingDef.bool(locatorMapFilterEnabled).visibleWhen(this::locatorModeEnabled)); - defs.add(SettingDef.text(locatorMapServer) - .visibleWhen(() -> locatorModeEnabled() && locatorMapFilterEnabled.get())); - defs.add(SettingDef.text(locatorMapUrl) - .visibleWhen(() -> locatorModeEnabled() && locatorMapFilterEnabled.get())); - - defs.add(SettingDef.bind(locatorTargetToggle, BindMode.PRESS).visibleWhen(this::locatorModeEnabled)); - defs.add(SettingDef.bind(locatorBarToggle, BindMode.PRESS).visibleWhen(this::locatorModeEnabled)); + settings.defineSettings(defs); } @Override protected void onLoaded() { - registerBind(locatorTargetToggle); - registerBind(locatorBarToggle); + settings.registerBinds(this); } @Override public void onTick() { - if (KeyManager.wasPressed(bindingName(locatorTargetToggle))) { - boolean next = !locatorSearchEnabled.get(); - locatorSearchEnabled.set(next); - HudNotifier.pushState("Locator target", next); - saveConfig(); - } - - if (KeyManager.wasPressed(bindingName(locatorBarToggle))) { - CustomBarModel.BarMode mode = barMode.get(); - if (mode != CustomBarModel.BarMode.AUTO) { - barMode.set(mode == CustomBarModel.BarMode.LOCATOR ? CustomBarModel.BarMode.LEVEL : CustomBarModel.BarMode.LOCATOR); - saveConfig(); - } - } + settings.handleToggles(this); } public boolean isHudBarEnabled() { @@ -308,42 +61,42 @@ public final class CustomBar extends AbstractHudElement { } public int getHudOverlayAlpha() { - int v = overlayAlpha.get(); + int v = settings.overlayAlpha.get(); if (v < 0) return 0; if (v > 255) return 255; return v; } public boolean isHotbarXpBarEnabled() { - return xpBar.get(); + return settings.xpBar.get(); } public boolean isHotbarXpBarShowLevel() { - return xpBarShowLevel.get(); + return settings.xpBarShowLevel.get(); } public CustomBarModel.BarMode getHotbarBarMode() { - return barMode.get(); + return settings.barMode.get(); } public int getHotbarXpBarBgRgb() { - return xpBarBgColor.getArgb() & 0x00FFFFFF; + return settings.xpBarBgColor.getArgb() & 0x00FFFFFF; } public int getHotbarXpBarFillStartRgb() { - return xpBarFillStart.getArgb() & 0x00FFFFFF; + return settings.xpBarFillStart.getArgb() & 0x00FFFFFF; } public int getHotbarXpBarFillEndRgb() { - return xpBarFillEnd.getArgb() & 0x00FFFFFF; + return settings.xpBarFillEnd.getArgb() & 0x00FFFFFF; } public int getHotbarXpBarTextRgb() { - return xpBarTextColor.getArgb() & 0x00FFFFFF; + return settings.xpBarTextColor.getArgb() & 0x00FFFFFF; } public CustomBarModel.XpBarStyle getHotbarXpBarStyle() { - return xpBarStyle.get(); + return settings.xpBarStyle.get(); } public float getBarWidth() { @@ -354,92 +107,79 @@ public final class CustomBar extends AbstractHudElement { return XP_BAR_HEIGHT; } - private boolean isCustomXpStyleEnabled() { - return xpBar.get() && isCustomMode(); - } - public float getXpTextScale() { return (float) XP_TEXT_SCALE; } public int getXpTextArgb() { - if (isCustomXpStyleEnabled()) { - return customTextColor.getArgb(); + if (settings.isCustomXpStyleEnabled()) { + return settings.customTextColor.getArgb(); } return HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.32f); } public boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); + return settings.isCustomMode(); + } + + // Custom color accessors consumed by the XP bar renderer. + public int customBgArgb() { + return settings.customBgColor.getArgb(); + } + + public int customFillArgb() { + return settings.customFillColor.getArgb(); + } + + public int customFillGradientArgb() { + return settings.customFillGradientColor.getArgb(); } public boolean isLocatorSearchEnabled() { - return locatorModeEnabled() && locatorSearchEnabled.get(); + return settings.locatorModeEnabled() && settings.locatorSearchEnabled.get(); } public Set getLocatorSearchPlayers() { - return locatorSearchPlayers.get(); + return settings.locatorSearchPlayers.get(); } public int getLocatorDimmedColorRgb() { - return locatorDimmedColor.getArgb() & 0x00FFFFFF; + return settings.locatorDimmedColor.getArgb() & 0x00FFFFFF; } public int getLocatorSearchColorRgb() { - return locatorSearchColor.getArgb() & 0x00FFFFFF; + return settings.locatorSearchColor.getArgb() & 0x00FFFFFF; } public boolean isLocatorDimmedScaleOnly() { - return locatorDimmedScaleOnly.get(); + return settings.locatorDimmedScaleOnly.get(); } public boolean isLocatorMapFilterEnabled() { - return locatorMapFilterEnabled.get(); + return settings.locatorMapFilterEnabled.get(); } public String getLocatorMapServer() { - return locatorMapServer.get(); + return settings.locatorMapServer.get(); } public String getLocatorMapUrl() { - return locatorMapUrl.get(); + return settings.locatorMapUrl.get(); } public boolean isLocatorLabelsEnabled() { - return locatorModeEnabled() && locatorLabels.get(); + return settings.locatorModeEnabled() && settings.locatorLabels.get(); } public boolean isLocatorLabelDistanceEnabled() { - return isLocatorLabelsEnabled() && locatorLabelDistance.get(); + return isLocatorLabelsEnabled() && settings.locatorLabelDistance.get(); } public float getLocatorLabelAngle() { - return locatorLabelAngle.get(); + return settings.locatorLabelAngle.get(); } public CustomBarModel.LocatorLabelMode getLocatorLabelMode() { - return locatorLabelMode.get(); - } - - private boolean locatorModeEnabled() { - if (!xpBar.get()) return false; - return barMode.get() != CustomBarModel.BarMode.LEVEL; - } - - private boolean locatorLabelModeUsesCenter() { - CustomBarModel.LocatorLabelMode mode = locatorLabelMode.get(); - return mode == CustomBarModel.LocatorLabelMode.CENTER || mode == CustomBarModel.LocatorLabelMode.CENTER_TARGETS; - } - - private String bindingName(KeyBindValue value) { - return name() + ":" + value.getName(); - } - - private void registerBind(KeyBindValue value) { - String name = bindingName(value); - KeyManager.unregisterAll(name); - if (!value.isNone()) { - KeyManager.registerCombo(name, value.get()); - } + return settings.locatorLabelMode.get(); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java index 4a737ea..6b866f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java @@ -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.gui.hud.nondraggable.impl.bars; import dev.loki.lovisual.config.values.mode.EnumValue; @@ -18,6 +11,58 @@ import java.util.UUID; public final class CustomBarModel { + // Layout, alpha and vanilla-texture constants shared by the bar/locator/jump renderers. + public static final String COLOR_THEME = "Theme"; + public static final String COLOR_CUSTOM = "Custom"; + public static final float XP_BAR_WIDTH = 182f; + public static final double XP_TEXT_SCALE = 0.59; + public static final int XP_BAR_OFFSET = 2; + public static final int XP_TEXT_OFFSET = 0; + public static final float XP_BAR_HEIGHT = 5.0f; + public static final float XP_BAR_RADIUS = 2.5f; + public static final float BAR_SOFTNESS = 1.0f; + public static final float BAR_BLUR_ALPHA = 0.72f; + public static final float BAR_GLASS_ALPHA = 0.58f; + public static final float BAR_TRACK_ALPHA = 0.54f; + public static final float BAR_FILL_ALPHA = 0.82f; + public static final float BAR_STROKE_ALPHA = 0.48f; + public static final Identifier XP_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/experience_bar_background"); + public static final Identifier XP_PROGRESS = Identifier.fromNamespaceAndPath("minecraft", "hud/experience_bar_progress"); + public static final Identifier LOCATOR_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_background"); + public static final Identifier JUMP_BACKGROUND = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_background"); + public static final Identifier JUMP_COOLDOWN = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_cooldown"); + public static final Identifier JUMP_PROGRESS = Identifier.fromNamespaceAndPath("minecraft", "hud/jump_bar_progress"); + public static final float LOCATOR_BAR_HEIGHT = XP_BAR_HEIGHT; + public static final int LOCATOR_TICK_COUNT = 6; + public static final float LOCATOR_HEAD_SIZE = 6.5f; + public static final float LOCATOR_HEAD_RADIUS = 1.6f; + public static final float LOCATOR_HEAD_GAP = 0f; + public static final float LOCATOR_HEAD_RANGE = XP_BAR_WIDTH - LOCATOR_HEAD_SIZE; + public static final float LOCATOR_ANGLE_RANGE = 60f; + public static final float LOCATOR_HEAD_SCALE_START = 24f; + public static final float LOCATOR_HEAD_SCALE_END = 96f; + public static final float LOCATOR_HEAD_MIN_SCALE = 0.7f; + public static final float LOCATOR_OUTLINE_DISTANCE = 64f; + public static final float LOCATOR_OUTLINE_MIN = 0.6f; + public static final float LOCATOR_OUTLINE_MAX = 1.6f; + public static final Identifier LOCATOR_ARROW_UP = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_arrow_up"); + public static final Identifier LOCATOR_ARROW_DOWN = Identifier.fromNamespaceAndPath("minecraft", "hud/locator_bar_arrow_down"); + public static final int LOCATOR_ARROW_W = 6; + public static final int LOCATOR_ARROW_H = 4; + public static final int LOCATOR_ARROW_GAP = 1; + public static final float LOCATOR_LABEL_SCALE = 0.285f; + public static final float LOCATOR_LABEL_PADDING_X = 3.0f; + public static final float LOCATOR_LABEL_PADDING_Y = 2.0f; + public static final float LOCATOR_LABEL_RADIUS = 4.8f; + public static final float LOCATOR_LABEL_OFFSET = 3.4f; + public static final float LOCATOR_LABEL_ANIM_SPEED = 14f; + public static final float LOCATOR_LABEL_ROW_GAP = 0.35f; + public static final float LOCATOR_LABEL_CHIP_PAD_X = 3.1f; + public static final float LOCATOR_LABEL_CHIP_PAD_Y = 1.35f; + public static final float LOCATOR_LABEL_CHIP_RADIUS = 3.6f; + public static final float LOCATOR_LABEL_ACCENT_W = 1.15f; + public static final int LOCATOR_LABEL_MAX_ROWS = 4; + private CustomBarModel() { } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java index 78b3083..41a0404 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java @@ -1,62 +1,27 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars; -import java.util.LinkedHashMap; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.util.Mth; -import net.minecraft.util.Util; +import java.util.List; + import dev.loki.lovisual.config.definition.SettingDef; import dev.loki.lovisual.config.values.mode.ModeValue; import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.math.RenderMath; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud; @HudElementRegister(order = 50) public final class CustomHealthBar extends AbstractHudElement { - private static final String LAYOUT_ID = "lovisual:api/hud/static/custom_health_bar"; private static final String STYLE_TARGET_HUD = "TargetHud"; private static final String STYLE_DYNAMIC = "Dynamic"; public static final CustomHealthBar INSTANCE = new CustomHealthBar(); private static final float HEALTH_BAR_WIDTH = 81f; private static final float HEALTH_BAR_HEIGHT = 9f; - private static final float HEALTH_BAR_RADIUS = 5.0f; - private static final float HEALTH_TEXT_SCALE = 0.24f; - private static final int HEALTH_TEXT_OFFSET_X = 0; - private static final int HEALTH_TEXT_OFFSET_Y = 0; - private static final int HEALTH_BAR_X_OFFSET = 0; - private static final int HEALTH_BAR_Y_OFFSET = 0; - private static final float HEALTH_ANIM_SPEED = 10.0f; - private static float animatedHealth = -1.0f; - private static float animatedAbsorb = -1.0f; - private static long lastHealthAnimMs = -1L; - private static int lastHealthPlayerId = Integer.MIN_VALUE; + private final NumberValue healthAlpha = new NumberValue<>("health_alpha", 255, 30, 255); private final ModeValue hotbarHealthBarStyle = @@ -67,8 +32,6 @@ public final class CustomHealthBar extends AbstractHudElement { new NumberValue<>("custom_text_scale", 0.24f, 0.12f, 0.6f); private final NumberValue customTextPadding = new NumberValue<>("custom_text_padding", 3, 0, 12); - private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); - private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); private CustomHealthBar() { super("vanilla_health", "Health", true); @@ -94,175 +57,11 @@ public final class CustomHealthBar extends AbstractHudElement { int x, int y, float maxHealth) { - if (!shouldRenderHealthBar(mc, player, maxHealth)) return null; - - CustomHealthBar bm = CustomHealthBar.get(); - if (bm == null || !bm.isHudHealthEnabled()) return null; - - float resolvedMaxHealth = Math.max(1.0f, player.getMaxHealth()); - float resolvedHealth = Mth.clamp(player.getHealth(), 0.0f, resolvedMaxHealth); - float resolvedAbsorb = Math.max(0.0f, player.getAbsorptionAmount()); - - updateHealthAnim(player, resolvedHealth, resolvedAbsorb); - - float baseRatio = Mth.clamp(animatedHealth / resolvedMaxHealth, 0.0f, 1.0f); - float absorbRatio = Mth.clamp(animatedAbsorb / resolvedMaxHealth, 0.0f, 1.0f); - - float scale = (float) mc.getWindow().getGuiScale(); - - float barX = x + HEALTH_BAR_X_OFFSET; - float barY = y + HEALTH_BAR_Y_OFFSET; - - drawBar(r2d, barX, barY, scale, baseRatio, absorbRatio, bm); - - String text = formatHealthText(resolvedHealth + resolvedAbsorb); - if (text.isEmpty()) return new HealthTextInfo("", 0, 0, 0, bm.getTextScale()); - - float framebufferScale = Math.max(1.0f, scale); - TextRenderer tr = getHealthTextRenderer(); - float textScale = bm.getTextScale() * framebufferScale; - tr.begin(textScale, false, false); - int textW = (int) Math.ceil(tr.getWidth(text, true)); - int textH = (int) Math.ceil(tr.getHeight(true)); - tr.end(); - - float barW = bm.getBarWidth() * framebufferScale; - float barH = bm.getBarHeight() * framebufferScale; - float pad = bm.getTextPadding() * framebufferScale; - int tx = Math.round(barX * framebufferScale + barW - textW - pad) - + Math.round(HEALTH_TEXT_OFFSET_X * framebufferScale); - int ty = Math.round(barY * framebufferScale + (barH - textH) * 0.5f) - + Math.round(HEALTH_TEXT_OFFSET_Y * framebufferScale); - - int color = applyHealthAlpha(bm.getTextArgb(), bm); - - return new HealthTextInfo(text, tx, ty, color, textScale); + return HealthBarPainter.renderBackground(r2d, mc, player, x, y, maxHealth); } - /** - * Renders HP digits in the currently active projection/batch. The health - * bar itself is submitted in framebuffer-space, so the text must stay in - * the same unscaled ordered batch; otherwise the text can end up in a - * different GUI stratum/projection and disappear behind later HUD work. - */ public static void renderHealthBarText(HealthTextInfo info) { - if (info == null || info.text.isEmpty()) return; - TextRenderer tr = getHealthTextRenderer(); - tr.begin(info.scale, false, false); - tr.render(info.text, info.x, info.y, new RenderColor(info.color), true); - tr.end(); - } - - public static void renderHealthBarText(GuiGraphicsExtractor ctx, HealthTextInfo info) { - if (ctx == null || info == null || info.text.isEmpty()) return; - ViewportContext.beginUnscaled(ctx); - renderHealthBarText(info); - Renderer2D.flushBatch(Renderer2D.FlushReason.EXPLICIT); - ViewportContext.end(ctx); - } - - private static TextRenderer getHealthTextRenderer() { - return Fonts.renderer("Iosevka"); - } - - private static int applyHealthAlpha(int argb, CustomHealthBar bm) { - if (bm == null) return argb; - int a = (argb >>> 24) & 0xFF; - int na = (int) (a * bm.getHudHealthAlphaFactor()); - return (argb & 0x00FFFFFF) | ((na & 0xFF) << 24); - } - - public static void renderPreview(Renderer2D r2d, float x, float y, float scale, float healthRatio, float absorbRatio) { - CustomHealthBar bm = CustomHealthBar.get(); - if (bm == null) return; - float baseRatio = Mth.clamp(healthRatio, 0.0f, 1.0f); - float absorb = Mth.clamp(absorbRatio, 0.0f, 1.0f); - drawBar(r2d, x, y, scale, baseRatio, absorb, bm); - } - - public static HealthTextInfo buildPreviewText(float barX, float barY, float scale, float value) { - CustomHealthBar bm = CustomHealthBar.get(); - if (bm == null) return new HealthTextInfo("", 0, 0, 0, HEALTH_TEXT_SCALE * scale); - String text = formatHealthText(value); - float textScale = bm.getTextScale() * scale; - if (text.isEmpty()) return new HealthTextInfo("", 0, 0, 0, textScale); - - TextRenderer tr = getHealthTextRenderer(); - tr.begin(textScale, false, false); - int textW = (int) Math.ceil(tr.getWidth(text, true)); - int textH = (int) Math.ceil(tr.getHeight(true)); - tr.end(); - - float barW = bm.getBarWidth() * scale; - float barH = bm.getBarHeight() * scale; - int pad = Math.round(bm.getTextPadding() * scale); - int tx = Math.round(barX * scale + barW - textW - pad) + Math.round(HEALTH_TEXT_OFFSET_X * scale); - int ty = Math.round(barY * scale + (barH - textH) * 0.5f) + Math.round(HEALTH_TEXT_OFFSET_Y * scale); - - int color = applyHealthAlpha(bm.getTextArgb(), bm); - return new HealthTextInfo(text, tx, ty, color, textScale); - } - - private static void drawBar(Renderer2D r2d, float barX, float barY, float scale, - float baseRatio, float absorbRatio, CustomHealthBar bm) { - if (r2d == null || bm == null) return; - - float barW = bm.getBarWidth(); - float barH = bm.getBarHeight(); - float radius = bm.getBarRadius(); - - float xFb = barX * scale; - float yFb = barY * scale; - float wFb = barW * scale; - float hFb = barH * scale; - float roundFb = radius * scale; - - float glassScale = HEALTH_BAR_RADIUS <= 0.0f ? 1.0f : roundFb / HEALTH_BAR_RADIUS; - Renderer2D.COLOR.liquidGlassRect( - xFb, - yFb, - wFb, - hFb, - roundFb, - 0xFFFFFFFF, - bm.getHudHealthAlphaFactor(), - Renderer2D.LiquidGlassPreset.HEALTH_BAR, - glassScale, - 0.0f - ); - bm.renderScriptedBar(r2d, xFb, yFb, wFb, hFb, roundFb, baseRatio, absorbRatio); - } - - private static String hex(int argb) { - return String.format("#%08X", argb); - } - - private static void updateHealthAnim(LocalPlayer player, float health, float absorb) { - int id = player.getId(); - long now = Util.getMillis(); - - if (id != lastHealthPlayerId || lastHealthAnimMs < 0L) { - animatedHealth = health; - animatedAbsorb = absorb; - lastHealthAnimMs = now; - lastHealthPlayerId = id; - return; - } - - float dt = (now - lastHealthAnimMs) / 1000.0f; - lastHealthAnimMs = now; - - animatedHealth = RenderMath.smoothLerp(animatedHealth, health, dt, HEALTH_ANIM_SPEED); - animatedAbsorb = RenderMath.smoothLerp(animatedAbsorb, absorb, dt, HEALTH_ANIM_SPEED); - } - - private static String formatHealthText(float value) { - if (value <= 0.0f) return "0"; - float rounded = Math.round(value * 10.0f) / 10.0f; - if (Math.abs(rounded - Math.round(rounded)) < 0.001f) { - return String.valueOf(Math.round(rounded)); - } - return String.valueOf(rounded); + HealthBarPainter.renderText(info); } @Override @@ -301,98 +100,32 @@ public final class CustomHealthBar extends AbstractHudElement { return customBarRadius.get(); } - private float getTextScale() { + float getTextScale() { return customTextScale.get(); } - private int getTextPadding() { + int getTextPadding() { return customTextPadding.get(); } - private int getTextArgb() { - return 0xFF000000 | (resolveTextRgb() & 0x00FFFFFF); + int getTextArgb() { + return 0xFF000000 | (theme().textPrimary() & 0x00FFFFFF); } - private void renderScriptedBar(Renderer2D r2d, - float x, - float y, - float width, - float height, - float radius, - float baseRatio, - float absorbRatio) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || width <= 0.0f || height <= 0.0f) return; - - HudScriptLayouts.pollReloadCombo(mc); - if (moduleHandle.consumeChanged()) { - scriptRuntime.reset(); - } - - UiScriptModule loaded = ensureModule(mc); - if (loaded == null) return; - - LinkedHashMap props = buildBarProps(width, height, radius, baseRatio, absorbRatio); - long signature = CachedUiScriptRuntime.signature(props); - UiRuntime runtime = scriptRuntime.bake( - moduleHandle, - loaded, - "custom_health_bar", - signature, - signature, - signature, - width, - height, - TextRenderer.get(), - x, - y, - width, - height, - () -> props, - null - ); - if (runtime == null) return; - runtime.render(new UiRenderContext(r2d, TextRenderer.get(), null, 0.0f, UiProjectionMode.CURRENT)); - } - - private LinkedHashMap buildBarProps(float width, - float height, - float radius, - float baseRatio, - float absorbRatio) { - float phase = (float) (Util.getMillis() / 1000.0); - - LinkedHashMap props = new LinkedHashMap<>(); - props.put("width", width); - props.put("height", height); - props.put("radius", radius); - props.put("healthRatio", Mth.clamp(baseRatio, 0.0f, 1.0f)); - props.put("absorbRatio", Mth.clamp(absorbRatio, 0.0f, 1.0f)); - props.put("alpha", getHudHealthAlphaFactor()); - props.put("phase", phase); - props.put("colorMode", isDynamicHealthColorMode() ? "dynamic" : "theme"); - props.put("themeAccent", hex(0xFF000000 | (theme().accent() & 0x00FFFFFF))); - props.put("themeAccentSoft", hex(0xFF000000 | (theme().accentSoft() & 0x00FFFFFF))); - props.put("themeTextPrimary", hex(0xFF000000 | (theme().textPrimary() & 0x00FFFFFF))); - return props; - } - - private UiScriptModule ensureModule(Minecraft mc) { - if (mc == null || mc.getResourceManager() == null) return null; - if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { - HudScriptLayouts.reportLoadError(moduleHandle); - return null; - } - moduleHandle.consumeChanged(); - return moduleHandle.module(); - } - - private boolean isDynamicHealthColorMode() { + boolean isDynamicHealthColorMode() { return STYLE_DYNAMIC.equals(hotbarHealthBarStyle.get()); } - private int resolveTextRgb() { - return theme().textPrimary() & 0x00FFFFFF; + int themeAccentArgb() { + return 0xFF000000 | (theme().accent() & 0x00FFFFFF); + } + + int themeAccentSoftArgb() { + return 0xFF000000 | (theme().accentSoft() & 0x00FFFFFF); + } + + int themeTextPrimaryArgb() { + return 0xFF000000 | (theme().textPrimary() & 0x00FFFFFF); } public static final class HealthTextInfo { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java index 96cee49..c21f77e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java @@ -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.gui.hud.nondraggable.impl.bars; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; @@ -19,6 +12,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import static dev.loki.lovisual.features.theme.Theme.theme; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; @@ -120,9 +114,9 @@ public final class CustomXpBarRenderer { if (bm != null && bm.isCustomMode()) { - int track = HudRenderUtil.scaleAlpha(bm.customBgColor.getArgb(), BAR_TRACK_ALPHA); - int fillStart = HudRenderUtil.scaleAlpha(bm.customFillColor.getArgb(), BAR_FILL_ALPHA); - int fillEnd = HudRenderUtil.scaleAlpha(bm.customFillGradientColor.getArgb(), BAR_FILL_ALPHA); + int track = HudRenderUtil.scaleAlpha(bm.customBgArgb(), BAR_TRACK_ALPHA); + int fillStart = HudRenderUtil.scaleAlpha(bm.customFillArgb(), BAR_FILL_ALPHA); + int fillEnd = HudRenderUtil.scaleAlpha(bm.customFillGradientArgb(), BAR_FILL_ALPHA); int stroke = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(fillEnd, track, 0.38f), BAR_STROKE_ALPHA); int textureTrack = forceOpaque(HudRenderUtil.mixColor(track, 0xFFFFFFFF, 0.18f)); int textureFill = forceOpaque(HudRenderUtil.mixColor(fillStart, fillEnd, 0.48f)); diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/BarSettings.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/BarSettings.java new file mode 100644 index 0000000..15911e1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/BarSettings.java @@ -0,0 +1,189 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.collection.SetValue; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.primitive.StringValue; +import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier; +import dev.loki.lovisual.util.input.KeyManager; + +import java.util.List; + +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.COLOR_CUSTOM; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.COLOR_THEME; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.XP_TEXT_SCALE; + +/** + * Settings schema and live value holders for {@link CustomBar}. + * Declared in the {@code bars} package (impl subfolder) so the facade can read + * these package-private fields directly from its getters. + */ +final class BarSettings { + + final NumberValue overlayAlpha = + new NumberValue<>("overlay_alpha", 255, 30, 255); + final BooleanValue xpBar = + new BooleanValue("xp_bar", true); + final BooleanValue xpBarShowLevel = + new BooleanValue("xp_bar_show_level", true); + final EnumValue barMode = + new EnumValue<>("bar_mode", CustomBarModel.BarMode.LEVEL, CustomBarModel.BarMode.values()); + final ModeValue colorMode = + new ModeValue("bar_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); + final RGBColorValue xpBarBgColor = + new RGBColorValue("xp_bar_bg_color", "#151515"); + final RGBColorValue xpBarFillStart = + new RGBColorValue("xp_bar_fill_start", "#4CB2FF"); + final RGBColorValue xpBarFillEnd = + new RGBColorValue("xp_bar_fill_end", "#76F0FF"); + final RGBColorValue xpBarTextColor = + new RGBColorValue("xp_bar_text_color", "#A0E5FF"); + final EnumValue xpBarStyle = + new EnumValue<>("xp_bar_style", CustomBarModel.XpBarStyle.GRADIENT, CustomBarModel.XpBarStyle.values()); + final RGBAColorValue customBgColor = + new RGBAColorValue("custom_bg_color", "#88151515"); + final RGBAColorValue customFillColor = + new RGBAColorValue("custom_fill_color", "#CC4CB2FF"); + final BooleanValue customFillGradient = + new BooleanValue("custom_fill_gradient", true); + final RGBAColorValue customFillGradientColor = + new RGBAColorValue("custom_fill_gradient_color", "#CC76F0FF"); + final NumberValue customFillGradientAngle = + new NumberValue<>("custom_fill_gradient_angle", 0.0f, 0.0f, 360.0f); + final RGBAColorValue customTextColor = + new RGBAColorValue("custom_text_color", "#FFA0E5FF"); + final NumberValue customTextScale = + new NumberValue<>("custom_text_scale", (float) XP_TEXT_SCALE, 0.3f, 1.2f); + final BooleanValue locatorLabels = + new BooleanValue("locator_labels", true); + final EnumValue locatorLabelMode = + new EnumValue<>("locator_label_mode", CustomBarModel.LocatorLabelMode.CENTER, CustomBarModel.LocatorLabelMode.values()); + final NumberValue locatorLabelAngle = + new NumberValue<>("locator_label_angle", 6.0f, 4.0f, 45.0f); + final BooleanValue locatorLabelDistance = + new BooleanValue("locator_label_distance", true); + final BooleanValue locatorSearchEnabled = + new BooleanValue("locator_search_enabled", true); + final SetValue locatorSearchPlayers = + new SetValue("locator_search_list", new java.util.LinkedHashSet<>(java.util.List.of("imhorny"))); + final RGBColorValue locatorSearchColor = + new RGBColorValue("locator_search_color", "#FFAA00"); + final RGBColorValue locatorDimmedColor = + new RGBColorValue("locator_dimmed_color", "#777777"); + final BooleanValue locatorDimmedScaleOnly = + new BooleanValue("locator_dimmed_scale_only", true); + final BooleanValue locatorMapFilterEnabled = + new BooleanValue("locator_map_filter_enabled", false); + final StringValue locatorMapServer = + new StringValue("locator_map_server", ""); + final StringValue locatorMapUrl = + new StringValue("locator_map_url", "example"); + final KeyBindValue locatorTargetToggle = + new KeyBindValue("locator_target_toggle", "NONE"); + final KeyBindValue locatorBarToggle = + new KeyBindValue("locator_bar_toggle", "U"); + + BarSettings() { + } + + void defineSettings(List defs) { + defs.add(SettingDef.bool(xpBar)); + defs.add(SettingDef.bool(xpBarShowLevel).visibleWhen(xpBar::get)); + defs.add(SettingDef.mode(barMode).visibleWhen(xpBar::get)); + defs.add(SettingDef.mode(colorMode).visibleWhen(xpBar::get)); + defs.add(SettingDef.color(customBgColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(customFillColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(customFillGradientColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(customTextColor).visibleWhen(this::isCustomMode)); + + defs.add(SettingDef.bool(locatorLabels).visibleWhen(this::locatorModeEnabled)); + defs.add(SettingDef.mode(locatorLabelMode) + .visibleWhen(() -> locatorModeEnabled() && locatorLabels.get())); + defs.add(SettingDef.number(locatorLabelAngle) + .visibleWhen(() -> locatorModeEnabled() + && locatorLabels.get() + && locatorLabelModeUsesCenter())); + defs.add(SettingDef.bool(locatorLabelDistance) + .visibleWhen(() -> locatorModeEnabled() && locatorLabels.get())); + + defs.add(SettingDef.bool(locatorSearchEnabled).visibleWhen(this::locatorModeEnabled)); + defs.add(SettingDef.textList(locatorSearchPlayers) + .visibleWhen(() -> locatorModeEnabled() && locatorSearchEnabled.get())); + defs.add(SettingDef.colorNoAlpha(locatorDimmedColor) + .visibleWhen(() -> locatorModeEnabled() + && locatorSearchEnabled.get() + && !locatorSearchPlayers.get().isEmpty())); + defs.add(SettingDef.colorNoAlpha(locatorSearchColor) + .visibleWhen(() -> locatorModeEnabled() + && locatorSearchEnabled.get() + && !locatorSearchPlayers.get().isEmpty())); + defs.add(SettingDef.bool(locatorDimmedScaleOnly).visibleWhen(this::locatorModeEnabled)); + defs.add(SettingDef.bool(locatorMapFilterEnabled).visibleWhen(this::locatorModeEnabled)); + defs.add(SettingDef.text(locatorMapServer) + .visibleWhen(() -> locatorModeEnabled() && locatorMapFilterEnabled.get())); + defs.add(SettingDef.text(locatorMapUrl) + .visibleWhen(() -> locatorModeEnabled() && locatorMapFilterEnabled.get())); + + defs.add(SettingDef.bind(locatorTargetToggle, BindMode.PRESS).visibleWhen(this::locatorModeEnabled)); + defs.add(SettingDef.bind(locatorBarToggle, BindMode.PRESS).visibleWhen(this::locatorModeEnabled)); + } + + void registerBinds(CustomBar bar) { + registerBind(bar, locatorTargetToggle); + registerBind(bar, locatorBarToggle); + } + + void handleToggles(CustomBar bar) { + if (KeyManager.wasPressed(bindingName(bar, locatorTargetToggle))) { + boolean next = !locatorSearchEnabled.get(); + locatorSearchEnabled.set(next); + HudNotifier.pushState("Locator target", next); + bar.saveConfig(); + } + + if (KeyManager.wasPressed(bindingName(bar, locatorBarToggle))) { + CustomBarModel.BarMode mode = barMode.get(); + if (mode != CustomBarModel.BarMode.AUTO) { + barMode.set(mode == CustomBarModel.BarMode.LOCATOR ? CustomBarModel.BarMode.LEVEL : CustomBarModel.BarMode.LOCATOR); + bar.saveConfig(); + } + } + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean locatorModeEnabled() { + if (!xpBar.get()) return false; + return barMode.get() != CustomBarModel.BarMode.LEVEL; + } + + boolean isCustomXpStyleEnabled() { + return xpBar.get() && isCustomMode(); + } + + private boolean locatorLabelModeUsesCenter() { + CustomBarModel.LocatorLabelMode mode = locatorLabelMode.get(); + return mode == CustomBarModel.LocatorLabelMode.CENTER || mode == CustomBarModel.LocatorLabelMode.CENTER_TARGETS; + } + + private String bindingName(CustomBar bar, KeyBindValue value) { + return bar.name() + ":" + value.getName(); + } + + private void registerBind(CustomBar bar, KeyBindValue value) { + String name = bindingName(bar, value); + KeyManager.unregisterAll(name); + if (!value.isNone()) { + KeyManager.registerCombo(name, value.get()); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthBarPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthBarPainter.java new file mode 100644 index 0000000..325602c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthBarPainter.java @@ -0,0 +1,169 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.RenderMath; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +/** + * Draw pipeline of the custom health bar: glass background, scripted fill, + * smoothed health/absorption animation and HP text layout. Package-private + * helper of {@link CustomHealthBar} (impl subfolder, {@code bars} package). + */ +final class HealthBarPainter { + + private static final float HEALTH_BAR_RADIUS = 5.0f; + private static final int HEALTH_TEXT_OFFSET_X = 0; + private static final int HEALTH_TEXT_OFFSET_Y = 0; + private static final int HEALTH_BAR_X_OFFSET = 0; + private static final int HEALTH_BAR_Y_OFFSET = 0; + private static final float HEALTH_ANIM_SPEED = 10.0f; + + private static final HealthScriptRenderer SCRIPT = new HealthScriptRenderer(); + + private static float animatedHealth = -1.0f; + private static float animatedAbsorb = -1.0f; + private static long lastHealthAnimMs = -1L; + private static int lastHealthPlayerId = Integer.MIN_VALUE; + + private HealthBarPainter() { + } + + static CustomHealthBar.HealthTextInfo renderBackground(Renderer2D r2d, Minecraft mc, LocalPlayer player, + int x, int y, float maxHealth) { + if (!CustomHealthBar.shouldRenderHealthBar(mc, player, maxHealth)) return null; + + CustomHealthBar bm = CustomHealthBar.get(); + if (bm == null || !bm.isHudHealthEnabled()) return null; + + float resolvedMaxHealth = Math.max(1.0f, player.getMaxHealth()); + float resolvedHealth = Mth.clamp(player.getHealth(), 0.0f, resolvedMaxHealth); + float resolvedAbsorb = Math.max(0.0f, player.getAbsorptionAmount()); + + updateHealthAnim(player, resolvedHealth, resolvedAbsorb); + + float baseRatio = Mth.clamp(animatedHealth / resolvedMaxHealth, 0.0f, 1.0f); + float absorbRatio = Mth.clamp(animatedAbsorb / resolvedMaxHealth, 0.0f, 1.0f); + + float scale = (float) mc.getWindow().getGuiScale(); + + float barX = x + HEALTH_BAR_X_OFFSET; + float barY = y + HEALTH_BAR_Y_OFFSET; + + drawBar(r2d, barX, barY, scale, baseRatio, absorbRatio, bm); + + String text = formatHealthText(resolvedHealth + resolvedAbsorb); + if (text.isEmpty()) return new CustomHealthBar.HealthTextInfo("", 0, 0, 0, bm.getTextScale()); + + return layoutText(bm, text, barX, barY, Math.max(1.0f, scale)); + } + + /** + * Lays out the HP digits in the currently active projection/batch. The bar + * itself is submitted in framebuffer-space, so the text must stay in the + * same unscaled ordered batch; otherwise the text can end up in a different + * GUI stratum/projection and disappear behind later HUD work. + */ + static void renderText(CustomHealthBar.HealthTextInfo info) { + if (info == null || info.text.isEmpty()) return; + TextRenderer tr = getHealthTextRenderer(); + tr.begin(info.scale, false, false); + tr.render(info.text, info.x, info.y, new RenderColor(info.color), true); + tr.end(); + } + + private static CustomHealthBar.HealthTextInfo layoutText(CustomHealthBar bm, String text, + float barX, float barY, float framebufferScale) { + float textScale = bm.getTextScale() * framebufferScale; + TextRenderer tr = getHealthTextRenderer(); + tr.begin(textScale, false, false); + int textW = (int) Math.ceil(tr.getWidth(text, true)); + int textH = (int) Math.ceil(tr.getHeight(true)); + tr.end(); + + float barW = bm.getBarWidth() * framebufferScale; + float barH = bm.getBarHeight() * framebufferScale; + float pad = bm.getTextPadding() * framebufferScale; + int tx = Math.round(barX * framebufferScale + barW - textW - pad) + + Math.round(HEALTH_TEXT_OFFSET_X * framebufferScale); + int ty = Math.round(barY * framebufferScale + (barH - textH) * 0.5f) + + Math.round(HEALTH_TEXT_OFFSET_Y * framebufferScale); + + int color = applyHealthAlpha(bm.getTextArgb(), bm); + return new CustomHealthBar.HealthTextInfo(text, tx, ty, color, textScale); + } + + private static void drawBar(Renderer2D r2d, float barX, float barY, float scale, + float baseRatio, float absorbRatio, CustomHealthBar bm) { + if (r2d == null || bm == null) return; + + float barW = bm.getBarWidth(); + float barH = bm.getBarHeight(); + float radius = bm.getBarRadius(); + + float xFb = barX * scale; + float yFb = barY * scale; + float wFb = barW * scale; + float hFb = barH * scale; + float roundFb = radius * scale; + + float glassScale = HEALTH_BAR_RADIUS <= 0.0f ? 1.0f : roundFb / HEALTH_BAR_RADIUS; + Renderer2D.COLOR.liquidGlassRect( + xFb, + yFb, + wFb, + hFb, + roundFb, + 0xFFFFFFFF, + bm.getHudHealthAlphaFactor(), + Renderer2D.LiquidGlassPreset.HEALTH_BAR, + glassScale, + 0.0f + ); + SCRIPT.render(bm, r2d, xFb, yFb, wFb, hFb, roundFb, baseRatio, absorbRatio); + } + + private static void updateHealthAnim(LocalPlayer player, float health, float absorb) { + int id = player.getId(); + long now = Util.getMillis(); + + if (id != lastHealthPlayerId || lastHealthAnimMs < 0L) { + animatedHealth = health; + animatedAbsorb = absorb; + lastHealthAnimMs = now; + lastHealthPlayerId = id; + return; + } + + float dt = (now - lastHealthAnimMs) / 1000.0f; + lastHealthAnimMs = now; + + animatedHealth = RenderMath.smoothLerp(animatedHealth, health, dt, HEALTH_ANIM_SPEED); + animatedAbsorb = RenderMath.smoothLerp(animatedAbsorb, absorb, dt, HEALTH_ANIM_SPEED); + } + + static String formatHealthText(float value) { + if (value <= 0.0f) return "0"; + float rounded = Math.round(value * 10.0f) / 10.0f; + if (Math.abs(rounded - Math.round(rounded)) < 0.001f) { + return String.valueOf(Math.round(rounded)); + } + return String.valueOf(rounded); + } + + private static TextRenderer getHealthTextRenderer() { + return Fonts.renderer("Iosevka"); + } + + private static int applyHealthAlpha(int argb, CustomHealthBar bm) { + if (bm == null) return argb; + int a = (argb >>> 24) & 0xFF; + int na = (int) (a * bm.getHudHealthAlphaFactor()); + return (argb & 0x00FFFFFF) | ((na & 0xFF) << 24); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthScriptRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthScriptRenderer.java new file mode 100644 index 0000000..703eae9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/impl/HealthScriptRenderer.java @@ -0,0 +1,101 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars; + +import java.util.LinkedHashMap; + +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +/** + * Scripted fill layer of the custom health bar: bakes the HUD script layout + * {@code custom_health_bar} with live props and renders it above the glass + * background. Owns the layout handle + cached runtime that the element used to + * carry as instance fields. + */ +final class HealthScriptRenderer { + + private static final String LAYOUT_ID = "lovisual:api/hud/static/custom_health_bar"; + + private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); + private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); + + void render(CustomHealthBar bm, Renderer2D r2d, + float x, float y, float width, float height, float radius, + float baseRatio, float absorbRatio) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || width <= 0.0f || height <= 0.0f) return; + + HudScriptLayouts.pollReloadCombo(mc); + if (moduleHandle.consumeChanged()) { + scriptRuntime.reset(); + } + + UiScriptModule loaded = ensureModule(mc); + if (loaded == null) return; + + LinkedHashMap props = buildBarProps(bm, width, height, radius, baseRatio, absorbRatio); + long signature = CachedUiScriptRuntime.signature(props); + UiRuntime runtime = scriptRuntime.bake( + moduleHandle, + loaded, + "custom_health_bar", + signature, + signature, + signature, + width, + height, + TextRenderer.get(), + x, + y, + width, + height, + () -> props, + null + ); + if (runtime == null) return; + runtime.render(new UiRenderContext(r2d, TextRenderer.get(), null, 0.0f, UiProjectionMode.CURRENT)); + } + + private LinkedHashMap buildBarProps(CustomHealthBar bm, + float width, float height, float radius, + float baseRatio, float absorbRatio) { + float phase = (float) (Util.getMillis() / 1000.0); + + LinkedHashMap props = new LinkedHashMap<>(); + props.put("width", width); + props.put("height", height); + props.put("radius", radius); + props.put("healthRatio", Mth.clamp(baseRatio, 0.0f, 1.0f)); + props.put("absorbRatio", Mth.clamp(absorbRatio, 0.0f, 1.0f)); + props.put("alpha", bm.getHudHealthAlphaFactor()); + props.put("phase", phase); + props.put("colorMode", bm.isDynamicHealthColorMode() ? "dynamic" : "theme"); + props.put("themeAccent", hex(bm.themeAccentArgb())); + props.put("themeAccentSoft", hex(bm.themeAccentSoftArgb())); + props.put("themeTextPrimary", hex(bm.themeTextPrimaryArgb())); + return props; + } + + private UiScriptModule ensureModule(Minecraft mc) { + if (mc == null || mc.getResourceManager() == null) return null; + if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { + HudScriptLayouts.reportLoadError(moduleHandle); + return null; + } + moduleHandle.consumeChanged(); + return moduleHandle.module(); + } + + private static String hex(int argb) { + return String.format("#%08X", argb); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java index 034f26c..6c1eab8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java @@ -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.gui.hud.nondraggable.impl.bars.jump; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.BarColors; @@ -15,6 +8,7 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomXpBarRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java index d2bd72d..cbc385e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java @@ -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.gui.hud.nondraggable.impl.buttons; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java index 9695ca8..850c398 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java @@ -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.gui.hud.nondraggable.impl.buttons; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java index 54980c1..164b656 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java @@ -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.gui.hud.nondraggable.impl.buttons.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java index 79956b2..f07d8c7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java @@ -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.gui.hud.nondraggable.impl.buttons.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java index a54830d..e86ccf7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java index 67b2471..6d0fdc1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java index bd2f128..8846da6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java @@ -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.gui.hud.nondraggable.impl.buttons.state; import net.minecraft.client.gui.components.AbstractWidget; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java index 3ae44a6..fe31ddd 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java @@ -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.gui.hud.nondraggable.impl.buttons.state; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java index 3952a6a..cf3c36d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java @@ -1,9 +1,4 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ + package dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.state; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips.BetterTooltips; import dev.loki.lovisual.mixins.accessors.widget.ClickableWidgetAccessor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java index 70bfbec..4b1092e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java @@ -1,105 +1,50 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiScreen; +import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace; +import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandUiUtil; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +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.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.media.session.MediaSessionService; import dev.loki.lovisual.util.screen.ClientScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; import org.lwjgl.glfw.GLFW; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiScreen; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandBoundsPatches; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.IslandRuntimePatches; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandUiUtil; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; -import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -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.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptProps; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.util.media.session.MediaSessionService; -import dev.loki.lovisual.util.pvp.state.PvpState; - -import java.time.LocalTime; import java.util.List; -import static dev.loki.lovisual.features.theme.Theme.theme; - /** - * Dynamic Island: оркестрация режимов (time/pvp/music/clickgui) и анимаций. - * Патчинг UI-скрипта, bounds-патчи, утилиты и геометрия медиа-контролов - * делегированы в {@code impl/dynamicisland/}. + * Dynamic Island: фасад оркестрации острова. + * Логика вынесена в {@code island/}: State, Animation, Layout, Renderer. */ @HudElementRegister(order = 60) public final class DynamicIsland extends AbstractHudElement { private static final String LAYOUT_ID = "lovisual:api/hud/static/dynamic_island"; public static final DynamicIsland INSTANCE = new DynamicIsland(); - - private static final float TOP_Y = 18f; - private static final float CLOSED_HEIGHT = 35f; - private static final float EXPANDED_HEIGHT = 98f; - private static final float CLOSED_RADIUS = 17f; - private static final float EXPANDED_RADIUS = 20f; - - private static final float TIME_WIDTH = 74f; - private static final float PVP_MIN_WIDTH = 68f; - private static final float PVP_MAX_WIDTH = 96f; - private static final float MUSIC_EXPANDED_WIDTH = 338f; - - private static final float TIME_CONTEXT_WIDTH = 57f; - - private static final float COMPACT_PAD_X = 11f; - private static final float COMPACT_GAP = 9f; - private static final float COMPACT_ART = 22f; - private static final float COMPACT_WAVE_W = 15f; - private static final float COMPACT_FADE = 15f; - private static final float EXPANDED_PAD_Y = 11f; - private static final float PROGRESS_H = 5f; - private static final float CONTROL_TIME_GAP = 10f; - - private static final int ICON_PAUSE = 0xEA02; - private static final int ICON_PLAY = 0xEA03; - private static final int ICON_PREV = 0xEA04; - private static final int ICON_NEXT = 0xEA05; - private static final int ICON_SHUFFLE = 0xEA06; - - private static final int STATUS_PVP = 0xFFFF6368; - private static final int PROGRESS_BG = 0x2AFFFFFF; - private final MediaSessionService mediaService = MediaSessionService.get(); private final BooleanValue syncTheme = new BooleanValue("dynamic_island_sync_theme", true); private final NumberValue themeAlpha = new NumberValue<>("dynamic_island_theme_alpha", 200, 0, 255); @@ -109,553 +54,105 @@ public final class DynamicIsland extends AbstractHudElement { private final RGBColorValue accentColor = new RGBColorValue("dynamic_island_accent", "#6E8DFF"); private final RGBColorValue textColor = new RGBColorValue("dynamic_island_text", "#F5F7FA"); private final RGBColorValue mutedColor = new RGBColorValue("dynamic_island_muted", "#A9B1BC"); - private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); private final IslandControls controls = new IslandControls(); + private final IslandState state = new IslandState(); + private final IslandAnimation anim = new IslandAnimation(); + private final IslandLayout layout = new IslandLayout(); + private final IslandRenderer rendererHelper = new IslandRenderer(); - private float visibilityAnim; - private float expandAnim; - private float widthAnim = -1f; - private float mainWidthAnim = -1f; - private float leftContextWidthAnim; - private float rightContextWidthAnim; - private float heightAnim = -1f; - private boolean expanded; - private boolean clickGuiShellVisible; - private String titleScrollKey = ""; - private long titleScrollStartMs; + private DynamicIsland() { super("dynamic_island", "Dynamic Island", true); } - private IslandMode currentMode = IslandMode.TIME; - private MediaSessionService.Snapshot currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); + public static boolean shouldRenderInScreenOverlay(Screen screen) { return INSTANCE != null && INSTANCE.isScreenOverlayAllowed(screen); } + public static void renderScreenOverlay(GuiGraphicsExtractor ctx, float tickDelta) { INSTANCE.renderScreenOverlayInternal(ctx, tickDelta); } + public static boolean shouldOwnClickGuiTabShell() { return INSTANCE != null && INSTANCE.isEnabled() && INSTANCE.state.clickGuiShellVisible; } - private int uiFillTop; - private int uiFillBottom; - private int uiTextPrimary; - private int uiTextSecondary; - private int uiTextMuted; - private int uiAccent; - private int uiAccentSoft; - - private DynamicIsland() { - super("dynamic_island", "Dynamic Island", true); - } - - public static boolean shouldRenderInScreenOverlay(Screen screen) { - return INSTANCE != null && INSTANCE.isScreenOverlayAllowed(screen); - } - - public static void renderScreenOverlay(GuiGraphicsExtractor ctx, float tickDelta) { - INSTANCE.renderScreenOverlayInternal(ctx, tickDelta); - } - - public static boolean shouldOwnClickGuiTabShell() { - if (INSTANCE == null || !INSTANCE.isEnabled()) return false; - return INSTANCE.clickGuiShellVisible; - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.bool(syncTheme)); - defs.add(SettingDef.number(themeAlpha).visibleWhen(syncTheme::get)); - defs.add(SettingDef.bool(blur)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(blur::get)); + @Override protected void defineSettings(List defs) { + defs.add(SettingDef.bool(syncTheme)); defs.add(SettingDef.number(themeAlpha).visibleWhen(syncTheme::get)); + defs.add(SettingDef.bool(blur)); defs.add(SettingDef.number(blurAlpha).visibleWhen(blur::get)); defs.add(SettingDef.color(bgColor).visibleWhen(() -> !syncTheme.get())); defs.add(SettingDef.colorNoAlpha(accentColor).visibleWhen(() -> !syncTheme.get())); defs.add(SettingDef.colorNoAlpha(textColor).visibleWhen(() -> !syncTheme.get())); defs.add(SettingDef.colorNoAlpha(mutedColor).visibleWhen(() -> !syncTheme.get())); } + @Override public boolean usesEngineRenderer() { return true; } + @Override public HudRenderSpace getRenderSpace() { return HudRenderSpace.UNSCALED_LOGICAL; } + @Override public HudPhase getHudPhase() { return HudPhase.AFTER_SUBTITLES; } + @Override public int getRenderOrder() { return 40; } + @Override public boolean isMouseOverInteractive(float mx, float my) { Minecraft mc = Minecraft.getInstance(); return mc != null && isInteractiveScreen(ClientScreen.current()) && contains(mx, my); } - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public HudRenderSpace getRenderSpace() { - return HudRenderSpace.UNSCALED_LOGICAL; - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.AFTER_SUBTITLES; - } - - @Override - public int getRenderOrder() { - return 40; - } - - @Override - public boolean isMouseOverInteractive(float mx, float my) { + @Override public boolean onMouseClicked(float mx, float my, int button) { Minecraft mc = Minecraft.getInstance(); - return mc != null && isInteractiveScreen(ClientScreen.current()) && contains(mx, my); - } - - @Override - public boolean onMouseClicked(float mx, float my, int button) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || !isInteractiveScreen(ClientScreen.current())) return false; - if (!contains(mx, my)) return false; - - if (currentMode == IslandMode.MUSIC && expandAnim > 0.01f && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { - if (currentSnapshot != null && currentSnapshot.supportsShuffle() - && IslandUiUtil.hit(mx, my, controls.shuffleX, controls.shuffleY, controls.controlSize, controls.controlSize)) { - mediaService.toggleShuffle(); - return true; - } - if (IslandUiUtil.hit(mx, my, controls.prevX, controls.prevY, controls.controlSize, controls.controlSize)) { - mediaService.previous(); - return true; - } - if (IslandUiUtil.hit(mx, my, controls.playX, controls.playY, controls.controlSize, controls.controlSize)) { - mediaService.playPause(); - return true; - } - if (IslandUiUtil.hit(mx, my, controls.nextX, controls.nextY, controls.controlSize, controls.controlSize)) { - mediaService.next(); - return true; - } - if (currentSnapshot != null && currentSnapshot.supportsRepeat() - && IslandUiUtil.hit(mx, my, controls.repeatX, controls.repeatY, controls.controlSize, controls.controlSize)) { - mediaService.cycleRepeatMode(); - return true; - } - } - - if (currentMode == IslandMode.MUSIC - && (button == GLFW.GLFW_MOUSE_BUTTON_LEFT || button == GLFW.GLFW_MOUSE_BUTTON_RIGHT)) { - expanded = !expanded; - return true; + if (mc == null || !isInteractiveScreen(ClientScreen.current()) || !contains(mx, my)) return false; + if (state.currentMode == IslandState.IslandMode.MUSIC && anim.isExpanded(0.01f) && button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { + if (state.currentSnapshot != null && state.currentSnapshot.supportsShuffle() && IslandUiUtil.hit(mx, my, controls.shuffleX, controls.shuffleY, controls.controlSize, controls.controlSize)) { mediaService.toggleShuffle(); return true; } + if (IslandUiUtil.hit(mx, my, controls.prevX, controls.prevY, controls.controlSize, controls.controlSize)) { mediaService.previous(); return true; } + if (IslandUiUtil.hit(mx, my, controls.playX, controls.playY, controls.controlSize, controls.controlSize)) { mediaService.playPause(); return true; } + if (IslandUiUtil.hit(mx, my, controls.nextX, controls.nextY, controls.controlSize, controls.controlSize)) { mediaService.next(); return true; } + if (state.currentSnapshot != null && state.currentSnapshot.supportsRepeat() && IslandUiUtil.hit(mx, my, controls.repeatX, controls.repeatY, controls.controlSize, controls.controlSize)) { mediaService.cycleRepeatMode(); return true; } } + if (state.currentMode == IslandState.IslandMode.MUSIC && (button == GLFW.GLFW_MOUSE_BUTTON_LEFT || button == GLFW.GLFW_MOUSE_BUTTON_RIGHT)) { state.expanded = !state.expanded; return true; } return false; } - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - renderEngineInternal(renderer, textRenderer, ctx, tickDelta, screenW, screenH, false); - } + @Override public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { renderEngineInternal(renderer, textRenderer, ctx, tickDelta, screenW, screenH, false); } private void renderScreenOverlayInternal(GuiGraphicsExtractor ctx, float tickDelta) { Minecraft mc = Minecraft.getInstance(); - if (!Lifecycle.canRunHud()) return; - if (mc == null || mc.getWindow() == null || ctx == null) return; - Screen currentScreen = ClientScreen.current(); - boolean clickGuiOverlay = isEnabled() && IslandUiUtil.clickGuiEnabled(); - if (!clickGuiOverlay && !isScreenOverlayAllowed(currentScreen)) return; - + if (!Lifecycle.canRunHud() || mc == null || mc.getWindow() == null || ctx == null) return; + Screen cur = ClientScreen.current(); boolean clickGuiOverlay = isEnabled() && IslandUiUtil.clickGuiEnabled(); + if (!clickGuiOverlay && !isScreenOverlayAllowed(cur)) return; FullScreenRenderer.ensureInit(); - int screenW = Math.max(1, Math.round(HudScale.virtualWidth( - mc.getWindow().getWidth(), - mc.getWindow().getHeight() - ))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight( - mc.getWindow().getWidth(), - mc.getWindow().getHeight() - ))); - + int screenW = Math.max(1, Math.round(HudScale.virtualWidth(mc.getWindow().getWidth(), mc.getWindow().getHeight()))); + int screenH = Math.max(1, Math.round(HudScale.virtualHeight(mc.getWindow().getWidth(), mc.getWindow().getHeight()))); LoVisualRenderSystem.updateFrameTiming(tickDelta, tickDelta, tickDelta); ViewportContext.beginUnscaledLogical(ctx); try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.SCREEN_TOP, "2d:screen:dynamic_island")) { - Renderer2D.COLOR.begin(); - renderEngineInternal(Renderer2D.COLOR, TextRenderer.get(), ctx, tickDelta, screenW, screenH, true); - Renderer2D.COLOR.render(); - } finally { - ViewportContext.end(ctx); - } + Renderer2D.COLOR.begin(); renderEngineInternal(Renderer2D.COLOR, TextRenderer.get(), ctx, tickDelta, screenW, screenH, true); Renderer2D.COLOR.render(); + } finally { ViewportContext.end(ctx); } } - private void renderEngineInternal(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH, - boolean screenOverlayPass) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return; - HudScriptLayouts.pollReloadCombo(mc); - if (moduleHandle.consumeChanged()) { - resetScriptRuntime(); - } - - Screen currentScreen = ClientScreen.current(); - boolean chatOpen = currentScreen instanceof ChatScreen; - boolean clickGuiVisible = IslandUiUtil.clickGuiEnabled(); - boolean clickGuiTabShellScreen = currentScreen instanceof ClickGuiScreen; + private void renderEngineInternal(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH, boolean screenOverlayPass) { + Minecraft mc = Minecraft.getInstance(); if (mc == null) return; + HudScriptLayouts.pollReloadCombo(mc); if (moduleHandle.consumeChanged()) scriptRuntime.reset(); + Screen currentScreen = ClientScreen.current(); boolean chatOpen = currentScreen instanceof ChatScreen; + boolean clickGuiVisible = IslandUiUtil.clickGuiEnabled(); boolean clickGuiTabShellScreen = currentScreen instanceof ClickGuiScreen; ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState(); - boolean clickGuiBridge = isEnabled() - && screenOverlayPass - && clickGuiVisible - && clickGuiTabShellScreen - && clickGuiState.lifecycle() > 0.001f - && clickGuiState.tabBarW() > 1.0f; - if (clickGuiVisible && clickGuiTabShellScreen && !clickGuiBridge) { - clickGuiShellVisible = false; - setBounds(0f, 0f, 0f, 0f); - controls.reset(); - return; - } - boolean shouldShow = clickGuiBridge - || (isEnabled() && (screenOverlayPass ? isScreenOverlayAllowed(currentScreen) : currentScreen == null || chatOpen)); - currentSnapshot = mediaService.snapshot(); - currentMode = clickGuiBridge ? IslandMode.CLICKGUI : resolveMode(currentSnapshot); - - if (!isInteractiveScreen(currentScreen) || currentMode != IslandMode.MUSIC) { - expanded = false; - } - - float dt = AnimationUtility.deltaTime(); - visibilityAnim = AnimationUtility.approach(visibilityAnim, shouldShow ? 1f : 0f, dt, 10f); - expandAnim = AnimationUtility.approach(expandAnim, expanded ? 1f : 0f, dt, 10f); - visibilityAnim = AnimationUtility.snap(visibilityAnim, shouldShow ? 1f : 0f, 0.002f); - expandAnim = AnimationUtility.snap(expandAnim, expanded ? 1f : 0f, 0.002f); - clickGuiShellVisible = clickGuiBridge && visibilityAnim > 0.01f; - - if (!shouldShow && visibilityAnim <= 0.01f) { - clickGuiShellVisible = false; - setBounds(0f, 0f, 0f, 0f); - controls.reset(); - return; - } - - updatePalette(); + boolean clickGuiBridge = isEnabled() && screenOverlayPass && clickGuiVisible && clickGuiTabShellScreen && clickGuiState.lifecycle() > 0.001f && clickGuiState.tabBarW() > 1.0f; + if (clickGuiVisible && clickGuiTabShellScreen && !clickGuiBridge) { state.clickGuiShellVisible = false; setBounds(0f, 0f, 0f, 0f); controls.reset(); return; } + boolean shouldShow = clickGuiBridge || (isEnabled() && (screenOverlayPass ? isScreenOverlayAllowed(currentScreen) : currentScreen == null || chatOpen)); + state.currentSnapshot = mediaService.snapshot(); state.currentMode = clickGuiBridge ? IslandState.IslandMode.CLICKGUI : state.resolveMode(state.currentSnapshot); + if (!isInteractiveScreen(currentScreen) || state.currentMode != IslandState.IslandMode.MUSIC) state.expanded = false; + float dt = AnimationUtility.deltaTime(); anim.updateVisibility(dt, shouldShow); anim.updateExpand(dt, state.expanded); + state.clickGuiShellVisible = clickGuiBridge && anim.isVisible(0.01f); + if (!shouldShow && !anim.isVisible(0.01f)) { state.clickGuiShellVisible = false; setBounds(0f, 0f, 0f, 0f); controls.reset(); return; } + rendererHelper.updatePalette(syncTheme, themeAlpha, bgColor, accentColor, textColor, mutedColor); TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); TextRenderer titleRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, fallback); TextRenderer valueRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, fallback); TextRenderer metaRenderer = Fonts.renderer("Inter", FontInfo.Type.Regular, fallback); - - IslandMetrics metrics = measureMetrics(titleRenderer, valueRenderer, metaRenderer); - float targetMainWidth = switch (currentMode) { - case TIME -> TIME_WIDTH; - case PVP -> IslandUiUtil.clamp(metrics.pvpWidth, PVP_MIN_WIDTH, PVP_MAX_WIDTH); - case MUSIC -> AnimationUtility.lerp(metrics.musicCompactWidth, MUSIC_EXPANDED_WIDTH, expandAnim); - case CLICKGUI -> clickGuiState.tabBarW(); - }; - float targetHeight = currentMode == IslandMode.CLICKGUI - ? clickGuiState.tabBarH() - : AnimationUtility.lerp(CLOSED_HEIGHT, currentMode == IslandMode.MUSIC ? EXPANDED_HEIGHT : CLOSED_HEIGHT, expandAnim); - - boolean musicContext = currentMode == IslandMode.MUSIC; - boolean clickGuiContext = currentMode == IslandMode.CLICKGUI; - float contextPresence = musicContext ? 1.0f - AnimationUtility.easeOutCubic(expandAnim) : (clickGuiContext ? 1.0f : 0.0f); - float targetLeftContextWidth = TIME_CONTEXT_WIDTH * contextPresence; - float targetRightContextWidth = PvpState.isActive() ? IslandBoundsPatches.PVP_CONTEXT_WIDTH * contextPresence : 0.0f; - - mainWidthAnim = mainWidthAnim < 0f ? targetMainWidth : AnimationUtility.approach(mainWidthAnim, targetMainWidth, dt, 12f); - heightAnim = heightAnim < 0f ? targetHeight : AnimationUtility.approach(heightAnim, targetHeight, dt, 12f); - leftContextWidthAnim = AnimationUtility.approach(leftContextWidthAnim, targetLeftContextWidth, dt, 13f); - rightContextWidthAnim = AnimationUtility.approach(rightContextWidthAnim, targetRightContextWidth, dt, 13f); - mainWidthAnim = AnimationUtility.snap(mainWidthAnim, targetMainWidth, 0.05f); - heightAnim = AnimationUtility.snap(heightAnim, targetHeight, 0.05f); - leftContextWidthAnim = AnimationUtility.snap(leftContextWidthAnim, targetLeftContextWidth, 0.05f); - rightContextWidthAnim = AnimationUtility.snap(rightContextWidthAnim, targetRightContextWidth, 0.05f); - - float drawMainWidth = mainWidthAnim; - float drawHeight = heightAnim; - float leftOccupied = leftContextWidthAnim > 0.5f ? leftContextWidthAnim + IslandBoundsPatches.CONTEXT_GAP : 0.0f; - float rightOccupied = rightContextWidthAnim > 0.5f ? IslandBoundsPatches.CONTEXT_GAP + rightContextWidthAnim : 0.0f; + IslandState.IslandMetrics metrics = layout.measureMetrics(titleRenderer, valueRenderer, state.predictedPvpSeconds(), state.currentSnapshot); + IslandLayout.LayoutResult lr = layout.computeLayout(state, anim, metrics, clickGuiState, screenW, screenH, mc); + anim.updateLayout(dt, lr.targetMainWidth(), lr.targetHeight(), lr.targetLeftContextWidth(), lr.targetRightContextWidth()); + float drawMainWidth = anim.mainWidthAnim < 0f ? lr.targetMainWidth() : anim.mainWidthAnim; + float drawHeight = anim.heightAnim < 0f ? lr.targetHeight() : anim.heightAnim; + float leftOccupied = anim.leftContextWidthAnim > 0.5f ? anim.leftContextWidthAnim + IslandBoundsPatches.CONTEXT_GAP : 0f; + float rightOccupied = anim.rightContextWidthAnim > 0.5f ? IslandBoundsPatches.CONTEXT_GAP + anim.rightContextWidthAnim : 0f; float drawWidth = leftOccupied + drawMainWidth + rightOccupied; - widthAnim = drawWidth; - float drawX = currentMode == IslandMode.CLICKGUI - ? clickGuiState.tabBarX() - leftOccupied - : (screenW - drawMainWidth) * 0.5f - leftOccupied; - float drawY = currentMode == IslandMode.CLICKGUI - ? clickGuiState.tabBarY() - (1.0f - AnimationUtility.easeOutCubic(clickGuiState.lifecycle())) * 18.0f - : TOP_Y + IslandUiUtil.bossBarOffset(mc, screenH, TOP_Y); - float mainX = leftOccupied; - float alpha = AnimationUtility.easeOutCubic(visibilityAnim); - float radius = currentMode == IslandMode.CLICKGUI ? drawHeight * 0.5f : AnimationUtility.lerp(CLOSED_RADIUS, EXPANDED_RADIUS, expandAnim); - int accent = accentFor(currentMode); - + float drawX = state.currentMode == IslandState.IslandMode.CLICKGUI ? clickGuiState.tabBarX() - leftOccupied : (screenW - drawMainWidth) * 0.5f - leftOccupied; + float drawY = state.currentMode == IslandState.IslandMode.CLICKGUI ? clickGuiState.tabBarY() - (1f - AnimationUtility.easeOutCubic(clickGuiState.lifecycle())) * 18f : IslandLayout.TOP_Y + IslandUiUtil.bossBarOffset(mc, screenH, IslandLayout.TOP_Y); + float mainX = leftOccupied; float alpha = anim.visibilityAlpha(); + float radius = state.currentMode == IslandState.IslandMode.CLICKGUI ? drawHeight * 0.5f : AnimationUtility.lerp(IslandLayout.CLOSED_RADIUS, IslandLayout.EXPANDED_RADIUS, anim.expandAnim); + int accent = rendererHelper.accentFor(state.currentMode, syncTheme, accentColor); setBounds(drawX, drawY, drawWidth, drawHeight); - - float expandedContentAlpha = currentMode == IslandMode.MUSIC ? AnimationUtility.easeOutCubic(expandAnim) * alpha : 0f; - float compactContentAlpha = currentMode == IslandMode.MUSIC ? (1f - AnimationUtility.easeOutCubic(expandAnim)) * alpha : 0f; - if (expandedContentAlpha > 0.01f) { - controls.updateLayout(drawX + mainX, drawY, drawMainWidth, valueRenderer, currentSnapshot); - controls.updateHover(isInteractiveScreen(currentScreen), mc, currentSnapshot); - } else { - controls.reset(); - } - - if (!renderScripted(renderer, fallback, titleRenderer, metaRenderer, ctx, tickDelta, screenW, screenH, drawWidth, drawHeight, - drawMainWidth, mainX, leftContextWidthAnim, rightContextWidthAnim, - alpha, compactContentAlpha, expandedContentAlpha, radius, accent)) { - IslandUiUtil.drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent, uiFillTop); - } + float expandedAlpha = state.currentMode == IslandState.IslandMode.MUSIC ? anim.expandedAlpha(alpha) : 0f; + float compactAlpha = state.currentMode == IslandState.IslandMode.MUSIC ? anim.compactAlpha(alpha) : 0f; + if (expandedAlpha > 0.01f) { controls.updateLayout(drawX + mainX, drawY, drawMainWidth, valueRenderer, state.currentSnapshot); controls.updateHover(isInteractiveScreen(currentScreen), mc, state.currentSnapshot); } else controls.reset(); + boolean rendered = rendererHelper.renderScripted(renderer, fallback, titleRenderer, metaRenderer, ctx, tickDelta, screenW, screenH, drawWidth, drawHeight, drawMainWidth, mainX, anim.leftContextWidthAnim, anim.rightContextWidthAnim, alpha, compactAlpha, expandedAlpha, radius, accent, x, y, state, anim, controls, syncTheme, blur, blurAlpha, moduleHandle, scriptRuntime); + if (!rendered) IslandUiUtil.drawFallback(renderer, drawX + mainX, drawY, drawMainWidth, drawHeight, radius, alpha, accent, rendererHelper.uiFillTop); } - private boolean renderScripted(Renderer2D renderer, - TextRenderer textRenderer, - TextRenderer titleRenderer, - TextRenderer metaRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH, - float width, - float height, - float mainWidth, - float mainX, - float leftContextWidth, - float rightContextWidth, - float alpha, - float compactAlpha, - float expandedAlpha, - float radius, - int accent) { - if (moduleHandle.isRuntimeBlocked()) return false; - - UiScriptModule loaded = ensureModule(); - if (loaded == null) return false; - - UiScriptProps props = buildProps( - screenW, screenH, width, height, mainWidth, mainX, leftContextWidth, rightContextWidth, - alpha, compactAlpha, expandedAlpha, radius, accent, - titleRenderer, metaRenderer - ); - long treeSignature = IslandRuntimePatches.islandStructuralSignature(props); - long dataSignature = CachedUiScriptRuntime.signature(props.asMap()); - long layoutSignature = treeSignature; - UiRuntime baked = scriptRuntime.bake( - moduleHandle, - loaded, - "dynamic_island", - treeSignature, - dataSignature, - layoutSignature, - width, - height, - textRenderer, - x, - y, - width, - height, - props::asMap, - () -> IslandRuntimePatches.islandRuntimePatches(props), - () -> IslandBoundsPatches.islandBoundsPatches(props, x, y, controls) - ); - if (baked == null) return false; - baked.render(new UiRenderContext(renderer, textRenderer, ctx, tickDelta, UiProjectionMode.UNSCALED_LOGICAL)); - return true; - } - - private UiScriptProps buildProps(int screenW, - int screenH, - float width, - float height, - float mainWidth, - float mainX, - float leftContextWidth, - float rightContextWidth, - float alpha, - float compactAlpha, - float expandedAlpha, - float radius, - int accent, - TextRenderer titleRenderer, - TextRenderer metaRenderer) { - UiScriptProps props = UiScriptProps.create(80); - String title = currentSnapshot != null ? currentSnapshot.title() : ""; - String artist = currentSnapshot != null ? currentSnapshot.artist() : ""; - long duration = currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L; - props.put("screenW", screenW); - props.put("screenH", screenH); - props.put("width", width); - props.put("height", height); - props.put("mainWidth", mainWidth); - props.put("mainX", mainX); - props.put("leftContextWidth", leftContextWidth); - props.put("rightContextWidth", rightContextWidth); - props.put("contextGap", IslandBoundsPatches.CONTEXT_GAP); - boolean pvpContextVisible = (currentMode == IslandMode.MUSIC || currentMode == IslandMode.CLICKGUI) && PvpState.isActive() && rightContextWidth > 0.5f; - props.put("pvpContextVisible", pvpContextVisible); - props.put("contextAlpha", currentMode == IslandMode.MUSIC ? compactAlpha : (currentMode == IslandMode.CLICKGUI ? alpha : 0.0f)); - props.put("mode", currentMode.name().toLowerCase()); - props.put("alpha", alpha); - props.put("compactAlpha", compactAlpha); - props.put("expandedAlpha", expandedAlpha); - props.put("expand", expandAnim); - props.put("expanded", expanded); - props.put("blur", blur.get()); - props.put("blurAlpha", blurAlpha.get() / 255.0f); - props.put("radius", radius); - props.put("time", IslandUiUtil.formatClock(LocalTime.now())); - props.put("pvpTimer", pvpContextVisible || currentMode == IslandMode.PVP ? IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()) : ""); - ClickGuiRenderer.ClickGuiIslandState clickGuiState = ClickGuiRenderer.islandState(); - props.put("clickGuiLabel", clickGuiState.activeLabel()); - props.put("clickGuiTabCount", clickGuiState.tabCount()); - props.put("clickGuiPicker", clickGuiState.pickerActive()); - props.put("title", title); - props.put("artist", artist); - props.put("titleWidthCompact", IslandUiUtil.measureWidth(titleRenderer, title, 0.82f)); - props.put("titleWidthExpanded", IslandUiUtil.measureWidth(titleRenderer, title, 0.86f)); - props.put("artistWidthExpanded", IslandUiUtil.measureWidth(metaRenderer, artist, 0.72f)); - props.put("titleScrollTime", titleScrollSeconds(title, artist, duration)); - props.put("elapsed", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L)); - props.put("total", IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.durationSeconds() : 0L)); - props.put("progress", currentSnapshot != null && currentSnapshot.durationSeconds() > 0 - ? AnimationUtility.clamp((float) currentSnapshot.predictedPositionSeconds() / (float) currentSnapshot.durationSeconds(), 0f, 1f) - : 0f); - props.put("playing", currentSnapshot != null && currentSnapshot.isPlaying()); - Identifier artwork = currentSnapshot != null ? currentSnapshot.artworkTexture() : null; - props.put("artworkTexture", artwork != null ? artwork.toString() : ""); - props.put("fillTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha))); - props.put("fillBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha))); - props.put("tintTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha))); - props.put("tintBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha))); - props.put("shadow", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(IslandUiUtil.BASE_SHADOW, 0.28f * alpha))); - props.put("stroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : IslandUiUtil.BASE_STROKE, accent, 0.18f), 0.18f * alpha))); - props.put("highlight", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha))); - props.put("contextFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha))); - props.put("contextStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); - props.put("timeFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha))); - props.put("timeStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); - props.put("pvpFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha))); - props.put("pvpStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha))); - props.put("accent", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(accent, alpha))); - props.put("accentSoft", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha))); - props.put("textPrimary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha))); - props.put("textSecondary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha))); - props.put("textMuted", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha))); - props.put("progressBg", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha))); - props.put("shuffleX", controls.shuffleX - x); - props.put("shuffleY", controls.shuffleY - y); - props.put("prevX", controls.prevX - x); - props.put("prevY", controls.prevY - y); - props.put("playX", controls.playX - x); - props.put("playY", controls.playY - y); - props.put("nextX", controls.nextX - x); - props.put("nextY", controls.nextY - y); - props.put("repeatX", controls.repeatX - x); - props.put("repeatY", controls.repeatY - y); - props.put("controlSize", controls.controlSize); - props.put("showShuffle", currentSnapshot != null && currentSnapshot.supportsShuffle()); - props.put("showRepeat", currentSnapshot != null && currentSnapshot.supportsRepeat()); - props.put("shuffleHover", controls.shuffleHover); - props.put("prevHover", controls.prevHover); - props.put("playHover", controls.playHover); - props.put("nextHover", controls.nextHover); - props.put("repeatHover", controls.repeatHover); - props.put("iconShuffle", IslandUiUtil.iconString(ICON_SHUFFLE)); - props.put("iconPrev", IslandUiUtil.iconString(ICON_PREV)); - props.put("iconPlay", IslandUiUtil.iconString(currentSnapshot != null && currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY)); - props.put("iconNext", IslandUiUtil.iconString(ICON_NEXT)); - props.put("shuffleColor", IslandUiUtil.hex(IslandUiUtil.shuffleColor(currentSnapshot))); - props.put("repeatColor", IslandUiUtil.hex(IslandUiUtil.repeatColor(currentSnapshot))); - props.put("repeatAsset", IslandUiUtil.repeatSvg(currentSnapshot)); - return props; - } - - private float titleScrollSeconds(String title, String artist, long durationSeconds) { - String key = (title != null ? title : "") + "\u0000" + (artist != null ? artist : ""); - long now = Util.getMillis(); - if (!key.equals(titleScrollKey)) { - titleScrollKey = key; - titleScrollStartMs = now; - return 0.0f; - } - return Math.max(0.0f, (now - titleScrollStartMs) / 1000.0f); - } - - private UiScriptModule ensureModule() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getResourceManager() == null) return null; - if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { - HudScriptLayouts.reportLoadError(moduleHandle); - return null; - } - moduleHandle.consumeChanged(); - return moduleHandle.module(); - } - - private void resetScriptRuntime() { - scriptRuntime.reset(); - } - - private IslandMode resolveMode(MediaSessionService.Snapshot snapshot) { - if (snapshot != null && snapshot.hasSession()) return IslandMode.MUSIC; - if (PvpState.isActive()) return IslandMode.PVP; - return IslandMode.TIME; - } - - private IslandMetrics measureMetrics(TextRenderer titleRenderer, - TextRenderer valueRenderer, - TextRenderer metaRenderer) { - float pvpTimerW = IslandUiUtil.reservedTimeWidth(valueRenderer, IslandUiUtil.formatSecondsOnly(predictedPvpSeconds()), 0.84f); - float pvpWidth = 24f + pvpTimerW; - - String title = currentSnapshot != null ? currentSnapshot.title() : ""; - float titleW = IslandUiUtil.measureWidth(titleRenderer, title, 0.82f); - String compactElapsed = IslandUiUtil.formatShortTime(currentSnapshot != null ? currentSnapshot.predictedPositionSeconds() : 0L); - float elapsedW = IslandUiUtil.reservedTimeWidth(valueRenderer, compactElapsed, 0.72f); - float musicWidth = 10f + elapsedW + 18f + COMPACT_ART + COMPACT_GAP + titleW + COMPACT_GAP + COMPACT_WAVE_W + 10f; - return new IslandMetrics(pvpWidth, IslandUiUtil.clamp(musicWidth, 226f, 308f)); - } - - private boolean isInteractiveScreen(Screen screen) { - return screen instanceof ChatScreen || isScreenOverlayAllowed(screen); - } - - private boolean isScreenOverlayAllowed(Screen screen) { - if (screen == null || screen instanceof ChatScreen) return false; - if (screen instanceof ClickGuiScreen) return false; - return !IslandUiUtil.isSettingsLikeScreen(screen); - } - - private int accentFor(IslandMode mode) { - int syncedAccent = syncTheme.get() ? theme().accent() : (accentColor.getArgb() | 0xFF000000); - return switch (mode) { - case TIME -> syncedAccent; - case MUSIC -> syncedAccent; - case PVP -> STATUS_PVP; - case CLICKGUI -> syncedAccent; - }; - } - - private void updatePalette() { - if (syncTheme.get()) { - int windowBg = HudRenderUtil.setAlpha(theme().windowBg(), themeAlpha.get()); - uiFillTop = HudRenderUtil.mixRgb(windowBg, 0xFFFFFFFF, 0.04f); - uiFillBottom = HudRenderUtil.mixRgb(windowBg, 0xFF000000, 0.08f); - uiTextPrimary = theme().textPrimary(); - uiTextSecondary = theme().textMuted(); - uiTextMuted = HudRenderUtil.mixColor(theme().textMuted(), windowBg, 0.18f); - uiAccent = theme().accent(); - uiAccentSoft = theme().accentSoft(); - return; - } - - int baseBg = bgColor.getArgb(); - uiFillTop = HudRenderUtil.mixRgb(baseBg, 0xFFFFFFFF, 0.04f); - uiFillBottom = HudRenderUtil.mixRgb(baseBg, 0xFF000000, 0.08f); - uiTextPrimary = textColor.getArgb() | 0xFF000000; - uiTextSecondary = mutedColor.getArgb() | 0xFF000000; - uiTextMuted = HudRenderUtil.mixColor(uiTextSecondary, baseBg, 0.16f); - uiAccent = accentColor.getArgb() | 0xFF000000; - uiAccentSoft = HudRenderUtil.mixColor(uiAccent, baseBg, 0.40f); - } - - private float predictedPvpSeconds() { - Float secondsLeft = PvpState.getSecondsLeft(); - if (!PvpState.isActive() || secondsLeft == null) return 0f; - long sinceUpdate = Math.max(0L, Util.getMillis() - PvpState.getLastUpdateMs()); - return Math.max(0f, secondsLeft - sinceUpdate / 1000f); - } - - private enum IslandMode { - TIME, - MUSIC, - PVP, - CLICKGUI - } - - private record IslandMetrics(float pvpWidth, float musicCompactWidth) { - } + private boolean isInteractiveScreen(Screen screen) { return screen instanceof ChatScreen || isScreenOverlayAllowed(screen); } + private boolean isScreenOverlayAllowed(Screen screen) { if (screen == null || screen instanceof ChatScreen) return false; if (screen instanceof ClickGuiScreen) return false; return !IslandUiUtil.isSettingsLikeScreen(screen); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java index d7b01a1..a56bc99 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java @@ -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.gui.hud.nondraggable.impl.dynamicisland; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java index 047a867..e9d8f6a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java @@ -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.gui.hud.nondraggable.impl.dynamicisland; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.patch.UiBoundsPatchSet; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java index c3f3ac1..122f0e6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java @@ -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.gui.hud.nondraggable.impl.dynamicisland; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java index 7908fcf..186bd44 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java @@ -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.gui.hud.nondraggable.impl.dynamicisland.controls; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandUiUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java index 817e74b..c06481e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java @@ -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.gui.hud.nondraggable.impl.dynamicisland.controls; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandAnimation.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandAnimation.java new file mode 100644 index 0000000..bfa3752 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandAnimation.java @@ -0,0 +1,71 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +/** + * Анимации Dynamic Island: появление и раскрытие. + */ +final class IslandAnimation { + + float visibilityAnim; + float expandAnim; + float widthAnim = -1f; + float mainWidthAnim = -1f; + float leftContextWidthAnim; + float rightContextWidthAnim; + float heightAnim = -1f; + + void updateVisibility(float dt, boolean shouldShow) { + float target = shouldShow ? 1f : 0f; + visibilityAnim = AnimationUtility.approach(visibilityAnim, target, dt, 10f); + visibilityAnim = AnimationUtility.snap(visibilityAnim, target, 0.002f); + } + + void updateExpand(float dt, boolean expanded) { + float target = expanded ? 1f : 0f; + expandAnim = AnimationUtility.approach(expandAnim, target, dt, 10f); + expandAnim = AnimationUtility.snap(expandAnim, target, 0.002f); + } + + void updateLayout(float dt, float targetMainWidth, float targetHeight, + float targetLeftContextWidth, float targetRightContextWidth) { + mainWidthAnim = mainWidthAnim < 0f + ? targetMainWidth + : AnimationUtility.approach(mainWidthAnim, targetMainWidth, dt, 12f); + heightAnim = heightAnim < 0f + ? targetHeight + : AnimationUtility.approach(heightAnim, targetHeight, dt, 12f); + leftContextWidthAnim = AnimationUtility.approach( + leftContextWidthAnim, targetLeftContextWidth, dt, 13f); + rightContextWidthAnim = AnimationUtility.approach( + rightContextWidthAnim, targetRightContextWidth, dt, 13f); + mainWidthAnim = AnimationUtility.snap(mainWidthAnim, targetMainWidth, 0.05f); + heightAnim = AnimationUtility.snap(heightAnim, targetHeight, 0.05f); + leftContextWidthAnim = AnimationUtility.snap(leftContextWidthAnim, targetLeftContextWidth, 0.05f); + rightContextWidthAnim = AnimationUtility.snap(rightContextWidthAnim, targetRightContextWidth, 0.05f); + } + + float compactAlpha(float alpha) { + return (1f - AnimationUtility.easeOutCubic(expandAnim)) * alpha; + } + + float expandedAlpha(float alpha) { + return AnimationUtility.easeOutCubic(expandAnim) * alpha; + } + + float visibilityAlpha() { + return AnimationUtility.easeOutCubic(visibilityAnim); + } + + boolean isVisible(float threshold) { + return visibilityAnim > threshold; + } + + boolean isExpanded(float threshold) { + return expandAnim > threshold; + } + + void resetWidth() { + widthAnim = -1f; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandLayout.java new file mode 100644 index 0000000..339a85b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandLayout.java @@ -0,0 +1,110 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandUiUtil; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import net.minecraft.client.Minecraft; + +/** + * Геометрия Dynamic Island: метрики и границы отрисовки. + */ +final class IslandLayout { + + static final float TOP_Y = 18f; + static final float CLOSED_HEIGHT = 35f; + static final float EXPANDED_HEIGHT = 98f; + static final float CLOSED_RADIUS = 17f; + static final float EXPANDED_RADIUS = 20f; + static final float TIME_WIDTH = 74f; + static final float PVP_MIN_WIDTH = 68f; + static final float PVP_MAX_WIDTH = 96f; + static final float MUSIC_EXPANDED_WIDTH = 338f; + static final float TIME_CONTEXT_WIDTH = 57f; + static final float COMPACT_PAD_X = 11f; + static final float COMPACT_GAP = 9f; + static final float COMPACT_ART = 22f; + static final float COMPACT_WAVE_W = 15f; + + IslandState.IslandMetrics measureMetrics(TextRenderer titleRenderer, + TextRenderer valueRenderer, + float predictedPvpSeconds, + MediaSessionService.Snapshot snapshot) { + float pvpTimerW = IslandUiUtil.reservedTimeWidth( + valueRenderer, IslandUiUtil.formatSecondsOnly(predictedPvpSeconds), 0.84f); + float pvpWidth = 24f + pvpTimerW; + String title = snapshot != null ? snapshot.title() : ""; + float titleW = IslandUiUtil.measureWidth(titleRenderer, title, 0.82f); + String compactElapsed = IslandUiUtil.formatShortTime( + snapshot != null ? snapshot.predictedPositionSeconds() : 0L); + float elapsedW = IslandUiUtil.reservedTimeWidth(valueRenderer, compactElapsed, 0.72f); + float musicWidth = 10f + elapsedW + 18f + COMPACT_ART + COMPACT_GAP + titleW + COMPACT_GAP + COMPACT_WAVE_W + 10f; + return new IslandState.IslandMetrics(pvpWidth, IslandUiUtil.clamp(musicWidth, 226f, 308f)); + } + + LayoutResult computeLayout(IslandState state, + IslandAnimation anim, + IslandState.IslandMetrics metrics, + ClickGuiRenderer.ClickGuiIslandState clickGuiState, + int screenW, + int screenH, + Minecraft mc) { + float targetMainWidth = switch (state.currentMode) { + case TIME -> TIME_WIDTH; + case PVP -> IslandUiUtil.clamp(metrics.pvpWidth(), PVP_MIN_WIDTH, PVP_MAX_WIDTH); + case MUSIC -> AnimationUtility.lerp(metrics.musicCompactWidth(), MUSIC_EXPANDED_WIDTH, anim.expandAnim); + case CLICKGUI -> clickGuiState.tabBarW(); + }; + float targetHeight = state.currentMode == IslandState.IslandMode.CLICKGUI + ? clickGuiState.tabBarH() + : AnimationUtility.lerp(CLOSED_HEIGHT, + state.currentMode == IslandState.IslandMode.MUSIC ? EXPANDED_HEIGHT : CLOSED_HEIGHT, + anim.expandAnim); + + boolean musicContext = state.currentMode == IslandState.IslandMode.MUSIC; + boolean clickGuiContext = state.currentMode == IslandState.IslandMode.CLICKGUI; + float contextPresence = musicContext + ? 1.0f - AnimationUtility.easeOutCubic(anim.expandAnim) + : (clickGuiContext ? 1.0f : 0.0f); + float targetLeftContextWidth = TIME_CONTEXT_WIDTH * contextPresence; + float targetRightContextWidth = dev.loki.lovisual.util.pvp.state.PvpState.isActive() + ? IslandBoundsPatches.PVP_CONTEXT_WIDTH * contextPresence : 0.0f; + + float drawMainWidth = anim.mainWidthAnim < 0f ? targetMainWidth : anim.mainWidthAnim; + float drawHeight = anim.heightAnim < 0f ? targetHeight : anim.heightAnim; + float leftOccupied = anim.leftContextWidthAnim > 0.5f + ? anim.leftContextWidthAnim + IslandBoundsPatches.CONTEXT_GAP : 0.0f; + float rightOccupied = anim.rightContextWidthAnim > 0.5f + ? IslandBoundsPatches.CONTEXT_GAP + anim.rightContextWidthAnim : 0.0f; + float drawWidth = leftOccupied + drawMainWidth + rightOccupied; + float drawX = state.currentMode == IslandState.IslandMode.CLICKGUI + ? clickGuiState.tabBarX() - leftOccupied + : (screenW - drawMainWidth) * 0.5f - leftOccupied; + float drawY = state.currentMode == IslandState.IslandMode.CLICKGUI + ? clickGuiState.tabBarY() - (1.0f - AnimationUtility.easeOutCubic(clickGuiState.lifecycle())) * 18.0f + : TOP_Y + IslandUiUtil.bossBarOffset(mc, screenH, TOP_Y); + float mainX = leftOccupied; + float alpha = AnimationUtility.easeOutCubic(anim.visibilityAnim); + float radius = state.currentMode == IslandState.IslandMode.CLICKGUI + ? drawHeight * 0.5f + : AnimationUtility.lerp(CLOSED_RADIUS, EXPANDED_RADIUS, anim.expandAnim); + + return new LayoutResult(targetMainWidth, targetHeight, targetLeftContextWidth, targetRightContextWidth, + drawMainWidth, drawHeight, drawWidth, drawX, drawY, mainX, alpha, radius); + } + + record LayoutResult(float targetMainWidth, + float targetHeight, + float targetLeftContextWidth, + float targetRightContextWidth, + float drawMainWidth, + float drawHeight, + float drawWidth, + float drawX, + float drawY, + float mainX, + float alpha, + float radius) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandRenderer.java new file mode 100644 index 0000000..928ba89 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandRenderer.java @@ -0,0 +1,138 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandControls; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.controls.IslandUiUtil; +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptProps; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.pvp.state.PvpState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import java.time.LocalTime; +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class IslandRenderer { + static final int STATUS_PVP = 0xFFFF6368; + static final int PROGRESS_BG = 0x2AFFFFFF; + static final int ICON_PAUSE = 0xEA02; + static final int ICON_PLAY = 0xEA03; + static final int ICON_PREV = 0xEA04; + static final int ICON_NEXT = 0xEA05; + static final int ICON_SHUFFLE = 0xEA06; + int uiFillTop; int uiFillBottom; int uiTextPrimary; int uiTextSecondary; int uiTextMuted; int uiAccent; int uiAccentSoft; + void updatePalette(BooleanValue syncTheme, NumberValue themeAlpha, RGBAColorValue bgColor, RGBColorValue accentColor, RGBColorValue textColor, RGBColorValue mutedColor) { + if (syncTheme.get()) { + int bg = HudRenderUtil.setAlpha(theme().windowBg(), themeAlpha.get()); + uiFillTop = HudRenderUtil.mixRgb(bg, 0xFFFFFFFF, 0.04f); + uiFillBottom = HudRenderUtil.mixRgb(bg, 0xFF000000, 0.08f); + uiTextPrimary = theme().textPrimary(); uiTextSecondary = theme().textMuted(); + uiTextMuted = HudRenderUtil.mixColor(theme().textMuted(), bg, 0.18f); + uiAccent = theme().accent(); uiAccentSoft = theme().accentSoft(); return; + } + int baseBg = bgColor.getArgb(); + uiFillTop = HudRenderUtil.mixRgb(baseBg, 0xFFFFFFFF, 0.04f); + uiFillBottom = HudRenderUtil.mixRgb(baseBg, 0xFF000000, 0.08f); + uiTextPrimary = textColor.getArgb() | 0xFF000000; uiTextSecondary = mutedColor.getArgb() | 0xFF000000; + uiTextMuted = HudRenderUtil.mixColor(uiTextSecondary, baseBg, 0.16f); + uiAccent = accentColor.getArgb() | 0xFF000000; uiAccentSoft = HudRenderUtil.mixColor(uiAccent, baseBg, 0.40f); + } + int accentFor(IslandState.IslandMode mode, BooleanValue syncTheme, RGBColorValue accentColor) { + int synced = syncTheme.get() ? theme().accent() : (accentColor.getArgb() | 0xFF000000); + return mode == IslandState.IslandMode.PVP ? STATUS_PVP : synced; + } + boolean renderScripted(Renderer2D renderer, TextRenderer textRenderer, TextRenderer titleRenderer, TextRenderer metaRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH, float width, float height, float mainWidth, float mainX, float leftContextWidth, float rightContextWidth, float alpha, float compactAlpha, float expandedAlpha, float radius, int accent, float x, float y, IslandState state, IslandAnimation anim, IslandControls controls, BooleanValue syncTheme, BooleanValue blur, NumberValue blurAlpha, UiScriptModuleHandle moduleHandle, CachedUiScriptRuntime scriptRuntime) { + if (moduleHandle.isRuntimeBlocked()) return false; + UiScriptModule loaded = ensureModule(moduleHandle); if (loaded == null) return false; + UiScriptProps props = buildProps(screenW, screenH, width, height, mainWidth, mainX, leftContextWidth, rightContextWidth, alpha, compactAlpha, expandedAlpha, radius, accent, x, y, titleRenderer, metaRenderer, state, anim, controls, syncTheme, blur, blurAlpha); + long treeSig = IslandRuntimePatches.islandStructuralSignature(props); + long dataSig = CachedUiScriptRuntime.signature(props.asMap()); + UiRuntime baked = scriptRuntime.bake(moduleHandle, loaded, "dynamic_island", treeSig, dataSig, treeSig, width, height, textRenderer, x, y, width, height, props::asMap, () -> IslandRuntimePatches.islandRuntimePatches(props), () -> IslandBoundsPatches.islandBoundsPatches(props, x, y, controls)); + if (baked == null) return false; + baked.render(new UiRenderContext(renderer, textRenderer, ctx, tickDelta, UiProjectionMode.UNSCALED_LOGICAL)); + return true; + } + UiScriptProps buildProps(int screenW, int screenH, float width, float height, float mainWidth, float mainX, float leftContextWidth, float rightContextWidth, float alpha, float compactAlpha, float expandedAlpha, float radius, int accent, float originX, float originY, TextRenderer titleRenderer, TextRenderer metaRenderer, IslandState state, IslandAnimation anim, IslandControls controls, BooleanValue syncTheme, BooleanValue blur, NumberValue blurAlpha) { + UiScriptProps p = UiScriptProps.create(80); + String title = state.currentSnapshot != null ? state.currentSnapshot.title() : ""; + String artist = state.currentSnapshot != null ? state.currentSnapshot.artist() : ""; + long duration = state.currentSnapshot != null ? state.currentSnapshot.durationSeconds() : 0L; + p.put("screenW", screenW); p.put("screenH", screenH); p.put("width", width); p.put("height", height); + p.put("mainWidth", mainWidth); p.put("mainX", mainX); p.put("leftContextWidth", leftContextWidth); p.put("rightContextWidth", rightContextWidth); + p.put("contextGap", IslandBoundsPatches.CONTEXT_GAP); + boolean pvpVis = (state.currentMode == IslandState.IslandMode.MUSIC || state.currentMode == IslandState.IslandMode.CLICKGUI) && PvpState.isActive() && rightContextWidth > 0.5f; + p.put("pvpContextVisible", pvpVis); + p.put("contextAlpha", state.currentMode == IslandState.IslandMode.MUSIC ? compactAlpha : (state.currentMode == IslandState.IslandMode.CLICKGUI ? alpha : 0.0f)); + p.put("mode", state.currentMode.name().toLowerCase()); p.put("alpha", alpha); p.put("compactAlpha", compactAlpha); p.put("expandedAlpha", expandedAlpha); + p.put("expand", anim.expandAnim); p.put("expanded", state.expanded); p.put("blur", blur.get()); p.put("blurAlpha", blurAlpha.get() / 255.0f); + p.put("radius", radius); p.put("time", IslandUiUtil.formatClock(LocalTime.now())); + p.put("pvpTimer", pvpVis || state.currentMode == IslandState.IslandMode.PVP ? IslandUiUtil.formatSecondsOnly(state.predictedPvpSeconds()) : ""); + ClickGuiRenderer.ClickGuiIslandState cs = ClickGuiRenderer.islandState(); + p.put("clickGuiLabel", cs.activeLabel()); p.put("clickGuiTabCount", cs.tabCount()); p.put("clickGuiPicker", cs.pickerActive()); + p.put("title", title); p.put("artist", artist); + p.put("titleWidthCompact", IslandUiUtil.measureWidth(titleRenderer, title, 0.82f)); + p.put("titleWidthExpanded", IslandUiUtil.measureWidth(titleRenderer, title, 0.86f)); + p.put("artistWidthExpanded", IslandUiUtil.measureWidth(metaRenderer, artist, 0.72f)); + p.put("titleScrollTime", state.titleScrollSeconds(title, artist, duration)); + p.put("elapsed", IslandUiUtil.formatShortTime(state.currentSnapshot != null ? state.currentSnapshot.predictedPositionSeconds() : 0L)); + p.put("total", IslandUiUtil.formatShortTime(state.currentSnapshot != null ? state.currentSnapshot.durationSeconds() : 0L)); + p.put("progress", state.currentSnapshot != null && state.currentSnapshot.durationSeconds() > 0 ? AnimationUtility.clamp((float) state.currentSnapshot.predictedPositionSeconds() / (float) state.currentSnapshot.durationSeconds(), 0f, 1f) : 0f); + p.put("playing", state.currentSnapshot != null && state.currentSnapshot.isPlaying()); + Identifier art = state.currentSnapshot != null ? state.currentSnapshot.artworkTexture() : null; + p.put("artworkTexture", art != null ? art.toString() : ""); + p.put("fillTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillTop, 0.88f * alpha))); + p.put("fillBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiFillBottom, 0.88f * alpha))); + p.put("tintTop", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.16f * alpha))); + p.put("tintBottom", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF000000, 0.25f), 0.112f * alpha))); + p.put("shadow", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(IslandUiUtil.BASE_SHADOW, 0.28f * alpha))); + p.put("stroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(syncTheme.get() ? theme().windowStroke() : IslandUiUtil.BASE_STROKE, accent, 0.18f), 0.18f * alpha))); + p.put("highlight", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.08f * alpha))); + p.put("contextFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha))); + p.put("contextStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); + p.put("timeFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFF000000, 0.48f * alpha))); + p.put("timeStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.10f * alpha))); + p.put("pvpFill", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(STATUS_PVP, 0.86f * alpha))); + p.put("pvpStroke", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(STATUS_PVP, 0xFFFFFFFF, 0.32f), 0.34f * alpha))); + p.put("accent", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(accent, alpha))); + p.put("accentSoft", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiAccentSoft, alpha))); + p.put("textPrimary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextPrimary, alpha))); + p.put("textSecondary", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextSecondary, alpha))); + p.put("textMuted", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(uiTextMuted, alpha))); + p.put("progressBg", IslandUiUtil.hex(HudRenderUtil.scaleAlpha(PROGRESS_BG, expandedAlpha))); + p.put("shuffleX", controls.shuffleX - originX); p.put("shuffleY", controls.shuffleY - originY); + p.put("prevX", controls.prevX - originX); p.put("prevY", controls.prevY - originY); + p.put("playX", controls.playX - originX); p.put("playY", controls.playY - originY); + p.put("nextX", controls.nextX - originX); p.put("nextY", controls.nextY - originY); + p.put("repeatX", controls.repeatX - originX); p.put("repeatY", controls.repeatY - originY); + p.put("controlSize", controls.controlSize); + p.put("showShuffle", state.currentSnapshot != null && state.currentSnapshot.supportsShuffle()); + p.put("showRepeat", state.currentSnapshot != null && state.currentSnapshot.supportsRepeat()); + p.put("shuffleHover", controls.shuffleHover); p.put("prevHover", controls.prevHover); p.put("playHover", controls.playHover); p.put("nextHover", controls.nextHover); p.put("repeatHover", controls.repeatHover); + p.put("iconShuffle", IslandUiUtil.iconString(ICON_SHUFFLE)); p.put("iconPrev", IslandUiUtil.iconString(ICON_PREV)); + p.put("iconPlay", IslandUiUtil.iconString(state.currentSnapshot != null && state.currentSnapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY)); + p.put("iconNext", IslandUiUtil.iconString(ICON_NEXT)); + p.put("shuffleColor", IslandUiUtil.hex(IslandUiUtil.shuffleColor(state.currentSnapshot))); + p.put("repeatColor", IslandUiUtil.hex(IslandUiUtil.repeatColor(state.currentSnapshot))); + p.put("repeatAsset", IslandUiUtil.repeatSvg(state.currentSnapshot)); + return p; + } + private UiScriptModule ensureModule(UiScriptModuleHandle h) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getResourceManager() == null) return null; + if (!h.ensureLoaded(mc.getResourceManager())) { HudScriptLayouts.reportLoadError(h); return null; } + h.consumeChanged(); return h.module(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandState.java new file mode 100644 index 0000000..01b497a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/island/IslandState.java @@ -0,0 +1,64 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland; + +import dev.loki.lovisual.util.media.session.MediaSessionService; +import dev.loki.lovisual.util.pvp.state.PvpState; +import net.minecraft.util.Util; + +/** + * Состояние Dynamic Island: режим, снапшот медиа, расширенность, скролл заголовка. + */ +final class IslandState { + + IslandMode currentMode = IslandMode.TIME; + MediaSessionService.Snapshot currentSnapshot = + MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); + boolean expanded; + boolean clickGuiShellVisible; + String titleScrollKey = ""; + long titleScrollStartMs; + + IslandMode resolveMode(MediaSessionService.Snapshot snapshot) { + if (snapshot != null && snapshot.hasSession()) { + return IslandMode.MUSIC; + } + if (PvpState.isActive()) { + return IslandMode.PVP; + } + return IslandMode.TIME; + } + + float predictedPvpSeconds() { + Float secondsLeft = PvpState.getSecondsLeft(); + if (!PvpState.isActive() || secondsLeft == null) { + return 0f; + } + long sinceUpdate = Math.max(0L, Util.getMillis() - PvpState.getLastUpdateMs()); + return Math.max(0f, secondsLeft - sinceUpdate / 1000f); + } + + float titleScrollSeconds(String title, String artist, long durationSeconds) { + String key = (title != null ? title : "") + "\u0000" + (artist != null ? artist : ""); + long now = Util.getMillis(); + if (!key.equals(titleScrollKey)) { + titleScrollKey = key; + titleScrollStartMs = now; + return 0.0f; + } + return Math.max(0.0f, (now - titleScrollStartMs) / 1000.0f); + } + + void resetScroll() { + titleScrollKey = ""; + titleScrollStartMs = Util.getMillis(); + } + + enum IslandMode { + TIME, + MUSIC, + PVP, + CLICKGUI + } + + record IslandMetrics(float pvpWidth, float musicCompactWidth) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java index 3a2c35a..5063881 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java @@ -1,392 +1,73 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar; -import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.config.definition.SettingDef; import dev.loki.lovisual.config.values.color.RGBAColorValue; import dev.loki.lovisual.config.values.color.RGBColorValue; import dev.loki.lovisual.config.values.mode.EnumValue; import dev.loki.lovisual.config.values.mode.ModeValue; import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.math.ColorMath; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.util.Util; -import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.visuals.player.Animations; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.ColorMath; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; import java.util.List; import static dev.loki.lovisual.features.theme.Theme.theme; +/** + * Module facade for the vanilla-replacement hotbar HUD element. Owns the ClickGui + * settings, resolves the effective palette (theme vs. custom colours) and delegates + * drawing to {@link HotbarRenderer} and per-frame animation to {@link HotbarSelection}. + * Only the tiny surface used by {@code GuiMixin} is public — every palette getter + * is package-private. + */ @HudElementRegister(order = 30) public final class CustomHotbar extends AbstractHudElement { + /** Sentinel values for the {@code bg_effect} / {@code hotbar_color_mode} mode-settings. */ private static final String EFFECT_NONE = "None"; private static final String EFFECT_BLUR = "Blur"; private static final String EFFECT_GLASS = "Glass"; private static final String COLOR_THEME = "Theme"; private static final String COLOR_CUSTOM = "Custom"; + public static final CustomHotbar INSTANCE = new CustomHotbar(); - private static final int BG_ALPHA = 0xAA; - private static final float RADIUS = 6.0f; - private static final float HOTBAR_SOFTNESS = 0.0f; - private static final float BLUR_QUALITY = 16.0f; - private static final float DIVIDER_WIDTH_PX = 1.05f; - private static final float DIVIDER_HEIGHT = 11.0f; - private static final float SELECT_STROKE_PX = 1.45f; - private static final float SELECT_STROKE_SOFTNESS = 0.85f; - private static final float SELECT_FILL_ALPHA = 0.16f; - private static final int ITEM_Y_OFFSET = 2; - private static final int ITEM_Y_OFFSET_SELECTED = 0; - private static final float SLOT_STEP = 20.0f; - private static final float SELECT_SLOT_STEP = SLOT_STEP; - private static final float SELECT_ANIMATION_MS = 145.0f; - private static final float SELECT_ANIMATION_MIN_PROGRESS = 0.015f; - private static int animatedSelectedSlot = -1; - private static float selectionStartSlot = 0.0f; - private static float selectionTargetSlot = 0.0f; - private static long selectionAnimStartMs = 0L; - private final EnumValue offHandMode = - new EnumValue<>("offhand_mode", OffHandMode.MERGED, OffHandMode.values()); - private final ModeValue colorMode = - new ModeValue("hotbar_color_mode", "Custom", COLOR_THEME, COLOR_CUSTOM); - private final RGBAColorValue bgColor = - new RGBAColorValue("bg_color", "#7A191414"); - private final NumberValue bgAlpha = - new NumberValue<>("hotbar_bg_alpha", 167, 0, 255); - private final RGBColorValue selectColor = - new RGBColorValue("select_color", "#2F6277"); - private final RGBColorValue dividerColor = - new RGBColorValue("divider_color", "#C13434"); - private final BooleanValue customSelectColor = - new BooleanValue("custom_select_color", false); - private final BooleanValue customDividerColor = - new BooleanValue("custom_divider_color", false); - private final NumberValue accentAlpha = - new NumberValue<>("accent_alpha", 255, 0, 255); - private final ModeValue bgEffect = - new ModeValue("bg_effect", "Glass", EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS); - private final NumberValue blurAlpha = - new NumberValue<>("blur_alpha", 255, 20, 255); + + private final EnumValue offHandMode = new EnumValue<>("offhand_mode", OffHandMode.MERGED, OffHandMode.values()); + private final ModeValue colorMode = new ModeValue("hotbar_color_mode", "Custom", COLOR_THEME, COLOR_CUSTOM); + private final RGBAColorValue bgColor = new RGBAColorValue("bg_color", "#7A191414"); + private final NumberValue bgAlpha = new NumberValue<>("hotbar_bg_alpha", 167, 0, 255); + private final RGBColorValue selectColor = new RGBColorValue("select_color", "#2F6277"); + private final RGBColorValue dividerColor = new RGBColorValue("divider_color", "#C13434"); + private final BooleanValue customSelectColor = new BooleanValue("custom_select_color", false); + private final BooleanValue customDividerColor = new BooleanValue("custom_divider_color", false); + private final NumberValue accentAlpha = new NumberValue<>("accent_alpha", 255, 0, 255); + private final ModeValue bgEffect = new ModeValue("bg_effect", "Glass", EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS); + private final NumberValue blurAlpha = new NumberValue<>("blur_alpha", 255, 20, 255); + private CustomHotbar() { super("vanilla_hotbar", "Hotbar", true); } + /** Singleton accessor used by {@code GuiMixin}. */ public static CustomHotbar get() { return INSTANCE; } + /** Public entry point that draws panel backdrop + selection box. */ public static void renderBackground(Renderer2D r2d, Minecraft mc) { - LocalPlayer player = mc.player; - if (player == null) return; - - float s = (float) mc.getWindow().getGuiScale(); - - float i = mc.getWindow().getGuiScaledWidth() / 2f; - float h = mc.getWindow().getGuiScaledHeight(); - - int selected = getSelectedSlot(player); - HotbarSelection selection = updateSelectionAnimation(selected); - CustomHotbar bm = CustomHotbar.get(); - if (bm == null || !bm.isHudHotbarEnabled()) return; - - ItemStack offhand = player.getOffhandItem(); - boolean hasOffhand = !offhand.isEmpty(); - - float baseY = h - 22f + hotbarRaiseOffset(); - - boolean blur = shouldBlur(bm); - boolean glass = shouldGlass(bm); - int bgRgb = glass ? HudRenderUtil.glassBackgroundRgb() : bm.getHotbarBgRgb(); - int bgAlpha = bm != null ? bm.getHotbarBgAlpha() : BG_ALPHA; - int bg = glass ? premultiply(HudRenderUtil.glassPanelBackground(1.0f)) : ColorMath.premultiplyAlpha(bgRgb, bgAlpha); - int bgColor = bg; - int accentAlpha = bm.getHotbarAccentAlpha(); - int dividerColor = ColorMath.premultiplyAlpha(bm.getHotbarDividerRgb(), accentAlpha); - SelectionGradient selectionGradient = bm.getHotbarSelectionGradient(accentAlpha); - - if (glass) { - if (!hasOffhand) { - drawGlass((i - 90f) * s, baseY * s, 180f * s, 20f * s, RADIUS * s); - } else if (getOffHandMode() == OffHandMode.MERGED) { - drawGlass((i - 111f) * s, baseY * s, 201f * s, 20f * s, RADIUS * s); - } else { - drawGlass((i - 90f) * s, baseY * s, 180f * s, 20f * s, RADIUS * s); - drawGlass((i - 112.5f) * s, baseY * s, 20f * s, 20f * s, RADIUS * s); - } - } else if (blur) { - if (!hasOffhand) { - drawBlur((i - 90f) * s, baseY * s, 180f * s, 20f * s, RADIUS * s, bgRgb); - } else if (getOffHandMode() == OffHandMode.MERGED) { - drawBlur((i - 111f) * s, baseY * s, 201f * s, 20f * s, RADIUS * s, bgRgb); - } else { - drawBlur((i - 90f) * s, baseY * s, 180f * s, 20f * s, RADIUS * s, bgRgb); - drawBlur((i - 112.5f) * s, baseY * s, 20f * s, 20f * s, RADIUS * s, bgRgb); - } - } - - if (glass) { - bgColor = premultiply(HudRenderUtil.glassPanelBackground(1.0f)); - } - - if (!hasOffhand) { - drawBase(r2d, (i - 90f) * s, baseY * s, 180f * s, 20f * s, bgColor, s); - } else { - if (getOffHandMode() == OffHandMode.MERGED) { - drawBase(r2d, (i - 111f) * s, baseY * s, 201f * s, 20f * s, bgColor, s); - - float divX = (i - 91f) * s; - float divY = (baseY + (20f - DIVIDER_HEIGHT) * 0.5f) * s; - r2d.quad(divX, divY, DIVIDER_WIDTH_PX, DIVIDER_HEIGHT * s, dividerColor); - } else { // SEPARATELY - drawBase(r2d, (i - 90f) * s, baseY * s, 180f * s, 20f * s, bgColor, s); - drawBase(r2d, (i - 112.5f) * s, baseY * s, 20f * s, 20f * s, bgColor, s); - } - } - - float selX = (i - 88f + selection.visualSlot() * SELECT_SLOT_STEP) * s; - float selY = (baseY + 1.5f) * s; - drawSelectionStroke( - r2d, - selX, - selY, - 17f * s, - 17f * s, - s, - selectionGradient.startArgb(), - selectionGradient.endArgb(), - selectionGradient.angleDeg() - ); - } - - private static void drawBase(Renderer2D r2d, float x, float y, float w, float h, int argb, float scale) { - if (RADIUS <= 0.0f) { - r2d.quad(x, y, w, h, argb); - return; - } - r2d.roundedRect(x, y, w, h, RADIUS * scale, HOTBAR_SOFTNESS * scale, argb); - } - - private static void drawSelectionStroke(Renderer2D r2d, - float x, - float y, - float w, - float h, - float scale, - int startArgb, - int endArgb, - float gradientAngleDeg) { - if (r2d == null) return; - float radius = Math.max(3.0f * scale, Math.min(w, h) * 0.22f); - float thickness = Math.max(1.15f, SELECT_STROKE_PX * Math.min(scale, 1.35f)); - float softness = SELECT_STROKE_SOFTNESS * Math.min(scale, 1.25f); - int fillStart = HudRenderUtil.scaleAlpha(startArgb, SELECT_FILL_ALPHA); - int fillEnd = HudRenderUtil.scaleAlpha(endArgb, SELECT_FILL_ALPHA * 0.82f); - r2d.roundedRectGradient(x, y, w, h, radius, softness, fillStart, fillEnd, gradientAngleDeg); - - float t = (Util.getMillis() % 2200L) / 2200.0f; - r2d.roundedRectStrokeAngularGradient( - x, - y, - w, - h, - radius, - softness, - thickness, - startArgb, - endArgb, - t - ); - int halo = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(startArgb, endArgb, 0.5f), 0.22f); - r2d.roundedRectStroke(x, y, w, h, radius, softness + 0.4f, thickness + 0.55f, halo); + HotbarRenderer.renderBackground(r2d, mc); } + /** Public entry point that draws every item icon in the hotbar row. */ public static void renderItems(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) { - LocalPlayer player = mc.player; - if (player == null) return; - CustomHotbar bm = CustomHotbar.get(); - if (bm == null || !bm.isHudHotbarEnabled()) return; - - int i = mc.getWindow().getGuiScaledWidth() / 2; - float baseY = mc.getWindow().getGuiScaledHeight() - 22 + hotbarRaiseOffset(); - - int selected = getSelectedSlot(player); - HotbarSelection selection = updateSelectionAnimation(selected); - - int seed = 1; - - // Offhand item with compact side positioning. - ItemStack offhand = player.getOffhandItem(); - if (!offhand.isEmpty()) { - int x = (getOffHandMode() == OffHandMode.MERGED) ? (i - 109) : (i - 111); - renderHotbarItem(ctx, tickCounter, player, offhand, x, baseY + ITEM_Y_OFFSET, seed++); - } - - // 9 slots - for (int slot = 0; slot < 9; slot++) { - double x = i - 90.0 + slot * SLOT_STEP + 2.0; - double y = baseY + animatedItemYOffset(slot, selection); - - ItemStack stack = player.getInventory().getItem(slot); - renderHotbarItem(ctx, tickCounter, player, stack, x, y, seed++); - } - } - - private static HotbarSelection updateSelectionAnimation(int selectedSlot) { - long now = Util.getMillis(); - selectedSlot = clampSlot(selectedSlot); - - if (animatedSelectedSlot < 0) { - animatedSelectedSlot = selectedSlot; - selectionStartSlot = selectedSlot; - selectionTargetSlot = selectedSlot; - selectionAnimStartMs = now; - return new HotbarSelection(selectedSlot, selectedSlot, selectedSlot, 1.0f); - } - - float visualSlot = currentVisualSelectedSlot(now); - if (selectedSlot != animatedSelectedSlot) { - selectionStartSlot = visualSlot; - selectionTargetSlot = selectedSlot; - selectionAnimStartMs = now; - animatedSelectedSlot = selectedSlot; - } - - float progress = selectionProgress(now); - float eased = AnimationUtility.easeOutCubic(progress); - visualSlot = AnimationUtility.lerp(selectionStartSlot, selectionTargetSlot, eased); - - return new HotbarSelection(visualSlot, selectionStartSlot, selectionTargetSlot, eased); - } - - private static float animatedItemYOffset(int slot, HotbarSelection selection) { - float lift = selection.liftFor(slot); - return AnimationUtility.lerp(ITEM_Y_OFFSET, ITEM_Y_OFFSET_SELECTED, lift); - } - - private static float currentVisualSelectedSlot(long now) { - float progress = selectionProgress(now); - float eased = AnimationUtility.easeOutCubic(progress); - return AnimationUtility.lerp(selectionStartSlot, selectionTargetSlot, eased); - } - - private static float selectionProgress(long now) { - float elapsed = Math.max(0.0f, (float) (now - selectionAnimStartMs)); - if (SELECT_ANIMATION_MS <= 0.0f) return 1.0f; - return AnimationUtility.clamp01(elapsed / SELECT_ANIMATION_MS); - } - - private static int clampSlot(int slot) { - if (slot < 0) return 0; - if (slot > 8) return 8; - return slot; - } - - private static void renderHotbarItem(GuiGraphicsExtractor ctx, - DeltaTracker tickCounter, - LocalPlayer player, - ItemStack stack, - double x, double y, - int seed) { - if (stack.isEmpty()) return; - - // vanilla bobbing animation (InGameHud#renderHotbarItem) - float f = (float) stack.getPopTime() - tickCounter.getGameTimeDeltaPartialTick(false); - if (f > 0.0F) { - float g = 1.0F + f / 5.0F; - Renderer2D.COLOR.itemPivot( - stack, - x, - y, - 1.0f / g, - (g + 1.0f) / 2.0f, - 8.0f, - 12.0f, - seed, - Renderer2D.ITEM_OVERLAY_NONE, - null - ); - Renderer2D.COLOR.itemOverlay(stack, x, y, 1.0f, Renderer2D.ITEM_OVERLAY_ALL, null); - } else { - Renderer2D.COLOR.item(stack, x, y, 1.0f, seed, Renderer2D.ITEM_OVERLAY_ALL, null); - } - } - - private static int getSelectedSlot(LocalPlayer player) { - return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); - } - - /** - * Vertical GUI-pixel offset applied to the whole hotbar while the Animations - * module slides it into place. The animated value itself is advanced once per - * frame inside the module; this only reads the settled value. - */ - private static float hotbarRaiseOffset() { - Animations animations = Modules.get(Animations.class); - return animations != null ? animations.getHotbarRaiseOffset() : 0.0f; - } - - private static OffHandMode getOffHandMode() { - CustomHotbar bm = CustomHotbar.get(); - if (bm == null) return OffHandMode.MERGED; - return bm.getHotbarOffHandMode(); - } - - private static boolean shouldBlur(CustomHotbar bm) { - return bm != null && bm.isHudHotbarEnabled() && bm.isHotbarBlurEnabled(); - } - - private static boolean shouldGlass(CustomHotbar bm) { - return bm != null && bm.isHudHotbarEnabled() && bm.isHotbarGlassEnabled(); - } - - private static void drawBlur(float x, float y, float w, float h, float radius, int tintRgb) { - CustomHotbar bm = CustomHotbar.get(); - float quality = BLUR_QUALITY; - float brightness = 1.0f; - float alpha = bm != null ? bm.getHotbarBlurAlpha() / 255f : 0.5f; - Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, brightness, alpha, 0xFFFFFF); - } - - private static void drawGlass(float x, float y, float w, float h, float radius) { - CustomHotbar bm = CustomHotbar.get(); - float blurStrength = bm != null ? bm.getHotbarBlurAlpha() / 255f : 0.5f; - float glassScale = RADIUS <= 0.0f ? 1.0f : radius / RADIUS; - HudRenderUtil.drawLiquidGlass(x, y, w, h, radius, glassScale, true, blurStrength, 1.0f); - } - - private static int withSelectorAlpha(int argb, int alpha) { - int sourceAlpha = (argb >>> 24) & 0xFF; - int outAlpha = Math.round(sourceAlpha * (clampAlpha(alpha) / 255.0f)); - return ColorMath.premultiplyAlpha(argb & 0x00FFFFFF, outAlpha); - } - - private static int clampAlpha(int alpha) { - if (alpha < 0) return 0; - if (alpha > 255) return 255; - return alpha; - } - - private static int premultiply(int argb) { - return ColorMath.premultiplyAlpha(argb & 0x00FFFFFF, (argb >>> 24) & 0xFF); + HotbarRenderer.renderItems(ctx, tickCounter, mc); } @Override @@ -408,96 +89,57 @@ public final class CustomHotbar extends AbstractHudElement { return isEnabled(); } - public OffHandMode getHotbarOffHandMode() { + OffHandMode getHotbarOffHandMode() { return offHandMode.get(); } - public int getHotbarBgArgb() { + int getHotbarBgArgb() { return ColorMath.premultiplyAlpha(getHotbarBgRgb(), getHotbarBgAlpha()); } - public int getHotbarBgRgb() { - if (isThemeMode()) { - return theme().windowBg() & 0x00FFFFFF; - } - return bgColor.getArgb() & 0x00FFFFFF; + int getHotbarBgRgb() { + return isThemeMode() ? theme().windowBg() & 0x00FFFFFF : bgColor.getArgb() & 0x00FFFFFF; } - public int getHotbarBgAlpha() { - if (isThemeMode()) { - return bgAlpha.get(); - } - return (bgColor.getArgb() >>> 24) & 0xFF; + int getHotbarBgAlpha() { + return isThemeMode() ? bgAlpha.get() : (bgColor.getArgb() >>> 24) & 0xFF; } - public int getHotbarSelectRgb() { - if (!customSelectColor.get()) { - return theme().accent() & 0x00FFFFFF; - } - return selectColor.getArgb() & 0x00FFFFFF; + int getHotbarSelectRgb() { + return customSelectColor.get() ? selectColor.getArgb() & 0x00FFFFFF : theme().accent() & 0x00FFFFFF; } - public int getHotbarSelectEndRgb() { - if (!customSelectColor.get()) { - return HudRenderUtil.mixColor(theme().accentSoft(), theme().textPrimary(), 0.18f) & 0x00FFFFFF; - } - return HudRenderUtil.mixColor(selectColor.getArgb(), 0xFFFFFFFF, 0.24f) & 0x00FFFFFF; + int getHotbarSelectEndRgb() { + if (customSelectColor.get()) return HudRenderUtil.mixColor(selectColor.getArgb(), 0xFFFFFFFF, 0.24f) & 0x00FFFFFF; + return HudRenderUtil.mixColor(theme().accentSoft(), theme().textPrimary(), 0.18f) & 0x00FFFFFF; } - public int getHotbarDividerRgb() { - if (!customDividerColor.get()) { - return HudRenderUtil.mixColor(theme().windowStroke(), theme().textMuted(), 0.32f) & 0x00FFFFFF; - } - return dividerColor.getArgb() & 0x00FFFFFF; + int getHotbarDividerRgb() { + if (customDividerColor.get()) return dividerColor.getArgb() & 0x00FFFFFF; + return HudRenderUtil.mixColor(theme().windowStroke(), theme().textMuted(), 0.32f) & 0x00FFFFFF; } - public int getHotbarAccentAlpha() { - int v = accentAlpha.get(); - if (v < 0) return 0; - if (v > 255) return 255; - return v; + int getHotbarAccentAlpha() { + return Math.max(0, Math.min(255, accentAlpha.get())); } - public float getHotbarAccentAlphaFactor() { - return getHotbarAccentAlpha() / 255f; - } - - public boolean isHotbarBlurEnabled() { + boolean isHotbarBlurEnabled() { return isBlurEffect(); } - public boolean isHotbarGlassEnabled() { + boolean isHotbarGlassEnabled() { return isGlassEffect(); } - public int getHotbarBlurAlpha() { + int getHotbarBlurAlpha() { return blurAlpha.get(); } - private SelectionGradient getHotbarSelectionGradient(int alpha) { - int a = clampAlpha(alpha); - if (customSelectColor.get()) { - int start = ColorMath.premultiplyAlpha(getHotbarSelectRgb(), a); - int end = ColorMath.premultiplyAlpha(getHotbarSelectEndRgb(), a); - return new SelectionGradient(start, end, 90.0f); - } - - Themes.ThemeEntry entry = Theme.currentEntry(); - Themes.GradientSpec gradient = entry != null ? entry.cardGradient() : null; - if (gradient != null && gradient.enabled()) { - return new SelectionGradient( - withSelectorAlpha(gradient.start(), a), - withSelectorAlpha(gradient.end(), a), - gradient.angleDeg() - ); - } - - int start = ColorMath.premultiplyAlpha(getHotbarSelectRgb(), a); - int end = ColorMath.premultiplyAlpha(getHotbarSelectEndRgb(), a); - return new SelectionGradient(start, end, 90.0f); + boolean customSelectColorEnabled() { + return customSelectColor.get(); } - private boolean hasEffect() { + boolean hasEffect() { return !EFFECT_NONE.equals(bgEffect.get()); } @@ -517,6 +159,7 @@ public final class CustomHotbar extends AbstractHudElement { return EFFECT_GLASS.equals(bgEffect.get()); } + /** How the off-hand slot is composed with the main 9-slot row. */ public enum OffHandMode implements EnumValue.IdProvider { MERGED("merged"), SEPARATELY("separately"); @@ -532,32 +175,4 @@ public final class CustomHotbar extends AbstractHudElement { return id; } } - - private record HotbarSelection(float visualSlot, int startSlot, int targetSlot, float progress) { - private HotbarSelection(float visualSlot, float startSlot, float targetSlot, float progress) { - this( - visualSlot, - clampSlot(Math.round(startSlot)), - clampSlot(Math.round(targetSlot)), - AnimationUtility.clamp01(progress) - ); - } - - private float liftFor(int slot) { - if (startSlot == targetSlot) { - return slot == targetSlot ? 1.0f : 0.0f; - } - if (slot == startSlot) { - return 1.0f - progress; - } - if (slot == targetSlot) { - return Math.max(progress, SELECT_ANIMATION_MIN_PROGRESS); - } - return 0.0f; - } - } - - private record SelectionGradient(int startArgb, int endArgb, float angleDeg) { - } - } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarRenderer.java new file mode 100644 index 0000000..cd86cdd --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarRenderer.java @@ -0,0 +1,175 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar; + +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.player.Animations; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; +import dev.loki.lovisual.render.engine.math.ColorMath; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Util; +import net.minecraft.world.item.ItemStack; + +/** + * Draws the vanilla-replacement hotbar: base panel(s), offhand divider, glass / + * blur backdrop, sliding selection stroke and every item icon. Owns the fixed + * pixel metrics (radius, dividers, slot pitch) so the module facade stays free + * of drawing concerns; per-frame selection state lives in {@link HotbarSelection}. + */ +final class HotbarRenderer { + + private static final int BG_ALPHA = 0xAA; + private static final float RADIUS = 6.0f; + private static final float HOTBAR_SOFTNESS = 0.0f; + private static final float BLUR_QUALITY = 16.0f; + private static final float DIVIDER_WIDTH_PX = 1.05f; + private static final float DIVIDER_HEIGHT = 11.0f; + private static final float SELECT_STROKE_PX = 1.45f; + private static final float SELECT_STROKE_SOFTNESS = 0.85f; + private static final float SELECT_FILL_ALPHA = 0.16f; + private static final float SLOT_STEP = 20.0f; + + private HotbarRenderer() { + } + + /** Paint the panel backdrop (glass / blur / flat base) plus the sliding selection box. */ + static void renderBackground(Renderer2D r2d, Minecraft mc) { + LocalPlayer player = mc.player; + CustomHotbar bm = CustomHotbar.get(); + if (player == null || bm == null || !bm.isHudHotbarEnabled()) return; + + float s = (float) mc.getWindow().getGuiScale(); + float cx = mc.getWindow().getGuiScaledWidth() / 2f; + float baseY = mc.getWindow().getGuiScaledHeight() - 22f + hotbarRaiseOffset(); + boolean hasOffhand = !player.getOffhandItem().isEmpty(); + boolean merged = bm.getHotbarOffHandMode() == CustomHotbar.OffHandMode.MERGED; + + int bgRgb = bm.getHotbarBgRgb(); + int bgAlpha = bm.getHotbarBgAlpha(); + boolean blur = bm.isHotbarBlurEnabled(); + boolean glass = bm.isHotbarGlassEnabled(); + int bgColor = glass ? HudRenderUtil.glassPanelBackground(1.0f) : ColorMath.premultiplyAlpha(bgRgb, bgAlpha); + int accentAlpha = bm.getHotbarAccentAlpha(); + int dividerColor = ColorMath.premultiplyAlpha(bm.getHotbarDividerRgb(), accentAlpha); + HotbarSelectionGradient.SelectionGradient selectionGradient = HotbarSelectionGradient.resolve(bm, accentAlpha); + + float[][] panels = panelsFor(cx, baseY, s, hasOffhand, merged); + float r = RADIUS * s; + for (float[] p : panels) { + if (glass) drawGlass(p[0], p[1], p[2], p[3], r); + else if (blur) drawBlur(p[0], p[1], p[2], p[3], r, bgRgb); + drawBase(r2d, p[0], p[1], p[2], p[3], bgColor, s); + } + if (hasOffhand && merged) { + float divX = (cx - 91f) * s; + float divY = (baseY + (20f - DIVIDER_HEIGHT) * 0.5f) * s; + r2d.quad(divX, divY, DIVIDER_WIDTH_PX, DIVIDER_HEIGHT * s, dividerColor); + } + + HotbarSelection.Frame frame = HotbarSelection.update(getSelectedSlot(player)); + float selX = (cx - 88f + frame.visualSlot() * SLOT_STEP) * s; + float selY = (baseY + 1.5f) * s; + drawSelectionStroke(r2d, selX, selY, 17f * s, 17f * s, s, + selectionGradient.startArgb(), selectionGradient.endArgb(), selectionGradient.angleDeg()); + } + + /** Panels (x, y, w, h in device pixels) covering the hotbar for the current off-hand mode. */ + private static float[][] panelsFor(float cx, float baseY, float s, boolean hasOffhand, boolean merged) { + float y = baseY * s; + float h = 20f * s; + if (!hasOffhand) return new float[][]{{(cx - 90f) * s, y, 180f * s, h}}; + if (merged) return new float[][]{{(cx - 111f) * s, y, 201f * s, h}}; + return new float[][]{{(cx - 90f) * s, y, 180f * s, h}, {(cx - 112.5f) * s, y, 20f * s, h}}; + } + + /** Draw every item icon (off-hand + 9 hotbar slots) with vanilla bobbing. */ + static void renderItems(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, Minecraft mc) { + LocalPlayer player = mc.player; + CustomHotbar bm = CustomHotbar.get(); + if (player == null || bm == null || !bm.isHudHotbarEnabled()) return; + + int cx = mc.getWindow().getGuiScaledWidth() / 2; + float baseY = mc.getWindow().getGuiScaledHeight() - 22 + hotbarRaiseOffset(); + HotbarSelection.Frame frame = HotbarSelection.update(getSelectedSlot(player)); + + int seed = 1; + ItemStack offhand = player.getOffhandItem(); + if (!offhand.isEmpty()) { + int x = (bm.getHotbarOffHandMode() == CustomHotbar.OffHandMode.MERGED) ? (cx - 109) : (cx - 111); + renderHotbarItem(ctx, tickCounter, offhand, x, baseY + HotbarSelection.ITEM_Y_OFFSET, seed++); + } + for (int slot = 0; slot < 9; slot++) { + double x = cx - 90.0 + slot * SLOT_STEP + 2.0; + double y = baseY + HotbarSelection.animatedItemYOffset(slot, frame); + renderHotbarItem(ctx, tickCounter, player.getInventory().getItem(slot), x, y, seed++); + } + } + + /** Vanilla-equivalent item draw with pop / bob handling. */ + private static void renderHotbarItem(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, ItemStack stack, double x, double y, int seed) { + if (stack.isEmpty()) return; + float f = (float) stack.getPopTime() - tickCounter.getGameTimeDeltaPartialTick(false); + if (f > 0.0F) { + float g = 1.0F + f / 5.0F; + Renderer2D.COLOR.itemPivot(stack, x, y, 1.0f / g, (g + 1.0f) / 2.0f, 8.0f, 12.0f, seed, Renderer2D.ITEM_OVERLAY_NONE, null); + Renderer2D.COLOR.itemOverlay(stack, x, y, 1.0f, Renderer2D.ITEM_OVERLAY_ALL, null); + } else { + Renderer2D.COLOR.item(stack, x, y, 1.0f, seed, Renderer2D.ITEM_OVERLAY_ALL, null); + } + } + + private static void drawBase(Renderer2D r2d, float x, float y, float w, float h, int argb, float scale) { + if (RADIUS <= 0.0f) { + r2d.quad(x, y, w, h, argb); + return; + } + r2d.roundedRect(x, y, w, h, RADIUS * scale, HOTBAR_SOFTNESS * scale, argb); + } + + /** Animated rectangular gradient + rotating angular stroke + soft halo around the selected slot. */ + private static void drawSelectionStroke(Renderer2D r2d, float x, float y, float w, float h, float scale, + int startArgb, int endArgb, float gradientAngleDeg) { + if (r2d == null) return; + float radius = Math.max(3.0f * scale, Math.min(w, h) * 0.22f); + float thickness = Math.max(1.15f, SELECT_STROKE_PX * Math.min(scale, 1.35f)); + float softness = SELECT_STROKE_SOFTNESS * Math.min(scale, 1.25f); + int fillStart = HudRenderUtil.scaleAlpha(startArgb, SELECT_FILL_ALPHA); + int fillEnd = HudRenderUtil.scaleAlpha(endArgb, SELECT_FILL_ALPHA * 0.82f); + r2d.roundedRectGradient(x, y, w, h, radius, softness, fillStart, fillEnd, gradientAngleDeg); + + float t = (Util.getMillis() % 2200L) / 2200.0f; + r2d.roundedRectStrokeAngularGradient(x, y, w, h, radius, softness, thickness, startArgb, endArgb, t); + int halo = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(startArgb, endArgb, 0.5f), 0.22f); + r2d.roundedRectStroke(x, y, w, h, radius, softness + 0.4f, thickness + 0.55f, halo); + } + + private static void drawBlur(float x, float y, float w, float h, float radius, int tintRgb) { + CustomHotbar bm = CustomHotbar.get(); + float alpha = bm != null ? bm.getHotbarBlurAlpha() / 255f : 0.5f; + Renderer2D.COLOR.blurRect(x, y, w, h, radius, BLUR_QUALITY, 1.0f, alpha, 0xFFFFFF); + } + + private static void drawGlass(float x, float y, float w, float h, float radius) { + CustomHotbar bm = CustomHotbar.get(); + float blurStrength = bm != null ? bm.getHotbarBlurAlpha() / 255f : 0.5f; + float glassScale = RADIUS <= 0.0f ? 1.0f : radius / RADIUS; + HudRenderUtil.drawLiquidGlass(x, y, w, h, radius, glassScale, true, blurStrength, 1.0f); + } + + private static int getSelectedSlot(LocalPlayer player) { + return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); + } + + /** + * Vertical GUI-pixel offset applied to the whole hotbar while the + * {@link Animations} module slides it into place. The animation itself is + * advanced once per frame inside the module; this only reads the settled value. + */ + private static float hotbarRaiseOffset() { + Animations animations = Modules.get(Animations.class); + return animations != null ? animations.getHotbarRaiseOffset() : 0.0f; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelection.java new file mode 100644 index 0000000..6dcf2a1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelection.java @@ -0,0 +1,91 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import net.minecraft.util.Util; + +/** + * Frame-to-frame animation of the hotbar selection rectangle. Tracks which slot + * is currently "settled" and interpolates towards the newly-clicked one, so + * {@link HotbarRenderer} can draw a smoothly sliding highlight and lift the + * incoming item out of its slot. + */ +final class HotbarSelection { + + /** Vertical offset (in GUI pixels) of a normal hotbar item within its slot. */ + static final int ITEM_Y_OFFSET = 2; + /** Vertical offset of the currently-selected item — a couple pixels higher. */ + static final int ITEM_Y_OFFSET_SELECTED = 0; + + private static final float SELECT_ANIMATION_MS = 145.0f; + private static final float SELECT_ANIMATION_MIN_PROGRESS = 0.015f; + + private static int animatedSelectedSlot = -1; + private static float selectionStartSlot = 0.0f; + private static float selectionTargetSlot = 0.0f; + private static long selectionAnimStartMs = 0L; + + private HotbarSelection() { + } + + /** Snapshot of the sliding-selection animation for the current frame. */ + record Frame(float visualSlot, int startSlot, int targetSlot, float progress) { + + Frame(float visualSlot, float startSlot, float targetSlot, float progress) { + this(visualSlot, clampSlot(Math.round(startSlot)), clampSlot(Math.round(targetSlot)), AnimationUtility.clamp01(progress)); + } + + /** Weight in [0..1] describing how "raised" the given slot should look this frame. */ + float liftFor(int slot) { + if (startSlot == targetSlot) return slot == targetSlot ? 1.0f : 0.0f; + if (slot == startSlot) return 1.0f - progress; + if (slot == targetSlot) return Math.max(progress, SELECT_ANIMATION_MIN_PROGRESS); + return 0.0f; + } + } + + /** Advance the sliding-selection animation towards {@code selectedSlot} and return the current frame. */ + static Frame update(int selectedSlot) { + long now = Util.getMillis(); + selectedSlot = clampSlot(selectedSlot); + if (animatedSelectedSlot < 0) { + animatedSelectedSlot = selectedSlot; + selectionStartSlot = selectedSlot; + selectionTargetSlot = selectedSlot; + selectionAnimStartMs = now; + return new Frame(selectedSlot, selectedSlot, selectedSlot, 1.0f); + } + float visualSlot = currentVisualSlot(now); + if (selectedSlot != animatedSelectedSlot) { + selectionStartSlot = visualSlot; + selectionTargetSlot = selectedSlot; + selectionAnimStartMs = now; + animatedSelectedSlot = selectedSlot; + } + float progress = progress(now); + float eased = AnimationUtility.easeOutCubic(progress); + visualSlot = AnimationUtility.lerp(selectionStartSlot, selectionTargetSlot, eased); + return new Frame(visualSlot, selectionStartSlot, selectionTargetSlot, eased); + } + + /** Interpolated Y offset applied to the item in {@code slot} to give the "hop" feel. */ + static float animatedItemYOffset(int slot, Frame frame) { + return AnimationUtility.lerp(ITEM_Y_OFFSET, ITEM_Y_OFFSET_SELECTED, frame.liftFor(slot)); + } + + private static float currentVisualSlot(long now) { + float eased = AnimationUtility.easeOutCubic(progress(now)); + return AnimationUtility.lerp(selectionStartSlot, selectionTargetSlot, eased); + } + + private static float progress(long now) { + float elapsed = Math.max(0.0f, (float) (now - selectionAnimStartMs)); + if (SELECT_ANIMATION_MS <= 0.0f) return 1.0f; + return AnimationUtility.clamp01(elapsed / SELECT_ANIMATION_MS); + } + + private static int clampSlot(int slot) { + if (slot < 0) return 0; + if (slot > 8) return 8; + return slot; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelectionGradient.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelectionGradient.java new file mode 100644 index 0000000..1132335 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/HotbarSelectionGradient.java @@ -0,0 +1,57 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar; + +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.math.ColorMath; + +/** + * Resolves the two-stop gradient used for the hotbar selection stroke and fill. + * Prefers the user's manual select colours when they opted in, falls back to the + * active theme's card gradient, and finally to flat RGB values. All helpers are + * pure so {@link HotbarRenderer} can call them per-frame without state. + */ +final class HotbarSelectionGradient { + + private static final float DEFAULT_ANGLE_DEG = 90.0f; + + private HotbarSelectionGradient() { + } + + /** Two-stop gradient used for the selection stroke + fill this frame. */ + record SelectionGradient(int startArgb, int endArgb, float angleDeg) { + } + + /** + * Resolve the two-stop gradient used for the selection stroke. Uses the manual + * select colours when the user opted in; otherwise inherits the active theme's + * card gradient. Alpha is applied uniformly to both stops. + */ + static SelectionGradient resolve(CustomHotbar bm, int alpha) { + int a = Math.max(0, Math.min(255, alpha)); + if (bm.customSelectColorEnabled()) { + return new SelectionGradient( + ColorMath.premultiplyAlpha(bm.getHotbarSelectRgb(), a), + ColorMath.premultiplyAlpha(bm.getHotbarSelectEndRgb(), a), + DEFAULT_ANGLE_DEG); + } + Themes.ThemeEntry entry = Theme.currentEntry(); + Themes.GradientSpec gradient = entry != null ? entry.cardGradient() : null; + if (gradient != null && gradient.enabled()) { + return new SelectionGradient( + withAlpha(gradient.start(), a), + withAlpha(gradient.end(), a), + gradient.angleDeg()); + } + return new SelectionGradient( + ColorMath.premultiplyAlpha(bm.getHotbarSelectRgb(), a), + ColorMath.premultiplyAlpha(bm.getHotbarSelectEndRgb(), a), + DEFAULT_ANGLE_DEG); + } + + /** Preserve the source ARGB's own alpha ratio while scaling it into {@code alpha}. */ + private static int withAlpha(int argb, int alpha) { + int sourceAlpha = (argb >>> 24) & 0xFF; + int outAlpha = Math.round(sourceAlpha * (alpha / 255.0f)); + return ColorMath.premultiplyAlpha(argb & 0x00FFFFFF, outAlpha); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java index b948bbf..b8ec5dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java @@ -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.gui.hud.nondraggable.impl.locator; import net.minecraft.client.Minecraft; @@ -40,7 +33,7 @@ public final class CustomLocatorCollector { if (wpUuid != null && !mapPlayers.isEmpty() && mapPlayers.contains(wpUuid)) return; double yaw = waypoint.yawAngleToCamera(mc.level, yawProvider, tickProgress); - if (yaw <= -CustomBar.LOCATOR_ANGLE_RANGE || yaw >= CustomBar.LOCATOR_ANGLE_RANGE) return; + if (yaw <= -CustomBarModel.LOCATOR_ANGLE_RANGE || yaw >= CustomBarModel.LOCATOR_ANGLE_RANGE) return; float dist = Mth.sqrt((float) waypoint.distanceSquared(mc.player)); String name = CustomLocatorResolver.getWaypointName(mc, waypoint, wpUuid); diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java index 2308164..35540a9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java @@ -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.gui.hud.nondraggable.impl.locator; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java index 26c7b49..58ae8d4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java @@ -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.gui.hud.nondraggable.impl.locator; import dev.loki.lovisual.render.engine.animation.AnimationUtility; @@ -15,6 +8,7 @@ import java.util.Map; import java.util.Set; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel; public final class CustomLocatorLabelCache { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java index 9a4e36c..b6e7888 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java @@ -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.gui.hud.nondraggable.impl.locator; import dev.loki.lovisual.render.engine.animation.AnimationUtility; @@ -21,6 +14,7 @@ import java.util.Comparator; import java.util.List; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import static dev.loki.lovisual.features.theme.Theme.theme; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java index 6b1291a..4788d8a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java @@ -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.gui.hud.nondraggable.impl.locator.panel; import dev.loki.lovisual.render.engine.math.ColorMath; @@ -12,6 +5,7 @@ import dev.loki.lovisual.render.engine.renderer.Renderer2D; import net.minecraft.util.Mth; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import static dev.loki.lovisual.features.theme.Theme.theme; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java index f28b106..3febf85 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java @@ -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.gui.hud.nondraggable.impl.locator.panel; import dev.loki.lovisual.render.engine.color.RenderColor; @@ -19,6 +12,7 @@ import java.util.Locale; import java.util.UUID; import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar.*; +import static dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel.*; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBarModel; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomXpBarRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java index 30cd6bc..d0a9a95 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java @@ -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.gui.hud.nondraggable.impl.locator.panel; import dev.loki.lovisual.render.engine.animation.AnimationUtility; @@ -50,8 +43,8 @@ public final class CustomLocatorRenderer { float screenH = mc.getWindow().getGuiScaledHeight(); float baseY = screenH - 22f; - float x = (screenW - CustomBar.XP_BAR_WIDTH) * 0.5f; - float y = baseY - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT; + float x = (screenW - CustomBarModel.XP_BAR_WIDTH) * 0.5f; + float y = baseY - CustomBarModel.XP_BAR_OFFSET - CustomBarModel.LOCATOR_BAR_HEIGHT; CustomXpBarRenderer.drawVanillaGlassTrack(r2d, x, y, scale, 1.0f, bm, true); } @@ -60,9 +53,9 @@ public final class CustomLocatorRenderer { if (ctx == null || mc == null || mc.player == null) return; CustomBar bm = CustomBar.get(); if (bm == null || !bm.isHudBarEnabled() || !bm.isHotbarXpBarEnabled()) return; - int x = Mth.floor((mc.getWindow().getGuiScaledWidth() - CustomBar.XP_BAR_WIDTH) * 0.5f); - int y = Mth.floor(mc.getWindow().getGuiScaledHeight() - 22f - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT); - ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, CustomBar.LOCATOR_BACKGROUND, x, y, 182, 5, + int x = Mth.floor((mc.getWindow().getGuiScaledWidth() - CustomBarModel.XP_BAR_WIDTH) * 0.5f); + int y = Mth.floor(mc.getWindow().getGuiScaledHeight() - 22f - CustomBarModel.XP_BAR_OFFSET - CustomBarModel.LOCATOR_BAR_HEIGHT); + ctx.blitSprite(LoVisualRenderPipelines.GUI_TEXTURE_LOOKUP, CustomBarModel.LOCATOR_BACKGROUND, x, y, 182, 5, CustomXpBarRenderer.applyBarAlpha(CustomXpBarRenderer.colors(bm).locatorTexture(), bm)); } @@ -93,10 +86,10 @@ public final class CustomLocatorRenderer { float screenW = mc.getWindow().getGuiScaledWidth(); float screenH = mc.getWindow().getGuiScaledHeight(); float baseY = screenH - 22f; - float barX = (screenW - CustomBar.XP_BAR_WIDTH) * 0.5f; - float barY = baseY - CustomBar.XP_BAR_OFFSET - CustomBar.LOCATOR_BAR_HEIGHT; + float barX = (screenW - CustomBarModel.XP_BAR_WIDTH) * 0.5f; + float barY = baseY - CustomBarModel.XP_BAR_OFFSET - CustomBarModel.LOCATOR_BAR_HEIGHT; - int startX = Mth.floor(barX + CustomBar.LOCATOR_HEAD_RANGE * 0.5f); + int startX = Mth.floor(barX + CustomBarModel.LOCATOR_HEAD_RANGE * 0.5f); PartialTickSupplier tickProgress = entity -> tickCounter.getGameTimeDeltaPartialTick(false); var camera = mc.gameRenderer.mainCamera(); diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java index 7ad88bd..4262a20 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java @@ -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.gui.hud.nondraggable.impl.locator.panel; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java index 45c951a..1e80b2e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java @@ -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.gui.hud.nondraggable.impl.overlay; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java index eaa6653..095516d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java @@ -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.gui.hud.nondraggable.impl.overlay; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java index ca8edda..a598ca0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java @@ -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.gui.hud.nondraggable.impl.tab; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java index 39bcf4c..c4832d9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java @@ -1,530 +1,137 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; -import com.mojang.authlib.GameProfile; -import dev.loki.lovisual.features.relations.category.CategoryService; import dev.loki.lovisual.features.relations.category.CategoryType; -import dev.loki.lovisual.mixins.accessors.hud.PlayerTabOverlayAccessor; -import dev.loki.lovisual.util.player.health.PlayerSkinResolver; -import dev.loki.lovisual.util.text.format.LegacyTextUtil; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.PlayerTabOverlay; -import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.network.chat.Style; import net.minecraft.resources.Identifier; -import net.minecraft.world.scores.DisplaySlot; -import net.minecraft.world.scores.Objective; -import net.minecraft.world.scores.ReadOnlyScoreInfo; -import net.minecraft.world.scores.ScoreHolder; -import net.minecraft.world.scores.Scoreboard; -import net.minecraft.world.level.GameType; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Set; import java.util.UUID; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabRichTextRenderer; +/** + * Public facade for the tab-list HUD data model. + * + *

    Exposes the immutable {@link Snapshot} / {@link Entry} records consumed by + * {@code CustomTabList} and its renderers, plus the three operations used by the + * HUD tick loop — full {@link #collect collection}, lightweight + * {@link #refreshDynamic dynamic refresh}, and the {@link #structureSignature FNV-1a} + * key used to decide when a full collect is required. Layout constants and helper + * APIs live package-privately so the {@code model/} collaborators + * ({@link TabListCollector}, {@link TabListReader}, {@link TabListTextSplitter}, + * {@link TabListSignature}) can share them without re-declaring.

    + */ public final class TabListModel { - private static final int MAX_PLAYERS = 80; - private static final int MAX_ROWS_PER_COLUMN = 13; - private static final float SHELL_PAD_X = 14f; - private static final float SHELL_PAD_TOP = 16f; - private static final float SHELL_PAD_BOTTOM = 15f; - private static final float HEADER_LINE_HEIGHT = 29f; - private static final float HEADER_PAD_Y = 8f; - private static final float HEADER_TO_ROWS_GAP = 4f; - private static final float ROW_HEIGHT = 47.5f; - private static final float FOOTER_LINE_HEIGHT = 25f; - private static final float FOOTER_PAD_Y = 7f; - private static final float ROWS_TO_FOOTER_GAP = 4f; - private static final float COLUMN_GAP = 11f; - private static final float MIN_COLUMN_WIDTH = 322f; - private static final float MAX_COLUMN_WIDTH = 482f; - private static final float RESERVED_RIGHT_WIDTH = 82f; - private static final float NAME_TEXT_SCALE_FROM_VANILLA = 24.5f / 9.0f; - private static final float SCORE_TEXT_SCALE_FROM_VANILLA = 20.5f / 9.0f; - private static final float HEADER_TEXT_SCALE_FROM_VANILLA = 24.0f / 9.0f; - private static final float FOOTER_TEXT_SCALE_FROM_VANILLA = 20.5f / 9.0f; + // Column / row limits --------------------------------------------------------------------- + static final int MAX_PLAYERS = 80; + static final int MAX_ROWS_PER_COLUMN = 13; - private TabListModel() { - } + // Shell padding ---------------------------------------------------------------------------- + static final float SHELL_PAD_X = 14f; + static final float SHELL_PAD_TOP = 16f; + static final float SHELL_PAD_BOTTOM = 15f; - public static int maxPlayers() { - return MAX_PLAYERS; - } + // Header / footer geometry ----------------------------------------------------------------- + static final float HEADER_LINE_HEIGHT = 29f; + static final float HEADER_PAD_Y = 8f; + static final float HEADER_TO_ROWS_GAP = 4f; + static final float FOOTER_LINE_HEIGHT = 25f; + static final float FOOTER_PAD_Y = 7f; + static final float ROWS_TO_FOOTER_GAP = 4f; - public static int maxRowsPerColumn() { - return MAX_ROWS_PER_COLUMN; - } + // Row / column spacing --------------------------------------------------------------------- + static final float ROW_HEIGHT = 47.5f; + static final float COLUMN_GAP = 11f; + static final float MIN_COLUMN_WIDTH = 322f; + static final float MAX_COLUMN_WIDTH = 482f; + static final float RESERVED_RIGHT_WIDTH = 82f; - public static Snapshot collect(Minecraft mc, int screenW) { - if (mc == null || mc.getConnection() == null) return Snapshot.empty(); + // Vanilla → HUD pixel scale factors -------------------------------------------------------- + static final float NAME_TEXT_SCALE_FROM_VANILLA = 24.5f / 9.0f; + static final float SCORE_TEXT_SCALE_FROM_VANILLA = 20.5f / 9.0f; + static final float HEADER_TEXT_SCALE_FROM_VANILLA = 24.0f / 9.0f; + static final float FOOTER_TEXT_SCALE_FROM_VANILLA = 20.5f / 9.0f; - PlayerTabOverlay overlay = mc.gui != null && mc.gui.hud != null ? mc.gui.hud.getTabList() : null; - Component header = tabText(overlay, true); - Component footer = tabText(overlay, false); - List headerLines = splitTabText(mc, header, Math.max(40, screenW - 50)); - List footerLines = splitTabText(mc, footer, Math.max(40, screenW - 50)); - Objective listObjective = listObjective(mc); - List entries = new ArrayList<>(); + private TabListModel() { } - Collection online = mc.getConnection().getListedOnlinePlayers(); - List players = new ArrayList<>(online); - players.sort(Comparator - .comparingInt((PlayerInfo info) -> -info.getTabListOrder()) - .thenComparing(info -> info.getGameMode() == GameType.SPECTATOR) - .thenComparing(info -> teamName(info).toLowerCase(Locale.ROOT)) - .thenComparing(info -> profileName(info).toLowerCase(Locale.ROOT), String.CASE_INSENSITIVE_ORDER)); + /** Cap on players drawn by the tab list (vanilla shows up to 80 in the sidebar). */ + public static int maxPlayers() { return MAX_PLAYERS; } - int maxNameVanillaWidth = 0; - int maxScoreVanillaWidth = 0; - Set visibleIds = new HashSet<>(); - int limit = Math.min(players.size(), MAX_PLAYERS); - for (int i = 0; i < limit; i++) { - PlayerInfo info = players.get(i); - GameProfile profile = info.getProfile(); - if (profile == null || profile.id() == null) continue; + /** Max rows before the tab list starts a new column. */ + public static int maxRowsPerColumn() { return MAX_ROWS_PER_COLUMN; } - UUID id = profile.id(); - String name = profile.name() != null ? profile.name() : ""; - Component display = displayName(mc, info, name); - Component scoreText = scoreText(mc, profile, listObjective); - CategoryType relation = CategoryService.get(name); - int relationColor = relation == CategoryType.DEFAULT ? 0 : CategoryService.getColor(name); - Identifier skin = PlayerSkinResolver.resolveProfileSkin(profile); + /** + * Full pass: sort online players, measure name / score widths, wrap header / + * footer lines and pack the result into an immutable {@link Snapshot}. + */ + public static Snapshot collect(Minecraft mc, int screenW) { return TabListCollector.collect(mc, screenW); } - if (mc.font != null) { - maxNameVanillaWidth = Math.max(maxNameVanillaWidth, mc.font.width(display)); - if (scoreText != null) { - maxScoreVanillaWidth = Math.max(maxScoreVanillaWidth, mc.font.width(scoreText)); - } - } + /** + * Cheap per-tick pass: only re-reads latency + score for the entries already + * present in {@code previous}, preserving object identity when unchanged. + */ + public static Snapshot refreshDynamic(Minecraft mc, Snapshot previous) { return TabListCollector.refreshDynamic(mc, previous); } - int latency = info.getLatency(); - EntryMetrics metrics = entryMetrics(scoreText, latency); - entries.add(new Entry( - id, - name, - display, - skin, - relation, - relationColor, - latency, - metrics.pingText(), - metrics.pingTextWidth(), - metrics.pingChipWidth(), - info.getGameMode() == GameType.SPECTATOR, - scoreText, - metrics.scoreWidth(), - metrics.scoreBoxWidth() - )); - visibleIds.add(id); - } - - int count = entries.size(); - int columns = Math.max(1, (int) Math.ceil(count / (float) MAX_ROWS_PER_COLUMN)); - int rows = Math.max(1, Math.min(MAX_ROWS_PER_COLUMN, count)); - - float nameDrivenWidth = 72f + maxNameVanillaWidth * NAME_TEXT_SCALE_FROM_VANILLA; - float scoreDrivenWidth = maxScoreVanillaWidth > 0 ? maxScoreVanillaWidth * SCORE_TEXT_SCALE_FROM_VANILLA + 18f : 0f; - float wantedColumnWidth = nameDrivenWidth + scoreDrivenWidth + RESERVED_RIGHT_WIDTH; - float maxUsableWidth = Math.max(MIN_COLUMN_WIDTH, screenW - 58f - (columns - 1) * COLUMN_GAP - SHELL_PAD_X * 2f); - float columnWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.min(wantedColumnWidth, maxUsableWidth / Math.max(1, columns)))); - float columnGap = columns > 1 ? COLUMN_GAP : 0f; - float contentWidth = columns * columnWidth + (columns - 1) * columnGap; - - float headerWidth = maxVanillaLineWidth(mc, headerLines, HEADER_TEXT_SCALE_FROM_VANILLA); - float footerWidth = maxVanillaLineWidth(mc, footerLines, FOOTER_TEXT_SCALE_FROM_VANILLA); - float width = Math.max(contentWidth, Math.max(headerWidth, footerWidth)) + SHELL_PAD_X * 2f; - float headerTop = SHELL_PAD_TOP; - float headerHeight = headerLines.isEmpty() ? 0f : headerLines.size() * HEADER_LINE_HEIGHT + HEADER_PAD_Y * 2f; - float rowTop = SHELL_PAD_TOP + (headerLines.isEmpty() ? 0f : headerHeight + HEADER_TO_ROWS_GAP); - float footerTop = rowTop + rows * ROW_HEIGHT + (footerLines.isEmpty() ? 0f : ROWS_TO_FOOTER_GAP); - float footerHeight = footerLines.isEmpty() ? 0f : footerLines.size() * FOOTER_LINE_HEIGHT + FOOTER_PAD_Y * 2f; - float height = footerTop + footerHeight + SHELL_PAD_BOTTOM; - - return new Snapshot(entries, Set.copyOf(visibleIds), header, footer, headerLines, footerLines, columns, rows, columnWidth, ROW_HEIGHT, - columnGap, rowTop, headerTop, headerHeight, footerTop, footerHeight, width, height); - } - - public static Snapshot refreshDynamic(Minecraft mc, Snapshot previous) { - if (mc == null || mc.getConnection() == null || previous == null || previous.entries().isEmpty()) return previous; - Objective listObjective = listObjective(mc); - List entries = new ArrayList<>(previous.entries().size()); - boolean changed = false; - for (Entry entry : previous.entries()) { - PlayerInfo info = mc.getConnection().getPlayerInfo(entry.id()); - if (info == null || info.getProfile() == null) { - return previous; - } - Component scoreText = scoreText(mc, info.getProfile(), listObjective); - int latency = info.getLatency(); - Entry next = withDynamic(entry, latency, scoreText); - changed |= next != entry; - entries.add(next); - } - if (!changed) return previous; - return previous.withEntries(entries); - } - - public static long structureSignature(Minecraft mc, int screenW) { - if (mc == null || mc.getConnection() == null) return 0L; - long h = 0x6A09E667F3BCC909L; - h = mix(h, screenW); - PlayerTabOverlay overlay = mc.gui != null && mc.gui.hud != null ? mc.gui.hud.getTabList() : null; - h = mix(h, tabText(overlay, true)); - h = mix(h, tabText(overlay, false)); - Objective objective = listObjective(mc); - h = mix(h, objective != null ? objective.getName() : ""); - h = mix(h, objective != null && objective.getRenderType() != null ? objective.getRenderType().name() : ""); - for (PlayerInfo info : mc.getConnection().getListedOnlinePlayers()) { - GameProfile profile = info.getProfile(); - if (profile == null || profile.id() == null) continue; - h = mix(h, profile.id()); - h = mix(h, profile.name()); - h = mix(h, info.getTabListOrder()); - h = mix(h, info.getGameMode() == GameType.SPECTATOR ? 1 : 0); - h = mix(h, teamName(info)); - h = mix(h, info.getTabListDisplayName()); - String name = profile.name() != null ? profile.name() : ""; - CategoryType relation = CategoryService.get(name); - h = mix(h, relation.ordinal()); - h = mix(h, relation == CategoryType.DEFAULT ? 0 : CategoryService.getColor(name)); - Component score = scoreText(mc, profile, objective); - h = mix(h, score); - } - return h; - } - - private static Entry withDynamic(Entry entry, int latency, Component scoreText) { - boolean sameScore = sameText(entry.scoreText(), scoreText); - if (entry.latency() == latency && sameScore) return entry; - EntryMetrics metrics = entryMetrics(scoreText, latency); - return new Entry( - entry.id(), - entry.name(), - entry.displayName(), - entry.skin(), - entry.relation(), - entry.relationColor(), - latency, - metrics.pingText(), - metrics.pingTextWidth(), - metrics.pingChipWidth(), - entry.spectator(), - scoreText, - metrics.scoreWidth(), - metrics.scoreBoxWidth() - ); - } - - private static EntryMetrics entryMetrics(Component scoreText, int latency) { - String pingText = latency < 0 ? "?" : String.valueOf(latency); - float pingWidth = TabRichTextRenderer.widthPlain(pingText, 17.5f); - float pingChipWidth = Math.max(43f, pingWidth + 24f); - float scoreWidth = scoreText != null ? Math.min(64f, TabRichTextRenderer.width(scoreText, 20.5f)) : 0f; - float scoreBoxWidth = scoreWidth > 0f ? scoreWidth + 16f : 0f; - return new EntryMetrics(pingText, pingWidth, pingChipWidth, scoreWidth, scoreBoxWidth); - } - - private static boolean sameText(Component a, Component b) { - if (a == b) return true; - if (a == null || b == null) return false; - return a.getString().equals(b.getString()); - } - - private static Component displayName(Minecraft mc, PlayerInfo info, String fallbackName) { - Component display = null; - if (mc != null && mc.gui != null && mc.gui.hud != null && mc.gui.hud.getTabList() != null) { - display = mc.gui.hud.getTabList().getNameForDisplay(info); - } - if (display == null) { - display = info.getTabListDisplayName(); - } - if (display == null) { - display = Component.literal(fallbackName); - } - return LegacyTextUtil.convertLegacyCodes(display); - } - - private static Component tabText(PlayerTabOverlay overlay, boolean header) { - if (!(overlay instanceof PlayerTabOverlayAccessor accessor)) return null; - Component value = header ? accessor.lovisual$getHeader() : accessor.lovisual$getFooter(); - if (value == null || value.getString().isBlank()) return null; - return LegacyTextUtil.convertLegacyCodes(value); - } - - private static List splitTabText(Minecraft mc, Component component, int maxVanillaWidth) { - if (component == null || component.getString().isBlank()) return List.of(); - if (mc == null || mc.font == null || maxVanillaWidth <= 0) { - return List.of(component); - } - - List fragments = flatten(component); - if (fragments.isEmpty()) return List.of(component); - - List lines = new ArrayList<>(); - MutableComponentBuilder current = new MutableComponentBuilder(); - for (StyledFragment fragment : fragments) { - appendStyledFragment(mc, lines, current, fragment, maxVanillaWidth); - } - flushStyledLine(lines, current); - - if (lines.isEmpty()) return List.of(component); - if (lines.size() == 1 && lines.get(0).getString().equals(component.getString())) return List.of(component); - return List.copyOf(lines); - } - - private static List flatten(Component component) { - List out = new ArrayList<>(); - component.visit((style, text) -> { - if (text != null && !text.isEmpty()) { - out.add(new StyledFragment(text, style == null ? Style.EMPTY : style)); - } - return java.util.Optional.empty(); - }, Style.EMPTY); - return out; - } - - private static void appendStyledFragment(Minecraft mc, - List out, - MutableComponentBuilder current, - StyledFragment fragment, - int maxVanillaWidth) { - String text = fragment.text(); - Style style = fragment.style(); - StringBuilder token = new StringBuilder(); - for (int i = 0; i < text.length(); ) { - int cp = text.codePointAt(i); - i += Character.charCount(cp); - - if (cp == '\r') { - if (i < text.length() && text.charAt(i) == '\n') i++; - appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); - token.setLength(0); - flushStyledLine(out, current); - continue; - } - if (cp == '\n') { - appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); - token.setLength(0); - flushStyledLine(out, current); - continue; - } - - boolean whitespace = Character.isWhitespace(cp); - if (whitespace && token.length() > 0 && !isWhitespaceToken(token)) { - appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); - token.setLength(0); - } else if (!whitespace && token.length() > 0 && isWhitespaceToken(token)) { - appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); - token.setLength(0); - } - token.appendCodePoint(cp); - } - appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); - } - - private static boolean isWhitespaceToken(StringBuilder token) { - if (token == null || token.length() == 0) return false; - for (int i = 0; i < token.length(); ) { - int cp = token.codePointAt(i); - if (!Character.isWhitespace(cp)) return false; - i += Character.charCount(cp); - } - return true; - } - - private static void appendStyledToken(Minecraft mc, - List out, - MutableComponentBuilder current, - String token, - Style style, - int maxVanillaWidth) { - if (token == null || token.isEmpty()) return; - if (current.isEmpty() && token.isBlank()) return; - - Component tokenComponent = Component.literal(token).withStyle(style); - int tokenWidth = mc.font.width(tokenComponent); - if (!current.isEmpty() && current.width + tokenWidth > maxVanillaWidth) { - flushStyledLine(out, current); - if (token.isBlank()) return; - } - - if (tokenWidth <= maxVanillaWidth) { - current.append(tokenComponent, tokenWidth); - return; - } - - for (int i = 0; i < token.length(); ) { - int cp = token.codePointAt(i); - String glyph = new String(Character.toChars(cp)); - Component glyphComponent = Component.literal(glyph).withStyle(style); - int glyphWidth = mc.font.width(glyphComponent); - if (!current.isEmpty() && current.width + glyphWidth > maxVanillaWidth) { - flushStyledLine(out, current); - } - current.append(glyphComponent, glyphWidth); - i += Character.charCount(cp); - } - } - - private static void flushStyledLine(List out, MutableComponentBuilder current) { - if (current == null || current.isEmpty()) return; - out.add(current.build()); - current.clear(); - } - - private static float maxVanillaLineWidth(Minecraft mc, List lines, float scale) { - if (mc == null || mc.font == null || lines == null || lines.isEmpty()) return 0f; - int max = 0; - for (Component line : lines) { - if (line != null) max = Math.max(max, mc.font.width(line)); - } - return max * scale; - } - - private static Objective listObjective(Minecraft mc) { - if (mc == null || mc.level == null) return null; - Scoreboard scoreboard = mc.level.getScoreboard(); - return scoreboard != null ? scoreboard.getDisplayObjective(DisplaySlot.LIST) : null; - } - - private static Component scoreText(Minecraft mc, GameProfile profile, Objective objective) { - if (mc == null || mc.level == null || profile == null || objective == null) return null; - Scoreboard scoreboard = mc.level.getScoreboard(); - if (scoreboard == null) return null; - ReadOnlyScoreInfo score = scoreboard.getPlayerScoreInfo(ScoreHolder.fromGameProfile(profile), objective); - if (score == null && profile.name() != null && !profile.name().isBlank()) { - score = scoreboard.getPlayerScoreInfo(ScoreHolder.forNameOnly(profile.name()), objective); - } - return score == null ? null : Component.literal(String.valueOf(score.value())); - } - - private static String teamName(PlayerInfo info) { - return info.getTeam() != null && info.getTeam().getName() != null ? info.getTeam().getName() : ""; - } - - private static String profileName(PlayerInfo info) { - GameProfile profile = info.getProfile(); - return profile != null && profile.name() != null ? profile.name() : ""; - } - - private static long mix(long h, int value) { - return (h ^ value) * 0x100000001B3L; - } - - private static long mix(long h, UUID value) { - if (value == null) return mix(h, 0); - h = (h ^ value.getMostSignificantBits()) * 0x100000001B3L; - return (h ^ value.getLeastSignificantBits()) * 0x100000001B3L; - } - - private static long mix(long h, String value) { - return (h ^ (value != null ? value.hashCode() : 0)) * 0x100000001B3L; - } - - private static long mix(long h, Component value) { - return mix(h, value != null ? value.getString() : ""); - } + /** FNV-1a key that changes whenever the tab-list structure (order / names / teams / scores) changes. */ + public static long structureSignature(Minecraft mc, int screenW) { return TabListSignature.compute(mc, screenW); } + /** One visible tab-list row: profile, relation, ping chip metrics and score box metrics. */ public record Entry(UUID id, - String name, - Component displayName, - Identifier skin, - CategoryType relation, - int relationColor, - int latency, - String pingText, - float pingTextWidth, - float pingChipWidth, - boolean spectator, - Component scoreText, - float scoreWidth, - float scoreBoxWidth) { - } - - private record EntryMetrics(String pingText, - float pingTextWidth, - float pingChipWidth, - float scoreWidth, - float scoreBoxWidth) { - } - - private record StyledFragment(String text, Style style) { - } - - private static final class MutableComponentBuilder { - private MutableComponent line = Component.empty(); - private int width; - private boolean empty = true; - - private boolean isEmpty() { - return empty; - } - - private void append(Component component, int componentWidth) { - if (component == null) return; - line.append(component); - width += Math.max(0, componentWidth); - empty = false; - } - - private Component build() { - return line; - } - - private void clear() { - line = Component.empty(); - width = 0; - empty = true; - } + String name, + Component displayName, + Identifier skin, + CategoryType relation, + int relationColor, + int latency, + String pingText, + float pingTextWidth, + float pingChipWidth, + boolean spectator, + Component scoreText, + float scoreWidth, + float scoreBoxWidth) { } + /** + * Immutable snapshot of everything the tab-list renderer needs for one frame: + * sorted entries, header / footer wrapped lines and the resolved column layout. + */ public record Snapshot(List entries, - Set visibleIds, - Component header, - Component footer, - List headerLines, - List footerLines, - int columns, - int rows, - float columnWidth, - float rowHeight, - float columnGap, - float rowTop, - float headerTop, - float headerHeight, - float footerTop, - float footerHeight, - float width, - float height) { + Set visibleIds, + Component header, + Component footer, + List headerLines, + List footerLines, + int columns, + int rows, + float columnWidth, + float rowHeight, + float columnGap, + float rowTop, + float headerTop, + float headerHeight, + float footerTop, + float footerHeight, + float width, + float height) { + + /** Rebuild with new entries; recomputes {@code visibleIds} so identity-based diffs stay cheap. */ Snapshot withEntries(List nextEntries) { if (nextEntries == null || nextEntries.isEmpty()) return this; Set ids = new HashSet<>(); for (Entry entry : nextEntries) { if (entry != null && entry.id() != null) ids.add(entry.id()); } - return new Snapshot(List.copyOf(nextEntries), Set.copyOf(ids), header, footer, headerLines, footerLines, columns, rows, - columnWidth, rowHeight, columnGap, rowTop, headerTop, headerHeight, footerTop, footerHeight, width, height); + return new Snapshot(List.copyOf(nextEntries), Set.copyOf(ids), header, footer, headerLines, footerLines, + columns, rows, columnWidth, rowHeight, columnGap, rowTop, headerTop, headerHeight, + footerTop, footerHeight, width, height); } + /** Placeholder snapshot used when there is no server connection or before the first collect. */ static Snapshot empty() { - return new Snapshot(List.of(), Set.of(), null, null, List.of(), List.of(), 1, 1, 322f, ROW_HEIGHT, 0f, + return new Snapshot(List.of(), Set.of(), null, null, List.of(), List.of(), 1, 1, MIN_COLUMN_WIDTH, ROW_HEIGHT, 0f, SHELL_PAD_TOP, SHELL_PAD_TOP, 0f, SHELL_PAD_TOP + ROW_HEIGHT, 0f, 350f, SHELL_PAD_TOP + ROW_HEIGHT + SHELL_PAD_BOTTOM); } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java index f341b0c..0a7df77 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java @@ -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.gui.hud.nondraggable.impl.tab; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListCollector.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListCollector.java new file mode 100644 index 0000000..44f95b0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListCollector.java @@ -0,0 +1,142 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import com.mojang.authlib.GameProfile; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabListModel.Entry; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabListModel.Snapshot; +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.features.relations.category.CategoryType; +import dev.loki.lovisual.util.player.health.PlayerSkinResolver; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.PlayerTabOverlay; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.GameType; +import net.minecraft.world.scores.Objective; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +/** + * Reads the vanilla {@link PlayerTabOverlay} + scoreboard once and builds an + * immutable {@link Snapshot} describing the tab-list layout. Extracted from + * {@link TabListModel} so the facade stays within the two-hundred-line cap; + * sibling folder sharing the parent's package. Field access goes through + * {@link TabListReader}, line splitting through {@link TabListTextSplitter}. + */ +final class TabListCollector { + + private TabListCollector() { } + + /** Sort + trim the online-player list, then measure name / score widths for column layout. */ + static Snapshot collect(Minecraft mc, int screenW) { + if (mc == null || mc.getConnection() == null) return Snapshot.empty(); + PlayerTabOverlay overlay = mc.gui != null && mc.gui.hud != null ? mc.gui.hud.getTabList() : null; + Component header = TabListReader.tabText(overlay, true); + Component footer = TabListReader.tabText(overlay, false); + List headerLines = TabListTextSplitter.split(mc, header, Math.max(40, screenW - 50)); + List footerLines = TabListTextSplitter.split(mc, footer, Math.max(40, screenW - 50)); + Objective listObjective = TabListReader.listObjective(mc); + List entries = new ArrayList<>(); + List players = new ArrayList<>(mc.getConnection().getListedOnlinePlayers()); + players.sort(Comparator + .comparingInt((PlayerInfo info) -> -info.getTabListOrder()) + .thenComparing(info -> info.getGameMode() == GameType.SPECTATOR) + .thenComparing(info -> TabListReader.teamName(info).toLowerCase(Locale.ROOT)) + .thenComparing(info -> TabListReader.profileName(info).toLowerCase(Locale.ROOT), String.CASE_INSENSITIVE_ORDER)); + int maxNameVanillaWidth = 0, maxScoreVanillaWidth = 0; + Set visibleIds = new HashSet<>(); + int limit = Math.min(players.size(), TabListModel.MAX_PLAYERS); + for (int i = 0; i < limit; i++) { + PlayerInfo info = players.get(i); + GameProfile profile = info.getProfile(); + if (profile == null || profile.id() == null) continue; + UUID id = profile.id(); + String name = profile.name() != null ? profile.name() : ""; + Component display = TabListReader.displayName(mc, info, name); + Component scoreText = TabListReader.scoreText(mc, profile, listObjective); + CategoryType relation = CategoryService.get(name); + int relationColor = relation == CategoryType.DEFAULT ? 0 : CategoryService.getColor(name); + Identifier skin = PlayerSkinResolver.resolveProfileSkin(profile); + if (mc.font != null) { + maxNameVanillaWidth = Math.max(maxNameVanillaWidth, mc.font.width(display)); + if (scoreText != null) maxScoreVanillaWidth = Math.max(maxScoreVanillaWidth, mc.font.width(scoreText)); + } + int latency = info.getLatency(); + EntryMetrics m = entryMetrics(scoreText, latency); + entries.add(new Entry(id, name, display, skin, relation, relationColor, latency, + m.pingText(), m.pingTextWidth(), m.pingChipWidth(), + info.getGameMode() == GameType.SPECTATOR, scoreText, m.scoreWidth(), m.scoreBoxWidth())); + visibleIds.add(id); + } + return buildSnapshot(entries, visibleIds, header, footer, headerLines, footerLines, maxNameVanillaWidth, maxScoreVanillaWidth, screenW); + } + + /** Recompute latency / score per entry against the previous snapshot; keeps identity when unchanged. */ + static Snapshot refreshDynamic(Minecraft mc, Snapshot previous) { + if (mc == null || mc.getConnection() == null || previous == null || previous.entries().isEmpty()) return previous; + Objective listObjective = TabListReader.listObjective(mc); + List entries = new ArrayList<>(previous.entries().size()); + boolean changed = false; + for (Entry entry : previous.entries()) { + PlayerInfo info = mc.getConnection().getPlayerInfo(entry.id()); + if (info == null || info.getProfile() == null) return previous; + Component scoreText = TabListReader.scoreText(mc, info.getProfile(), listObjective); + int latency = info.getLatency(); + Entry next = withDynamic(entry, latency, scoreText); + changed |= next != entry; + entries.add(next); + } + return changed ? previous.withEntries(entries) : previous; + } + + private static Snapshot buildSnapshot(List entries, Set visibleIds, Component header, Component footer, + List headerLines, List footerLines, + int maxNameVanillaWidth, int maxScoreVanillaWidth, int screenW) { + int count = entries.size(); + int columns = Math.max(1, (int) Math.ceil(count / (float) TabListModel.MAX_ROWS_PER_COLUMN)); + int rows = Math.max(1, Math.min(TabListModel.MAX_ROWS_PER_COLUMN, count)); + float nameDrivenWidth = 72f + maxNameVanillaWidth * TabListModel.NAME_TEXT_SCALE_FROM_VANILLA; + float scoreDrivenWidth = maxScoreVanillaWidth > 0 ? maxScoreVanillaWidth * TabListModel.SCORE_TEXT_SCALE_FROM_VANILLA + 18f : 0f; + float wantedColumnWidth = nameDrivenWidth + scoreDrivenWidth + TabListModel.RESERVED_RIGHT_WIDTH; + float maxUsableWidth = Math.max(TabListModel.MIN_COLUMN_WIDTH, screenW - 58f - (columns - 1) * TabListModel.COLUMN_GAP - TabListModel.SHELL_PAD_X * 2f); + float columnWidth = Math.max(TabListModel.MIN_COLUMN_WIDTH, Math.min(TabListModel.MAX_COLUMN_WIDTH, Math.min(wantedColumnWidth, maxUsableWidth / Math.max(1, columns)))); + float columnGap = columns > 1 ? TabListModel.COLUMN_GAP : 0f; + float contentWidth = columns * columnWidth + (columns - 1) * columnGap; + float headerWidth = TabListReader.maxVanillaLineWidth(headerLines, TabListModel.HEADER_TEXT_SCALE_FROM_VANILLA); + float footerWidth = TabListReader.maxVanillaLineWidth(footerLines, TabListModel.FOOTER_TEXT_SCALE_FROM_VANILLA); + float width = Math.max(contentWidth, Math.max(headerWidth, footerWidth)) + TabListModel.SHELL_PAD_X * 2f; + float headerTop = TabListModel.SHELL_PAD_TOP; + float headerHeight = headerLines.isEmpty() ? 0f : headerLines.size() * TabListModel.HEADER_LINE_HEIGHT + TabListModel.HEADER_PAD_Y * 2f; + float rowTop = TabListModel.SHELL_PAD_TOP + (headerLines.isEmpty() ? 0f : headerHeight + TabListModel.HEADER_TO_ROWS_GAP); + float footerTop = rowTop + rows * TabListModel.ROW_HEIGHT + (footerLines.isEmpty() ? 0f : TabListModel.ROWS_TO_FOOTER_GAP); + float footerHeight = footerLines.isEmpty() ? 0f : footerLines.size() * TabListModel.FOOTER_LINE_HEIGHT + TabListModel.FOOTER_PAD_Y * 2f; + float height = footerTop + footerHeight + TabListModel.SHELL_PAD_BOTTOM; + return new Snapshot(List.copyOf(entries), Set.copyOf(visibleIds), header, footer, headerLines, footerLines, columns, rows, + columnWidth, TabListModel.ROW_HEIGHT, columnGap, rowTop, headerTop, headerHeight, footerTop, footerHeight, width, height); + } + + private static Entry withDynamic(Entry entry, int latency, Component scoreText) { + if (entry.latency() == latency && TabListReader.sameText(entry.scoreText(), scoreText)) return entry; + EntryMetrics m = entryMetrics(scoreText, latency); + return new Entry(entry.id(), entry.name(), entry.displayName(), entry.skin(), entry.relation(), entry.relationColor(), + latency, m.pingText(), m.pingTextWidth(), m.pingChipWidth(), entry.spectator(), scoreText, m.scoreWidth(), m.scoreBoxWidth()); + } + + private static EntryMetrics entryMetrics(Component scoreText, int latency) { + String pingText = latency < 0 ? "?" : String.valueOf(latency); + float pingWidth = TabRichTextRenderer.widthPlain(pingText, 17.5f); + float pingChipWidth = Math.max(43f, pingWidth + 24f); + float scoreWidth = scoreText != null ? Math.min(64f, TabRichTextRenderer.width(scoreText, 20.5f)) : 0f; + float scoreBoxWidth = scoreWidth > 0f ? scoreWidth + 16f : 0f; + return new EntryMetrics(pingText, pingWidth, pingChipWidth, scoreWidth, scoreBoxWidth); + } + + /** Latency + score pixel measurements needed to size the ping chip and score box. */ + record EntryMetrics(String pingText, float pingTextWidth, float pingChipWidth, float scoreWidth, float scoreBoxWidth) { } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListReader.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListReader.java new file mode 100644 index 0000000..3e6ace1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListReader.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import com.mojang.authlib.GameProfile; +import dev.loki.lovisual.mixins.accessors.hud.PlayerTabOverlayAccessor; +import dev.loki.lovisual.util.text.format.LegacyTextUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.PlayerTabOverlay; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ReadOnlyScoreInfo; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.Scoreboard; + +import java.util.List; + +/** + * Thin accessors that resolve the fields {@link TabListCollector} and + * {@link TabListSignature} need from Minecraft's tab-list overlay, scoreboard and + * player-info entries. Each method normalises legacy colour codes where relevant. + */ +final class TabListReader { + + private TabListReader() { } + + /** Overlay display name → tab-list override → literal fallback; always legacy-code converted. */ + static Component displayName(Minecraft mc, PlayerInfo info, String fallbackName) { + Component display = null; + if (mc != null && mc.gui != null && mc.gui.hud != null && mc.gui.hud.getTabList() != null) { + display = mc.gui.hud.getTabList().getNameForDisplay(info); + } + if (display == null) display = info.getTabListDisplayName(); + if (display == null) display = Component.literal(fallbackName); + return LegacyTextUtil.convertLegacyCodes(display); + } + + /** Header / footer component pulled from the vanilla overlay via mixin accessor; blank → null. */ + static Component tabText(PlayerTabOverlay overlay, boolean header) { + if (!(overlay instanceof PlayerTabOverlayAccessor accessor)) return null; + Component value = header ? accessor.lovisual$getHeader() : accessor.lovisual$getFooter(); + if (value == null || value.getString().isBlank()) return null; + return LegacyTextUtil.convertLegacyCodes(value); + } + + /** The score the vanilla tab-list shows for {@code profile}, or null when no objective is bound. */ + static Component scoreText(Minecraft mc, GameProfile profile, Objective objective) { + if (mc == null || mc.level == null || profile == null || objective == null) return null; + Scoreboard scoreboard = mc.level.getScoreboard(); + if (scoreboard == null) return null; + ReadOnlyScoreInfo score = scoreboard.getPlayerScoreInfo(ScoreHolder.fromGameProfile(profile), objective); + if (score == null && profile.name() != null && !profile.name().isBlank()) { + score = scoreboard.getPlayerScoreInfo(ScoreHolder.forNameOnly(profile.name()), objective); + } + return score == null ? null : Component.literal(String.valueOf(score.value())); + } + + static Objective listObjective(Minecraft mc) { + if (mc == null || mc.level == null) return null; + Scoreboard scoreboard = mc.level.getScoreboard(); + return scoreboard != null ? scoreboard.getDisplayObjective(DisplaySlot.LIST) : null; + } + + static String teamName(PlayerInfo info) { + return info.getTeam() != null && info.getTeam().getName() != null ? info.getTeam().getName() : ""; + } + + static String profileName(PlayerInfo info) { + GameProfile profile = info.getProfile(); + return profile != null && profile.name() != null ? profile.name() : ""; + } + + /** Widest vanilla-font line, scaled to the LoVisual HUD pixel budget. */ + static float maxVanillaLineWidth(List lines, float scale) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.font == null || lines == null || lines.isEmpty()) return 0f; + int max = 0; + for (Component line : lines) if (line != null) max = Math.max(max, mc.font.width(line)); + return max * scale; + } + + static boolean sameText(Component a, Component b) { + if (a == b) return true; + if (a == null || b == null) return false; + return a.getString().equals(b.getString()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListSignature.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListSignature.java new file mode 100644 index 0000000..83777c1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListSignature.java @@ -0,0 +1,71 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import com.mojang.authlib.GameProfile; +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.features.relations.category.CategoryType; +import dev.loki.lovisual.mixins.accessors.hud.PlayerTabOverlayAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.PlayerTabOverlay; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.world.level.GameType; +import net.minecraft.world.scores.Objective; + +import java.util.UUID; + +/** + * FNV-1a style rolling hash over every field that changes the tab-list structure + * (order, name, team, relation, spectator flag, score text). Used to detect when + * a full {@link TabListCollector#collect} pass is required vs a lightweight + * latency / score refresh. Sibling folder sharing the parent's package. + */ +final class TabListSignature { + + private TabListSignature() { } + + /** Stable across frames as long as the tab-list structure (not just ping / score) is unchanged. */ + static long compute(Minecraft mc, int screenW) { + if (mc == null || mc.getConnection() == null) return 0L; + long h = 0x6A09E667F3BCC909L; + h = mix(h, screenW); + PlayerTabOverlay overlay = mc.gui != null && mc.gui.hud != null ? mc.gui.hud.getTabList() : null; + h = mix(h, readTabTextRaw(overlay, true)); + h = mix(h, readTabTextRaw(overlay, false)); + Objective objective = TabListReader.listObjective(mc); + h = mix(h, objective != null ? objective.getName() : ""); + h = mix(h, objective != null && objective.getRenderType() != null ? objective.getRenderType().name() : ""); + for (PlayerInfo info : mc.getConnection().getListedOnlinePlayers()) { + GameProfile profile = info.getProfile(); + if (profile == null || profile.id() == null) continue; + h = mix(h, profile.id()); + h = mix(h, profile.name()); + h = mix(h, info.getTabListOrder()); + h = mix(h, info.getGameMode() == GameType.SPECTATOR ? 1 : 0); + h = mix(h, TabListReader.teamName(info)); + h = mix(h, info.getTabListDisplayName()); + String name = profile.name() != null ? profile.name() : ""; + CategoryType relation = CategoryService.get(name); + h = mix(h, relation.ordinal()); + h = mix(h, relation == CategoryType.DEFAULT ? 0 : CategoryService.getColor(name)); + h = mix(h, TabListReader.scoreText(mc, profile, objective)); + } + return h; + } + + private static Component readTabTextRaw(PlayerTabOverlay overlay, boolean header) { + if (!(overlay instanceof PlayerTabOverlayAccessor accessor)) return null; + return header ? accessor.lovisual$getHeader() : accessor.lovisual$getFooter(); + } + + private static long mix(long h, int value) { return (h ^ value) * 0x100000001B3L; } + + private static long mix(long h, UUID value) { + if (value == null) return mix(h, 0); + h = (h ^ value.getMostSignificantBits()) * 0x100000001B3L; + return (h ^ value.getLeastSignificantBits()) * 0x100000001B3L; + } + + private static long mix(long h, String value) { return (h ^ (value != null ? value.hashCode() : 0)) * 0x100000001B3L; } + + private static long mix(long h, Component value) { return mix(h, value != null ? value.getString() : ""); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListTextSplitter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListTextSplitter.java new file mode 100644 index 0000000..7f78785 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/model/TabListTextSplitter.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Word / grapheme-aware line splitter for tab-list header and footer components. + * Preserves per-fragment {@link Style}s so rich text survives the wrap; falls back + * to grapheme-level splitting when a single token exceeds the vanilla font width. + * Extracted from {@link TabListModel} so the model class stays within the file cap. + */ +final class TabListTextSplitter { + + private TabListTextSplitter() { } + + /** Flatten a component to styled fragments then wrap into lines that fit {@code maxVanillaWidth}. */ + static List split(Minecraft mc, Component component, int maxVanillaWidth) { + if (component == null || component.getString().isBlank()) return List.of(); + if (mc == null || mc.font == null || maxVanillaWidth <= 0) return List.of(component); + List fragments = flatten(component); + if (fragments.isEmpty()) return List.of(component); + List lines = new ArrayList<>(); + MutableComponentBuilder current = new MutableComponentBuilder(); + for (StyledFragment fragment : fragments) appendStyledFragment(mc, lines, current, fragment, maxVanillaWidth); + flushStyledLine(lines, current); + if (lines.isEmpty()) return List.of(component); + if (lines.size() == 1 && lines.get(0).getString().equals(component.getString())) return List.of(component); + return List.copyOf(lines); + } + + private static List flatten(Component component) { + List out = new ArrayList<>(); + component.visit((style, text) -> { + if (text != null && !text.isEmpty()) out.add(new StyledFragment(text, style == null ? Style.EMPTY : style)); + return Optional.empty(); + }, Style.EMPTY); + return out; + } + + private static void appendStyledFragment(Minecraft mc, List out, MutableComponentBuilder current, StyledFragment fragment, int maxVanillaWidth) { + String text = fragment.text(); + Style style = fragment.style(); + StringBuilder token = new StringBuilder(); + for (int i = 0; i < text.length(); ) { + int cp = text.codePointAt(i); + i += Character.charCount(cp); + if (cp == '\r') { + if (i < text.length() && text.charAt(i) == '\n') i++; + appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); + token.setLength(0); + flushStyledLine(out, current); + continue; + } + if (cp == '\n') { + appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); + token.setLength(0); + flushStyledLine(out, current); + continue; + } + boolean whitespace = Character.isWhitespace(cp); + if (whitespace && token.length() > 0 && !isWhitespaceToken(token)) { + appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); + token.setLength(0); + } else if (!whitespace && token.length() > 0 && isWhitespaceToken(token)) { + appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); + token.setLength(0); + } + token.appendCodePoint(cp); + } + appendStyledToken(mc, out, current, token.toString(), style, maxVanillaWidth); + } + + private static boolean isWhitespaceToken(StringBuilder token) { + if (token == null || token.length() == 0) return false; + for (int i = 0; i < token.length(); ) { + int cp = token.codePointAt(i); + if (!Character.isWhitespace(cp)) return false; + i += Character.charCount(cp); + } + return true; + } + + private static void appendStyledToken(Minecraft mc, List out, MutableComponentBuilder current, String token, Style style, int maxVanillaWidth) { + if (token == null || token.isEmpty()) return; + if (current.isEmpty() && token.isBlank()) return; + Component tokenComponent = Component.literal(token).withStyle(style); + int tokenWidth = mc.font.width(tokenComponent); + if (!current.isEmpty() && current.width + tokenWidth > maxVanillaWidth) { + flushStyledLine(out, current); + if (token.isBlank()) return; + } + if (tokenWidth <= maxVanillaWidth) { + current.append(tokenComponent, tokenWidth); + return; + } + for (int i = 0; i < token.length(); ) { + int cp = token.codePointAt(i); + String glyph = new String(Character.toChars(cp)); + Component glyphComponent = Component.literal(glyph).withStyle(style); + int glyphWidth = mc.font.width(glyphComponent); + if (!current.isEmpty() && current.width + glyphWidth > maxVanillaWidth) flushStyledLine(out, current); + current.append(glyphComponent, glyphWidth); + i += Character.charCount(cp); + } + } + + private static void flushStyledLine(List out, MutableComponentBuilder current) { + if (current == null || current.isEmpty()) return; + out.add(current.build()); + current.clear(); + } + + /** Text + style pair produced by {@link Component#visit}. */ + record StyledFragment(String text, Style style) { } + + /** Accumulates {@link Component}s into one line and tracks the running vanilla pixel width. */ + static final class MutableComponentBuilder { + private MutableComponent line = Component.empty(); + private int width; + private boolean empty = true; + boolean isEmpty() { return empty; } + void append(Component component, int componentWidth) { + if (component == null) return; + line.append(component); + width += Math.max(0, componentWidth); + empty = false; + } + Component build() { return line; } + void clear() { line = Component.empty(); width = 0; empty = true; } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java index 082eb44..6e57716 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java @@ -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.gui.hud.nondraggable.impl.tooltips; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java index eee0494..c4fc099 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java @@ -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.gui.hud.nondraggable.impl.tooltips; import dev.loki.lovisual.render.engine.core.context.ViewportContext; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java index 4254802..59a3a75 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java @@ -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.gui.hud.nondraggable.impl.tooltips; import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java index fa0fbe3..d93f638 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java @@ -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.gui.hud.nondraggable.impl.tooltips; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java index 06f4e7b..08bbf6e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java @@ -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.gui.hud.nondraggable.impl.tooltips; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java index 607d70e..a406c50 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java @@ -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.gui.hud.registry; import io.github.classgraph.ClassGraph; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java index 3f70c8a..b620bbb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java @@ -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.gui.hud.registry; import dev.loki.lovisual.events.meta.UsedImplicitly; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java index 1037cb9..d8e8f0b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java @@ -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.gui.hud.registry; import dev.loki.lovisual.events.meta.UsedImplicitly; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java index 08a4b22..da4856e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java @@ -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.gui.hud.registry; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java index a92ffb0..cb2f971 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java @@ -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.gui.hud.script; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java index 5a3058e..1d0fdeb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java @@ -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.gui.hud.script; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java index 7f58fcf..243edcf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java @@ -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.gui.hud.script; import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java index 527ae26..6352e14 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java @@ -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.gui.hud.script; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java index 7158438..008e200 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java @@ -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.gui.hud.script; final class CompactHudCodec { diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java index a102013..8fef7f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java @@ -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.gui.hud.script; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java index 3930550..c677e32 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java @@ -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.gui.hud.script; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java index 0a22b28..df6014a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java @@ -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.gui.hud.script; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java index e433346..65df9fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java @@ -1,156 +1,44 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.mainmenu; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.addon.core.AddonIssue; -import dev.loki.lovisual.addon.core.AddonManager; -import dev.loki.lovisual.addon.pipeline.AddonSnapshot; import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; import dev.loki.lovisual.util.screen.ClientScreen; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.resources.language.I18n; import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; -import net.minecraft.util.Util; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +/** + * Addon manager screen facade: wires the controller and renderer together. + */ public final class LoVisualAddonManagerScreen extends Screen { - private static final float SCALE = 2.28f; - private static final float TEXT_SCALE = 1.30f; - private static final float LEFT_W = 112f * SCALE; - private static final float LEFT_TOP_H = 137f * SCALE; - private static final float LEFT_BOTTOM_H = 58f * SCALE; - private static final float RIGHT_W = 300f * SCALE; - private static final float RIGHT_H = 165f * SCALE; - private static final float GAP = 5f * SCALE; - private static final float PANEL_R = 6f * SCALE; - private static final float PANEL_SHADOW = 12f * SCALE; - private static final float HEADER_H = 22f * SCALE; - private static final float LEFT_CARD_R = 4f * SCALE; - private static final float CARD_MARKER_W = 2.2f * SCALE; - private static final float MARQUEE_SPEED = 24f * SCALE; - private static final float MARQUEE_GAP = 18f * SCALE; - private static final float MARQUEE_PAUSE = 0.55f; - private static final float TITLE_FONT = 0.84f * TEXT_SCALE; - private static final float BODY_FONT = 0.60f * TEXT_SCALE; - private static final float SMALL_FONT = 0.49f * TEXT_SCALE; - private static final float METRIC_FONT = 0.56f * TEXT_SCALE; - private static final float DESCRIPTION_FONT = 0.53f * TEXT_SCALE; - private static final float SELECTED_DESCRIPTION_FONT = 0.54f * TEXT_SCALE; - private static final float ADDON_NAME_FONT = 0.66f * TEXT_SCALE; - private static final float SELECTED_NAME_FONT = 0.70f * TEXT_SCALE; - private static final float BUTTON_FONT = 0.52f * TEXT_SCALE; - private static final float BUTTON_H = 13f * SCALE; - private static final float METRIC_H = 13f * SCALE; - private static final float METRIC_GAP = 2.5f * SCALE; - private static final float CARD_H = 40f * SCALE; - private static final float CARD_GAP = 5f * SCALE; - private final Screen parent; - private final Map cardHover = new HashMap<>(); - private final Map cardActive = new HashMap<>(); - private final Map cardPress = new HashMap<>(); - private String selectedId = ""; - private String status = ""; - private long statusUntilMillis; - private List scanIssues = List.of(); - private float fixedWidth; - private float fixedHeight; - private float openAnim; - private float scrollOffset; - private float targetScrollOffset; - private float toggleHover; - private float togglePress; - private float scanHover; - private float scanPress; - private float modsHover; - private float modsPress; - private float configHover; - private float configPress; - private float backHover; - private float backPress; - private TextRenderer titleRenderer; - private TextRenderer bodyRenderer; + private final AddonManagerController controller = new AddonManagerController(); + private AddonManagerRenderer renderer; public LoVisualAddonManagerScreen(Screen parent) { super(Component.translatable("screen.lovisual.addon_manager.title")); this.parent = parent; } - private static float animate(float current, float target, float dt, float speed) { - float next = AnimationUtility.approach(current, target, dt, speed); - return AnimationUtility.snap(next, target, 0.001f); - } - - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static int withAlpha(int rgb, int alpha) { - return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); - } - - private static String tr(String key, Object... args) { - return I18n.get(key, args); - } - @Override protected void init() { - titleRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - bodyRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, titleRenderer); - ensureSelection(); + renderer = AddonManagerRenderer.create(); + controller.ensureSelection(); } @Override public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - updateUiMetrics(); - ensureSelection(); - clampScroll(); + controller.updateUiMetrics(minecraft, width, height); + controller.ensureSelection(); + controller.clampScroll(); renderShaderBackground(context); float fx = toFixedX(mouseX); float fy = toFixedY(mouseY); - updateAnimations(fx, fy); - - ViewportContext.beginUnscaledLogical(context); - Renderer2D.COLOR.begin(); - try { - renderDimmer(); - renderUi(fx, fy); - } finally { - Renderer2D.COLOR.render(); - ViewportContext.end(context); - } + controller.updateAnimations(fx, fy); + renderer.drawFrame(context, controller, fx, fy); } @Override @@ -160,22 +48,15 @@ public final class LoVisualAddonManagerScreen extends Screen { @Override public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { if (click.button() != 0) return true; - return handleClick(toFixedX((float) click.x()), toFixedY((float) click.y())); + float mx = toFixedX((float) click.x()); + float my = toFixedY((float) click.y()); + controller.handleClick(mx, my, this::closeToParent); + return true; } @Override public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { - float fx = toFixedX((float) mouseX); - float fy = toFixedY((float) mouseY); - Layout l = layout(); - float listX = l.rightX + 5f * SCALE; - float listY = bodyY(l); - float listW = RIGHT_W - 10f * SCALE; - float listH = listHeight(l); - if (!inside(fx, fy, listX, listY, listW, listH)) return false; - targetScrollOffset -= (float) verticalAmount * (25f * SCALE); - clampScroll(); - return true; + return controller.scrollBy(toFixedX((float) mouseX), toFixedY((float) mouseY), (float) verticalAmount); } @Override @@ -197,629 +78,8 @@ public final class LoVisualAddonManagerScreen extends Screen { return false; } - private void renderUi(float mouseX, float mouseY) { - Layout l = layout(); - float open = AnimationUtility.easeOutCubic(openAnim); - PanelColors c = colors(open); - - renderPanel(l.leftX, l.topY, LEFT_W, LEFT_TOP_H, c, open); - renderPanel(l.leftX, l.bottomY, LEFT_W, LEFT_BOTTOM_H, c, open); - renderPanel(l.rightX, l.topY, RIGHT_W, RIGHT_H, c, open); - - renderDetails(l, c, mouseX, mouseY); - renderActionsPanel(l, c, mouseX, mouseY); - renderList(l, c, mouseX, mouseY); - } - - private void renderDetails(Layout l, PanelColors c, float mouseX, float mouseY) { - List addons = AddonManager.snapshots(); - AddonSnapshot selected = selected(addons); - int active = 0; - int warnings = 0; - int errors = 0; - for (AddonSnapshot addon : addons) { - if (addon.enabled()) active++; - for (AddonIssue issue : addon.issues()) { - if (issue.severity() == AddonIssue.Severity.ERROR) errors++; - else if (issue.severity() == AddonIssue.Severity.WARNING) warnings++; - } - } - for (AddonIssue issue : scanIssues) { - if (issue.severity() == AddonIssue.Severity.ERROR) errors++; - else if (issue.severity() == AddonIssue.Severity.WARNING) warnings++; - } - - float x = l.leftX + 5f * SCALE; - float y = l.topY; - float w = LEFT_W - 10f * SCALE; - draw(titleRenderer, tr("screen.lovisual.addon_manager.header"), l.leftX + 8f * SCALE, y + 7f * SCALE, TITLE_FONT, c.title); - renderSelectedDetails(x, y + 27f * SCALE, w, c, selected, mouseX, mouseY); - - float metricsY = y + 75f * SCALE; - float metricStep = METRIC_H + METRIC_GAP; - renderMetricLine(x, metricsY, w, tr("screen.lovisual.addon_manager.installed"), String.valueOf(addons.size()), "package", c, inside(mouseX, mouseY, x, metricsY, w, METRIC_H)); - renderMetricLine(x, metricsY + metricStep, w, tr("screen.lovisual.addon_manager.active"), String.valueOf(active), "check", c, inside(mouseX, mouseY, x, metricsY + metricStep, w, METRIC_H)); - renderMetricLine(x, metricsY + metricStep * 2f, w, tr("screen.lovisual.addon_manager.issues"), errors + "/" + warnings, errors > 0 ? "folder-x" : "folder-clock", c, inside(mouseX, mouseY, x, metricsY + metricStep * 2f, w, METRIC_H)); - - if (statusVisible()) { - float statusY = y + LEFT_TOP_H - 15f * SCALE; - renderStatusLine(x, statusY, w, status, c, inside(mouseX, mouseY, x, statusY, w, 9f * SCALE)); - } - } - - private void renderActionsPanel(Layout l, PanelColors c, float mouseX, float mouseY) { - List addons = AddonManager.snapshots(); - AddonSnapshot selected = selected(addons); - float x = l.leftX + 5f * SCALE; - float y = l.bottomY + 6f * SCALE; - float w = LEFT_W - 10f * SCALE; - float half = (w - GAP) * 0.5f; - - renderAction(x, y, half, BUTTON_H, selected != null && selected.enabled() - ? tr("screen.lovisual.addon_manager.disable") - : tr("screen.lovisual.addon_manager.enable"), selected != null && selected.enabled(), selected != null && selected.enabled() ? "toggle-left" : "toggle-right", toggleHover, togglePress, c); - renderAction(x + half + GAP, y, half, BUTTON_H, tr("screen.lovisual.addon_manager.scan"), false, "folder-sync", scanHover, scanPress, c); - renderAction(x, y + 17f * SCALE, half, BUTTON_H, tr("screen.lovisual.addon_manager.mods"), false, "folder-open", modsHover, modsPress, c); - renderAction(x + half + GAP, y + 17f * SCALE, half, BUTTON_H, tr("screen.lovisual.addon_manager.config"), false, "folder-cog", configHover, configPress, c); - renderAction(x, y + 34f * SCALE, w, BUTTON_H, tr("screen.lovisual.addon_manager.back"), false, "x", backHover, backPress, c); - } - - private void renderList(Layout l, PanelColors c, float mouseX, float mouseY) { - renderListHeader(l.rightX + 8f * SCALE, l.topY, RIGHT_W - 16f * SCALE, c, mouseX, mouseY); - List addons = AddonManager.snapshots(); - float listX = l.rightX + 5f * SCALE; - float listY = bodyY(l); - float listW = RIGHT_W - 10f * SCALE; - float listH = listHeight(l); - if (addons.isEmpty()) { - renderEmptyState(listX, listY, listW, listH, c); - return; - } - - int columns = listColumns(addons.size(), listW); - float cardW = cardWidth(listW, columns); - boolean clipped = ScissorFunction.pushRaw(listX, listY - 3f * SCALE, listW, listH + 6f * SCALE); - try { - for (int i = 0; i < addons.size(); i++) { - int col = i % columns; - int row = i / columns; - float cardX = listX + col * (cardW + CARD_GAP); - float cardY = listY + row * (CARD_H + CARD_GAP) - scrollOffset; - if (cardY + CARD_H < listY - 10f || cardY > listY + listH + 10f) continue; - renderAddonCard(addons.get(i), cardX, cardY, cardW, CARD_H, c, inside(mouseX, mouseY, cardX, cardY, cardW, CARD_H)); - } - } finally { - if (clipped) ScissorFunction.pop(); - } - - int rows = rowsFor(addons.size(), columns); - float contentH = Math.max(0f, rows * CARD_H + Math.max(0, rows - 1) * CARD_GAP); - if (contentH > listH + 1f) { - float trackX = listX + listW + 2.5f * SCALE; - float thumbH = Math.max(18f * SCALE, listH * listH / contentH); - float thumbY = listY + (listH - thumbH) * (scrollOffset / Math.max(1f, contentH - listH)); - Renderer2D.COLOR.roundedRect(trackX, listY, 1.4f * SCALE, listH, 0.7f * SCALE, 1f, c.scrollTrack); - Renderer2D.COLOR.roundedRect(trackX - 0.2f * SCALE, thumbY, 1.8f * SCALE, thumbH, 0.9f * SCALE, 1f, c.accent); - } - } - - private void renderAddonCard(AddonSnapshot addon, float x, float y, float w, float h, PanelColors c, boolean hovered) { - float hover = AnimationUtility.easeOutCubic(cardHover.getOrDefault(addon.id(), 0f)); - float active = AnimationUtility.easeOutCubic(cardActive.getOrDefault(addon.id(), 0f)); - float press = AnimationUtility.easeOutCubic(cardPress.getOrDefault(addon.id(), 0f)); - Themes.Theme theme = Theme.theme(); - int activeAccent = withAlpha(theme.accent(), Math.round(165f * active)); - - int fillBase = HudRenderUtil.mixColor(c.surface, c.surfaceHover, 0.38f * hover + 0.10f * press); - int fillTop = HudRenderUtil.mixColor(fillBase, c.surfaceHover, 0.03f * press); - int fillBottom = HudRenderUtil.mixColor(fillBase, 0xFF05070A, 0.18f); - int stroke = HudRenderUtil.mixColor(c.stroke, c.strokeSoft, 0.45f * hover); - stroke = HudRenderUtil.mixColor(stroke, activeAccent, 0.38f * active + 0.10f * press); - - Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 4f * SCALE, 8f * SCALE, 0.018f + hover * 0.018f + active * 0.010f, c.shadow); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 4f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); - renderActiveCardMarker(x, y, w, h, active, theme); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 4f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f + hover * 0.03f), stroke, 90f); - - float iconBox = 24f * SCALE; - float iconX = x + 7f * SCALE; - float iconY = y + 8f * SCALE; - Renderer2D.COLOR.roundedRectGradientQuad(iconX, iconY, iconBox, iconBox, 3f * SCALE, 1f, withAlpha(c.surface, 170), withAlpha(c.surfaceHover, 180), withAlpha(0xFF0D1118, 184), withAlpha(0xFF0D1118, 184)); - Renderer2D.COLOR.roundedRectStrokeGradient(iconX, iconY, iconBox, iconBox, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(c.stroke, c.strokeSoft, 0.35f), c.stroke, 90f); - drawSvgGradient(addonIcon(addon), iconX + 6.2f * SCALE, iconY + 6.2f * SCALE, 11.6f * SCALE, 11.6f * SCALE, c); - - String state = statusText(addon); - float stateW = miniStatusWidth(state); - float stateX = x + w - stateW - 6f * SCALE; - renderMiniStatusPill(stateX, y + 7f * SCALE, state, statusColor(addon, c), c); - - float textX = x + 37f * SCALE; - float titleW = Math.max(1f, stateX - textX - 5f * SCALE); - drawFitOrMarquee(bodyRenderer, addon.name(), textX, y + 8.3f * SCALE, ADDON_NAME_FONT, titleW, c.title, hovered); - - String description = addonInfo(addon); - if (!description.isBlank()) { - float descW = Math.max(1f, x + w - 7f * SCALE - textX); - drawFitOrMarquee(bodyRenderer, description, textX, y + 20.3f * SCALE, DESCRIPTION_FONT, descW, addon.restartRequired() ? c.label : c.muted, hovered); - } - drawFitOrMarquee(bodyRenderer, countsText(addon), textX, y + 29.6f * SCALE, SMALL_FONT, Math.max(1f, x + w - 7f * SCALE - textX), c.mutedLabel, hovered); - } - - private void renderMetricLine(float x, float y, float w, String label, String value, String svg, PanelColors c, boolean hovered) { - float r = 3.5f * SCALE; - int fillTop = HudRenderUtil.mixColor(withAlpha(c.surface, 144), withAlpha(c.surfaceHover, 166), hovered ? 0.34f : 0.12f); - int fillBottom = HudRenderUtil.mixColor(withAlpha(0xFF0D1118, 158), withAlpha(c.surface, 150), hovered ? 0.18f : 0.05f); - int stroke = HudRenderUtil.mixColor(c.stroke, c.strokeSoft, hovered ? 0.42f : 0.20f); - Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, METRIC_H, r, r, r, r, 1f, fillTop, fillTop, fillBottom, fillBottom); - Renderer2D.COLOR.roundedRectStrokeCorners(x, y, w, METRIC_H, r, r, r, r, 0.55f, stroke); - drawSvgGradient(svg, x + 4.0f * SCALE, y + 3.0f * SCALE, 7.0f * SCALE, 7.0f * SCALE, c); - float valueW = width(bodyRenderer, value, METRIC_FONT); - float textY = y + 3.15f * SCALE; - drawFitOrMarquee(bodyRenderer, label, x + 14.0f * SCALE, textY, METRIC_FONT, Math.max(1f, w - valueW - 23f * SCALE), c.muted, hovered); - draw(bodyRenderer, value, x + w - valueW - 5f * SCALE, textY, METRIC_FONT, c.title); - } - - private void renderSelectedDetails(float x, float y, float w, PanelColors c, AddonSnapshot selected, float mouseX, float mouseY) { - float h = 42f * SCALE; - Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, h, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, 1f, - withAlpha(c.surface, 160), withAlpha(c.surfaceHover, 170), withAlpha(0xFF0D1118, 178), withAlpha(0xFF0D1118, 178)); - Renderer2D.COLOR.roundedRectStrokeCorners(x, y, w, h, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, 0.55f, HudRenderUtil.mixColor(c.stroke, c.strokeSoft, 0.36f)); - if (selected == null) { - drawCentered(bodyRenderer, tr("screen.lovisual.addon_manager.empty"), x + w * 0.5f, y + 17f * SCALE, BODY_FONT, c.mutedLabel); - return; - } - float iconBox = 18f * SCALE; - float iconX = x + 6f * SCALE; - float iconY = y + 7f * SCALE; - Renderer2D.COLOR.roundedRectGradientQuad(iconX, iconY, iconBox, iconBox, 3f * SCALE, 1f, withAlpha(c.surface, 176), withAlpha(c.surfaceHover, 186), withAlpha(0xFF0D1118, 190), withAlpha(0xFF0D1118, 190)); - Renderer2D.COLOR.roundedRectStrokeGradient(iconX, iconY, iconBox, iconBox, 3f * SCALE, 1f, 0.5f, c.stroke, c.strokeSoft, 90f); - drawSvgGradient(addonIcon(selected), iconX + 4.3f * SCALE, iconY + 4.3f * SCALE, 9.4f * SCALE, 9.4f * SCALE, c); - - String state = statusText(selected); - float stateW = miniStatusWidth(state); - float titleX = iconX + iconBox + 5f * SCALE; - float stateX = x + w - stateW - 6f * SCALE; - boolean drawState = stateX > titleX + 30f * SCALE; - if (drawState) { - renderMiniStatusPill(stateX, y + 6f * SCALE, state, statusColor(selected, c), c); - } - - float titleW = Math.max(1f, (drawState ? stateX - titleX - 5f * SCALE : x + w - titleX - 7f * SCALE)); - drawFitOrMarquee(bodyRenderer, selected.name(), titleX, y + 7.4f * SCALE, SELECTED_NAME_FONT, titleW, c.title, inside(mouseX, mouseY, titleX, y + 6f * SCALE, titleW, 10f * SCALE)); - - String info = addonInfo(selected); - if (!info.isBlank()) { - drawFitOrMarquee(bodyRenderer, info, titleX, y + 19.4f * SCALE, SELECTED_DESCRIPTION_FONT, Math.max(1f, x + w - titleX - 7f * SCALE), selected.restartRequired() ? c.label : c.muted, inside(mouseX, mouseY, titleX, y + 18f * SCALE, w - 35f * SCALE, 10f * SCALE)); - } - drawFitOrMarquee(bodyRenderer, countsText(selected), x + 7f * SCALE, y + 31.0f * SCALE, SMALL_FONT, w - 14f * SCALE, c.mutedLabel, inside(mouseX, mouseY, x + 7f * SCALE, y + 30f * SCALE, w - 14f * SCALE, 10f * SCALE)); - } - - private void renderStatusLine(float x, float y, float w, String text, PanelColors c, boolean hovered) { - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, 9f * SCALE, 4.5f * SCALE, 1f, c.statusTop, c.statusTop, c.statusBottom, c.statusBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, 9f * SCALE, 4.5f * SCALE, 1.2f, 0.4f, c.strokeSoft, c.stroke, 90f); - drawFitOrMarquee(bodyRenderer, text, x + 5f * SCALE, y + 1.8f * SCALE, SMALL_FONT, w - 10f * SCALE, c.muted, hovered); - } - - private void renderListHeader(float x, float headerTop, float w, PanelColors c, float mouseX, float mouseY) { - float centerY = headerTop + HEADER_H * 0.5f; - float iconSize = 13.0f * SCALE; - float iconX = x + 7.0f * SCALE; - float iconY = centerY - iconSize * 0.5f; - drawSvgGradient("folder-tree", iconX, iconY, iconSize, iconSize, c); - String title = tr("screen.lovisual.addon_manager.list"); - float titleX = x + 25.0f * SCALE; - float titleW = Math.max(1f, w - 33f * SCALE); - float titleY = centerY - 4.8f * SCALE; - drawFitOrMarquee(titleRenderer, title, titleX, titleY, TITLE_FONT, titleW, c.title, inside(mouseX, mouseY, titleX, titleY - 1.5f * SCALE, titleW, 12f * SCALE)); - if (!scanIssues.isEmpty()) { - String label = tr("screen.lovisual.addon_manager.status.scanned", scanIssues.size()); - float labelW = Math.min(w * 0.34f, Math.max(42f * SCALE, width(bodyRenderer, label, SMALL_FONT))); - float labelX = x + w - labelW - 8f * SCALE; - drawFitOrMarquee(bodyRenderer, label, labelX, centerY - 3.6f * SCALE, SMALL_FONT, labelW, c.muted, inside(mouseX, mouseY, labelX, centerY - 5f * SCALE, labelW, 10f * SCALE)); - } - } - - private void renderEmptyState(float x, float y, float w, float h, PanelColors c) { - float boxW = Math.min(w - 18f * SCALE, 110f * SCALE); - float boxH = 44f * SCALE; - float boxX = x + w * 0.5f - boxW * 0.5f; - float boxY = y + h * 0.5f - boxH * 0.5f; - Renderer2D.COLOR.roundedRectGradientQuad(boxX, boxY, boxW, boxH, 7f * SCALE, 1f, c.detailTop, c.detailTop, c.detailBottom, c.detailBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(boxX, boxY, boxW, boxH, 7f * SCALE, 1.2f, 0.5f, c.strokeSoft, c.stroke, 90f); - drawSvgGradient("folder-open", boxX + boxW * 0.5f - 8f * SCALE, boxY + 7f * SCALE, 16f * SCALE, 16f * SCALE, c); - drawCentered(bodyRenderer, tr("screen.lovisual.addon_manager.empty"), boxX + boxW * 0.5f, boxY + 27f * SCALE, SMALL_FONT, c.muted); - } - - private float miniStatusWidth(String label) { - return Math.max(28f * SCALE, width(bodyRenderer, label, SMALL_FONT) + 11f * SCALE); - } - - private void renderActiveCardMarker(float x, float y, float w, float h, float active, Themes.Theme theme) { - float reveal = AnimationUtility.clamp01(active); - if (reveal <= 0.001f) return; - - float markerW = CARD_MARKER_W * reveal; - if (markerW <= 0.01f) return; - - int accent = withAlpha(theme.accent(), Math.round(255f * reveal)); - boolean clipped = ScissorFunction.pushRaw(x, y, markerW, h); - if (!clipped) return; - try { - Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, h, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, LEFT_CARD_R, 1f, accent, accent, accent, accent); - } finally { - ScissorFunction.pop(); - } - } - - private void renderMiniStatusPill(float x, float y, String label, int color, PanelColors c) { - float w = miniStatusWidth(label); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, 9f * SCALE, 4.5f * SCALE, 1f, HudRenderUtil.mixColor(withAlpha(c.surface, 170), color, 0.08f), withAlpha(c.surface, 170), withAlpha(0xFF0D1118, 178), withAlpha(0xFF0D1118, 178)); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, 9f * SCALE, 4.5f * SCALE, 1f, 0.42f, withAlpha(color, 112), c.stroke, 90f); - Renderer2D.COLOR.roundedRect(x + 3.6f * SCALE, y + 3.2f * SCALE, 2.4f * SCALE, 2.4f * SCALE, 1.2f * SCALE, 1f, color); - draw(bodyRenderer, label, x + 8f * SCALE, y + 1.75f * SCALE, SMALL_FONT, c.label); - } - - private void renderAction(float x, float y, float w, float h, String label, boolean destructive, String svgName, float hover, float press, PanelColors c) { - float hoverAnim = AnimationUtility.easeOutCubic(hover); - float pressAnim = AnimationUtility.easeOutCubic(press); - int fillTop; - int fillBottom; - int stroke; - int text; - if (destructive) { - fillTop = HudRenderUtil.mixColor(withAlpha(0x1A1416, 170), withAlpha(0x4B2224, 212), 0.25f + hoverAnim * 0.45f + pressAnim * 0.12f); - fillBottom = HudRenderUtil.mixColor(withAlpha(0x120E10, 165), withAlpha(0x261012, 220), 0.20f + hoverAnim * 0.40f + pressAnim * 0.10f); - stroke = HudRenderUtil.mixColor(withAlpha(0x352A2A, 215), withAlpha(0x8C5054, 235), 0.22f + hoverAnim * 0.58f + pressAnim * 0.12f); - text = HudRenderUtil.mixColor(withAlpha(0xD0A0A0, 255), withAlpha(0xFFB2B2, 255), 0.24f + hoverAnim * 0.56f + pressAnim * 0.08f); - } else { - fillTop = HudRenderUtil.mixColor(withAlpha(c.surface, 176), withAlpha(0x223746, 212), 0.20f + hoverAnim * 0.44f + pressAnim * 0.10f); - fillBottom = HudRenderUtil.mixColor(withAlpha(c.surface, 166), withAlpha(0x13292F, 220), 0.16f + hoverAnim * 0.40f + pressAnim * 0.08f); - stroke = HudRenderUtil.mixColor(c.stroke, withAlpha(0x466A76, 228), 0.18f + hoverAnim * 0.50f + pressAnim * 0.10f); - text = HudRenderUtil.mixColor(withAlpha(0xD0D8E4, 255), withAlpha(0xEEF8FF, 255), 0.18f + hoverAnim * 0.38f + pressAnim * 0.08f); - } - - Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 3f * SCALE, 7f * SCALE, 0.018f + hoverAnim * 0.015f, c.shadow); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); - float contentLift = hoverAnim * 0.65f * SCALE + pressAnim * 0.55f * SCALE; - drawFitOrMarquee(bodyRenderer, label, x + 5f * SCALE, y + 3.7f * SCALE - contentLift, BUTTON_FONT, w - 18f * SCALE, text, hoverAnim > 0.55f); - float iconSize = 6.7f * SCALE; - Renderer2D.COLOR.svg(svgName, x + w - 10.7f * SCALE, y + (h - iconSize) * 0.5f - contentLift, iconSize, iconSize, SvgRenderOptions.overrideColor(text)); - } - - private void drawFitOrMarquee(TextRenderer renderer, String text, float x, float y, float size, float maxWidth, int color, boolean marquee) { - if (text == null || text.isEmpty() || maxWidth <= 1f) return; - float textW = width(renderer, text, size); - if (textW <= maxWidth) { - draw(renderer, text, x, y, size, color); - return; - } - if (!marquee) { - draw(renderer, ellipsize(renderer, text, size, maxWidth), x, y, size, color); - return; - } - drawScrollingText(renderer, text, x, y, textW, maxWidth, size, color); - } - - private void drawScrollingText(TextRenderer renderer, String text, float x, float y, float fullWidth, float viewWidth, float size, int color) { - if (renderer == null || text == null || text.isEmpty() || viewWidth <= 1f) return; - float cycleDistance = fullWidth + MARQUEE_GAP; - float cycleTime = MARQUEE_PAUSE + cycleDistance / Math.max(1f, MARQUEE_SPEED); - float time = (Util.getMillis() / 1000.0f) % cycleTime; - float offset = time <= MARQUEE_PAUSE ? 0f : -(time - MARQUEE_PAUSE) * MARQUEE_SPEED; - float fade = Math.min(viewWidth * 0.25f, 8f * SCALE); - RenderColor renderColor = new RenderColor(color); - renderer.begin(size, false, false); - try { - renderer.renderHorizontalFadeClipped(text, x + offset, y, renderColor, x, x + viewWidth, fade, fade, false); - if (cycleDistance > viewWidth * 0.5f) { - renderer.renderHorizontalFadeClipped(text, x + offset + cycleDistance, y, renderColor, x, x + viewWidth, fade, fade, false); - } - } finally { - renderer.end(); - } - } - - private int listColumns(int count, float listW) { - if (count <= 1) return 1; - return listW >= 250f * SCALE ? 2 : 1; - } - - private int rowsFor(int count, int columns) { - if (count <= 0) return 0; - return (count + Math.max(1, columns) - 1) / Math.max(1, columns); - } - - private float cardWidth(float listW, int columns) { - int safeColumns = Math.max(1, columns); - return safeColumns == 1 ? listW : (listW - CARD_GAP * (safeColumns - 1)) / safeColumns; - } - - private boolean handleClick(float mouseX, float mouseY) { - Layout l = layout(); - List addons = AddonManager.snapshots(); - AddonSnapshot selected = selected(addons); - float x = l.leftX + 5f * SCALE; - float y = l.bottomY + 6f * SCALE; - float w = LEFT_W - 10f * SCALE; - float half = (w - GAP) * 0.5f; - - if (selected != null && inside(mouseX, mouseY, x, y, half, BUTTON_H)) { - togglePress = 1f; - boolean next = !selected.enabled(); - if (AddonManager.setEnabled(selected.id(), next)) { - setStatus(""); - } - return true; - } - if (inside(mouseX, mouseY, x + half + GAP, y, half, BUTTON_H)) { - scanPress = 1f; - scanIssues = AddonManager.scan(); - setStatus(scanIssues.isEmpty() ? "" : tr("screen.lovisual.addon_manager.status.scanned", scanIssues.size())); - return true; - } - if (inside(mouseX, mouseY, x, y + 17f * SCALE, half, BUTTON_H)) { - modsPress = 1f; - if (openPath(FabricLoader.getInstance().getGameDir().resolve("mods"))) { - setStatus(tr("screen.lovisual.addon_manager.status.opened_mods")); - } - return true; - } - if (inside(mouseX, mouseY, x + half + GAP, y + 17f * SCALE, half, BUTTON_H)) { - configPress = 1f; - if (openPath(FabricLoader.getInstance().getConfigDir().resolve("lovisual").resolve("addons"))) { - setStatus(tr("screen.lovisual.addon_manager.status.opened_config")); - } - return true; - } - if (inside(mouseX, mouseY, x, y + 34f * SCALE, w, BUTTON_H)) { - backPress = 1f; - closeToParent(); - return true; - } - - float listX = l.rightX + 5f * SCALE; - float listY = bodyY(l); - float listW = RIGHT_W - 10f * SCALE; - float listH = listHeight(l); - int columns = listColumns(addons.size(), listW); - float cardW = cardWidth(listW, columns); - if (!inside(mouseX, mouseY, listX, listY, listW, listH)) return true; - for (int i = 0; i < addons.size(); i++) { - int col = i % columns; - int row = i / columns; - float cardX = listX + col * (cardW + CARD_GAP); - float cardY = listY + row * (CARD_H + CARD_GAP) - scrollOffset; - if (inside(mouseX, mouseY, cardX, cardY, cardW, CARD_H)) { - selectedId = addons.get(i).id(); - cardPress.put(selectedId, 1f); - setStatus(""); - return true; - } - } - return true; - } - - private void updateAnimations(float mouseX, float mouseY) { - float dt = AnimationUtility.deltaTime(); - openAnim = animate(openAnim, 1f, dt, 7f); - Layout l = layout(); - float x = l.leftX + 5f * SCALE; - float y = l.bottomY + 6f * SCALE; - float w = LEFT_W - 10f * SCALE; - float half = (w - GAP) * 0.5f; - toggleHover = animate(toggleHover, inside(mouseX, mouseY, x, y, half, BUTTON_H) ? 1f : 0f, dt, 11f); - scanHover = animate(scanHover, inside(mouseX, mouseY, x + half + GAP, y, half, BUTTON_H) ? 1f : 0f, dt, 11f); - modsHover = animate(modsHover, inside(mouseX, mouseY, x, y + 17f * SCALE, half, BUTTON_H) ? 1f : 0f, dt, 11f); - configHover = animate(configHover, inside(mouseX, mouseY, x + half + GAP, y + 17f * SCALE, half, BUTTON_H) ? 1f : 0f, dt, 11f); - backHover = animate(backHover, inside(mouseX, mouseY, x, y + 34f * SCALE, w, BUTTON_H) ? 1f : 0f, dt, 11f); - togglePress = animate(togglePress, 0f, dt, 8f); - scanPress = animate(scanPress, 0f, dt, 8f); - modsPress = animate(modsPress, 0f, dt, 8f); - configPress = animate(configPress, 0f, dt, 8f); - backPress = animate(backPress, 0f, dt, 8f); - if (!status.isBlank() && statusUntilMillis > 0L && Util.getMillis() > statusUntilMillis) { - status = ""; - statusUntilMillis = 0L; - } - - List addons = AddonManager.snapshots(); - Set keys = new HashSet<>(); - float listX = l.rightX + 5f * SCALE; - float listY = bodyY(l); - float listW = RIGHT_W - 10f * SCALE; - int columns = listColumns(addons.size(), listW); - float cardW = cardWidth(listW, columns); - for (int i = 0; i < addons.size(); i++) { - AddonSnapshot addon = addons.get(i); - keys.add(addon.id()); - int col = i % columns; - int row = i / columns; - float cardX = listX + col * (cardW + CARD_GAP); - float cardY = listY + row * (CARD_H + CARD_GAP) - scrollOffset; - boolean hover = inside(mouseX, mouseY, cardX, cardY, cardW, CARD_H); - cardHover.put(addon.id(), animate(cardHover.getOrDefault(addon.id(), 0f), hover ? 1f : 0f, dt, 10f)); - cardActive.put(addon.id(), animate(cardActive.getOrDefault(addon.id(), 0f), addon.id().equals(selectedId) ? 1f : 0f, dt, 8f)); - cardPress.put(addon.id(), animate(cardPress.getOrDefault(addon.id(), 0f), 0f, dt, 7f)); - } - prune(cardHover, keys); - prune(cardActive, keys); - prune(cardPress, keys); - scrollOffset = animate(scrollOffset, targetScrollOffset, dt, 12f); - } - - private void ensureSelection() { - List addons = AddonManager.snapshots(); - if (addons.isEmpty()) { - selectedId = ""; - return; - } - if (selected(addons) == null) { - selectedId = addons.get(0).id(); - } - } - - private AddonSnapshot selected(List addons) { - for (AddonSnapshot addon : addons) { - if (addon.id().equals(selectedId)) return addon; - } - return null; - } - - private void clampScroll() { - List addons = AddonManager.snapshots(); - float listW = RIGHT_W - 10f * SCALE; - int columns = listColumns(addons.size(), listW); - int rows = rowsFor(addons.size(), columns); - float listH = listHeight(layout()); - float contentH = Math.max(0f, rows * CARD_H + Math.max(0, rows - 1) * CARD_GAP); - float max = Math.max(0f, contentH - listH); - targetScrollOffset = Mth.clamp(targetScrollOffset, 0f, max); - scrollOffset = Mth.clamp(scrollOffset, 0f, max); - } - - private boolean openPath(Path path) { - try { - Files.createDirectories(path); - Util.getPlatform().openUri(path.toUri()); - return true; - } catch (Throwable ignored) { - setStatus(tr("screen.lovisual.addon_manager.status.open_failed")); - return false; - } - } - - private String addonIcon(AddonSnapshot addon) { - return switch (addon.status()) { - case ACTIVE -> addon.enabled() ? "folder-check" : "folder"; - case ERROR -> "folder-x"; - case DISABLED -> "folder"; - case SUSPENDED -> "folder-clock"; - case SHUTDOWN -> "folder-output"; - default -> "package"; - }; - } - - private String statusText(AddonSnapshot addon) { - if (!addon.enabled()) return tr("screen.lovisual.addon_manager.status.disabled_short"); - return switch (addon.status()) { - case ACTIVE -> tr("screen.lovisual.addon_manager.state.active"); - case ERROR -> tr("screen.lovisual.addon_manager.state.error"); - case DISABLED -> tr("screen.lovisual.addon_manager.state.disabled"); - case SUSPENDED -> tr("screen.lovisual.addon_manager.state.suspended"); - case SHUTDOWN -> tr("screen.lovisual.addon_manager.state.shutdown"); - default -> addon.status().name().toLowerCase(Locale.ROOT); - }; - } - - private String countsText(AddonSnapshot addon) { - int hud = addon.draggableHudElements() + addon.staticHudElements(); - return tr("screen.lovisual.addon_manager.metric.modules") + " " + addon.modules() - + " · " + tr("screen.lovisual.addon_manager.metric.hud") + " " + hud - + " · " + tr("screen.lovisual.addon_manager.metric.commands") + " " + addon.commands() - + " · " + tr("screen.lovisual.addon_manager.metric.gui") + " " + addon.clickGuiSections(); - } - - private int statusColor(AddonSnapshot addon, PanelColors c) { - return switch (addon.status()) { - case ACTIVE -> c.accent; - case ERROR -> withAlpha(0xFF8888, 255); - case DISABLED -> c.muted; - default -> c.label; - }; - } - - private void renderPanel(float x, float y, float w, float h, PanelColors c, float open) { - Renderer2D.COLOR.blurRect(x, y, w, h, PANEL_R, 15f, 1.0f, 0.30f, 0xFFFFFF); - Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, PANEL_R, PANEL_SHADOW, 0.022f + open * 0.012f, c.shadow); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, PANEL_R, 1f, c.bgTopLeft, c.bgTopRight, c.bgBottomRight, c.bgBottomLeft); - Renderer2D.COLOR.roundedRectMaskedQuad(x, y, w, HEADER_H, x, y, w, h, PANEL_R, 1f, - c.headerTopLeft, c.headerTopRight, c.headerBottomRight, c.headerBottomLeft); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, PANEL_R, 1f, 1f, HudRenderUtil.mixColor(c.stroke, 0xFFFFFFFF, 0.04f), c.stroke, 90f); - } - - private PanelColors colors(float alpha) { - int bgAlpha = Math.round(alpha * 120f); - int headerAlpha = Math.round(alpha * 150f); - int strokeAlpha = Math.round(alpha * 100f); - int shadowAlpha = Math.round(alpha * 80f); - int titleAlpha = Math.round(alpha * 255f); - int labelAlpha = Math.round(alpha * 155f); - - Themes.Theme theme = Theme.theme(); - int themeAccent = theme != null ? theme.accent() : 0xFFFFFFFF; - int themeAccentSoft = theme != null ? theme.accentSoft() : themeAccent; - - int bgTopLeft = withAlpha(0x0D0F14, bgAlpha); - int bgTopRight = withAlpha(0x101218, bgAlpha); - int bgBottomLeft = withAlpha(0x08090C, bgAlpha); - int bgBottomRight = withAlpha(0x0D0F14, bgAlpha); - int headerTopLeft = withAlpha(0x14171F, headerAlpha); - int headerTopRight = withAlpha(0x181B24, headerAlpha); - int headerBottomLeft = withAlpha(0x10131A, headerAlpha); - int headerBottomRight = withAlpha(0x14171F, headerAlpha); - int stroke = withAlpha(0x252A36, strokeAlpha); - int strokeSoft = withAlpha(0x3A4A5A, Math.round(alpha * 150f)); - int surface = withAlpha(0x1A1D24, Math.round(alpha * 160f)); - int surfaceHover = withAlpha(0x1A1F28, Math.round(alpha * 200f)); - int accent = withAlpha(themeAccent, titleAlpha); - int accentSoft = withAlpha(themeAccentSoft, Math.round(alpha * 180f)); - int detailTop = withAlpha(0x1A1D24, Math.round(alpha * 160f)); - int detailBottom = withAlpha(0x0D1118, Math.round(alpha * 185f)); - int statusTop = withAlpha(0x1A1D24, Math.round(alpha * 150f)); - int statusBottom = withAlpha(0x0D1118, Math.round(alpha * 175f)); - int iconTop = withAlpha(themeAccent, titleAlpha); - int iconBottom = withAlpha(HudRenderUtil.mixColor(themeAccent, themeAccentSoft, 0.58f), titleAlpha); - int iconShadow = withAlpha(0x060810, Math.round(alpha * 150f)); - - return new PanelColors( - bgTopLeft, bgTopRight, bgBottomRight, bgBottomLeft, - headerTopLeft, headerTopRight, headerBottomRight, headerBottomLeft, - stroke, strokeSoft, - withAlpha(0x060810, shadowAlpha), - withAlpha(0xFFFFFF, titleAlpha), - withAlpha(0xFFFFFF, labelAlpha), - withAlpha(0x808890, titleAlpha), - withAlpha(0x606878, labelAlpha), - surface, surfaceHover, - accent, accentSoft, - detailTop, detailBottom, - statusTop, statusBottom, - withAlpha(0x3A4A5A, Math.round(alpha * 132f)), - iconTop, iconBottom, iconShadow - ); - } - - private void renderDimmer() { - Themes.Theme t = Theme.theme(); - int soft = withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.46f), 140); - int deep = withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.68f), 180); - Renderer2D.COLOR.quad(0, 0, fixedWidth, fixedHeight, soft, soft, deep, deep); - } - - private void renderShaderBackground(GuiGraphicsExtractor context) { - Minecraft mc = this.minecraft; - if (mc == null) return; - MainConfig cfg = MainConfig.get(); - String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; - if (mode == null || mode.equalsIgnoreCase("off")) { - context.fill(0, 0, width, height, 0xFF000000); - return; - } - MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); - } - - private void updateUiMetrics() { - Minecraft mc = minecraft; - if (mc == null) { - fixedWidth = width; - fixedHeight = height; - return; - } - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - fixedWidth = Math.max(1f, HudScale.virtualWidth(fbw, fbh)); - fixedHeight = Math.max(1f, HudScale.virtualHeight(fbw, fbh)); + private void closeToParent() { + if (minecraft != null) ClientScreen.show(minecraft, parent); } private float toFixedX(float screenX) { @@ -838,152 +98,15 @@ public final class LoVisualAddonManagerScreen extends Screen { return screenY * mc.getWindow().getGuiScale() / hudScale; } - private Layout layout() { - float totalW = LEFT_W + GAP + RIGHT_W; - float totalH = LEFT_TOP_H + GAP + LEFT_BOTTOM_H; - float topY = fixedHeight * 0.5f - totalH * 0.5f; - float leftX = fixedWidth * 0.5f - totalW * 0.5f; - return new Layout(leftX, topY, leftX + LEFT_W + GAP, topY + LEFT_TOP_H + GAP); - } - - private float bodyY(Layout l) { - return l.topY + 28f * SCALE; - } - - private float listHeight(Layout l) { - return Math.max(1f, RIGHT_H - 31f * SCALE); - } - - private void closeToParent() { - if (minecraft != null) ClientScreen.show(minecraft, parent); - } - - private void draw(TextRenderer renderer, String text, float x, float y, float size, int argb) { - if (renderer == null || text == null || text.isEmpty()) return; - renderer.setAlpha(1.0); - renderer.begin(size, false, false); - renderer.render(text, x, y, new RenderColor(argb), false); - renderer.end(); - } - - private void drawCentered(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { - draw(renderer, text, centerX - width(renderer, text, size) * 0.5f, y, size, argb); - } - private SvgRenderOptions svgGradient(PanelColors c) { - return SvgRenderOptions.linearGradient(c.iconTop, c.iconBottom, 92f) - .withTextureCache(false) - .withCurveFlatness(0.18f); - } - - private void drawSvgGradient(String svg, float x, float y, float w, float h, PanelColors c) { - Renderer2D.COLOR.svg(svg, x + 0.38f * SCALE, y + 0.42f * SCALE, w, h, - SvgRenderOptions.overrideColor(c.iconShadow).withTextureCache(false).withCurveFlatness(0.18f)); - Renderer2D.COLOR.svg(svg, x, y, w, h, svgGradient(c)); - } - - private float width(TextRenderer renderer, String text, float size) { - if (renderer == null || text == null) return 0f; - renderer.begin(size, true, false); - float w = (float) renderer.getWidth(text, false); - renderer.end(); - return w; - } - - private String ellipsize(String text, float size, float maxWidth) { - return ellipsize(bodyRenderer, text, size, maxWidth); - } - - private String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) { - if (text == null || text.isEmpty()) return ""; - if (width(renderer, text, size) <= maxWidth) return text; - String out = text; - while (out.length() > 3 && width(renderer, out + "...", size) > maxWidth) { - out = out.substring(0, out.length() - 1); - } - return out + "..."; - } - - private boolean statusVisible() { - return !status.isBlank() && (statusUntilMillis <= 0L || Util.getMillis() <= statusUntilMillis); - } - - private void setStatus(String text) { - if (text == null || text.isBlank()) { - status = ""; - statusUntilMillis = 0L; + private void renderShaderBackground(GuiGraphicsExtractor context) { + Minecraft mc = this.minecraft; + if (mc == null) return; + MainConfig cfg = MainConfig.get(); + String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; + if (mode == null || mode.equalsIgnoreCase("off")) { + context.fill(0, 0, width, height, 0xFF000000); return; } - status = text; - statusUntilMillis = Util.getMillis() + 1600L; - } - - private String addonInfo(AddonSnapshot addon) { - if (addon == null) return ""; - AddonIssue issue = primaryIssue(addon); - if (issue != null) return issueText(issue); - if (addon.restartRequired()) return tr("screen.lovisual.addon_manager.restart_required"); - return addonDescription(addon); - } - - private AddonIssue primaryIssue(AddonSnapshot addon) { - AddonIssue fallback = null; - for (AddonIssue issue : addon.issues()) { - if (issue.severity() == AddonIssue.Severity.ERROR) return issue; - if (fallback == null && issue.severity() == AddonIssue.Severity.WARNING) fallback = issue; - } - return fallback; - } - - private String issueText(AddonIssue issue) { - if (issue == null) return ""; - String message = issue.message() == null ? "" : issue.message().trim(); - String detail = issue.detail() == null ? "" : issue.detail().trim(); - String body = !message.isEmpty() ? message : detail; - if (!message.isEmpty() && !detail.isEmpty()) body = message + " · " + detail; - if (body.isEmpty()) body = issue.severity().name().toLowerCase(Locale.ROOT); - return body; - } - - private String addonDescription(AddonSnapshot addon) { - if (addon == null || addon.description() == null) return ""; - return addon.description().trim(); - } - - private static void prune(Map map, Set keys) { - map.keySet().removeIf(key -> !keys.contains(key)); - } - - private record Layout(float leftX, float topY, float rightX, float bottomY) { - } - - private record PanelColors( - int bgTopLeft, - int bgTopRight, - int bgBottomRight, - int bgBottomLeft, - int headerTopLeft, - int headerTopRight, - int headerBottomRight, - int headerBottomLeft, - int stroke, - int strokeSoft, - int shadow, - int title, - int label, - int muted, - int mutedLabel, - int surface, - int surfaceHover, - int accent, - int accentSoft, - int detailTop, - int detailBottom, - int statusTop, - int statusBottom, - int scrollTrack, - int iconTop, - int iconBottom, - int iconShadow - ) { + MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java index 6b1094b..09ec153 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java @@ -1,102 +1,80 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - package dev.loki.lovisual.features.gui.mainmenu; +import dev.loki.lovisual.config.i18n.MainConfig; import dev.loki.lovisual.features.gui.mainmenu.alt.LoVisualAltManagerScreen; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.features.account.AccountConfig; -import dev.loki.lovisual.features.account.AccountEntry; -import dev.loki.lovisual.features.account.SkinManager; +import dev.loki.lovisual.render.engine.animation.AnimatedClockText; import dev.loki.lovisual.util.screen.ClientScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.TitleScreen; import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen; import net.minecraft.client.gui.screens.options.OptionsScreen; import net.minecraft.client.gui.screens.worldselection.SelectWorldScreen; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.resources.language.I18n; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.minecraft.util.Util; -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.animation.AnimatedClockText; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; -import dev.loki.lovisual.util.logging.DebugLog; -import java.time.LocalDate; -import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Locale; +/** + * Custom main-menu screen: shader background + clock/date header + row of six + * round action buttons (world / multiplayer / accounts / addons / options / quit) + * + optional Microsoft-auth warning card. Draw pass lives in {@link MainMenuRenderer}, + * layout / hit-test / animation in {@link MainMenuPainter}, text primitives in + * {@link MainMenuTextUtil}; the screen class only wires Minecraft's {@link Screen} + * lifecycle to those helpers and owns the shared mutable state. + */ public final class LoVisualMainMenuScreen extends Screen { - private static final float MENU_SCALE = 1.22f; - private static final DateTimeFormatter DATE_FORMAT = - DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.ENGLISH); - private static final float BUTTON_SIZE = 46f * MENU_SCALE; - private static final float BUTTON_RADIUS = BUTTON_SIZE * 0.5f; - private static final float BUTTON_SPACING = 18f * MENU_SCALE; - private static final float BUTTON_HOVER_SCALE = 1.09f; - private static final float BUTTON_SHADOW = 18f * MENU_SCALE; - private static final float BUTTON_SOFTNESS = 1.0f; - private static final String[] BUTTON_ICONS = {"a", "b", "x", "", "s", "i"}; - private static final String[] BUTTON_SVGS = {null, null, null, "folder-pen", null, null}; + // Layout / appearance constants (also referenced by painter + renderer helpers). + static final float MENU_SCALE = 1.22f; + static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.ENGLISH); + static final float BUTTON_SIZE = 46f * MENU_SCALE; + static final float BUTTON_RADIUS = BUTTON_SIZE * 0.5f; + static final float BUTTON_SPACING = 18f * MENU_SCALE; + static final float BUTTON_HOVER_SCALE = 1.09f; + static final float BUTTON_SHADOW = 18f * MENU_SCALE; + static final float BUTTON_SOFTNESS = 1.0f; + static final String[] BUTTON_ICONS = {"a", "b", "x", "", "s", "i"}; + static final String[] BUTTON_SVGS = {null, null, null, "folder-pen", null, null}; + static final long MENU_APPEAR_DURATION = 800L; + static final float TIME_FONT = 4.95f * MENU_SCALE; + static final float DATE_FONT = 0.94f * MENU_SCALE; + static final float ICON_FONT = 1.52f * MENU_SCALE; + static final float ICON_Y_OFFSET = -0.75f * MENU_SCALE; + static final float AUTH_WARNING_W = 250f * MENU_SCALE; + static final float AUTH_WARNING_H = 42f * MENU_SCALE; + static final float AUTH_WARNING_R = 8f * MENU_SCALE; + static final float AUTH_WARNING_HEAD = 26f * MENU_SCALE; + static final long AUTH_WARNING_PROGRESS_MS = 3200L; + static volatile boolean forceVanillaTitleScreen; - private static final long MENU_APPEAR_DURATION = 800L; - private static final float TIME_FONT = 4.95f * MENU_SCALE; - private static final float DATE_FONT = 0.94f * MENU_SCALE; - private static final float ICON_FONT = 1.52f * MENU_SCALE; - private static final float ICON_Y_OFFSET = -0.75f * MENU_SCALE; - private static final float AUTH_WARNING_W = 250f * MENU_SCALE; - private static final float AUTH_WARNING_H = 42f * MENU_SCALE; - private static final float AUTH_WARNING_R = 8f * MENU_SCALE; - private static final float AUTH_WARNING_HEAD = 26f * MENU_SCALE; - private static final long AUTH_WARNING_PROGRESS_MS = 3200L; - private static volatile boolean forceVanillaTitleScreen; - - private final float[] buttonScales = new float[BUTTON_ICONS.length]; - private final float[] buttonHoverProgress = new float[BUTTON_ICONS.length]; - private final AnimatedClockText clockText = new AnimatedClockText(); - - private boolean initialized; - private long openTime; - private long lastRenderTime; - private int hoveredButton = -1; - private float quitHoverProgress; - private boolean authWarningHovered; - private float authWarningHoverProgress; - private float fixedWidth; - private float fixedHeight; - private float renderScale; + // Per-instance state; package-private so painter / renderer can read + write. + final float[] buttonScales = new float[BUTTON_ICONS.length]; + final float[] buttonHoverProgress = new float[BUTTON_ICONS.length]; + final AnimatedClockText clockText = new AnimatedClockText(); + final MainMenuPainter painter = new MainMenuPainter(this); + final MainMenuRenderer renderer = new MainMenuRenderer(this, painter); + boolean initialized; + long openTime; + long lastRenderTime; + int hoveredButton = -1; + float quitHoverProgress; + boolean authWarningHovered; + float authWarningHoverProgress; + float fixedWidth; + float fixedHeight; + float renderScale; public LoVisualMainMenuScreen() { super(Component.literal("LoVisual")); - for (int i = 0; i < buttonScales.length; i++) { - buttonScales[i] = 1f; - buttonHoverProgress[i] = 0f; - } + for (int i = 0; i < buttonScales.length; i++) { buttonScales[i] = 1f; buttonHoverProgress[i] = 0f; } } + /** Whether the user disabled the custom menu (or a previous draw failed) and we should show vanilla. */ public static boolean shouldUseVanillaTitleScreen() { if (forceVanillaTitleScreen) return true; MainConfig cfg = MainConfig.get(); @@ -104,455 +82,80 @@ public final class LoVisualMainMenuScreen extends Screen { return mode != null && mode.equalsIgnoreCase("off"); } - private static float easeOutCubic(float x) { - return 1f - (float) Math.pow(1f - x, 3f); - } + Minecraft mc() { return minecraft; } + int screenW() { return width; } + int screenH() { return height; } + boolean superKeyPressed(KeyEvent input) { return super.keyPressed(input); } - private static float easeOutQuart(float x) { - return 1f - (float) Math.pow(1f - x, 4f); - } - - private static int withAlpha(int rgb, int alpha) { - return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); - } - - private static String tr(String key, Object... args) { - return I18n.get(key, args); - } - - @Override - protected void init() { - initialized = false; - } + @Override protected void init() { initialized = false; } @Override public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - if (shouldUseVanillaTitleScreen()) { - switchToVanillaTitleScreen(null, false); - return; - } - + if (shouldUseVanillaTitleScreen()) { renderer.switchToVanillaTitleScreen(null, false); return; } long now = Util.getMillis(); - if (!initialized) { - initialized = true; - openTime = now; - lastRenderTime = now; - } - + if (!initialized) { initialized = true; openTime = now; lastRenderTime = now; } float deltaTime = Mth.clamp((now - lastRenderTime) / 1000f, 0f, 0.1f); lastRenderTime = now; - updateUiMetrics(); - - float menuProgress = easeOutQuart(getMenuProgress(now)); - float fixedMouseX = toFixedX(mouseX); - float fixedMouseY = toFixedY(mouseY); - - hoveredButton = canInteract(menuProgress) ? getHoveredButton(fixedMouseX, fixedMouseY, menuProgress) : -1; - authWarningHovered = canInteract(menuProgress) && getAuthWarningAccount() != null && authWarningBounds(menuProgress).contains(fixedMouseX, fixedMouseY); - updateButtonAnimations(deltaTime); - + painter.updateUiMetrics(); + float menuProgress = MainMenuPainter.easeOutQuart(painter.getMenuProgress(now)); + float fixedMouseX = painter.toFixedX(mouseX); + float fixedMouseY = painter.toFixedY(mouseY); + hoveredButton = painter.canInteract(menuProgress) ? painter.getHoveredButton(fixedMouseX, fixedMouseY, menuProgress) : -1; + authWarningHovered = painter.canInteract(menuProgress) && painter.getAuthWarningAccount() != null && painter.authWarningBounds(menuProgress).contains(fixedMouseX, fixedMouseY); + painter.updateButtonAnimations(deltaTime); try { - renderShaderBackground(context); - ViewportContext.beginUnscaledLogical(context); - Renderer2D.COLOR.begin(); - drawDimmer(); + renderer.renderShaderBackground(context); + dev.loki.lovisual.render.engine.core.context.ViewportContext.beginUnscaledLogical(context); + dev.loki.lovisual.render.engine.renderer.Renderer2D.COLOR.begin(); + renderer.drawDimmer(); if (menuProgress > 0.01f) { - renderTime(menuProgress); - renderButtons(menuProgress); - renderAuthWarning(context, menuProgress, now); + renderer.renderTime(menuProgress); + renderer.renderButtons(menuProgress); + renderer.renderAuthWarning(context, menuProgress, now); } - Renderer2D.COLOR.render(); + dev.loki.lovisual.render.engine.renderer.Renderer2D.COLOR.render(); } catch (Throwable t) { - switchToVanillaTitleScreen(t, true); + renderer.switchToVanillaTitleScreen(t, true); } finally { try { - ViewportContext.end(context); + dev.loki.lovisual.render.engine.core.context.ViewportContext.end(context); } catch (Throwable t) { - DebugLog.errorOnce("main_menu_viewport_end", "Main menu viewport end failed", t); + dev.loki.lovisual.util.logging.DebugLog.errorOnce("main_menu_viewport_end", "Main menu viewport end failed", t); } } } - @Override - public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - } - - @Override - public void mouseMoved(double mouseX, double mouseY) { - } + @Override public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { } + @Override public void mouseMoved(double mouseX, double mouseY) { } @Override public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { if (click.button() != 0) return true; - - updateUiMetrics(); - float menuProgress = easeOutQuart(getMenuProgress(Util.getMillis())); - int index = canInteract(menuProgress) - ? getHoveredButton(toFixedX((float) click.x()), toFixedY((float) click.y()), menuProgress) - : -1; - AccountEntry warningAccount = getAuthWarningAccount(); - if (warningAccount != null && minecraft != null && canInteract(menuProgress) - && authWarningBounds(menuProgress).contains(toFixedX((float) click.x()), toFixedY((float) click.y()))) { - ClientScreen.show(minecraft, new LoVisualAltManagerScreen(this, true)); - return true; - } - if (index >= 0) { - handleButton(index); - } + painter.updateUiMetrics(); + float menuProgress = MainMenuPainter.easeOutQuart(painter.getMenuProgress(Util.getMillis())); + float fx = painter.toFixedX((float) click.x()), fy = painter.toFixedY((float) click.y()); + if (renderer.handleAuthWarningClick(fx, fy, menuProgress)) return true; + int index = painter.canInteract(menuProgress) ? painter.getHoveredButton(fx, fy, menuProgress) : -1; + if (index >= 0) handleButton(index); return true; } - @Override - public boolean keyPressed(KeyEvent input) { - return input.key() == 256 || super.keyPressed(input); - } - - @Override - public boolean shouldCloseOnEsc() { - return false; - } - - @Override - public boolean isPauseScreen() { - return false; - } - - private void renderShaderBackground(GuiGraphicsExtractor context) { - Minecraft mc = this.minecraft; - if (mc == null) return; - - MainConfig cfg = MainConfig.get(); - String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; - if (mode == null || mode.equalsIgnoreCase("off")) { - switchToVanillaTitleScreen(null, false); - return; - } - MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); - } - - private void switchToVanillaTitleScreen(Throwable t, boolean sticky) { - if (sticky) { - forceVanillaTitleScreen = true; - } - if (t != null) { - DebugLog.errorOnce("main_menu_vanilla_fallback", "Main menu failed, switching to vanilla title screen", t); - } else if (sticky) { - DebugLog.warnOnce("main_menu_vanilla_fallback", "Main menu switched to vanilla title screen"); - } - Minecraft mc = minecraft != null ? minecraft : Minecraft.getInstance(); - if (mc != null && !(ClientScreen.current() instanceof TitleScreen)) { - ClientScreen.show(mc, new TitleScreen(false)); - } - } - - private void drawDimmer() { - Themes.Theme theme = Theme.theme(); - int base = HudRenderUtil.mixColor(theme.windowBg(), 0xFF000000, 0.46f); - int soft = withAlpha(base, 150); - int deep = withAlpha(HudRenderUtil.mixColor(theme.windowBg(), 0xFF000000, 0.72f), 195); - Renderer2D.COLOR.quad(0, 0, fixedWidth, fixedHeight, soft, soft, deep, deep); - } - - private void renderTime(float opacity) { - float centerX = fixedWidth * 0.5f; - float slideOffset = (1f - opacity) * 40f * MENU_SCALE; - float centerY = fixedHeight * 0.5f - 55f * MENU_SCALE + slideOffset; - - long nowMs = Util.getMillis(); - MainConfig cfg = MainConfig.get(); - boolean showSeconds = cfg != null && cfg.isMenuClockShowSeconds(); - clockText.update(LocalTime.now(), showSeconds, nowMs); - - String dateText = LocalDate.now().format(DATE_FORMAT); - - TextRenderer timeRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer dateRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, timeRenderer); - - int timeColor = withAlpha(0xFFFFFF, Math.round(opacity * 255f)); - int dateColor = withAlpha(0xFFFFFF, Math.round(opacity * 200f)); - - float timeHeight = clockText.height(timeRenderer, TIME_FONT); - clockText.renderLiquidGlassCentered(timeRenderer, centerX, centerY - timeHeight * 0.5f, TIME_FONT, timeColor, nowMs); - drawCenteredText(dateRenderer, dateText, centerX, centerY + timeHeight * 0.5f + 4f * MENU_SCALE, DATE_FONT, dateColor); - } - - private void renderButtons(float opacity) { - float totalWidth = BUTTON_SIZE * BUTTON_ICONS.length + BUTTON_SPACING * (BUTTON_ICONS.length - 1); - float startX = (fixedWidth - totalWidth) * 0.5f; - float slideOffset = (1f - opacity) * 60f * MENU_SCALE; - float y = fixedHeight * 0.5f + 34f * MENU_SCALE + slideOffset; - - for (int i = 0; i < BUTTON_ICONS.length; i++) { - float delay = i * 0.12f; - float progress = Mth.clamp((opacity - delay) / (1f - delay * 0.5f), 0f, 1f); - float eased = easeOutCubic(progress); - if (eased <= 0f) continue; - float x = startX + i * (BUTTON_SIZE + BUTTON_SPACING); - renderCircleButton(i, x, y, eased * opacity); - } - } - - private void renderCircleButton(int index, float x, float y, float opacity) { - if (opacity < 0.01f) return; - - float scale = buttonScales[index]; - float radius = BUTTON_RADIUS * scale; - float centerX = x + BUTTON_RADIUS; - float centerY = y + BUTTON_RADIUS; - - float hover = buttonHoverProgress[index]; - boolean quit = index == BUTTON_ICONS.length - 1; - Themes.Theme theme = Theme.theme(); - - int tint; - if (quit) { - float q = quitHoverProgress; - tint = HudRenderUtil.mixColor(0xFFFF4C4C, 0xFFFF1F32, q * 0.55f + hover * 0.25f); - } else { - int accent = theme.accent(); - int baseTint = HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.18f); - tint = HudRenderUtil.mixColor(baseTint, 0xFFFFFFFF, hover * 0.10f); - } - - int tintArgb = withAlpha(tint, Math.round(opacity * (quit ? 235f : 220f))); - int icon = quit - ? withAlpha(HudRenderUtil.mixColor(0xFFFFFFFF, 0xFFFFD6D6, quitHoverProgress), Math.round(opacity * 255f)) - : withAlpha(0xFFFFFF, Math.round(opacity * 255f)); - int shadow = withAlpha(quit ? 0x1F0508 : 0x060810, Math.round(opacity * (quit ? 118f : 92f))); - - Renderer2D.COLOR.circleSoftShadow(centerX, centerY, radius, - BUTTON_SHADOW, 0.052f + hover * 0.020f, shadow); - Renderer2D.COLOR.liquidGlassCircle(centerX, centerY, radius, - BUTTON_SOFTNESS, - tintArgb, - opacity, - -20.0f, - 1.0f, - 1.0f, - quit ? 0.66f : 0.58f, - (0.100f + hover * 0.032f) * MENU_SCALE, - 0.0f); - - String svg = BUTTON_SVGS[index]; - if (svg != null && !svg.isBlank()) { - float iconSize = 20.5f * MENU_SCALE * scale; - Renderer2D.COLOR.svg(svg, centerX - iconSize * 0.5f, centerY - iconSize * 0.5f - 0.45f * MENU_SCALE, - iconSize, iconSize, SvgRenderOptions.overrideColor(icon)); - } else { - TextRenderer icons = Fonts.renderer("MainMenuIcons", FontInfo.Type.Regular, TextRenderer.get()); - String glyph = BUTTON_ICONS[index]; - float iconSize = ICON_FONT * scale; - float iconW = measureWidth(icons, glyph, iconSize); - float iconH = measureHeight(icons, iconSize); - drawText(icons, glyph, - centerX - iconW * 0.5f + 0.5f * MENU_SCALE, - centerY - iconH * 0.5f + ICON_Y_OFFSET, - iconSize, - icon); - } - } - - private void renderAuthWarning(GuiGraphicsExtractor context, float opacity, long now) { - AccountEntry entry = getAuthWarningAccount(); - if (entry == null || opacity <= 0.01f) return; - - Bounds b = authWarningBounds(opacity); - float hover = AnimationUtility.easeOutCubic(authWarningHoverProgress); - float y = b.y - hover * 1.4f * MENU_SCALE; - Themes.Theme theme = Theme.theme(); - - int shadow = withAlpha(0x060810, Math.round(opacity * (96f + hover * 24f))); - int top = HudRenderUtil.mixColor(withAlpha(0x171B22, Math.round(opacity * 226f)), withAlpha(theme.accent(), Math.round(opacity * 78f)), 0.16f + hover * 0.10f); - int bottom = HudRenderUtil.mixColor(withAlpha(0x0D1118, Math.round(opacity * 236f)), withAlpha(theme.accent(), Math.round(opacity * 52f)), 0.08f + hover * 0.08f); - int stroke = HudRenderUtil.mixColor(withAlpha(0x46505D, Math.round(opacity * 170f)), withAlpha(theme.accent(), Math.round(opacity * 218f)), 0.32f + hover * 0.28f); - - Renderer2D.COLOR.roundedRectSoftShadow(b.x, y + 1.2f * MENU_SCALE, b.w, b.h, AUTH_WARNING_R, 10f * MENU_SCALE, 0.026f + hover * 0.014f, shadow); - Renderer2D.COLOR.roundedRectGradientQuad(b.x, y, b.w, b.h, AUTH_WARNING_R, 1f, top, top, bottom, bottom); - Renderer2D.COLOR.roundedRectStrokeGradient(b.x + 0.65f * MENU_SCALE, y + 0.65f * MENU_SCALE, b.w - 1.3f * MENU_SCALE, b.h - 1.3f * MENU_SCALE, - AUTH_WARNING_R - 0.65f * MENU_SCALE, 1f, 0.65f * MENU_SCALE, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.08f), stroke, 90f); - - float progress = (now % AUTH_WARNING_PROGRESS_MS) / (float) AUTH_WARNING_PROGRESS_MS; - float lineX = b.x + 8f * MENU_SCALE; - float lineY = y + 4f * MENU_SCALE; - float lineW = b.w - 16f * MENU_SCALE; - float lineH = 1.35f * MENU_SCALE; - Renderer2D.COLOR.roundedRect(lineX, lineY, lineW, lineH, lineH * 0.5f, 1f, withAlpha(0xFFFFFF, Math.round(opacity * 42f))); - Renderer2D.COLOR.roundedRect(lineX, lineY, lineW * (1f - progress), lineH, lineH * 0.5f, 1f, withAlpha(theme.accent(), Math.round(opacity * 170f))); - - float headX = b.x + 10f * MENU_SCALE; - float headY = y + 10f * MENU_SCALE; - PlayerHeadRenderer.drawRounded(context, headX, headY, AUTH_WARNING_HEAD, 4f * MENU_SCALE, SkinManager.getSkin(entry.getName()), - new RenderColor(withAlpha(0xFFFFFF, Math.round(opacity * 255f))), true, new RenderColor(withAlpha(theme.accent(), Math.round(opacity * 120f))), 0.75f * MENU_SCALE, false); - - TextRenderer title = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer body = Fonts.renderer("InterMedium", FontInfo.Type.Regular, title); - float textX = headX + AUTH_WARNING_HEAD + 8f * MENU_SCALE; - drawText(title, tr("screen.lovisual.main_menu.auth_warning.title"), textX, y + 12.2f * MENU_SCALE, 0.72f * MENU_SCALE, withAlpha(0xFFFFFF, Math.round(opacity * 245f))); - String message = tr("screen.lovisual.main_menu.auth_warning.body", entry.getName()); - drawText(body, ellipsize(body, message, 0.55f * MENU_SCALE, b.x + b.w - textX - 10f * MENU_SCALE), textX, y + 25.4f * MENU_SCALE, 0.55f * MENU_SCALE, withAlpha(0xC8D0DC, Math.round(opacity * 225f))); - } - - private void updateButtonAnimations(float deltaTime) { - for (int i = 0; i < BUTTON_ICONS.length; i++) { - float targetHover = hoveredButton == i ? 1f : 0f; - buttonHoverProgress[i] = Mth.lerp(Math.min(1f, deltaTime * 10f), buttonHoverProgress[i], targetHover); - - float targetScale = hoveredButton == i ? BUTTON_HOVER_SCALE : 1f; - buttonScales[i] = Mth.lerp(Math.min(1f, deltaTime * 12f), buttonScales[i], targetScale); - } - float targetQuit = hoveredButton == BUTTON_ICONS.length - 1 ? 1f : 0f; - quitHoverProgress = Mth.lerp(Math.min(1f, deltaTime * 8f), quitHoverProgress, targetQuit); - authWarningHoverProgress = Mth.lerp(Math.min(1f, deltaTime * 10f), authWarningHoverProgress, authWarningHovered ? 1f : 0f); - } - - private int getHoveredButton(float mouseX, float mouseY, float menuProgress) { - if (!canInteract(menuProgress)) return -1; - float totalWidth = BUTTON_SIZE * BUTTON_ICONS.length + BUTTON_SPACING * (BUTTON_ICONS.length - 1); - float startX = (fixedWidth - totalWidth) * 0.5f; - float y = fixedHeight * 0.5f + 34f * MENU_SCALE + (1f - menuProgress) * 60f * MENU_SCALE; - - for (int i = 0; i < BUTTON_ICONS.length; i++) { - float buttonX = startX + i * (BUTTON_SIZE + BUTTON_SPACING); - float centerX = buttonX + BUTTON_SIZE * 0.5f; - float centerY = y + BUTTON_SIZE * 0.5f; - float dx = mouseX - centerX; - float dy = mouseY - centerY; - if (dx * dx + dy * dy <= BUTTON_RADIUS * BUTTON_RADIUS) { - return i; - } - } - return -1; - } - - private boolean canInteract(float menuProgress) { - return menuProgress > 0.8f; - } - - private AccountEntry getAuthWarningAccount() { - return AccountConfig.get().getMicrosoftAuthorizationRequiredAccount(); - } - - private Bounds authWarningBounds(float menuProgress) { - float slideOffset = (1f - menuProgress) * 60f * MENU_SCALE; - float buttonY = fixedHeight * 0.5f + 34f * MENU_SCALE + slideOffset; - float y = buttonY + BUTTON_SIZE + 18f * MENU_SCALE; - y = Math.min(y, fixedHeight - AUTH_WARNING_H - 16f * MENU_SCALE); - return new Bounds((fixedWidth - AUTH_WARNING_W) * 0.5f, y, AUTH_WARNING_W, AUTH_WARNING_H); - } - - private void updateUiMetrics() { - Minecraft mc = minecraft; - if (mc == null) { - fixedWidth = width; - fixedHeight = height; - renderScale = 1f; - return; - } - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - fixedWidth = Math.max(1f, HudScale.virtualWidth(fbw, fbh)); - fixedHeight = Math.max(1f, HudScale.virtualHeight(fbw, fbh)); - renderScale = 1f; - } - - private float toFixedX(float screenX) { - if (width <= 0) return screenX; - Minecraft mc = minecraft; - if (mc == null) return screenX; - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float hudScale = HudScale.scale(fbw, fbh); - if (hudScale <= 0f) return screenX; - return screenX * mc.getWindow().getGuiScale() / hudScale; - } - - private float toFixedY(float screenY) { - if (height <= 0) return screenY; - Minecraft mc = minecraft; - if (mc == null) return screenY; - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float hudScale = HudScale.scale(fbw, fbh); - if (hudScale <= 0f) return screenY; - return screenY * mc.getWindow().getGuiScale() / hudScale; - } + @Override public boolean keyPressed(KeyEvent input) { return painter.onKeyPressed(input); } + @Override public boolean shouldCloseOnEsc() { return false; } + @Override public boolean isPauseScreen() { return false; } + /** Route index → target screen (world / multiplayer / accounts / addons / options / quit). */ private void handleButton(int index) { - if (minecraft == null) return; + Minecraft mc = minecraft; + if (mc == null) return; switch (index) { - case 0 -> ClientScreen.show(minecraft, new SelectWorldScreen(this)); - case 1 -> ClientScreen.show(minecraft, new JoinMultiplayerScreen(this)); - case 2 -> ClientScreen.show(minecraft, new LoVisualAltManagerScreen(this)); - case 3 -> ClientScreen.show(minecraft, new LoVisualAddonManagerScreen(this)); - case 4 -> - ClientScreen.show(minecraft, new OptionsScreen(this, minecraft.options, false)); - case 5 -> minecraft.stop(); - default -> { - } - } - } - - private float getMenuProgress(long now) { - if (!initialized) return 0f; - long elapsed = now - openTime; - return Mth.clamp(elapsed / (float) MENU_APPEAR_DURATION, 0f, 1f); - } - - private void drawText(TextRenderer renderer, String text, float x, float y, float size, int argb) { - if (renderer == null || text == null || text.isEmpty()) return; - renderer.setAlpha(1.0); - renderer.begin(size, false, false); - renderer.render(text, x, y, new RenderColor(argb), false); - renderer.end(); - } - - private void drawCenteredText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { - float x = centerX - measureWidth(renderer, text, size) * 0.5f; - drawText(renderer, text, x, y, size, argb); - } - - private void drawLiquidGlassText(TextRenderer renderer, String text, float x, float y, float size, int argb) { - if (renderer == null || text == null || text.isEmpty()) return; - renderer.setAlpha(1.0); - renderer.begin(size, false, true); - renderer.renderLiquidGlass(text, x, y, new RenderColor(argb), false); - renderer.end(); - } - - private void drawCenteredLiquidGlassText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { - float x = centerX - measureWidth(renderer, text, size) * 0.5f; - drawLiquidGlassText(renderer, text, x, y, size, argb); - } - - private float measureWidth(TextRenderer renderer, String text, float size) { - if (renderer == null || text == null) return 0f; - renderer.begin(size, true, false); - float width = (float) renderer.getWidth(text, false); - renderer.end(); - return width; - } - - private float measureHeight(TextRenderer renderer, float size) { - if (renderer == null) return 0f; - renderer.begin(size, true, false); - float height = (float) renderer.getHeight(false); - renderer.end(); - return height; - } - - private String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) { - if (text == null || text.isEmpty()) return ""; - if (measureWidth(renderer, text, size) <= maxWidth) return text; - String out = text; - while (out.length() > 3 && measureWidth(renderer, out + "...", size) > maxWidth) { - out = out.substring(0, out.length() - 1); - } - return out + "..."; - } - - private record Bounds(float x, float y, float w, float h) { - private boolean contains(float mx, float my) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; + case 0 -> ClientScreen.show(mc, new SelectWorldScreen(this)); + case 1 -> ClientScreen.show(mc, new JoinMultiplayerScreen(this)); + case 2 -> ClientScreen.show(mc, new LoVisualAltManagerScreen(this)); + case 3 -> ClientScreen.show(mc, new LoVisualAddonManagerScreen(this)); + case 4 -> ClientScreen.show(mc, new OptionsScreen(this, mc.options, false)); + case 5 -> mc.stop(); + default -> { } } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java deleted file mode 100644 index d3e9727..0000000 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java +++ /dev/null @@ -1,590 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.mainmenu; - -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.proxy.ProxyBackend; -import dev.loki.lovisual.util.proxy.model.ProxyEntry; -import dev.loki.lovisual.util.proxy.model.ProxyType; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.input.CharacterEvent; -import net.minecraft.client.input.KeyEvent; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; - -public final class LoVisualProxyManagerScreen extends Screen { - private static final float SCALE = 2.28f; - private static final float TEXT_SCALE = 1.30f; - private static final float PANEL_W = 184f * SCALE; - private static final float PANEL_H = 184f * SCALE; - private static final float PANEL_R = 6f * SCALE; - private static final float FIELD_H = 14f * SCALE; - private static final float BUTTON_H = 16f * SCALE; - private static final float GAP = 5f * SCALE; - private static final float TITLE_FONT = 0.84f * TEXT_SCALE; - private static final float LABEL_FONT = 0.55f * TEXT_SCALE; - private static final float FIELD_FONT = 0.60f * TEXT_SCALE; - private static final float BUTTON_FONT = 0.58f * TEXT_SCALE; - private static final int FIELD_COUNT = 3; - - private final Screen parent; - private final String[] values = {"", "", ""}; - private final float[] fieldHover = new float[FIELD_COUNT]; - private final float[] fieldFocus = new float[FIELD_COUNT]; - private Field focusedField; - private ProxyType type = ProxyType.SOCKS5; - private boolean enabled; - private String status = ""; - private float fixedWidth; - private float fixedHeight; - private float openAnim; - private float enableHover; - private float enablePress; - private float socks4Hover; - private float socks5Hover; - private float socks4Press; - private float socks5Press; - private float saveHover; - private float savePress; - private float clearHover; - private float clearPress; - private float backHover; - private float backPress; - private TextRenderer titleRenderer; - private TextRenderer bodyRenderer; - - public LoVisualProxyManagerScreen(Screen parent) { - super(Component.translatable("screen.lovisual.proxy_manager.title")); - this.parent = parent; - } - - private static float animate(float current, float target, float dt, float speed) { - float next = AnimationUtility.approach(current, target, dt, speed); - return AnimationUtility.snap(next, target, 0.001f); - } - - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static int withAlpha(int rgb, int alpha) { - return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); - } - - private static String tr(String key, Object... args) { - return I18n.get(key, args); - } - - @Override - protected void init() { - titleRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - bodyRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, titleRenderer); - loadProxy(); - } - - @Override - public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - updateUiMetrics(); - renderShaderBackground(context); - float fx = toFixedX(mouseX); - float fy = toFixedY(mouseY); - updateAnimations(fx, fy); - - ViewportContext.beginUnscaledLogical(context); - Renderer2D.COLOR.begin(); - try { - renderDimmer(); - renderPanel(fx, fy); - } finally { - Renderer2D.COLOR.render(); - ViewportContext.end(context); - } - } - - @Override - public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - } - - @Override - public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { - if (click.button() != 0) return true; - return handleClick(toFixedX((float) click.x()), toFixedY((float) click.y())); - } - - @Override - public boolean keyPressed(KeyEvent input) { - if (focusedField != null) { - int key = input.key(); - if (key == 259) { - int index = focusedField.index; - if (!values[index].isEmpty()) values[index] = values[index].substring(0, values[index].length() - 1); - return true; - } - if (key == 258) { - focusedField = focusedField.next(); - return true; - } - if (key == 257 || key == 335) { - saveProxy(); - return true; - } - if (key == 256) { - focusedField = null; - return true; - } - } - - if (input.key() == 256) { - closeToParent(); - return true; - } - return super.keyPressed(input); - } - - @Override - public boolean charTyped(CharacterEvent input) { - if (focusedField == null) return super.charTyped(input); - int cp = input.codepoint(); - if (cp < 32 || cp > 126) return true; - int index = focusedField.index; - int max = focusedField == Field.ADDRESS ? 96 : 64; - if (values[index].length() >= max) return true; - values[index] += Character.toString(cp); - return true; - } - - @Override - public boolean isPauseScreen() { - return false; - } - - @Override - public boolean shouldCloseOnEsc() { - return false; - } - - private void renderPanel(float mouseX, float mouseY) { - Bounds b = bounds(); - float open = AnimationUtility.easeOutCubic(openAnim); - PanelColors c = colors(open); - - Renderer2D.COLOR.blurRect(b.x, b.y, PANEL_W, PANEL_H, PANEL_R, 15f, 1.0f, 0.30f, 0xFFFFFF); - Renderer2D.COLOR.roundedRectSoftShadow(b.x, b.y, PANEL_W, PANEL_H, PANEL_R, 12f * SCALE, 0.022f + open * 0.012f, c.shadow); - Renderer2D.COLOR.roundedRectGradientQuad(b.x, b.y, PANEL_W, PANEL_H, PANEL_R, 1f, c.bgTopLeft, c.bgTopRight, c.bgBottomRight, c.bgBottomLeft); - Renderer2D.COLOR.roundedRectMaskedQuad(b.x, b.y, PANEL_W, 22f * SCALE, b.x, b.y, PANEL_W, PANEL_H, PANEL_R, 1f, - c.headerTopLeft, c.headerTopRight, c.headerBottomRight, c.headerBottomLeft); - Renderer2D.COLOR.roundedRectStrokeGradient(b.x, b.y, PANEL_W, PANEL_H, PANEL_R, 1f, 1f, HudRenderUtil.mixColor(c.stroke, 0xFFFFFFFF, 0.04f), c.stroke, 90f); - - drawCentered(titleRenderer, tr("screen.lovisual.proxy_manager.header"), b.x + PANEL_W * 0.5f, b.y + 7f * SCALE, TITLE_FONT, c.title); - - float innerX = b.x + 8f * SCALE; - float innerW = PANEL_W - 16f * SCALE; - float rowY = b.y + 28f * SCALE; - renderToggle(innerX, rowY, 66f * SCALE, 13f * SCALE, tr("screen.lovisual.proxy_manager.enabled"), enabled, enableHover, enablePress, c); - renderTypeToggle(innerX + 72f * SCALE, rowY, innerW - 72f * SCALE, 13f * SCALE, c); - - float fieldY = b.y + 53f * SCALE; - renderField(Field.ADDRESS, tr("screen.lovisual.proxy_manager.address"), tr("screen.lovisual.proxy_manager.placeholder.address"), innerX, fieldY, innerW, c); - renderField(Field.USERNAME, tr("screen.lovisual.proxy_manager.username"), tr("screen.lovisual.proxy_manager.placeholder.optional"), innerX, fieldY + 27f * SCALE, innerW, c); - renderField(Field.PASSWORD, tr("screen.lovisual.proxy_manager.password"), tr("screen.lovisual.proxy_manager.placeholder.optional"), innerX, fieldY + 54f * SCALE, innerW, c); - - float buttonY = b.y + PANEL_H - 26f * SCALE; - float third = (innerW - GAP * 2f) / 3f; - renderAction(innerX, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.save"), false, saveHover, savePress, c); - renderAction(innerX + third + GAP, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.clear"), true, clearHover, clearPress, c); - renderAction(innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.back"), false, backHover, backPress, c); - - String info = status.isBlank() ? currentSummary() : status; - draw(bodyRenderer, ellipsize(info, FIELD_FONT, innerW), innerX, b.y + PANEL_H - 42f * SCALE, FIELD_FONT, c.muted); - } - - private void renderField(Field field, String label, String placeholder, float x, float y, float w, PanelColors c) { - int i = field.index; - draw(bodyRenderer, label, x, y, LABEL_FONT, c.label); - float fy = y + 9f * SCALE; - boolean focused = focusedField == field; - float hoverAnim = AnimationUtility.easeOutCubic(fieldHover[i]); - float focusAnim = AnimationUtility.easeOutCubic(fieldFocus[i]); - int fillTop = HudRenderUtil.mixColor(withAlpha(c.surface, 188), withAlpha(c.surfaceHover, 208), hoverAnim * 0.45f + focusAnim * 0.16f); - int fillBottom = HudRenderUtil.mixColor(withAlpha(c.surface, 176), withAlpha(0xFF0D1118, 196), 0.18f); - int stroke = focused ? HudRenderUtil.mixColor(c.strokeSoft, c.accentSoft, 0.55f) : HudRenderUtil.mixColor(c.stroke, c.strokeSoft, hoverAnim * 0.72f); - Renderer2D.COLOR.roundedRectGradientQuad(x, fy, w, FIELD_H, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(x, fy, w, FIELD_H, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); - if (focusAnim > 0.01f) { - Renderer2D.COLOR.roundedRectGradientQuad(x + 1.2f * SCALE, fy + FIELD_H - 1.6f * SCALE, w - 2.4f * SCALE, 1.1f * SCALE, 0.6f * SCALE, 1f, - withAlpha(c.accentSoft, Math.round(48f * focusAnim)), withAlpha(c.accent, Math.round(28f * focusAnim)), - withAlpha(c.accent, Math.round(28f * focusAnim)), withAlpha(c.accentSoft, Math.round(48f * focusAnim))); - } - - String value = values[i]; - String display = field == Field.PASSWORD && !value.isEmpty() ? "*".repeat(value.length()) : value; - if (display.isEmpty() && !focused) display = placeholder; - if (focused && AnimationUtility.blink(500L)) display += "|"; - int color = values[i].isEmpty() && !focused ? c.mutedLabel : c.title; - draw(bodyRenderer, ellipsize(display, FIELD_FONT, w - 8f * SCALE), x + 4f * SCALE, fy + 4.3f * SCALE, FIELD_FONT, color); - } - - private void renderTypeToggle(float x, float y, float w, float h, PanelColors c) { - float gap = 2f * SCALE; - float itemW = (w - gap) * 0.5f; - renderChoice(x, y, itemW, h, "SOCKS5", type == ProxyType.SOCKS5, socks5Hover, socks5Press, c); - renderChoice(x + itemW + gap, y, itemW, h, "SOCKS4", type == ProxyType.SOCKS4, socks4Hover, socks4Press, c); - } - - private void renderToggle(float x, float y, float w, float h, String label, boolean selected, float hover, float press, PanelColors c) { - renderChoice(x, y, w, h, label, selected, hover, press, c); - } - - private void renderChoice(float x, float y, float w, float h, String label, boolean selected, float hover, float press, PanelColors c) { - float hoverAnim = AnimationUtility.easeOutCubic(hover); - float pressAnim = AnimationUtility.easeOutCubic(press); - float active = selected ? 1f : 0f; - int fillTop = HudRenderUtil.mixColor(withAlpha(c.surface, 170), withAlpha(c.surfaceHover, 210), hoverAnim * 0.42f + active * 0.38f + pressAnim * 0.10f); - int fillBottom = HudRenderUtil.mixColor(withAlpha(c.surface, 158), withAlpha(0x1A2418, 210), active * 0.24f + hoverAnim * 0.16f); - int stroke = HudRenderUtil.mixColor(c.stroke, selected ? c.accentSoft : c.strokeSoft, active * 0.70f + hoverAnim * 0.30f + pressAnim * 0.10f); - int text = selected ? HudRenderUtil.mixColor(c.title, c.accent, 0.28f) : HudRenderUtil.mixColor(c.muted, c.title, hoverAnim * 0.35f); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); - drawCentered(bodyRenderer, label, x + w * 0.5f, y + 4.1f * SCALE - pressAnim * 0.35f * SCALE, BUTTON_FONT, text); - } - - private void renderAction(float x, float y, float w, float h, String label, boolean destructive, float hover, float press, PanelColors c) { - float hoverAnim = AnimationUtility.easeOutCubic(hover); - float pressAnim = AnimationUtility.easeOutCubic(press); - int fillTop; - int fillBottom; - int stroke; - int text; - if (destructive) { - fillTop = HudRenderUtil.mixColor(withAlpha(0x1A1416, 170), withAlpha(0x4B2224, 212), 0.25f + hoverAnim * 0.45f + pressAnim * 0.12f); - fillBottom = HudRenderUtil.mixColor(withAlpha(0x120E10, 165), withAlpha(0x261012, 220), 0.20f + hoverAnim * 0.40f + pressAnim * 0.10f); - stroke = HudRenderUtil.mixColor(withAlpha(0x352A2A, 215), withAlpha(0x8C5054, 235), 0.22f + hoverAnim * 0.58f + pressAnim * 0.12f); - text = HudRenderUtil.mixColor(withAlpha(0xD0A0A0, 255), withAlpha(0xFFB2B2, 255), 0.24f + hoverAnim * 0.56f + pressAnim * 0.08f); - } else { - fillTop = HudRenderUtil.mixColor(withAlpha(c.surface, 176), withAlpha(0x223746, 212), 0.20f + hoverAnim * 0.44f + pressAnim * 0.10f); - fillBottom = HudRenderUtil.mixColor(withAlpha(c.surface, 166), withAlpha(0x13292F, 220), 0.16f + hoverAnim * 0.40f + pressAnim * 0.08f); - stroke = HudRenderUtil.mixColor(c.stroke, withAlpha(0x466A76, 228), 0.18f + hoverAnim * 0.50f + pressAnim * 0.10f); - text = HudRenderUtil.mixColor(withAlpha(0xD0D8E4, 255), withAlpha(0xEEF8FF, 255), 0.18f + hoverAnim * 0.38f + pressAnim * 0.08f); - } - Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 3f * SCALE, 7f * SCALE, 0.018f + hoverAnim * 0.015f, c.shadow); - Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); - Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); - drawCentered(bodyRenderer, label, x + w * 0.5f, y + 5f * SCALE - (hoverAnim * 0.65f + pressAnim * 0.55f) * SCALE, BUTTON_FONT, text); - } - - private boolean handleClick(float mouseX, float mouseY) { - Bounds b = bounds(); - float innerX = b.x + 8f * SCALE; - float innerW = PANEL_W - 16f * SCALE; - float rowY = b.y + 28f * SCALE; - focusedField = null; - - if (inside(mouseX, mouseY, innerX, rowY, 66f * SCALE, 13f * SCALE)) { - enabled = !enabled; - enablePress = 1f; - status = enabled ? tr("screen.lovisual.proxy_manager.status.enabled") : tr("screen.lovisual.proxy_manager.status.disabled"); - return true; - } - - float typeX = innerX + 72f * SCALE; - float typeW = innerW - 72f * SCALE; - float typeItemW = (typeW - 2f * SCALE) * 0.5f; - if (inside(mouseX, mouseY, typeX, rowY, typeItemW, 13f * SCALE)) { - type = ProxyType.SOCKS5; - socks5Press = 1f; - return true; - } - if (inside(mouseX, mouseY, typeX + typeItemW + 2f * SCALE, rowY, typeItemW, 13f * SCALE)) { - type = ProxyType.SOCKS4; - socks4Press = 1f; - return true; - } - - float fieldY = b.y + 53f * SCALE; - for (Field field : Field.values()) { - float y = fieldY + field.index * 27f * SCALE + 9f * SCALE; - if (inside(mouseX, mouseY, innerX, y, innerW, FIELD_H)) { - focusedField = field; - return true; - } - } - - float buttonY = b.y + PANEL_H - 26f * SCALE; - float third = (innerW - GAP * 2f) / 3f; - if (inside(mouseX, mouseY, innerX, buttonY, third, BUTTON_H)) { - savePress = 1f; - saveProxy(); - return true; - } - if (inside(mouseX, mouseY, innerX + third + GAP, buttonY, third, BUTTON_H)) { - clearPress = 1f; - clearProxy(); - return true; - } - if (inside(mouseX, mouseY, innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H)) { - backPress = 1f; - closeToParent(); - return true; - } - return true; - } - - private void loadProxy() { - ProxyEntry proxy = ProxyBackend.getDefaultProxy(); - type = proxy.type(); - values[Field.ADDRESS.index] = proxy.ipPort(); - values[Field.USERNAME.index] = proxy.username(); - values[Field.PASSWORD.index] = proxy.password(); - enabled = ProxyBackend.isEnabled(); - status = ""; - } - - private void saveProxy() { - String address = values[Field.ADDRESS.index].trim(); - if (!address.isEmpty() && !ProxyEntry.isValidIpPort(address)) { - status = tr("screen.lovisual.proxy_manager.status.invalid_address"); - return; - } - if (address.isEmpty()) { - enabled = false; - } - ProxyEntry entry = address.isEmpty() - ? ProxyEntry.empty() - : new ProxyEntry(type, address, values[Field.USERNAME.index], values[Field.PASSWORD.index]); - ProxyBackend.setDefaultProxy(entry); - ProxyBackend.setEnabled(enabled); - status = address.isEmpty() - ? tr("screen.lovisual.proxy_manager.status.cleared") - : tr("screen.lovisual.proxy_manager.status.saved", type.name()); - } - - private void clearProxy() { - values[0] = ""; - values[1] = ""; - values[2] = ""; - enabled = false; - type = ProxyType.SOCKS5; - focusedField = null; - ProxyBackend.setDefaultProxy(ProxyEntry.empty()); - ProxyBackend.setEnabled(false); - status = tr("screen.lovisual.proxy_manager.status.cleared"); - } - - private String currentSummary() { - if (!enabled) return tr("screen.lovisual.proxy_manager.status.disabled"); - String address = values[Field.ADDRESS.index].trim(); - if (address.isEmpty()) return tr("screen.lovisual.proxy_manager.status.enabled_no_address"); - return type.name() + " " + address; - } - - private void updateAnimations(float mouseX, float mouseY) { - float dt = AnimationUtility.deltaTime(); - openAnim = animate(openAnim, 1f, dt, 7f); - Bounds b = bounds(); - float innerX = b.x + 8f * SCALE; - float innerW = PANEL_W - 16f * SCALE; - float rowY = b.y + 28f * SCALE; - enableHover = animate(enableHover, inside(mouseX, mouseY, innerX, rowY, 66f * SCALE, 13f * SCALE) ? 1f : 0f, dt, 11f); - enablePress = animate(enablePress, 0f, dt, 8f); - - float typeX = innerX + 72f * SCALE; - float typeW = innerW - 72f * SCALE; - float itemW = (typeW - 2f * SCALE) * 0.5f; - socks5Hover = animate(socks5Hover, inside(mouseX, mouseY, typeX, rowY, itemW, 13f * SCALE) ? 1f : 0f, dt, 11f); - socks4Hover = animate(socks4Hover, inside(mouseX, mouseY, typeX + itemW + 2f * SCALE, rowY, itemW, 13f * SCALE) ? 1f : 0f, dt, 11f); - socks5Press = animate(socks5Press, 0f, dt, 8f); - socks4Press = animate(socks4Press, 0f, dt, 8f); - - float fieldY = b.y + 53f * SCALE; - for (Field field : Field.values()) { - float y = fieldY + field.index * 27f * SCALE + 9f * SCALE; - fieldHover[field.index] = animate(fieldHover[field.index], inside(mouseX, mouseY, innerX, y, innerW, FIELD_H) ? 1f : 0f, dt, 11f); - fieldFocus[field.index] = animate(fieldFocus[field.index], focusedField == field ? 1f : 0f, dt, 10f); - } - - float buttonY = b.y + PANEL_H - 26f * SCALE; - float third = (innerW - GAP * 2f) / 3f; - saveHover = animate(saveHover, inside(mouseX, mouseY, innerX, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); - clearHover = animate(clearHover, inside(mouseX, mouseY, innerX + third + GAP, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); - backHover = animate(backHover, inside(mouseX, mouseY, innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); - savePress = animate(savePress, 0f, dt, 8f); - clearPress = animate(clearPress, 0f, dt, 8f); - backPress = animate(backPress, 0f, dt, 8f); - } - - private PanelColors colors(float alpha) { - int titleAlpha = Math.round(alpha * 255f); - int labelAlpha = Math.round(alpha * 155f); - return new PanelColors( - withAlpha(0x0D0F14, Math.round(alpha * 120f)), - withAlpha(0x101218, Math.round(alpha * 120f)), - withAlpha(0x0D0F14, Math.round(alpha * 120f)), - withAlpha(0x08090C, Math.round(alpha * 120f)), - withAlpha(0x14171F, Math.round(alpha * 150f)), - withAlpha(0x181B24, Math.round(alpha * 150f)), - withAlpha(0x14171F, Math.round(alpha * 150f)), - withAlpha(0x10131A, Math.round(alpha * 150f)), - withAlpha(0x252A36, Math.round(alpha * 100f)), - withAlpha(0x3A4A5A, Math.round(alpha * 150f)), - withAlpha(0x060810, Math.round(alpha * 80f)), - withAlpha(0xFFFFFF, titleAlpha), - withAlpha(0xFFFFFF, labelAlpha), - withAlpha(0x808890, titleAlpha), - withAlpha(0x606878, labelAlpha), - withAlpha(0x1A1D24, Math.round(alpha * 160f)), - withAlpha(0x1A1F28, Math.round(alpha * 200f)), - withAlpha(0xFFD700, titleAlpha), - withAlpha(0x4A3A10, Math.round(alpha * 180f)) - ); - } - - private void renderDimmer() { - Themes.Theme t = Theme.theme(); - int soft = withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.46f), 140); - int deep = withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.68f), 180); - Renderer2D.COLOR.quad(0, 0, fixedWidth, fixedHeight, soft, soft, deep, deep); - } - - private void renderShaderBackground(GuiGraphicsExtractor context) { - Minecraft mc = this.minecraft; - if (mc == null) return; - MainConfig cfg = MainConfig.get(); - String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; - if (mode == null || mode.equalsIgnoreCase("off")) { - context.fill(0, 0, width, height, 0xFF000000); - return; - } - MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); - } - - private void updateUiMetrics() { - Minecraft mc = minecraft; - if (mc == null) { - fixedWidth = width; - fixedHeight = height; - return; - } - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - fixedWidth = Math.max(1f, HudScale.virtualWidth(fbw, fbh)); - fixedHeight = Math.max(1f, HudScale.virtualHeight(fbw, fbh)); - } - - private float toFixedX(float screenX) { - Minecraft mc = minecraft; - if (mc == null) return screenX; - float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - if (hudScale <= 0f) return screenX; - return screenX * mc.getWindow().getGuiScale() / hudScale; - } - - private float toFixedY(float screenY) { - Minecraft mc = minecraft; - if (mc == null) return screenY; - float hudScale = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - if (hudScale <= 0f) return screenY; - return screenY * mc.getWindow().getGuiScale() / hudScale; - } - - private Bounds bounds() { - return new Bounds(fixedWidth * 0.5f - PANEL_W * 0.5f, fixedHeight * 0.5f - PANEL_H * 0.5f); - } - - private void closeToParent() { - if (minecraft != null) ClientScreen.show(minecraft, parent); - } - - private void draw(TextRenderer renderer, String text, float x, float y, float size, int argb) { - if (renderer == null || text == null || text.isEmpty()) return; - renderer.setAlpha(1.0); - renderer.begin(size, false, false); - renderer.render(text, x, y, new RenderColor(argb), false); - renderer.end(); - } - - private void drawCentered(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { - draw(renderer, text, centerX - width(renderer, text, size) * 0.5f, y, size, argb); - } - - private float width(TextRenderer renderer, String text, float size) { - if (renderer == null || text == null) return 0f; - renderer.begin(size, true, false); - float w = (float) renderer.getWidth(text, false); - renderer.end(); - return w; - } - - private String ellipsize(String text, float size, float maxWidth) { - if (text == null || text.isEmpty()) return ""; - if (width(bodyRenderer, text, size) <= maxWidth) return text; - String out = text; - while (out.length() > 3 && width(bodyRenderer, out + "...", size) > maxWidth) { - out = out.substring(0, out.length() - 1); - } - return out + "..."; - } - - private enum Field { - ADDRESS(0), - USERNAME(1), - PASSWORD(2); - - private final int index; - - Field(int index) { - this.index = index; - } - - private Field next() { - return values()[(ordinal() + 1) % values().length]; - } - } - - private record Bounds(float x, float y) { - } - - private record PanelColors( - int bgTopLeft, - int bgTopRight, - int bgBottomRight, - int bgBottomLeft, - int headerTopLeft, - int headerTopRight, - int headerBottomRight, - int headerBottomLeft, - int stroke, - int strokeSoft, - int shadow, - int title, - int label, - int muted, - int mutedLabel, - int surface, - int surfaceHover, - int accent, - int accentSoft - ) { - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerController.java new file mode 100644 index 0000000..49a4c04 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerController.java @@ -0,0 +1,164 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.core.AddonManager; +import dev.loki.lovisual.addon.pipeline.AddonSnapshot; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class AddonManagerController { + private final AddonPanelAnims anims = new AddonPanelAnims(); + private final AddonScanState scanState = new AddonScanState(); + private final Map cardHover = new HashMap<>(); + private final Map cardActive = new HashMap<>(); + private final Map cardPress = new HashMap<>(); + private String selectedId = ""; + private float fixedWidth; + private float fixedHeight; + private float scrollOffset; + private float targetScrollOffset; + AddonPanelAnims anims() { return anims; } + AddonScanState scanState() { return scanState; } + Map cardHover() { return cardHover; } + Map cardActive() { return cardActive; } + Map cardPress() { return cardPress; } + String selectedId() { return selectedId; } + void setSelectedId(String id) { selectedId = id == null ? "" : id; } + float fixedWidth() { return fixedWidth; } + float fixedHeight() { return fixedHeight; } + float scrollOffset() { return scrollOffset; } + void setStatus(String text) { scanState.setStatus(text); } + boolean statusVisible() { return scanState.statusVisible(); } + String status() { return scanState.status(); } + void updateUiMetrics(Minecraft mc, int width, int height) { + if (mc == null) { fixedWidth = width; fixedHeight = height; return; } + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + fixedWidth = Math.max(1f, HudScale.virtualWidth(fbw, fbh)); + fixedHeight = Math.max(1f, HudScale.virtualHeight(fbw, fbh)); + } + AddonUiMetrics.Layout layout() { + float totalW = AddonUiMetrics.LEFT_W + AddonUiMetrics.GAP + AddonUiMetrics.RIGHT_W; + float totalH = AddonUiMetrics.LEFT_TOP_H + AddonUiMetrics.GAP + AddonUiMetrics.LEFT_BOTTOM_H; + float topY = fixedHeight * 0.5f - totalH * 0.5f; + float leftX = fixedWidth * 0.5f - totalW * 0.5f; + return new AddonUiMetrics.Layout(leftX, topY, leftX + AddonUiMetrics.LEFT_W + AddonUiMetrics.GAP, topY + AddonUiMetrics.LEFT_TOP_H + AddonUiMetrics.GAP); + } + float bodyY(AddonUiMetrics.Layout l) { return l.topY() + 28f * AddonUiMetrics.SCALE; } + float listHeight(AddonUiMetrics.Layout l) { return Math.max(1f, AddonUiMetrics.RIGHT_H - 31f * AddonUiMetrics.SCALE); } + void ensureSelection() { + List addons = AddonManager.snapshots(); + if (addons.isEmpty()) { selectedId = ""; return; } + if (selected(addons) == null) selectedId = addons.get(0).id(); + } + AddonSnapshot selected(List addons) { + for (AddonSnapshot addon : addons) if (addon.id().equals(selectedId)) return addon; + return null; + } + void clampScroll() { + List addons = AddonManager.snapshots(); + float listW = AddonUiMetrics.RIGHT_W - 10f * AddonUiMetrics.SCALE; + int columns = AddonManagerOps.listColumns(addons.size(), listW); + int rows = AddonManagerOps.rowsFor(addons.size(), columns); + float listH = listHeight(layout()); + float contentH = Math.max(0f, rows * AddonUiMetrics.CARD_H + Math.max(0, rows - 1) * AddonUiMetrics.CARD_GAP); + float max = Math.max(0f, contentH - listH); + targetScrollOffset = Mth.clamp(targetScrollOffset, 0f, max); + scrollOffset = Mth.clamp(scrollOffset, 0f, max); + } + boolean scrollBy(float mouseX, float mouseY, float amount) { + AddonUiMetrics.Layout l = layout(); + float listX = l.rightX() + 5f * AddonUiMetrics.SCALE; + float listY = bodyY(l); + float listW = AddonUiMetrics.RIGHT_W - 10f * AddonUiMetrics.SCALE; + float listH = listHeight(l); + if (!AddonUiMetrics.inside(mouseX, mouseY, listX, listY, listW, listH)) return false; + targetScrollOffset -= amount * (25f * AddonUiMetrics.SCALE); + clampScroll(); + return true; + } + boolean handleClick(float mouseX, float mouseY, Runnable closeToParent) { + AddonUiMetrics.Layout l = layout(); + List addons = AddonManager.snapshots(); + AddonSnapshot selected = selected(addons); + float x = l.leftX() + 5f * AddonUiMetrics.SCALE; + float y = l.bottomY() + 6f * AddonUiMetrics.SCALE; + float w = AddonUiMetrics.LEFT_W - 10f * AddonUiMetrics.SCALE; + float half = (w - AddonUiMetrics.GAP) * 0.5f; + if (selected != null && AddonUiMetrics.inside(mouseX, mouseY, x, y, half, AddonUiMetrics.BUTTON_H)) { + anims.pressToggle(); + boolean next = !selected.enabled(); + if (AddonManager.setEnabled(selected.id(), next)) setStatus(""); + return true; + } + if (AddonUiMetrics.inside(mouseX, mouseY, x + half + AddonUiMetrics.GAP, y, half, AddonUiMetrics.BUTTON_H)) { + anims.pressScan(); scanState.scan(); return true; + } + if (AddonUiMetrics.inside(mouseX, mouseY, x, y + 17f * AddonUiMetrics.SCALE, half, AddonUiMetrics.BUTTON_H)) { + anims.pressMods(); + if (AddonManagerOps.openPath(FabricLoader.getInstance().getGameDir().resolve("mods"), this)) setStatus(AddonUiMetrics.tr("screen.lovisual.addon_manager.status.opened_mods")); + return true; + } + if (AddonUiMetrics.inside(mouseX, mouseY, x + half + AddonUiMetrics.GAP, y + 17f * AddonUiMetrics.SCALE, half, AddonUiMetrics.BUTTON_H)) { + anims.pressConfig(); + if (AddonManagerOps.openPath(FabricLoader.getInstance().getConfigDir().resolve("lovisual").resolve("addons"), this)) setStatus(AddonUiMetrics.tr("screen.lovisual.addon_manager.status.opened_config")); + return true; + } + if (AddonUiMetrics.inside(mouseX, mouseY, x, y + 34f * AddonUiMetrics.SCALE, w, AddonUiMetrics.BUTTON_H)) { + anims.pressBack(); closeToParent.run(); return true; + } + float listX = l.rightX() + 5f * AddonUiMetrics.SCALE; + float listY = bodyY(l); + float listW = AddonUiMetrics.RIGHT_W - 10f * AddonUiMetrics.SCALE; + float listH = listHeight(l); + int columns = AddonManagerOps.listColumns(addons.size(), listW); + float cardW = AddonManagerOps.cardWidth(listW, columns); + if (!AddonUiMetrics.inside(mouseX, mouseY, listX, listY, listW, listH)) return true; + for (int i = 0; i < addons.size(); i++) { + int col = i % columns; + int row = i / columns; + float cardX = listX + col * (cardW + AddonUiMetrics.CARD_GAP); + float cardY = listY + row * (AddonUiMetrics.CARD_H + AddonUiMetrics.CARD_GAP) - scrollOffset; + if (AddonUiMetrics.inside(mouseX, mouseY, cardX, cardY, cardW, AddonUiMetrics.CARD_H)) { + selectedId = addons.get(i).id(); cardPress.put(selectedId, 1f); setStatus(""); return true; + } + } + return true; + } + void updateAnimations(float mouseX, float mouseY) { + float dt = AnimationUtility.deltaTime(); + AddonUiMetrics.Layout l = layout(); + anims.updateHover(mouseX, mouseY, l); + scanState.tickStatusExpiry(); + List addons = AddonManager.snapshots(); + Set keys = new HashSet<>(); + float listX = l.rightX() + 5f * AddonUiMetrics.SCALE; + float listY = bodyY(l); + float listW = AddonUiMetrics.RIGHT_W - 10f * AddonUiMetrics.SCALE; + int columns = AddonManagerOps.listColumns(addons.size(), listW); + float cardW = AddonManagerOps.cardWidth(listW, columns); + for (int i = 0; i < addons.size(); i++) { + AddonSnapshot addon = addons.get(i); + keys.add(addon.id()); + int col = i % columns; + int row = i / columns; + float cardX = listX + col * (cardW + AddonUiMetrics.CARD_GAP); + float cardY = listY + row * (AddonUiMetrics.CARD_H + AddonUiMetrics.CARD_GAP) - scrollOffset; + boolean hover = AddonUiMetrics.inside(mouseX, mouseY, cardX, cardY, cardW, AddonUiMetrics.CARD_H); + cardHover.put(addon.id(), AddonUiMetrics.animate(cardHover.getOrDefault(addon.id(), 0f), hover ? 1f : 0f, dt, 10f)); + cardActive.put(addon.id(), AddonUiMetrics.animate(cardActive.getOrDefault(addon.id(), 0f), addon.id().equals(selectedId) ? 1f : 0f, dt, 8f)); + cardPress.put(addon.id(), AddonUiMetrics.animate(cardPress.getOrDefault(addon.id(), 0f), 0f, dt, 7f)); + } + AddonUiMetrics.prune(cardHover, keys); + AddonUiMetrics.prune(cardActive, keys); + AddonUiMetrics.prune(cardPress, keys); + scrollOffset = AddonUiMetrics.animate(scrollOffset, targetScrollOffset, dt, 12f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerOps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerOps.java new file mode 100644 index 0000000..2260a3d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonManagerOps.java @@ -0,0 +1,112 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.core.AddonIssue; +import dev.loki.lovisual.addon.pipeline.AddonSnapshot; +import net.minecraft.util.Util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +final class AddonManagerOps { + private AddonManagerOps() { + } + + static boolean openPath(Path path, AddonManagerController controller) { + try { + Files.createDirectories(path); + Util.getPlatform().openUri(path.toUri()); + return true; + } catch (Throwable ignored) { + controller.setStatus(AddonUiMetrics.tr("screen.lovisual.addon_manager.status.open_failed")); + return false; + } + } + + static String addonIcon(AddonSnapshot addon) { + return switch (addon.status()) { + case ACTIVE -> addon.enabled() ? "folder-check" : "folder"; + case ERROR -> "folder-x"; + case DISABLED -> "folder"; + case SUSPENDED -> "folder-clock"; + case SHUTDOWN -> "folder-output"; + default -> "package"; + }; + } + + static String statusText(AddonSnapshot addon) { + if (!addon.enabled()) return AddonUiMetrics.tr("screen.lovisual.addon_manager.status.disabled_short"); + return switch (addon.status()) { + case ACTIVE -> AddonUiMetrics.tr("screen.lovisual.addon_manager.state.active"); + case ERROR -> AddonUiMetrics.tr("screen.lovisual.addon_manager.state.error"); + case DISABLED -> AddonUiMetrics.tr("screen.lovisual.addon_manager.state.disabled"); + case SUSPENDED -> AddonUiMetrics.tr("screen.lovisual.addon_manager.state.suspended"); + case SHUTDOWN -> AddonUiMetrics.tr("screen.lovisual.addon_manager.state.shutdown"); + default -> addon.status().name().toLowerCase(Locale.ROOT); + }; + } + + static String countsText(AddonSnapshot addon) { + int hud = addon.draggableHudElements() + addon.staticHudElements(); + return AddonUiMetrics.tr("screen.lovisual.addon_manager.metric.modules") + " " + addon.modules() + + " \u00b7 " + AddonUiMetrics.tr("screen.lovisual.addon_manager.metric.hud") + " " + hud + + " \u00b7 " + AddonUiMetrics.tr("screen.lovisual.addon_manager.metric.commands") + " " + addon.commands() + + " \u00b7 " + AddonUiMetrics.tr("screen.lovisual.addon_manager.metric.gui") + " " + addon.clickGuiSections(); + } + + static int statusColor(AddonSnapshot addon, AddonUiMetrics.PanelColors c) { + return switch (addon.status()) { + case ACTIVE -> c.accent(); + case ERROR -> AddonUiMetrics.withAlpha(0xFF8888, 255); + case DISABLED -> c.muted(); + default -> c.label(); + }; + } + + static String addonInfo(AddonSnapshot addon) { + if (addon == null) return ""; + AddonIssue issue = primaryIssue(addon); + if (issue != null) return issueText(issue); + if (addon.restartRequired()) return AddonUiMetrics.tr("screen.lovisual.addon_manager.restart_required"); + return addonDescription(addon); + } + + static AddonIssue primaryIssue(AddonSnapshot addon) { + AddonIssue fallback = null; + for (AddonIssue issue : addon.issues()) { + if (issue.severity() == AddonIssue.Severity.ERROR) return issue; + if (fallback == null && issue.severity() == AddonIssue.Severity.WARNING) fallback = issue; + } + return fallback; + } + + static String issueText(AddonIssue issue) { + if (issue == null) return ""; + String message = issue.message() == null ? "" : issue.message().trim(); + String detail = issue.detail() == null ? "" : issue.detail().trim(); + String body = !message.isEmpty() ? message : detail; + if (!message.isEmpty() && !detail.isEmpty()) body = message + " \u00b7 " + detail; + if (body.isEmpty()) body = issue.severity().name().toLowerCase(Locale.ROOT); + return body; + } + + static String addonDescription(AddonSnapshot addon) { + if (addon == null || addon.description() == null) return ""; + return addon.description().trim(); + } + + static int listColumns(int count, float listW) { + if (count <= 1) return 1; + return listW >= 250f * AddonUiMetrics.SCALE ? 2 : 1; + } + + static int rowsFor(int count, int columns) { + if (count <= 0) return 0; + return (count + Math.max(1, columns) - 1) / Math.max(1, columns); + } + + static float cardWidth(float listW, int columns) { + int safeColumns = Math.max(1, columns); + return safeColumns == 1 ? listW : (listW - AddonUiMetrics.CARD_GAP * (safeColumns - 1)) / safeColumns; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonPanelAnims.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonPanelAnims.java new file mode 100644 index 0000000..2785e2b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/AddonPanelAnims.java @@ -0,0 +1,56 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import static dev.loki.lovisual.features.gui.mainmenu.AddonUiMetrics.BUTTON_H; +import static dev.loki.lovisual.features.gui.mainmenu.AddonUiMetrics.GAP; +import static dev.loki.lovisual.features.gui.mainmenu.AddonUiMetrics.LEFT_W; +import static dev.loki.lovisual.features.gui.mainmenu.AddonUiMetrics.SCALE; + +final class AddonPanelAnims { + private float toggleHover; + private float togglePress; + private float scanHover; + private float scanPress; + private float modsHover; + private float modsPress; + private float configHover; + private float configPress; + private float backHover; + private float backPress; + + float toggleHover() { return toggleHover; } + float togglePress() { return togglePress; } + float scanHover() { return scanHover; } + float scanPress() { return scanPress; } + float modsHover() { return modsHover; } + float modsPress() { return modsPress; } + float configHover() { return configHover; } + float configPress() { return configPress; } + float backHover() { return backHover; } + float backPress() { return backPress; } + + void pressToggle() { togglePress = 1f; } + void pressScan() { scanPress = 1f; } + void pressMods() { modsPress = 1f; } + void pressConfig() { configPress = 1f; } + void pressBack() { backPress = 1f; } + + void updateHover(float mouseX, float mouseY, AddonUiMetrics.Layout layout) { + float dt = AnimationUtility.deltaTime(); + float x = layout.leftX() + 5f * SCALE; + float y = layout.bottomY() + 6f * SCALE; + float w = LEFT_W - 10f * SCALE; + float half = (w - GAP) * 0.5f; + toggleHover = AddonUiMetrics.animate(toggleHover, AddonUiMetrics.inside(mouseX, mouseY, x, y, half, BUTTON_H) ? 1f : 0f, dt, 11f); + scanHover = AddonUiMetrics.animate(scanHover, AddonUiMetrics.inside(mouseX, mouseY, x + half + GAP, y, half, BUTTON_H) ? 1f : 0f, dt, 11f); + modsHover = AddonUiMetrics.animate(modsHover, AddonUiMetrics.inside(mouseX, mouseY, x, y + 17f * SCALE, half, BUTTON_H) ? 1f : 0f, dt, 11f); + configHover = AddonUiMetrics.animate(configHover, AddonUiMetrics.inside(mouseX, mouseY, x + half + GAP, y + 17f * SCALE, half, BUTTON_H) ? 1f : 0f, dt, 11f); + backHover = AddonUiMetrics.animate(backHover, AddonUiMetrics.inside(mouseX, mouseY, x, y + 34f * SCALE, w, BUTTON_H) ? 1f : 0f, dt, 11f); + togglePress = AddonUiMetrics.animate(togglePress, 0f, dt, 8f); + scanPress = AddonUiMetrics.animate(scanPress, 0f, dt, 8f); + modsPress = AddonUiMetrics.animate(modsPress, 0f, dt, 8f); + configPress = AddonUiMetrics.animate(configPress, 0f, dt, 8f); + backPress = AddonUiMetrics.animate(backPress, 0f, dt, 8f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/auth/AddonScanState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/auth/AddonScanState.java new file mode 100644 index 0000000..f095c9f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/auth/AddonScanState.java @@ -0,0 +1,52 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.core.AddonIssue; +import dev.loki.lovisual.addon.core.AddonManager; +import net.minecraft.util.Util; + +import java.util.List; + +final class AddonScanState { + private List scanIssues = List.of(); + private String status = ""; + private long statusUntilMillis; + + List scanIssues() { + return scanIssues; + } + + String status() { + return status; + } + + boolean statusVisible() { + return !status.isBlank() && (statusUntilMillis <= 0L || Util.getMillis() <= statusUntilMillis); + } + + void setStatus(String text) { + if (text == null || text.isBlank()) { + status = ""; + statusUntilMillis = 0L; + return; + } + status = text; + statusUntilMillis = Util.getMillis() + 1600L; + } + + void tickStatusExpiry() { + if (!status.isBlank() && statusUntilMillis > 0L && Util.getMillis() > statusUntilMillis) { + status = ""; + statusUntilMillis = 0L; + } + } + + void scan() { + scanIssues = AddonManager.scan(); + if (scanIssues.isEmpty()) setStatus(""); + else setStatus(AddonUiMetrics.tr("screen.lovisual.addon_manager.status.scanned", scanIssues.size())); + } + + void clearIssues() { + scanIssues = List.of(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonManagerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonManagerRenderer.java new file mode 100644 index 0000000..9097729 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonManagerRenderer.java @@ -0,0 +1,115 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +final class AddonManagerRenderer { + private final TextRenderer titleRenderer; + private final TextRenderer bodyRenderer; + private final AddonPanelsRenderer panels; + private final AddonCardsRenderer cards; + private float openAnim; + + private AddonManagerRenderer(TextRenderer titleRenderer, TextRenderer bodyRenderer, AddonPanelsRenderer panels, AddonCardsRenderer cards) { + this.titleRenderer = titleRenderer; + this.bodyRenderer = bodyRenderer; + this.panels = panels; + this.cards = cards; + } + + static AddonManagerRenderer create() { + TextRenderer title = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer body = Fonts.renderer("InterMedium", FontInfo.Type.Regular, title); + AddonCardsRenderer cards = new AddonCardsRenderer(title, body); + AddonPanelsRenderer panels = new AddonPanelsRenderer(title, body); + return new AddonManagerRenderer(title, body, panels, cards); + } + + void drawFrame(GuiGraphicsExtractor context, AddonManagerController controller, float mouseX, float mouseY) { + controller.clampScroll(); + openAnim = AddonUiMetrics.animate(openAnim, 1f, AnimationUtility.deltaTime(), 7f); + float open = AnimationUtility.easeOutCubic(openAnim); + AddonUiMetrics.PanelColors c = colors(open); + AddonUiMetrics.Layout l = controller.layout(); + + ViewportContext.beginUnscaledLogical(context); + Renderer2D.COLOR.begin(); + try { + renderDimmer(controller); + renderPanel(l.leftX(), l.topY(), AddonUiMetrics.LEFT_W, AddonUiMetrics.LEFT_TOP_H, c, open); + renderPanel(l.leftX(), l.bottomY(), AddonUiMetrics.LEFT_W, AddonUiMetrics.LEFT_BOTTOM_H, c, open); + renderPanel(l.rightX(), l.topY(), AddonUiMetrics.RIGHT_W, AddonUiMetrics.RIGHT_H, c, open); + + panels.draw(controller, mouseX, mouseY, l, c); + cards.draw(controller, mouseX, mouseY, l, c); + } finally { + Renderer2D.COLOR.render(); + ViewportContext.end(context); + } + } + + private void renderPanel(float x, float y, float w, float h, AddonUiMetrics.PanelColors c, float open) { + Renderer2D.COLOR.blurRect(x, y, w, h, AddonUiMetrics.PANEL_R, 15f, 1.0f, 0.30f, 0xFFFFFF); + Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, AddonUiMetrics.PANEL_R, AddonUiMetrics.PANEL_SHADOW, 0.022f + open * 0.012f, c.shadow()); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, AddonUiMetrics.PANEL_R, 1f, c.bgTopLeft(), c.bgTopRight(), c.bgBottomRight(), c.bgBottomLeft()); + Renderer2D.COLOR.roundedRectMaskedQuad(x, y, w, AddonUiMetrics.HEADER_H, x, y, w, h, AddonUiMetrics.PANEL_R, 1f, + c.headerTopLeft(), c.headerTopRight(), c.headerBottomRight(), c.headerBottomLeft()); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, AddonUiMetrics.PANEL_R, 1f, 1f, HudRenderUtil.mixColor(c.stroke(), 0xFFFFFFFF, 0.04f), c.stroke(), 90f); + } + + private AddonUiMetrics.PanelColors colors(float alpha) { + int bgAlpha = Math.round(alpha * 120f); + int headerAlpha = Math.round(alpha * 150f); + int strokeAlpha = Math.round(alpha * 100f); + int shadowAlpha = Math.round(alpha * 80f); + int titleAlpha = Math.round(alpha * 255f); + int labelAlpha = Math.round(alpha * 155f); + Themes.Theme theme = Theme.theme(); + int themeAccent = theme != null ? theme.accent() : 0xFFFFFFFF; + int themeAccentSoft = theme != null ? theme.accentSoft() : themeAccent; + return new AddonUiMetrics.PanelColors( + AddonUiMetrics.withAlpha(0x0D0F14, bgAlpha), + AddonUiMetrics.withAlpha(0x101218, bgAlpha), + AddonUiMetrics.withAlpha(0x0D0F14, bgAlpha), + AddonUiMetrics.withAlpha(0x08090C, bgAlpha), + AddonUiMetrics.withAlpha(0x14171F, headerAlpha), + AddonUiMetrics.withAlpha(0x181B24, headerAlpha), + AddonUiMetrics.withAlpha(0x14171F, headerAlpha), + AddonUiMetrics.withAlpha(0x10131A, headerAlpha), + AddonUiMetrics.withAlpha(0x252A36, strokeAlpha), + AddonUiMetrics.withAlpha(0x3A4A5A, Math.round(alpha * 150f)), + AddonUiMetrics.withAlpha(0x060810, shadowAlpha), + AddonUiMetrics.withAlpha(0xFFFFFF, titleAlpha), + AddonUiMetrics.withAlpha(0xFFFFFF, labelAlpha), + AddonUiMetrics.withAlpha(0x808890, titleAlpha), + AddonUiMetrics.withAlpha(0x606878, labelAlpha), + AddonUiMetrics.withAlpha(0x1A1D24, Math.round(alpha * 160f)), + AddonUiMetrics.withAlpha(0x1A1F28, Math.round(alpha * 200f)), + AddonUiMetrics.withAlpha(themeAccent, titleAlpha), + AddonUiMetrics.withAlpha(themeAccentSoft, Math.round(alpha * 180f)), + AddonUiMetrics.withAlpha(0x1A1D24, Math.round(alpha * 160f)), + AddonUiMetrics.withAlpha(0x0D1118, Math.round(alpha * 185f)), + AddonUiMetrics.withAlpha(0x1A1D24, Math.round(alpha * 150f)), + AddonUiMetrics.withAlpha(0x0D1118, Math.round(alpha * 175f)), + AddonUiMetrics.withAlpha(0x3A4A5A, Math.round(alpha * 132f)), + AddonUiMetrics.withAlpha(themeAccent, titleAlpha), + AddonUiMetrics.withAlpha(HudRenderUtil.mixColor(themeAccent, themeAccentSoft, 0.58f), titleAlpha), + AddonUiMetrics.withAlpha(0x060810, Math.round(alpha * 150f)) + ); + } + + private void renderDimmer(AddonManagerController controller) { + Themes.Theme t = Theme.theme(); + int soft = AddonUiMetrics.withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.46f), 140); + int deep = AddonUiMetrics.withAlpha(HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.68f), 180); + Renderer2D.COLOR.quad(0, 0, controller.fixedWidth(), controller.fixedHeight(), soft, soft, deep, deep); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonPanelsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonPanelsRenderer.java new file mode 100644 index 0000000..615ba35 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonPanelsRenderer.java @@ -0,0 +1,162 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.core.AddonIssue; +import dev.loki.lovisual.addon.core.AddonManager; +import dev.loki.lovisual.addon.pipeline.AddonSnapshot; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +import java.util.List; + +final class AddonPanelsRenderer { + private final TextRenderer titleRenderer; + private final TextRenderer bodyRenderer; + + AddonPanelsRenderer(TextRenderer titleRenderer, TextRenderer bodyRenderer) { + this.titleRenderer = titleRenderer; + this.bodyRenderer = bodyRenderer; + } + + void draw(AddonManagerController controller, float mouseX, float mouseY, AddonUiMetrics.Layout l, AddonUiMetrics.PanelColors c) { + renderDetails(l, c, mouseX, mouseY, controller); + renderActionsPanel(l, c, controller); + } + + private void renderDetails(AddonUiMetrics.Layout l, AddonUiMetrics.PanelColors c, float mouseX, float mouseY, AddonManagerController controller) { + List addons = AddonManager.snapshots(); + AddonSnapshot selected = controller.selected(addons); + int active = 0; + int warnings = 0; + int errors = 0; + for (AddonSnapshot addon : addons) { + if (addon.enabled()) active++; + for (AddonIssue issue : addon.issues()) { + if (issue.severity() == AddonIssue.Severity.ERROR) errors++; + else if (issue.severity() == AddonIssue.Severity.WARNING) warnings++; + } + } + for (AddonIssue issue : controller.scanState().scanIssues()) { + if (issue.severity() == AddonIssue.Severity.ERROR) errors++; + else if (issue.severity() == AddonIssue.Severity.WARNING) warnings++; + } + float x = l.leftX() + 5f * AddonUiMetrics.SCALE; + float y = l.topY(); + float w = AddonUiMetrics.LEFT_W - 10f * AddonUiMetrics.SCALE; + AddonUiMetrics.draw(titleRenderer, AddonUiMetrics.tr("screen.lovisual.addon_manager.header"), l.leftX() + 8f * AddonUiMetrics.SCALE, y + 7f * AddonUiMetrics.SCALE, AddonUiMetrics.TITLE_FONT, c.title()); + renderSelectedDetails(x, y + 27f * AddonUiMetrics.SCALE, w, c, selected, mouseX, mouseY); + float metricsY = y + 75f * AddonUiMetrics.SCALE; + float metricStep = AddonUiMetrics.METRIC_H + AddonUiMetrics.METRIC_GAP; + renderMetricLine(x, metricsY, w, AddonUiMetrics.tr("screen.lovisual.addon_manager.installed"), String.valueOf(addons.size()), "package", c, AddonUiMetrics.inside(mouseX, mouseY, x, metricsY, w, AddonUiMetrics.METRIC_H)); + renderMetricLine(x, metricsY + metricStep, w, AddonUiMetrics.tr("screen.lovisual.addon_manager.active"), String.valueOf(active), "check", c, AddonUiMetrics.inside(mouseX, mouseY, x, metricsY + metricStep, w, AddonUiMetrics.METRIC_H)); + renderMetricLine(x, metricsY + metricStep * 2f, w, AddonUiMetrics.tr("screen.lovisual.addon_manager.issues"), errors + "/" + warnings, errors > 0 ? "folder-x" : "folder-clock", c, AddonUiMetrics.inside(mouseX, mouseY, x, metricsY + metricStep * 2f, w, AddonUiMetrics.METRIC_H)); + if (controller.statusVisible()) { + float statusY = y + AddonUiMetrics.LEFT_TOP_H - 15f * AddonUiMetrics.SCALE; + renderStatusLine(x, statusY, w, controller.status(), c, AddonUiMetrics.inside(mouseX, mouseY, x, statusY, w, 9f * AddonUiMetrics.SCALE)); + } + } + + private void renderActionsPanel(AddonUiMetrics.Layout l, AddonUiMetrics.PanelColors c, AddonManagerController controller) { + List addons = AddonManager.snapshots(); + AddonSnapshot selected = controller.selected(addons); + float x = l.leftX() + 5f * AddonUiMetrics.SCALE; + float y = l.bottomY() + 6f * AddonUiMetrics.SCALE; + float w = AddonUiMetrics.LEFT_W - 10f * AddonUiMetrics.SCALE; + float half = (w - AddonUiMetrics.GAP) * 0.5f; + AddonPanelAnims a = controller.anims(); + renderAction(x, y, half, AddonUiMetrics.BUTTON_H, selected != null && selected.enabled() + ? AddonUiMetrics.tr("screen.lovisual.addon_manager.disable") + : AddonUiMetrics.tr("screen.lovisual.addon_manager.enable"), selected != null && selected.enabled(), selected != null && selected.enabled() ? "toggle-left" : "toggle-right", a.toggleHover(), a.togglePress(), c); + renderAction(x + half + AddonUiMetrics.GAP, y, half, AddonUiMetrics.BUTTON_H, AddonUiMetrics.tr("screen.lovisual.addon_manager.scan"), false, "folder-sync", a.scanHover(), a.scanPress(), c); + renderAction(x, y + 17f * AddonUiMetrics.SCALE, half, AddonUiMetrics.BUTTON_H, AddonUiMetrics.tr("screen.lovisual.addon_manager.mods"), false, "folder-open", a.modsHover(), a.modsPress(), c); + renderAction(x + half + AddonUiMetrics.GAP, y + 17f * AddonUiMetrics.SCALE, half, AddonUiMetrics.BUTTON_H, AddonUiMetrics.tr("screen.lovisual.addon_manager.config"), false, "folder-cog", a.configHover(), a.configPress(), c); + renderAction(x, y + 34f * AddonUiMetrics.SCALE, w, AddonUiMetrics.BUTTON_H, AddonUiMetrics.tr("screen.lovisual.addon_manager.back"), false, "x", a.backHover(), a.backPress(), c); + } + + private void renderMetricLine(float x, float y, float w, String label, String value, String svg, AddonUiMetrics.PanelColors c, boolean hovered) { + float r = 3.5f * AddonUiMetrics.SCALE; + int fillTop = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(c.surface(), 144), AddonUiMetrics.withAlpha(c.surfaceHover(), 166), hovered ? 0.34f : 0.12f); + int fillBottom = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0xFF0D1118, 158), AddonUiMetrics.withAlpha(c.surface(), 150), hovered ? 0.18f : 0.05f); + int stroke = HudRenderUtil.mixColor(c.stroke(), c.strokeSoft(), hovered ? 0.42f : 0.20f); + Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, AddonUiMetrics.METRIC_H, r, r, r, r, 1f, fillTop, fillTop, fillBottom, fillBottom); + Renderer2D.COLOR.roundedRectStrokeCorners(x, y, w, AddonUiMetrics.METRIC_H, r, r, r, r, 0.55f, stroke); + AddonUiMetrics.drawSvgGradient(svg, x + 4.0f * AddonUiMetrics.SCALE, y + 3.0f * AddonUiMetrics.SCALE, 7.0f * AddonUiMetrics.SCALE, 7.0f * AddonUiMetrics.SCALE, c); + float valueW = AddonUiMetrics.width(bodyRenderer, value, AddonUiMetrics.METRIC_FONT); + float textY = y + 3.15f * AddonUiMetrics.SCALE; + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, label, x + 14.0f * AddonUiMetrics.SCALE, textY, AddonUiMetrics.METRIC_FONT, Math.max(1f, w - valueW - 23f * AddonUiMetrics.SCALE), c.muted(), hovered); + AddonUiMetrics.draw(bodyRenderer, value, x + w - valueW - 5f * AddonUiMetrics.SCALE, textY, AddonUiMetrics.METRIC_FONT, c.title()); + } + + private void renderSelectedDetails(float x, float y, float w, AddonUiMetrics.PanelColors c, AddonSnapshot selected, float mouseX, float mouseY) { + float h = 42f * AddonUiMetrics.SCALE; + Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, h, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, 1f, + AddonUiMetrics.withAlpha(c.surface(), 160), AddonUiMetrics.withAlpha(c.surfaceHover(), 170), AddonUiMetrics.withAlpha(0xFF0D1118, 178), AddonUiMetrics.withAlpha(0xFF0D1118, 178)); + Renderer2D.COLOR.roundedRectStrokeCorners(x, y, w, h, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, 0.55f, HudRenderUtil.mixColor(c.stroke(), c.strokeSoft(), 0.36f)); + if (selected == null) { + AddonUiMetrics.drawCentered(bodyRenderer, AddonUiMetrics.tr("screen.lovisual.addon_manager.empty"), x + w * 0.5f, y + 17f * AddonUiMetrics.SCALE, AddonUiMetrics.BODY_FONT, c.mutedLabel()); + return; + } + float iconBox = 18f * AddonUiMetrics.SCALE; + float iconX = x + 6f * AddonUiMetrics.SCALE; + float iconY = y + 7f * AddonUiMetrics.SCALE; + Renderer2D.COLOR.roundedRectGradientQuad(iconX, iconY, iconBox, iconBox, 3f * AddonUiMetrics.SCALE, 1f, AddonUiMetrics.withAlpha(c.surface(), 176), AddonUiMetrics.withAlpha(c.surfaceHover(), 186), AddonUiMetrics.withAlpha(0xFF0D1118, 190), AddonUiMetrics.withAlpha(0xFF0D1118, 190)); + Renderer2D.COLOR.roundedRectStrokeGradient(iconX, iconY, iconBox, iconBox, 3f * AddonUiMetrics.SCALE, 1f, 0.5f, c.stroke(), c.strokeSoft(), 90f); + AddonUiMetrics.drawSvgGradient(AddonManagerOps.addonIcon(selected), iconX + 4.3f * AddonUiMetrics.SCALE, iconY + 4.3f * AddonUiMetrics.SCALE, 9.4f * AddonUiMetrics.SCALE, 9.4f * AddonUiMetrics.SCALE, c); + String state = AddonManagerOps.statusText(selected); + float stateW = AddonUiMetrics.miniStatusWidth(bodyRenderer, state); + float titleX = iconX + iconBox + 5f * AddonUiMetrics.SCALE; + float stateX = x + w - stateW - 6f * AddonUiMetrics.SCALE; + boolean drawState = stateX > titleX + 30f * AddonUiMetrics.SCALE; + if (drawState) renderMiniStatusPill(stateX, y + 6f * AddonUiMetrics.SCALE, state, AddonManagerOps.statusColor(selected, c), c); + float titleW = Math.max(1f, (drawState ? stateX - titleX - 5f * AddonUiMetrics.SCALE : x + w - titleX - 7f * AddonUiMetrics.SCALE)); + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, selected.name(), titleX, y + 7.4f * AddonUiMetrics.SCALE, AddonUiMetrics.SELECTED_NAME_FONT, titleW, c.title(), AddonUiMetrics.inside(mouseX, mouseY, titleX, y + 6f * AddonUiMetrics.SCALE, titleW, 10f * AddonUiMetrics.SCALE)); + String info = AddonManagerOps.addonInfo(selected); + if (!info.isBlank()) { + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, info, titleX, y + 19.4f * AddonUiMetrics.SCALE, AddonUiMetrics.SELECTED_DESCRIPTION_FONT, Math.max(1f, x + w - titleX - 7f * AddonUiMetrics.SCALE), selected.restartRequired() ? c.label() : c.muted(), AddonUiMetrics.inside(mouseX, mouseY, titleX, y + 18f * AddonUiMetrics.SCALE, w - 35f * AddonUiMetrics.SCALE, 10f * AddonUiMetrics.SCALE)); + } + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, AddonManagerOps.countsText(selected), x + 7f * AddonUiMetrics.SCALE, y + 31.0f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, w - 14f * AddonUiMetrics.SCALE, c.mutedLabel(), AddonUiMetrics.inside(mouseX, mouseY, x + 7f * AddonUiMetrics.SCALE, y + 30f * AddonUiMetrics.SCALE, w - 14f * AddonUiMetrics.SCALE, 10f * AddonUiMetrics.SCALE)); + } + + private void renderStatusLine(float x, float y, float w, String text, AddonUiMetrics.PanelColors c, boolean hovered) { + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1f, c.statusTop(), c.statusTop(), c.statusBottom(), c.statusBottom()); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1.2f, 0.4f, c.strokeSoft(), c.stroke(), 90f); + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, text, x + 5f * AddonUiMetrics.SCALE, y + 1.8f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, w - 10f * AddonUiMetrics.SCALE, c.muted(), hovered); + } + + private void renderMiniStatusPill(float x, float y, String label, int color, AddonUiMetrics.PanelColors c) { + float w = AddonUiMetrics.miniStatusWidth(bodyRenderer, label); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1f, HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(c.surface(), 170), color, 0.08f), AddonUiMetrics.withAlpha(c.surface(), 170), AddonUiMetrics.withAlpha(0xFF0D1118, 178), AddonUiMetrics.withAlpha(0xFF0D1118, 178)); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1f, 0.42f, AddonUiMetrics.withAlpha(color, 112), c.stroke(), 90f); + Renderer2D.COLOR.roundedRect(x + 3.6f * AddonUiMetrics.SCALE, y + 3.2f * AddonUiMetrics.SCALE, 2.4f * AddonUiMetrics.SCALE, 2.4f * AddonUiMetrics.SCALE, 1.2f * AddonUiMetrics.SCALE, 1f, color); + AddonUiMetrics.draw(bodyRenderer, label, x + 8f * AddonUiMetrics.SCALE, y + 1.75f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, c.label()); + } + + private void renderAction(float x, float y, float w, float h, String label, boolean destructive, String svgName, float hover, float press, AddonUiMetrics.PanelColors c) { + float hoverAnim = AnimationUtility.easeOutCubic(hover); + float pressAnim = AnimationUtility.easeOutCubic(press); + int fillTop; + int fillBottom; + int stroke; + int text; + if (destructive) { + fillTop = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0x1A1416, 170), AddonUiMetrics.withAlpha(0x4B2224, 212), 0.25f + hoverAnim * 0.45f + pressAnim * 0.12f); + fillBottom = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0x120E10, 165), AddonUiMetrics.withAlpha(0x261012, 220), 0.20f + hoverAnim * 0.40f + pressAnim * 0.10f); + stroke = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0x352A2A, 215), AddonUiMetrics.withAlpha(0x8C5054, 235), 0.22f + hoverAnim * 0.58f + pressAnim * 0.12f); + text = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0xD0A0A0, 255), AddonUiMetrics.withAlpha(0xFFB2B2, 255), 0.24f + hoverAnim * 0.56f + pressAnim * 0.08f); + } else { + fillTop = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(c.surface(), 176), AddonUiMetrics.withAlpha(0x223746, 212), 0.20f + hoverAnim * 0.44f + pressAnim * 0.10f); + fillBottom = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(c.surface(), 166), AddonUiMetrics.withAlpha(0x13292F, 220), 0.16f + hoverAnim * 0.40f + pressAnim * 0.08f); + stroke = HudRenderUtil.mixColor(c.stroke(), AddonUiMetrics.withAlpha(0x466A76, 228), 0.18f + hoverAnim * 0.50f + pressAnim * 0.10f); + text = HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(0xD0D8E4, 255), AddonUiMetrics.withAlpha(0xEEF8FF, 255), 0.18f + hoverAnim * 0.38f + pressAnim * 0.08f); + } + Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 3f * AddonUiMetrics.SCALE, 7f * AddonUiMetrics.SCALE, 0.018f + hoverAnim * 0.015f, c.shadow()); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * AddonUiMetrics.SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * AddonUiMetrics.SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); + float contentLift = hoverAnim * 0.65f * AddonUiMetrics.SCALE + pressAnim * 0.55f * AddonUiMetrics.SCALE; + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, label, x + 5f * AddonUiMetrics.SCALE, y + 3.7f * AddonUiMetrics.SCALE - contentLift, AddonUiMetrics.BUTTON_FONT, w - 18f * AddonUiMetrics.SCALE, text, hoverAnim > 0.55f); + float iconSize = 6.7f * AddonUiMetrics.SCALE; + Renderer2D.COLOR.svg(svgName, x + w - 10.7f * AddonUiMetrics.SCALE, y + (h - iconSize) * 0.5f - contentLift, iconSize, iconSize, SvgRenderOptions.overrideColor(text)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonUiMetrics.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonUiMetrics.java new file mode 100644 index 0000000..cbb1656 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/AddonUiMetrics.java @@ -0,0 +1,183 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +import java.util.Map; +import java.util.Set; + +final class AddonUiMetrics { + static final float SCALE = 2.28f; + static final float TEXT_SCALE = 1.30f; + static final float LEFT_W = 112f * SCALE; + static final float LEFT_TOP_H = 137f * SCALE; + static final float LEFT_BOTTOM_H = 58f * SCALE; + static final float RIGHT_W = 300f * SCALE; + static final float RIGHT_H = 165f * SCALE; + static final float GAP = 5f * SCALE; + static final float PANEL_R = 6f * SCALE; + static final float PANEL_SHADOW = 12f * SCALE; + static final float HEADER_H = 22f * SCALE; + static final float LEFT_CARD_R = 4f * SCALE; + static final float CARD_MARKER_W = 2.2f * SCALE; + static final float MARQUEE_SPEED = 24f * SCALE; + static final float MARQUEE_GAP = 18f * SCALE; + static final float MARQUEE_PAUSE = 0.55f; + static final float TITLE_FONT = 0.84f * TEXT_SCALE; + static final float BODY_FONT = 0.60f * TEXT_SCALE; + static final float SMALL_FONT = 0.49f * TEXT_SCALE; + static final float METRIC_FONT = 0.56f * TEXT_SCALE; + static final float DESCRIPTION_FONT = 0.53f * TEXT_SCALE; + static final float SELECTED_DESCRIPTION_FONT = 0.54f * TEXT_SCALE; + static final float ADDON_NAME_FONT = 0.66f * TEXT_SCALE; + static final float SELECTED_NAME_FONT = 0.70f * TEXT_SCALE; + static final float BUTTON_FONT = 0.52f * TEXT_SCALE; + static final float BUTTON_H = 13f * SCALE; + static final float METRIC_H = 13f * SCALE; + static final float METRIC_GAP = 2.5f * SCALE; + static final float CARD_H = 40f * SCALE; + static final float CARD_GAP = 5f * SCALE; + + private AddonUiMetrics() { + } + + static float animate(float current, float target, float dt, float speed) { + float next = AnimationUtility.approach(current, target, dt, speed); + return AnimationUtility.snap(next, target, 0.001f); + } + + static void prune(Map map, Set keys) { + map.keySet().removeIf(key -> !keys.contains(key)); + } + + static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + static int withAlpha(int rgb, int alpha) { + return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); + } + + static String tr(String key, Object... args) { + return I18n.get(key, args); + } + + static void draw(TextRenderer renderer, String text, float x, float y, float size, int argb) { + if (renderer == null || text == null || text.isEmpty()) return; + renderer.setAlpha(1.0); + renderer.begin(size, false, false); + renderer.render(text, x, y, new RenderColor(argb), false); + renderer.end(); + } + + static void drawCentered(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { + draw(renderer, text, centerX - width(renderer, text, size) * 0.5f, y, size, argb); + } + + static float width(TextRenderer renderer, String text, float size) { + if (renderer == null || text == null) return 0f; + renderer.begin(size, true, false); + float w = (float) renderer.getWidth(text, false); + renderer.end(); + return w; + } + + static String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) { + if (text == null || text.isEmpty()) return ""; + if (width(renderer, text, size) <= maxWidth) return text; + String out = text; + while (out.length() > 3 && width(renderer, out + "...", size) > maxWidth) { + out = out.substring(0, out.length() - 1); + } + return out + "..."; + } + + static SvgRenderOptions svgGradient(PanelColors c) { + return SvgRenderOptions.linearGradient(c.iconTop, c.iconBottom, 92f) + .withTextureCache(false) + .withCurveFlatness(0.18f); + } + + static void drawSvgGradient(String svg, float x, float y, float w, float h, PanelColors c) { + Renderer2D.COLOR.svg(svg, x + 0.38f * SCALE, y + 0.42f * SCALE, w, h, + SvgRenderOptions.overrideColor(c.iconShadow).withTextureCache(false).withCurveFlatness(0.18f)); + Renderer2D.COLOR.svg(svg, x, y, w, h, svgGradient(c)); + } + + static void drawFitOrMarquee(TextRenderer renderer, String text, float x, float y, float size, float maxWidth, int color, boolean marquee) { + if (text == null || text.isEmpty() || maxWidth <= 1f) return; + float textW = width(renderer, text, size); + if (textW <= maxWidth) { + draw(renderer, text, x, y, size, color); + return; + } + if (!marquee) { + draw(renderer, ellipsize(renderer, text, size, maxWidth), x, y, size, color); + return; + } + drawScrollingText(renderer, text, x, y, textW, maxWidth, size, color); + } + + static void drawScrollingText(TextRenderer renderer, String text, float x, float y, float fullWidth, float viewWidth, float size, int color) { + if (renderer == null || text == null || text.isEmpty() || viewWidth <= 1f) return; + float cycleDistance = fullWidth + MARQUEE_GAP; + float cycleTime = MARQUEE_PAUSE + cycleDistance / Math.max(1f, MARQUEE_SPEED); + float time = (Util.getMillis() / 1000.0f) % cycleTime; + float offset = time <= MARQUEE_PAUSE ? 0f : -(time - MARQUEE_PAUSE) * MARQUEE_SPEED; + float fade = Math.min(viewWidth * 0.25f, 8f * SCALE); + RenderColor renderColor = new RenderColor(color); + renderer.begin(size, false, false); + try { + renderer.renderHorizontalFadeClipped(text, x + offset, y, renderColor, x, x + viewWidth, fade, fade, false); + if (cycleDistance > viewWidth * 0.5f) { + renderer.renderHorizontalFadeClipped(text, x + offset + cycleDistance, y, renderColor, x, x + viewWidth, fade, fade, false); + } + } finally { + renderer.end(); + } + } + + static float miniStatusWidth(TextRenderer bodyRenderer, String label) { + return Math.max(28f * SCALE, width(bodyRenderer, label, SMALL_FONT) + 11f * SCALE); + } + + record Layout(float leftX, float topY, float rightX, float bottomY) { + } + + record PanelColors( + int bgTopLeft, + int bgTopRight, + int bgBottomRight, + int bgBottomLeft, + int headerTopLeft, + int headerTopRight, + int headerBottomRight, + int headerBottomLeft, + int stroke, + int strokeSoft, + int shadow, + int title, + int label, + int muted, + int mutedLabel, + int surface, + int surfaceHover, + int accent, + int accentSoft, + int detailTop, + int detailBottom, + int statusTop, + int statusBottom, + int scrollTrack, + int iconTop, + int iconBottom, + int iconShadow + ) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardRenderer.java new file mode 100644 index 0000000..0c53795 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardRenderer.java @@ -0,0 +1,79 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.pipeline.AddonSnapshot; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +final class AddonCardRenderer { + private final TextRenderer titleRenderer; + private final TextRenderer bodyRenderer; + + AddonCardRenderer(TextRenderer titleRenderer, TextRenderer bodyRenderer) { + this.titleRenderer = titleRenderer; + this.bodyRenderer = bodyRenderer; + } + + void renderCard(AddonSnapshot addon, float x, float y, float w, float h, AddonUiMetrics.PanelColors c, boolean hovered, AddonManagerController controller) { + float hover = AnimationUtility.easeOutCubic(controller.cardHover().getOrDefault(addon.id(), 0f)); + float active = AnimationUtility.easeOutCubic(controller.cardActive().getOrDefault(addon.id(), 0f)); + float press = AnimationUtility.easeOutCubic(controller.cardPress().getOrDefault(addon.id(), 0f)); + Themes.Theme theme = Theme.theme(); + int activeAccent = AddonUiMetrics.withAlpha(theme.accent(), Math.round(165f * active)); + int fillBase = HudRenderUtil.mixColor(c.surface(), c.surfaceHover(), 0.38f * hover + 0.10f * press); + int fillTop = HudRenderUtil.mixColor(fillBase, c.surfaceHover(), 0.03f * press); + int fillBottom = HudRenderUtil.mixColor(fillBase, 0xFF05070A, 0.18f); + int stroke = HudRenderUtil.mixColor(c.stroke(), c.strokeSoft(), 0.45f * hover); + stroke = HudRenderUtil.mixColor(stroke, activeAccent, 0.38f * active + 0.10f * press); + Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 4f * AddonUiMetrics.SCALE, 8f * AddonUiMetrics.SCALE, 0.018f + hover * 0.018f + active * 0.010f, c.shadow()); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 4f * AddonUiMetrics.SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); + renderActiveCardMarker(x, y, w, h, active, theme); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 4f * AddonUiMetrics.SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f + hover * 0.03f), stroke, 90f); + float iconBox = 24f * AddonUiMetrics.SCALE; + float iconX = x + 7f * AddonUiMetrics.SCALE; + float iconY = y + 8f * AddonUiMetrics.SCALE; + Renderer2D.COLOR.roundedRectGradientQuad(iconX, iconY, iconBox, iconBox, 3f * AddonUiMetrics.SCALE, 1f, AddonUiMetrics.withAlpha(c.surface(), 170), AddonUiMetrics.withAlpha(c.surfaceHover(), 180), AddonUiMetrics.withAlpha(0xFF0D1118, 184), AddonUiMetrics.withAlpha(0xFF0D1118, 184)); + Renderer2D.COLOR.roundedRectStrokeGradient(iconX, iconY, iconBox, iconBox, 3f * AddonUiMetrics.SCALE, 1f, 0.5f, HudRenderUtil.mixColor(c.stroke(), c.strokeSoft(), 0.35f), c.stroke(), 90f); + AddonUiMetrics.drawSvgGradient(AddonManagerOps.addonIcon(addon), iconX + 6.2f * AddonUiMetrics.SCALE, iconY + 6.2f * AddonUiMetrics.SCALE, 11.6f * AddonUiMetrics.SCALE, 11.6f * AddonUiMetrics.SCALE, c); + String state = AddonManagerOps.statusText(addon); + float stateW = AddonUiMetrics.miniStatusWidth(bodyRenderer, state); + float stateX = x + w - stateW - 6f * AddonUiMetrics.SCALE; + renderMiniStatusPill(stateX, y + 7f * AddonUiMetrics.SCALE, state, AddonManagerOps.statusColor(addon, c), c); + float textX = x + 37f * AddonUiMetrics.SCALE; + float titleW = Math.max(1f, stateX - textX - 5f * AddonUiMetrics.SCALE); + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, addon.name(), textX, y + 8.3f * AddonUiMetrics.SCALE, AddonUiMetrics.ADDON_NAME_FONT, titleW, c.title(), hovered); + String description = AddonManagerOps.addonInfo(addon); + if (!description.isBlank()) { + float descW = Math.max(1f, x + w - 7f * AddonUiMetrics.SCALE - textX); + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, description, textX, y + 20.3f * AddonUiMetrics.SCALE, AddonUiMetrics.DESCRIPTION_FONT, descW, addon.restartRequired() ? c.label() : c.muted(), hovered); + } + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, AddonManagerOps.countsText(addon), textX, y + 29.6f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, Math.max(1f, x + w - 7f * AddonUiMetrics.SCALE - textX), c.mutedLabel(), hovered); + } + + private void renderActiveCardMarker(float x, float y, float w, float h, float active, Themes.Theme theme) { + float reveal = AnimationUtility.clamp01(active); + if (reveal <= 0.001f) return; + float markerW = AddonUiMetrics.CARD_MARKER_W * reveal; + if (markerW <= 0.01f) return; + int accent = AddonUiMetrics.withAlpha(theme.accent(), Math.round(255f * reveal)); + boolean clipped = ScissorFunction.pushRaw(x, y, markerW, h); + if (!clipped) return; + try { + Renderer2D.COLOR.roundedRectCornersQuad(x, y, w, h, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, AddonUiMetrics.LEFT_CARD_R, 1f, accent, accent, accent, accent); + } finally { + ScissorFunction.pop(); + } + } + + private void renderMiniStatusPill(float x, float y, String label, int color, AddonUiMetrics.PanelColors c) { + float w = AddonUiMetrics.miniStatusWidth(bodyRenderer, label); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1f, HudRenderUtil.mixColor(AddonUiMetrics.withAlpha(c.surface(), 170), color, 0.08f), AddonUiMetrics.withAlpha(c.surface(), 170), AddonUiMetrics.withAlpha(0xFF0D1118, 178), AddonUiMetrics.withAlpha(0xFF0D1118, 178)); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, 9f * AddonUiMetrics.SCALE, 4.5f * AddonUiMetrics.SCALE, 1f, 0.42f, AddonUiMetrics.withAlpha(color, 112), c.stroke(), 90f); + Renderer2D.COLOR.roundedRect(x + 3.6f * AddonUiMetrics.SCALE, y + 3.2f * AddonUiMetrics.SCALE, 2.4f * AddonUiMetrics.SCALE, 2.4f * AddonUiMetrics.SCALE, 1.2f * AddonUiMetrics.SCALE, 1f, color); + AddonUiMetrics.draw(bodyRenderer, label, x + 8f * AddonUiMetrics.SCALE, y + 1.75f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, c.label()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardsRenderer.java new file mode 100644 index 0000000..082862c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/addon/render/cards/AddonCardsRenderer.java @@ -0,0 +1,88 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.addon.core.AddonManager; +import dev.loki.lovisual.addon.pipeline.AddonSnapshot; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +import java.util.List; + +final class AddonCardsRenderer { + private final TextRenderer titleRenderer; + private final TextRenderer bodyRenderer; + private final AddonCardRenderer cardRenderer; + + AddonCardsRenderer(TextRenderer titleRenderer, TextRenderer bodyRenderer) { + this.titleRenderer = titleRenderer; + this.bodyRenderer = bodyRenderer; + this.cardRenderer = new AddonCardRenderer(titleRenderer, bodyRenderer); + } + + void draw(AddonManagerController controller, float mouseX, float mouseY, AddonUiMetrics.Layout l, AddonUiMetrics.PanelColors c) { + renderListHeader(l.rightX() + 8f * AddonUiMetrics.SCALE, l.topY(), AddonUiMetrics.RIGHT_W - 16f * AddonUiMetrics.SCALE, c, mouseX, mouseY, controller); + List addons = AddonManager.snapshots(); + float listX = l.rightX() + 5f * AddonUiMetrics.SCALE; + float listY = controller.bodyY(l); + float listW = AddonUiMetrics.RIGHT_W - 10f * AddonUiMetrics.SCALE; + float listH = controller.listHeight(l); + if (addons.isEmpty()) { + renderEmptyState(listX, listY, listW, listH, c); + return; + } + int columns = AddonManagerOps.listColumns(addons.size(), listW); + float cardW = AddonManagerOps.cardWidth(listW, columns); + boolean clipped = ScissorFunction.pushRaw(listX, listY - 3f * AddonUiMetrics.SCALE, listW, listH + 6f * AddonUiMetrics.SCALE); + try { + for (int i = 0; i < addons.size(); i++) { + int col = i % columns; + int row = i / columns; + float cardX = listX + col * (cardW + AddonUiMetrics.CARD_GAP); + float cardY = listY + row * (AddonUiMetrics.CARD_H + AddonUiMetrics.CARD_GAP) - controller.scrollOffset(); + if (cardY + AddonUiMetrics.CARD_H < listY - 10f || cardY > listY + listH + 10f) continue; + cardRenderer.renderCard(addons.get(i), cardX, cardY, cardW, AddonUiMetrics.CARD_H, c, AddonUiMetrics.inside(mouseX, mouseY, cardX, cardY, cardW, AddonUiMetrics.CARD_H), controller); + } + } finally { + if (clipped) ScissorFunction.pop(); + } + int rows = AddonManagerOps.rowsFor(addons.size(), columns); + float contentH = Math.max(0f, rows * AddonUiMetrics.CARD_H + Math.max(0, rows - 1) * AddonUiMetrics.CARD_GAP); + if (contentH > listH + 1f) { + float trackX = listX + listW + 2.5f * AddonUiMetrics.SCALE; + float thumbH = Math.max(18f * AddonUiMetrics.SCALE, listH * listH / contentH); + float thumbY = listY + (listH - thumbH) * (controller.scrollOffset() / Math.max(1f, contentH - listH)); + Renderer2D.COLOR.roundedRect(trackX, listY, 1.4f * AddonUiMetrics.SCALE, listH, 0.7f * AddonUiMetrics.SCALE, 1f, c.scrollTrack()); + Renderer2D.COLOR.roundedRect(trackX - 0.2f * AddonUiMetrics.SCALE, thumbY, 1.8f * AddonUiMetrics.SCALE, thumbH, 0.9f * AddonUiMetrics.SCALE, 1f, c.accent()); + } + } + + private void renderListHeader(float x, float headerTop, float w, AddonUiMetrics.PanelColors c, float mouseX, float mouseY, AddonManagerController controller) { + float centerY = headerTop + AddonUiMetrics.HEADER_H * 0.5f; + float iconSize = 13.0f * AddonUiMetrics.SCALE; + float iconX = x + 7.0f * AddonUiMetrics.SCALE; + float iconY = centerY - iconSize * 0.5f; + AddonUiMetrics.drawSvgGradient("folder-tree", iconX, iconY, iconSize, iconSize, c); + String title = AddonUiMetrics.tr("screen.lovisual.addon_manager.list"); + float titleX = x + 25.0f * AddonUiMetrics.SCALE; + float titleW = Math.max(1f, w - 33f * AddonUiMetrics.SCALE); + float titleY = centerY - 4.8f * AddonUiMetrics.SCALE; + AddonUiMetrics.drawFitOrMarquee(titleRenderer, title, titleX, titleY, AddonUiMetrics.TITLE_FONT, titleW, c.title(), AddonUiMetrics.inside(mouseX, mouseY, titleX, titleY - 1.5f * AddonUiMetrics.SCALE, titleW, 12f * AddonUiMetrics.SCALE)); + if (!controller.scanState().scanIssues().isEmpty()) { + String label = AddonUiMetrics.tr("screen.lovisual.addon_manager.status.scanned", controller.scanState().scanIssues().size()); + float labelW = Math.min(w * 0.34f, Math.max(42f * AddonUiMetrics.SCALE, AddonUiMetrics.width(bodyRenderer, label, AddonUiMetrics.SMALL_FONT))); + float labelX = x + w - labelW - 8f * AddonUiMetrics.SCALE; + AddonUiMetrics.drawFitOrMarquee(bodyRenderer, label, labelX, centerY - 3.6f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, labelW, c.muted(), AddonUiMetrics.inside(mouseX, mouseY, labelX, centerY - 5f * AddonUiMetrics.SCALE, labelW, 10f * AddonUiMetrics.SCALE)); + } + } + + private void renderEmptyState(float x, float y, float w, float h, AddonUiMetrics.PanelColors c) { + float boxW = Math.min(w - 18f * AddonUiMetrics.SCALE, 110f * AddonUiMetrics.SCALE); + float boxH = 44f * AddonUiMetrics.SCALE; + float boxX = x + w * 0.5f - boxW * 0.5f; + float boxY = y + h * 0.5f - boxH * 0.5f; + Renderer2D.COLOR.roundedRectGradientQuad(boxX, boxY, boxW, boxH, 7f * AddonUiMetrics.SCALE, 1f, c.detailTop(), c.detailTop(), c.detailBottom(), c.detailBottom()); + Renderer2D.COLOR.roundedRectStrokeGradient(boxX, boxY, boxW, boxH, 7f * AddonUiMetrics.SCALE, 1.2f, 0.5f, c.strokeSoft(), c.stroke(), 90f); + AddonUiMetrics.drawSvgGradient("folder-open", boxX + boxW * 0.5f - 8f * AddonUiMetrics.SCALE, boxY + 7f * AddonUiMetrics.SCALE, 16f * AddonUiMetrics.SCALE, 16f * AddonUiMetrics.SCALE, c); + AddonUiMetrics.drawCentered(bodyRenderer, AddonUiMetrics.tr("screen.lovisual.addon_manager.empty"), boxX + boxW * 0.5f, boxY + 27f * AddonUiMetrics.SCALE, AddonUiMetrics.SMALL_FONT, c.muted()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java index f17a10d..006d20e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java @@ -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.gui.mainmenu.alt; import dev.loki.lovisual.features.account.AccountConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java index 7130fba..9e49578 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java @@ -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.gui.mainmenu.alt; import dev.loki.lovisual.features.account.AccountConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java index 16dc87f..4235e96 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java @@ -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.gui.mainmenu.alt; import dev.loki.lovisual.features.gui.mainmenu.alt.render.AltUiMetrics; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java index d9dac19..48a75b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java @@ -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.gui.mainmenu.alt; import dev.loki.lovisual.config.i18n.MainConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java index fbbd1e4..278ffd1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java @@ -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.gui.mainmenu.alt.auth; import dev.loki.lovisual.features.account.AccountConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java index 7d59cc8..6a5af92 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java @@ -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.gui.mainmenu.alt.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java index 47b9a07..d14d59a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java @@ -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.gui.mainmenu.alt.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java index 8b9d5f1..1eca297 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java @@ -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.gui.mainmenu.alt.render; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java index 3b85e18..2fa048c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java @@ -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.gui.mainmenu.alt.render; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java index bee035c..df7752d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java @@ -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.gui.mainmenu.alt.render.cards; import dev.loki.lovisual.features.account.AccountConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java index c69c76f..8057197 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java @@ -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.gui.mainmenu.alt.render.cards; import dev.loki.lovisual.features.account.AccountConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuPainter.java new file mode 100644 index 0000000..658e5af --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuPainter.java @@ -0,0 +1,126 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.features.account.AccountConfig; +import dev.loki.lovisual.features.account.AccountEntry; +import net.minecraft.client.Minecraft; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +/** + * Layout / animation / hit-test helpers for {@link LoVisualMainMenuScreen}. Owns the + * per-frame UI metric recomputation, cursor coordinate remapping (screen to fixed + * logical viewport), button hover / scale interpolation and the click-target query + * used by both the extract-render-state pass and the mouse-click handler. Kept in a + * sibling folder sharing the parent's package so the screen class stays compact. + */ +final class MainMenuPainter { + + /** Rectangle in fixed-logical coordinates; used for hit-testing the auth warning card. */ + record Bounds(float x, float y, float w, float h) { + boolean contains(float mx, float my) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + } + + private final LoVisualMainMenuScreen owner; + + MainMenuPainter(LoVisualMainMenuScreen owner) { + this.owner = owner; + } + + static float easeOutCubic(float x) { return 1f - (float) Math.pow(1f - x, 3f); } + static float easeOutQuart(float x) { return 1f - (float) Math.pow(1f - x, 4f); } + static int withAlpha(int rgb, int alpha) { return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); } + static String tr(String key, Object... args) { return I18n.get(key, args); } + + /** True once the fade-in is past {@code 0.8}; blocks clicks during the initial slide. */ + boolean canInteract(float menuProgress) { return menuProgress > 0.8f; } + + /** Elapsed time since open, clamped to {@code MENU_APPEAR_DURATION}; 0 before init. */ + float getMenuProgress(long now) { + if (!owner.initialized) return 0f; + return Mth.clamp((now - owner.openTime) / (float) LoVisualMainMenuScreen.MENU_APPEAR_DURATION, 0f, 1f); + } + + /** Recompute {@code fixedWidth} / {@code fixedHeight} from the framebuffer using {@link dev.loki.lovisual.render.engine.math.HudScale}. */ + void updateUiMetrics() { + Minecraft mc = owner.mc(); + if (mc == null) { + owner.fixedWidth = owner.screenW(); + owner.fixedHeight = owner.screenH(); + owner.renderScale = 1f; + return; + } + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + owner.fixedWidth = MainMenuTextUtil.virtualWidth(fbw, fbh); + owner.fixedHeight = MainMenuTextUtil.virtualHeight(fbw, fbh); + owner.renderScale = 1f; + } + + /** Map a screen-space x to the fixed logical viewport used by painter draw calls. */ + float toFixedX(float screenX) { + Minecraft mc = owner.mc(); + if (owner.screenW() <= 0 || mc == null) return screenX; + float hudScale = dev.loki.lovisual.render.engine.math.HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + return hudScale <= 0f ? screenX : screenX * mc.getWindow().getGuiScale() / hudScale; + } + + float toFixedY(float screenY) { + Minecraft mc = owner.mc(); + if (owner.screenH() <= 0 || mc == null) return screenY; + float hudScale = dev.loki.lovisual.render.engine.math.HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + return hudScale <= 0f ? screenY : screenY * mc.getWindow().getGuiScale() / hudScale; + } + + /** Index of the button whose circle encloses {@code (mouseX, mouseY)}; {@code -1} if none / not interactive. */ + int getHoveredButton(float mouseX, float mouseY, float menuProgress) { + if (!canInteract(menuProgress)) return -1; + float totalWidth = LoVisualMainMenuScreen.BUTTON_SIZE * LoVisualMainMenuScreen.BUTTON_ICONS.length + + LoVisualMainMenuScreen.BUTTON_SPACING * (LoVisualMainMenuScreen.BUTTON_ICONS.length - 1); + float startX = (owner.fixedWidth - totalWidth) * 0.5f; + float y = owner.fixedHeight * 0.5f + 34f * LoVisualMainMenuScreen.MENU_SCALE + (1f - menuProgress) * 60f * LoVisualMainMenuScreen.MENU_SCALE; + for (int i = 0; i < LoVisualMainMenuScreen.BUTTON_ICONS.length; i++) { + float centerX = startX + i * (LoVisualMainMenuScreen.BUTTON_SIZE + LoVisualMainMenuScreen.BUTTON_SPACING) + LoVisualMainMenuScreen.BUTTON_SIZE * 0.5f; + float centerY = y + LoVisualMainMenuScreen.BUTTON_SIZE * 0.5f; + float dx = mouseX - centerX, dy = mouseY - centerY; + if (dx * dx + dy * dy <= LoVisualMainMenuScreen.BUTTON_RADIUS * LoVisualMainMenuScreen.BUTTON_RADIUS) return i; + } + return -1; + } + + /** The auth-warning card sits below the button row, clamped so it never leaves the viewport. */ + Bounds authWarningBounds(float menuProgress) { + float s = LoVisualMainMenuScreen.MENU_SCALE; + float slideOffset = (1f - menuProgress) * 60f * s; + float buttonY = owner.fixedHeight * 0.5f + 34f * s + slideOffset; + float y = Math.min(buttonY + LoVisualMainMenuScreen.BUTTON_SIZE + 18f * s, owner.fixedHeight - LoVisualMainMenuScreen.AUTH_WARNING_H - 16f * s); + return new Bounds((owner.fixedWidth - LoVisualMainMenuScreen.AUTH_WARNING_W) * 0.5f, y, + LoVisualMainMenuScreen.AUTH_WARNING_W, LoVisualMainMenuScreen.AUTH_WARNING_H); + } + + /** The pending-authentication account that triggers the warning card, or {@code null}. */ + AccountEntry getAuthWarningAccount() { + return AccountConfig.get().getMicrosoftAuthorizationRequiredAccount(); + } + + /** Per-frame hover / scale interpolation for buttons, the quit tint and the warning card. */ + void updateButtonAnimations(float deltaTime) { + for (int i = 0; i < LoVisualMainMenuScreen.BUTTON_ICONS.length; i++) { + float targetHover = owner.hoveredButton == i ? 1f : 0f; + owner.buttonHoverProgress[i] = Mth.lerp(Math.min(1f, deltaTime * 10f), owner.buttonHoverProgress[i], targetHover); + float targetScale = owner.hoveredButton == i ? LoVisualMainMenuScreen.BUTTON_HOVER_SCALE : 1f; + owner.buttonScales[i] = Mth.lerp(Math.min(1f, deltaTime * 12f), owner.buttonScales[i], targetScale); + } + float targetQuit = owner.hoveredButton == LoVisualMainMenuScreen.BUTTON_ICONS.length - 1 ? 1f : 0f; + owner.quitHoverProgress = Mth.lerp(Math.min(1f, deltaTime * 8f), owner.quitHoverProgress, targetQuit); + owner.authWarningHoverProgress = Mth.lerp(Math.min(1f, deltaTime * 10f), owner.authWarningHoverProgress, owner.authWarningHovered ? 1f : 0f); + } + + /** ESC is swallowed to keep the user on the custom menu; everything else falls through. */ + boolean onKeyPressed(KeyEvent input) { + return input.key() == 256 || owner.superKeyPressed(input); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuRenderer.java new file mode 100644 index 0000000..04ede73 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuRenderer.java @@ -0,0 +1,191 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.account.AccountEntry; +import dev.loki.lovisual.features.account.SkinManager; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.mainmenu.alt.LoVisualAltManagerScreen; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +import java.time.LocalDate; +import java.time.LocalTime; + +/** + * Draw pass for {@link LoVisualMainMenuScreen}: shader background, dimmer gradient, + * clock + date header, the six round action buttons (with liquid-glass circle and + * SVG / icon-font glyphs) and the Microsoft-auth warning card. All state reads go + * through the owner so this class stays purely functional. + */ +final class MainMenuRenderer { + + private final LoVisualMainMenuScreen owner; + private final MainMenuPainter painter; + + MainMenuRenderer(LoVisualMainMenuScreen owner, MainMenuPainter painter) { + this.owner = owner; + this.painter = painter; + } + + /** Kick off the aurora / plain menu background; hands control back to vanilla if the mode is off. */ + void renderShaderBackground(GuiGraphicsExtractor context) { + Minecraft mc = owner.mc(); + if (mc == null) return; + MainConfig cfg = MainConfig.get(); + String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; + if (mode == null || mode.equalsIgnoreCase("off")) { + switchToVanillaTitleScreen(null, false); + return; + } + MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); + } + + /** Fall back to the vanilla {@link TitleScreen} on any draw error or when the user disabled the menu. */ + void switchToVanillaTitleScreen(Throwable t, boolean sticky) { + if (sticky) LoVisualMainMenuScreen.forceVanillaTitleScreen = true; + if (t != null) dev.loki.lovisual.util.logging.DebugLog.errorOnce("main_menu_vanilla_fallback", "Main menu failed, switching to vanilla title screen", t); + else if (sticky) dev.loki.lovisual.util.logging.DebugLog.warnOnce("main_menu_vanilla_fallback", "Main menu switched to vanilla title screen"); + Minecraft mc = owner.mc() != null ? owner.mc() : Minecraft.getInstance(); + if (mc != null && !(ClientScreen.current() instanceof TitleScreen)) ClientScreen.show(mc, new TitleScreen(false)); + } + + /** Two-stop vertical dimmer over the shader, darkening the bottom half a bit more. */ + void drawDimmer() { + Themes.Theme theme = Theme.theme(); + int base = HudRenderUtil.mixColor(theme.windowBg(), 0xFF000000, 0.46f); + int soft = MainMenuPainter.withAlpha(base, 150); + int deep = MainMenuPainter.withAlpha(HudRenderUtil.mixColor(theme.windowBg(), 0xFF000000, 0.72f), 195); + Renderer2D.COLOR.quad(0, 0, owner.fixedWidth, owner.fixedHeight, soft, soft, deep, deep); + } + + /** Animated clock + "EEEE, MMMM d" date, sliding down as the menu fades in. */ + void renderTime(float opacity) { + float s = LoVisualMainMenuScreen.MENU_SCALE; + float centerX = owner.fixedWidth * 0.5f; + float centerY = owner.fixedHeight * 0.5f - 55f * s + (1f - opacity) * 40f * s; + long nowMs = Util.getMillis(); + MainConfig cfg = MainConfig.get(); + boolean showSeconds = cfg != null && cfg.isMenuClockShowSeconds(); + owner.clockText.update(LocalTime.now(), showSeconds, nowMs); + String dateText = LocalDate.now().format(LoVisualMainMenuScreen.DATE_FORMAT); + TextRenderer timeRenderer = MainMenuTextUtil.interBold(TextRenderer.get()); + TextRenderer dateRenderer = MainMenuTextUtil.interMedium(timeRenderer); + int timeColor = MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 255f)); + int dateColor = MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 200f)); + float timeHeight = owner.clockText.height(timeRenderer, LoVisualMainMenuScreen.TIME_FONT); + owner.clockText.renderLiquidGlassCentered(timeRenderer, centerX, centerY - timeHeight * 0.5f, LoVisualMainMenuScreen.TIME_FONT, timeColor, nowMs); + MainMenuTextUtil.drawCenteredText(dateRenderer, dateText, centerX, centerY + timeHeight * 0.5f + 4f * s, LoVisualMainMenuScreen.DATE_FONT, dateColor); + } + + /** Row of six circular buttons, each with a per-index slide / fade stagger. */ + void renderButtons(float opacity) { + float s = LoVisualMainMenuScreen.MENU_SCALE; + int n = LoVisualMainMenuScreen.BUTTON_ICONS.length; + float totalWidth = LoVisualMainMenuScreen.BUTTON_SIZE * n + LoVisualMainMenuScreen.BUTTON_SPACING * (n - 1); + float startX = (owner.fixedWidth - totalWidth) * 0.5f; + float y = owner.fixedHeight * 0.5f + 34f * s + (1f - opacity) * 60f * s; + for (int i = 0; i < n; i++) { + float delay = i * 0.12f; + float progress = Mth.clamp((opacity - delay) / (1f - delay * 0.5f), 0f, 1f); + float eased = MainMenuPainter.easeOutCubic(progress); + if (eased <= 0f) continue; + renderCircleButton(i, startX + i * (LoVisualMainMenuScreen.BUTTON_SIZE + LoVisualMainMenuScreen.BUTTON_SPACING), y, eased * opacity); + } + } + + /** Liquid-glass circle + shadow + glyph for one button; {@code quit} tints red on hover. */ + void renderCircleButton(int index, float x, float y, float opacity) { + if (opacity < 0.01f) return; + float s = LoVisualMainMenuScreen.MENU_SCALE; + float scale = owner.buttonScales[index]; + float radius = LoVisualMainMenuScreen.BUTTON_RADIUS * scale; + float centerX = x + LoVisualMainMenuScreen.BUTTON_RADIUS; + float centerY = y + LoVisualMainMenuScreen.BUTTON_RADIUS; + float hover = owner.buttonHoverProgress[index]; + boolean quit = index == LoVisualMainMenuScreen.BUTTON_ICONS.length - 1; + Themes.Theme theme = Theme.theme(); + int tint; + if (quit) { + tint = HudRenderUtil.mixColor(0xFFFF4C4C, 0xFFFF1F32, owner.quitHoverProgress * 0.55f + hover * 0.25f); + } else { + int baseTint = HudRenderUtil.mixColor(theme.accent(), 0xFFFFFFFF, 0.18f); + tint = HudRenderUtil.mixColor(baseTint, 0xFFFFFFFF, hover * 0.10f); + } + int tintArgb = MainMenuPainter.withAlpha(tint, Math.round(opacity * (quit ? 235f : 220f))); + int icon = quit + ? MainMenuPainter.withAlpha(HudRenderUtil.mixColor(0xFFFFFFFF, 0xFFFFD6D6, owner.quitHoverProgress), Math.round(opacity * 255f)) + : MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 255f)); + int shadow = MainMenuPainter.withAlpha(quit ? 0x1F0508 : 0x060810, Math.round(opacity * (quit ? 118f : 92f))); + Renderer2D.COLOR.circleSoftShadow(centerX, centerY, radius, LoVisualMainMenuScreen.BUTTON_SHADOW, 0.052f + hover * 0.020f, shadow); + Renderer2D.COLOR.liquidGlassCircle(centerX, centerY, radius, LoVisualMainMenuScreen.BUTTON_SOFTNESS, tintArgb, opacity, + -20.0f, 1.0f, 1.0f, quit ? 0.66f : 0.58f, (0.100f + hover * 0.032f) * s, 0.0f); + String svg = LoVisualMainMenuScreen.BUTTON_SVGS[index]; + if (svg != null && !svg.isBlank()) { + float iconSize = 20.5f * s * scale; + Renderer2D.COLOR.svg(svg, centerX - iconSize * 0.5f, centerY - iconSize * 0.5f - 0.45f * s, iconSize, iconSize, SvgRenderOptions.overrideColor(icon)); + } else { + TextRenderer icons = MainMenuTextUtil.iconFont(TextRenderer.get()); + String glyph = LoVisualMainMenuScreen.BUTTON_ICONS[index]; + float iconSize = LoVisualMainMenuScreen.ICON_FONT * scale; + float iconW = MainMenuTextUtil.measureWidth(icons, glyph, iconSize); + float iconH = MainMenuTextUtil.measureHeight(icons, iconSize); + MainMenuTextUtil.drawText(icons, glyph, centerX - iconW * 0.5f + 0.5f * s, centerY - iconH * 0.5f + LoVisualMainMenuScreen.ICON_Y_OFFSET, iconSize, icon); + } + } + + /** Rounded card under the button row prompting Microsoft re-auth for a specific account. */ + void renderAuthWarning(GuiGraphicsExtractor context, float opacity, long now) { + AccountEntry entry = painter.getAuthWarningAccount(); + if (entry == null || opacity <= 0.01f) return; + float s = LoVisualMainMenuScreen.MENU_SCALE; + MainMenuPainter.Bounds b = painter.authWarningBounds(opacity); + float hover = AnimationUtility.easeOutCubic(owner.authWarningHoverProgress); + float y = b.y() - hover * 1.4f * s; + Themes.Theme theme = Theme.theme(); + int shadow = MainMenuPainter.withAlpha(0x060810, Math.round(opacity * (96f + hover * 24f))); + int top = HudRenderUtil.mixColor(MainMenuPainter.withAlpha(0x171B22, Math.round(opacity * 226f)), MainMenuPainter.withAlpha(theme.accent(), Math.round(opacity * 78f)), 0.16f + hover * 0.10f); + int bottom = HudRenderUtil.mixColor(MainMenuPainter.withAlpha(0x0D1118, Math.round(opacity * 236f)), MainMenuPainter.withAlpha(theme.accent(), Math.round(opacity * 52f)), 0.08f + hover * 0.08f); + int stroke = HudRenderUtil.mixColor(MainMenuPainter.withAlpha(0x46505D, Math.round(opacity * 170f)), MainMenuPainter.withAlpha(theme.accent(), Math.round(opacity * 218f)), 0.32f + hover * 0.28f); + Renderer2D.COLOR.roundedRectSoftShadow(b.x(), y + 1.2f * s, b.w(), b.h(), LoVisualMainMenuScreen.AUTH_WARNING_R, 10f * s, 0.026f + hover * 0.014f, shadow); + Renderer2D.COLOR.roundedRectGradientQuad(b.x(), y, b.w(), b.h(), LoVisualMainMenuScreen.AUTH_WARNING_R, 1f, top, top, bottom, bottom); + Renderer2D.COLOR.roundedRectStrokeGradient(b.x() + 0.65f * s, y + 0.65f * s, b.w() - 1.3f * s, b.h() - 1.3f * s, + LoVisualMainMenuScreen.AUTH_WARNING_R - 0.65f * s, 1f, 0.65f * s, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.08f), stroke, 90f); + float progress = (now % LoVisualMainMenuScreen.AUTH_WARNING_PROGRESS_MS) / (float) LoVisualMainMenuScreen.AUTH_WARNING_PROGRESS_MS; + float lineX = b.x() + 8f * s, lineY = y + 4f * s, lineW = b.w() - 16f * s, lineH = 1.35f * s; + Renderer2D.COLOR.roundedRect(lineX, lineY, lineW, lineH, lineH * 0.5f, 1f, MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 42f))); + Renderer2D.COLOR.roundedRect(lineX, lineY, lineW * (1f - progress), lineH, lineH * 0.5f, 1f, MainMenuPainter.withAlpha(theme.accent(), Math.round(opacity * 170f))); + float headX = b.x() + 10f * s, headY = y + 10f * s; + PlayerHeadRenderer.drawRounded(context, headX, headY, LoVisualMainMenuScreen.AUTH_WARNING_HEAD, 4f * s, SkinManager.getSkin(entry.getName()), + new RenderColor(MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 255f))), true, + new RenderColor(MainMenuPainter.withAlpha(theme.accent(), Math.round(opacity * 120f))), 0.75f * s, false); + TextRenderer title = MainMenuTextUtil.interBold(TextRenderer.get()); + TextRenderer body = MainMenuTextUtil.interMedium(title); + float textX = headX + LoVisualMainMenuScreen.AUTH_WARNING_HEAD + 8f * s; + MainMenuTextUtil.drawText(title, MainMenuPainter.tr("screen.lovisual.main_menu.auth_warning.title"), textX, y + 12.2f * s, 0.72f * s, MainMenuPainter.withAlpha(0xFFFFFF, Math.round(opacity * 245f))); + String message = MainMenuPainter.tr("screen.lovisual.main_menu.auth_warning.body", entry.getName()); + MainMenuTextUtil.drawText(body, MainMenuTextUtil.ellipsize(body, message, 0.55f * s, b.x() + b.w() - textX - 10f * s), textX, y + 25.4f * s, 0.55f * s, MainMenuPainter.withAlpha(0xC8D0DC, Math.round(opacity * 225f))); + } + + /** Route the warning-card click to the alt manager. Returns true when handled. */ + boolean handleAuthWarningClick(float fixedX, float fixedY, float menuProgress) { + AccountEntry warningAccount = painter.getAuthWarningAccount(); + Minecraft mc = owner.mc(); + if (warningAccount == null || mc == null || !painter.canInteract(menuProgress)) return false; + if (!painter.authWarningBounds(menuProgress).contains(fixedX, fixedY)) return false; + ClientScreen.show(mc, new LoVisualAltManagerScreen(owner, true)); + return true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuTextUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuTextUtil.java new file mode 100644 index 0000000..0396cc1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/impl/MainMenuTextUtil.java @@ -0,0 +1,80 @@ +package dev.loki.lovisual.features.gui.mainmenu; + +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.color.RenderColor; + +/** + * Shared text primitives for the LoVisual main menu. Wraps the {@link TextRenderer} + * begin/end measure-and-draw cycle so painter code stays focused on layout + colour. + * Lives in the same package as {@link LoVisualMainMenuScreen}; kept as a separate + * class so the screen file stays within the two-hundred-line cap. + */ +final class MainMenuTextUtil { + + private MainMenuTextUtil() { } + + static float measureWidth(TextRenderer renderer, String text, float size) { + if (renderer == null || text == null) return 0f; + renderer.begin(size, true, false); + float width = (float) renderer.getWidth(text, false); + renderer.end(); + return width; + } + + static float measureHeight(TextRenderer renderer, float size) { + if (renderer == null) return 0f; + renderer.begin(size, true, false); + float height = (float) renderer.getHeight(false); + renderer.end(); + return height; + } + + static void drawText(TextRenderer renderer, String text, float x, float y, float size, int argb) { + if (renderer == null || text == null || text.isEmpty()) return; + renderer.setAlpha(1.0); + renderer.begin(size, false, false); + renderer.render(text, x, y, new RenderColor(argb), false); + renderer.end(); + } + + static void drawCenteredText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { + float x = centerX - measureWidth(renderer, text, size) * 0.5f; + drawText(renderer, text, x, y, size, argb); + } + + static void drawLiquidGlassText(TextRenderer renderer, String text, float x, float y, float size, int argb) { + if (renderer == null || text == null || text.isEmpty()) return; + renderer.setAlpha(1.0); + renderer.begin(size, false, true); + renderer.renderLiquidGlass(text, x, y, new RenderColor(argb), false); + renderer.end(); + } + + static void drawCenteredLiquidGlassText(TextRenderer renderer, String text, float centerX, float y, float size, int argb) { + float x = centerX - measureWidth(renderer, text, size) * 0.5f; + drawLiquidGlassText(renderer, text, x, y, size, argb); + } + + /** Truncate {@code text} with a "..." suffix until it fits within {@code maxWidth} logical pixels. */ + static String ellipsize(TextRenderer renderer, String text, float size, float maxWidth) { + if (text == null || text.isEmpty()) return ""; + if (measureWidth(renderer, text, size) <= maxWidth) return text; + String out = text; + while (out.length() > 3 && measureWidth(renderer, out + "...", size) > maxWidth) { + out = out.substring(0, out.length() - 1); + } + return out + "..."; + } + + /** Resolve the Inter Bold or Inter Medium {@link TextRenderer} for main-menu text, with a fallback chain. */ + static TextRenderer interBold(TextRenderer fallback) { return Fonts.renderer("Inter", FontInfo.Type.Bold, fallback); } + static TextRenderer interMedium(TextRenderer fallback) { return Fonts.renderer("InterMedium", FontInfo.Type.Regular, fallback); } + static TextRenderer iconFont(TextRenderer fallback) { return Fonts.renderer("MainMenuIcons", FontInfo.Type.Regular, fallback); } + + /** Recompute the fixed logical viewport from the framebuffer size, matching {@link HudScale}. */ + static float virtualWidth(int fbw, int fbh) { return Math.max(1f, HudScale.virtualWidth(fbw, fbh)); } + static float virtualHeight(int fbw, int fbh) { return Math.max(1f, HudScale.virtualHeight(fbw, fbh)); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/LoVisualProxyManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/LoVisualProxyManagerScreen.java new file mode 100644 index 0000000..247989c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/LoVisualProxyManagerScreen.java @@ -0,0 +1,125 @@ +package dev.loki.lovisual.features.gui.mainmenu.proxy; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; + +/** + * Proxy manager screen facade: wires controller and renderer together. + */ +public final class LoVisualProxyManagerScreen extends Screen { + private final Screen parent; + private final ProxyManagerController controller = new ProxyManagerController(); + private final ProxyManagerRenderer renderer = new ProxyManagerRenderer(); + + public LoVisualProxyManagerScreen(Screen parent) { + super(Component.translatable("screen.lovisual.proxy_manager.title")); + this.parent = parent; + } + + @Override + protected void init() { + renderer.init(); + controller.loadProxy(); + } + + @Override + public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + controller.updateUiMetrics(minecraft, width, height); + renderShaderBackground(context); + float fx = toFixedX(mouseX); + float fy = toFixedY(mouseY); + renderer.updateAnimations(controller, fx, fy); + + ViewportContext.beginUnscaledLogical(context); + Renderer2D.COLOR.begin(); + try { + renderDimmer(); + renderer.renderPanel(controller, fx, fy); + } finally { + Renderer2D.COLOR.render(); + ViewportContext.end(context); + } + } + + @Override + public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { + } + + @Override + public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { + if (click.button() != 0) return true; + boolean back = controller.handleClick(toFixedX((float) click.x()), toFixedY((float) click.y())); + if (back) closeToParent(); + return true; + } + + @Override + public boolean keyPressed(KeyEvent input) { + if (controller.handleKey(input)) return true; + if (input.key() == 256) { closeToParent(); return true; } + return super.keyPressed(input); + } + + @Override + public boolean charTyped(CharacterEvent input) { + if (controller.handleChar(input)) return true; + return super.charTyped(input); + } + + @Override + public boolean isPauseScreen() { return false; } + + @Override + public boolean shouldCloseOnEsc() { return false; } + + private void renderDimmer() { + Themes.Theme t = Theme.theme(); + int soft = (HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.46f) & 0x00FFFFFF) | (140 << 24); + int deep = (HudRenderUtil.mixColor(t.windowBg(), 0xFF000000, 0.68f) & 0x00FFFFFF) | (180 << 24); + Renderer2D.COLOR.quad(0, 0, controller.fixedWidth(), controller.fixedHeight(), soft, soft, deep, deep); + } + + private void closeToParent() { + if (minecraft != null) ClientScreen.show(minecraft, parent); + } + + private float toFixedX(float screenX) { + Minecraft mc = minecraft; + if (mc == null) return screenX; + float hs = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + return hs <= 0f ? screenX : screenX * mc.getWindow().getGuiScale() / hs; + } + + private float toFixedY(float screenY) { + Minecraft mc = minecraft; + if (mc == null) return screenY; + float hs = HudScale.scale(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + return hs <= 0f ? screenY : screenY * mc.getWindow().getGuiScale() / hs; + } + + private void renderShaderBackground(GuiGraphicsExtractor context) { + Minecraft mc = this.minecraft; + if (mc == null) return; + MainConfig cfg = MainConfig.get(); + String mode = cfg != null ? cfg.getMenuBackgroundMode() : "off"; + if (mode == null || mode.equalsIgnoreCase("off")) { + context.fill(0, 0, width, height, 0xFF000000); + return; + } + MenuBackgroundRenderer.render(mc, mode.equalsIgnoreCase("aurora")); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerController.java new file mode 100644 index 0000000..b205113 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerController.java @@ -0,0 +1,181 @@ +package dev.loki.lovisual.features.gui.mainmenu.proxy; + +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.util.proxy.ProxyBackend; +import dev.loki.lovisual.util.proxy.model.ProxyEntry; +import dev.loki.lovisual.util.proxy.model.ProxyType; +import net.minecraft.client.Minecraft; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.resources.language.I18n; + +/** + * Business state, input routing and proxy CRUD for the proxy manager screen. + */ +public final class ProxyManagerController { + public static final float SCALE = 2.28f; + public static final float TEXT_SCALE = 1.30f; + public static final float PANEL_W = 184f * SCALE; + public static final float PANEL_H = 184f * SCALE; + public static final float PANEL_R = 6f * SCALE; + public static final float FIELD_H = 14f * SCALE; + public static final float BUTTON_H = 16f * SCALE; + public static final float GAP = 5f * SCALE; + public static final float TITLE_FONT = 0.84f * TEXT_SCALE; + public static final float LABEL_FONT = 0.55f * TEXT_SCALE; + public static final float FIELD_FONT = 0.60f * TEXT_SCALE; + public static final float BUTTON_FONT = 0.58f * TEXT_SCALE; + public static final int FIELD_COUNT = 3; + + private final String[] values = {"", "", ""}; + private Field focusedField; + private ProxyType type = ProxyType.SOCKS5; + private boolean enabled; + private String status = ""; + private float fixedWidth; + private float fixedHeight; + + public String[] values() { return values; } + public Field focusedField() { return focusedField; } + public void setFocusedField(Field f) { this.focusedField = f; } + public ProxyType type() { return type; } + public boolean enabled() { return enabled; } + public String status() { return status; } + public void setStatus(String s) { this.status = s; } + public float fixedWidth() { return fixedWidth; } + public float fixedHeight() { return fixedHeight; } + + public static String tr(String key, Object... args) { + return I18n.get(key, args); + } + + public static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + public float[] panelOrigin() { + return new float[]{fixedWidth * 0.5f - PANEL_W * 0.5f, fixedHeight * 0.5f - PANEL_H * 0.5f}; + } + + public void updateUiMetrics(Minecraft mc, int width, int height) { + if (mc == null) { fixedWidth = width; fixedHeight = height; return; } + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + fixedWidth = Math.max(1f, HudScale.virtualWidth(fbw, fbh)); + fixedHeight = Math.max(1f, HudScale.virtualHeight(fbw, fbh)); + } + + public void loadProxy() { + ProxyEntry proxy = ProxyBackend.getDefaultProxy(); + type = proxy.type(); + values[Field.ADDRESS.index] = proxy.ipPort(); + values[Field.USERNAME.index] = proxy.username(); + values[Field.PASSWORD.index] = proxy.password(); + enabled = ProxyBackend.isEnabled(); + status = ""; + } + + /** @return true if "back" button was clicked; caller should close. */ + public boolean handleClick(float mouseX, float mouseY) { + float[] o = panelOrigin(); + float innerX = o[0] + 8f * SCALE; + float innerW = PANEL_W - 16f * SCALE; + float rowY = o[1] + 28f * SCALE; + focusedField = null; + + if (inside(mouseX, mouseY, innerX, rowY, 66f * SCALE, 13f * SCALE)) { + enabled = !enabled; + status = enabled ? tr("screen.lovisual.proxy_manager.status.enabled") + : tr("screen.lovisual.proxy_manager.status.disabled"); + return false; + } + float typeX = innerX + 72f * SCALE; + float typeW = innerW - 72f * SCALE; + float typeItemW = (typeW - 2f * SCALE) * 0.5f; + if (inside(mouseX, mouseY, typeX, rowY, typeItemW, 13f * SCALE)) { + type = ProxyType.SOCKS5; + return false; + } + if (inside(mouseX, mouseY, typeX + typeItemW + 2f * SCALE, rowY, typeItemW, 13f * SCALE)) { + type = ProxyType.SOCKS4; + return false; + } + float fieldY = o[1] + 53f * SCALE; + for (Field field : Field.values()) { + float y = fieldY + field.index * 27f * SCALE + 9f * SCALE; + if (inside(mouseX, mouseY, innerX, y, innerW, FIELD_H)) { + focusedField = field; + return false; + } + } + float buttonY = o[1] + PANEL_H - 26f * SCALE; + float third = (innerW - GAP * 2f) / 3f; + if (inside(mouseX, mouseY, innerX, buttonY, third, BUTTON_H)) { saveProxy(); return false; } + if (inside(mouseX, mouseY, innerX + third + GAP, buttonY, third, BUTTON_H)) { clearProxy(); return false; } + if (inside(mouseX, mouseY, innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H)) { return true; } + return false; + } + + public boolean handleKey(KeyEvent input) { + if (focusedField != null) { + int key = input.key(); + if (key == 259) { + int i = focusedField.index; + if (!values[i].isEmpty()) values[i] = values[i].substring(0, values[i].length() - 1); + return true; + } + if (key == 258) { focusedField = focusedField.next(); return true; } + if (key == 257 || key == 335) { saveProxy(); return true; } + if (key == 256) { focusedField = null; return true; } + } + return false; + } + + public boolean handleChar(CharacterEvent input) { + if (focusedField == null) return false; + int cp = input.codepoint(); + if (cp < 32 || cp > 126) return true; + int i = focusedField.index; + int max = focusedField == Field.ADDRESS ? 96 : 64; + if (values[i].length() >= max) return true; + values[i] += Character.toString(cp); + return true; + } + + public String currentSummary() { + if (!enabled) return tr("screen.lovisual.proxy_manager.status.disabled"); + String addr = values[Field.ADDRESS.index].trim(); + if (addr.isEmpty()) return tr("screen.lovisual.proxy_manager.status.enabled_no_address"); + return type.name() + " " + addr; + } + + private void saveProxy() { + String address = values[Field.ADDRESS.index].trim(); + if (!address.isEmpty() && !ProxyEntry.isValidIpPort(address)) { + status = tr("screen.lovisual.proxy_manager.status.invalid_address"); + return; + } + if (address.isEmpty()) enabled = false; + ProxyEntry entry = address.isEmpty() ? ProxyEntry.empty() + : new ProxyEntry(type, address, values[Field.USERNAME.index], values[Field.PASSWORD.index]); + ProxyBackend.setDefaultProxy(entry); + ProxyBackend.setEnabled(enabled); + status = address.isEmpty() ? tr("screen.lovisual.proxy_manager.status.cleared") + : tr("screen.lovisual.proxy_manager.status.saved", type.name()); + } + + private void clearProxy() { + values[0] = ""; values[1] = ""; values[2] = ""; + enabled = false; type = ProxyType.SOCKS5; focusedField = null; + ProxyBackend.setDefaultProxy(ProxyEntry.empty()); + ProxyBackend.setEnabled(false); + status = tr("screen.lovisual.proxy_manager.status.cleared"); + } + + public enum Field { + ADDRESS(0), USERNAME(1), PASSWORD(2); + public final int index; + Field(int index) { this.index = index; } + public Field next() { return values()[(ordinal() + 1) % values().length]; } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerRenderer.java new file mode 100644 index 0000000..5b4cf14 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/proxy/ProxyManagerRenderer.java @@ -0,0 +1,190 @@ +package dev.loki.lovisual.features.gui.mainmenu.proxy; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.proxy.model.ProxyType; +import net.minecraft.util.Mth; + +import static dev.loki.lovisual.features.gui.mainmenu.proxy.ProxyManagerController.*; + +/** + * Animation state and rendering for the proxy manager screen. + */ +final class ProxyManagerRenderer { + private final float[] fieldHover = new float[FIELD_COUNT]; + private final float[] fieldFocus = new float[FIELD_COUNT]; + private float openAnim; + private float enableHover, enablePress; + private float socks4Hover, socks5Hover, socks4Press, socks5Press; + private float saveHover, savePress, clearHover, clearPress, backHover, backPress; + private TextRenderer titleRenderer; + private TextRenderer bodyRenderer; + void init() { + titleRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + bodyRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, titleRenderer); + } + void updateAnimations(ProxyManagerController c, float mouseX, float mouseY) { + float dt = AnimationUtility.deltaTime(); + openAnim = animate(openAnim, 1f, dt, 7f); + float[] o = c.panelOrigin(); + float innerX = o[0] + 8f * SCALE; + float innerW = PANEL_W - 16f * SCALE; + float rowY = o[1] + 28f * SCALE; + enableHover = animate(enableHover, inside(mouseX, mouseY, innerX, rowY, 66f * SCALE, 13f * SCALE) ? 1f : 0f, dt, 11f); + enablePress = animate(enablePress, 0f, dt, 8f); + float typeX = innerX + 72f * SCALE; + float typeItemW = (innerW - 72f * SCALE - 2f * SCALE) * 0.5f; + socks5Hover = animate(socks5Hover, inside(mouseX, mouseY, typeX, rowY, typeItemW, 13f * SCALE) ? 1f : 0f, dt, 11f); + socks4Hover = animate(socks4Hover, inside(mouseX, mouseY, typeX + typeItemW + 2f * SCALE, rowY, typeItemW, 13f * SCALE) ? 1f : 0f, dt, 11f); + socks5Press = animate(socks5Press, 0f, dt, 8f); + socks4Press = animate(socks4Press, 0f, dt, 8f); + float fieldY = o[1] + 53f * SCALE; + for (Field field : Field.values()) { + float y = fieldY + field.index * 27f * SCALE + 9f * SCALE; + fieldHover[field.index] = animate(fieldHover[field.index], inside(mouseX, mouseY, innerX, y, innerW, FIELD_H) ? 1f : 0f, dt, 11f); + fieldFocus[field.index] = animate(fieldFocus[field.index], c.focusedField() == field ? 1f : 0f, dt, 10f); + } + float buttonY = o[1] + PANEL_H - 26f * SCALE; + float third = (innerW - GAP * 2f) / 3f; + saveHover = animate(saveHover, inside(mouseX, mouseY, innerX, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); + clearHover = animate(clearHover, inside(mouseX, mouseY, innerX + third + GAP, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); + backHover = animate(backHover, inside(mouseX, mouseY, innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H) ? 1f : 0f, dt, 11f); + savePress = animate(savePress, 0f, dt, 8f); + clearPress = animate(clearPress, 0f, dt, 8f); + backPress = animate(backPress, 0f, dt, 8f); + } + void renderPanel(ProxyManagerController c, float mouseX, float mouseY) { + float[] o = c.panelOrigin(); + float bX = o[0], bY = o[1]; + float open = AnimationUtility.easeOutCubic(openAnim); + PanelColors col = colors(open); + Renderer2D.COLOR.blurRect(bX, bY, PANEL_W, PANEL_H, PANEL_R, 15f, 1.0f, 0.30f, 0xFFFFFF); + Renderer2D.COLOR.roundedRectSoftShadow(bX, bY, PANEL_W, PANEL_H, PANEL_R, 12f * SCALE, 0.022f + open * 0.012f, col.shadow); + Renderer2D.COLOR.roundedRectGradientQuad(bX, bY, PANEL_W, PANEL_H, PANEL_R, 1f, col.bgTopLeft, col.bgTopRight, col.bgBottomRight, col.bgBottomLeft); + Renderer2D.COLOR.roundedRectMaskedQuad(bX, bY, PANEL_W, 22f * SCALE, bX, bY, PANEL_W, PANEL_H, PANEL_R, 1f, + col.headerTopLeft, col.headerTopRight, col.headerBottomRight, col.headerBottomLeft); + Renderer2D.COLOR.roundedRectStrokeGradient(bX, bY, PANEL_W, PANEL_H, PANEL_R, 1f, 1f, HudRenderUtil.mixColor(col.stroke, 0xFFFFFFFF, 0.04f), col.stroke, 90f); + drawCentered(titleRenderer, tr("screen.lovisual.proxy_manager.header"), bX + PANEL_W * 0.5f, bY + 7f * SCALE, TITLE_FONT, col.title); + float innerX = bX + 8f * SCALE; + float innerW = PANEL_W - 16f * SCALE; + float rowY = bY + 28f * SCALE; + renderChoice(innerX, rowY, 66f * SCALE, 13f * SCALE, tr("screen.lovisual.proxy_manager.enabled"), c.enabled(), enableHover, enablePress, col); + renderTypeToggle(c, innerX + 72f * SCALE, rowY, innerW - 72f * SCALE, 13f * SCALE, col); + float fieldY = bY + 53f * SCALE; + renderField(c, Field.ADDRESS, tr("screen.lovisual.proxy_manager.address"), tr("screen.lovisual.proxy_manager.placeholder.address"), innerX, fieldY, innerW, col); + renderField(c, Field.USERNAME, tr("screen.lovisual.proxy_manager.username"), tr("screen.lovisual.proxy_manager.placeholder.optional"), innerX, fieldY + 27f * SCALE, innerW, col); + renderField(c, Field.PASSWORD, tr("screen.lovisual.proxy_manager.password"), tr("screen.lovisual.proxy_manager.placeholder.optional"), innerX, fieldY + 54f * SCALE, innerW, col); + float buttonY = bY + PANEL_H - 26f * SCALE; + float third = (innerW - GAP * 2f) / 3f; + renderAction(innerX, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.save"), false, saveHover, savePress, col); + renderAction(innerX + third + GAP, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.clear"), true, clearHover, clearPress, col); + renderAction(innerX + (third + GAP) * 2f, buttonY, third, BUTTON_H, tr("screen.lovisual.proxy_manager.back"), false, backHover, backPress, col); + String info = c.status().isBlank() ? c.currentSummary() : c.status(); + draw(bodyRenderer, ellipsize(info, FIELD_FONT, innerW), innerX, bY + PANEL_H - 42f * SCALE, FIELD_FONT, col.muted); + } + private void renderTypeToggle(ProxyManagerController c, float x, float y, float w, float h, PanelColors col) { + float gap = 2f * SCALE; + float itemW = (w - gap) * 0.5f; + renderChoice(x, y, itemW, h, "SOCKS5", c.type() == ProxyType.SOCKS5, socks5Hover, socks5Press, col); + renderChoice(x + itemW + gap, y, itemW, h, "SOCKS4", c.type() == ProxyType.SOCKS4, socks4Hover, socks4Press, col); + } + private void renderField(ProxyManagerController c, Field field, String label, String placeholder, float x, float y, float w, PanelColors col) { + int i = field.index; + draw(bodyRenderer, label, x, y, LABEL_FONT, col.label); + float fy = y + 9f * SCALE; + boolean focused = c.focusedField() == field; + float hoverAnim = AnimationUtility.easeOutCubic(fieldHover[i]); + float focusAnim = AnimationUtility.easeOutCubic(fieldFocus[i]); + int fillTop = HudRenderUtil.mixColor(withAlpha(col.surface, 188), withAlpha(col.surfaceHover, 208), hoverAnim * 0.45f + focusAnim * 0.16f); + int fillBottom = HudRenderUtil.mixColor(withAlpha(col.surface, 176), withAlpha(0xFF0D1118, 196), 0.18f); + int stroke = focused ? HudRenderUtil.mixColor(col.strokeSoft, col.accentSoft, 0.55f) : HudRenderUtil.mixColor(col.stroke, col.strokeSoft, hoverAnim * 0.72f); + Renderer2D.COLOR.roundedRectGradientQuad(x, fy, w, FIELD_H, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); + Renderer2D.COLOR.roundedRectStrokeGradient(x, fy, w, FIELD_H, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); + if (focusAnim > 0.01f) { + Renderer2D.COLOR.roundedRectGradientQuad(x + 1.2f * SCALE, fy + FIELD_H - 1.6f * SCALE, w - 2.4f * SCALE, 1.1f * SCALE, 0.6f * SCALE, 1f, + withAlpha(col.accentSoft, Math.round(48f * focusAnim)), withAlpha(col.accent, Math.round(28f * focusAnim)), + withAlpha(col.accent, Math.round(28f * focusAnim)), withAlpha(col.accentSoft, Math.round(48f * focusAnim))); + } + String value = c.values()[i]; + String display = field == Field.PASSWORD && !value.isEmpty() ? "*".repeat(value.length()) : value; + if (display.isEmpty() && !focused) display = placeholder; + if (focused && AnimationUtility.blink(500L)) display += "|"; + int color = value.isEmpty() && !focused ? col.mutedLabel : col.title; + draw(bodyRenderer, ellipsize(display, FIELD_FONT, w - 8f * SCALE), x + 4f * SCALE, fy + 4.3f * SCALE, FIELD_FONT, color); + } + private void renderChoice(float x, float y, float w, float h, String label, boolean selected, float hover, float press, PanelColors col) { + float hoverAnim = AnimationUtility.easeOutCubic(hover); + float pressAnim = AnimationUtility.easeOutCubic(press); + float active = selected ? 1f : 0f; + int fillTop = HudRenderUtil.mixColor(withAlpha(col.surface, 170), withAlpha(col.surfaceHover, 210), hoverAnim * 0.42f + active * 0.38f + pressAnim * 0.10f); + int fillBottom = HudRenderUtil.mixColor(withAlpha(col.surface, 158), withAlpha(0x1A2418, 210), active * 0.24f + hoverAnim * 0.16f); + int stroke = HudRenderUtil.mixColor(col.stroke, selected ? col.accentSoft : col.strokeSoft, active * 0.70f + hoverAnim * 0.30f + pressAnim * 0.10f); + int text = selected ? HudRenderUtil.mixColor(col.title, col.accent, 0.28f) : HudRenderUtil.mixColor(col.muted, col.title, hoverAnim * 0.35f); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); + drawCentered(bodyRenderer, label, x + w * 0.5f, y + 4.1f * SCALE - pressAnim * 0.35f * SCALE, BUTTON_FONT, text); + } + private void renderAction(float x, float y, float w, float h, String label, boolean destructive, float hover, float press, PanelColors col) { + float hoverAnim = AnimationUtility.easeOutCubic(hover); + float pressAnim = AnimationUtility.easeOutCubic(press); + int fillTop, fillBottom, stroke, text; + if (destructive) { + fillTop = HudRenderUtil.mixColor(withAlpha(0x1A1416, 170), withAlpha(0x4B2224, 212), 0.25f + hoverAnim * 0.45f + pressAnim * 0.12f); + fillBottom = HudRenderUtil.mixColor(withAlpha(0x120E10, 165), withAlpha(0x261012, 220), 0.20f + hoverAnim * 0.40f + pressAnim * 0.10f); + stroke = HudRenderUtil.mixColor(withAlpha(0x352A2A, 215), withAlpha(0x8C5054, 235), 0.22f + hoverAnim * 0.58f + pressAnim * 0.12f); + text = HudRenderUtil.mixColor(withAlpha(0xD0A0A0, 255), withAlpha(0xFFB2B2, 255), 0.24f + hoverAnim * 0.56f + pressAnim * 0.08f); + } else { + fillTop = HudRenderUtil.mixColor(withAlpha(col.surface, 176), withAlpha(0x223746, 212), 0.20f + hoverAnim * 0.44f + pressAnim * 0.10f); + fillBottom = HudRenderUtil.mixColor(withAlpha(col.surface, 166), withAlpha(0x13292F, 220), 0.16f + hoverAnim * 0.40f + pressAnim * 0.08f); + stroke = HudRenderUtil.mixColor(col.stroke, withAlpha(0x466A76, 228), 0.18f + hoverAnim * 0.50f + pressAnim * 0.10f); + text = HudRenderUtil.mixColor(withAlpha(0xD0D8E4, 255), withAlpha(0xEEF8FF, 255), 0.18f + hoverAnim * 0.38f + pressAnim * 0.08f); + } + Renderer2D.COLOR.roundedRectSoftShadow(x, y, w, h, 3f * SCALE, 7f * SCALE, 0.018f + hoverAnim * 0.015f, col.shadow); + Renderer2D.COLOR.roundedRectGradientQuad(x, y, w, h, 3f * SCALE, 1f, fillTop, fillTop, fillBottom, fillBottom); + Renderer2D.COLOR.roundedRectStrokeGradient(x, y, w, h, 3f * SCALE, 1f, 0.5f, HudRenderUtil.mixColor(stroke, 0xFFFFFFFF, 0.04f), stroke, 90f); + drawCentered(bodyRenderer, label, x + w * 0.5f, y + 5f * SCALE - (hoverAnim * 0.65f + pressAnim * 0.55f) * SCALE, BUTTON_FONT, text); + } + private PanelColors colors(float a) { + int ta = Math.round(a * 255f), la = Math.round(a * 155f); + int bg = Math.round(a * 120f), hd = Math.round(a * 150f), st = Math.round(a * 100f); + return new PanelColors( + withAlpha(0x0D0F14, bg), withAlpha(0x101218, bg), withAlpha(0x0D0F14, bg), withAlpha(0x08090C, bg), + withAlpha(0x14171F, hd), withAlpha(0x181B24, hd), withAlpha(0x14171F, hd), withAlpha(0x10131A, hd), + withAlpha(0x252A36, st), withAlpha(0x3A4A5A, hd), withAlpha(0x060810, Math.round(a * 80f)), + withAlpha(0xFFFFFF, ta), withAlpha(0xFFFFFF, la), withAlpha(0x808890, ta), withAlpha(0x606878, la), + withAlpha(0x1A1D24, Math.round(a * 160f)), withAlpha(0x1A1F28, Math.round(a * 200f)), + withAlpha(0xFFD700, ta), withAlpha(0x4A3A10, Math.round(a * 180f))); + } + private void draw(TextRenderer r, String text, float x, float y, float size, int argb) { + if (r == null || text == null || text.isEmpty()) return; + r.setAlpha(1.0); r.begin(size, false, false); + r.render(text, x, y, new RenderColor(argb), false); r.end(); + } + private void drawCentered(TextRenderer r, String text, float cx, float y, float size, int argb) { + draw(r, text, cx - width(r, text, size) * 0.5f, y, size, argb); + } + private float width(TextRenderer r, String text, float size) { + if (r == null || text == null) return 0f; + r.begin(size, true, false); + float w = (float) r.getWidth(text, false); r.end(); return w; + } + private String ellipsize(String text, float size, float maxW) { + if (text == null || text.isEmpty()) return ""; + if (width(bodyRenderer, text, size) <= maxW) return text; + String out = text; + while (out.length() > 3 && width(bodyRenderer, out + "...", size) > maxW) out = out.substring(0, out.length() - 1); + return out + "..."; + } + private static float animate(float c, float t, float dt, float sp) { return AnimationUtility.snap(AnimationUtility.approach(c, t, dt, sp), t, 0.001f); } + private static int withAlpha(int rgb, int alpha) { return (rgb & 0x00FFFFFF) | (Mth.clamp(alpha, 0, 255) << 24); } + private record PanelColors( + int bgTopLeft, int bgTopRight, int bgBottomRight, int bgBottomLeft, + int headerTopLeft, int headerTopRight, int headerBottomRight, int headerBottomLeft, + int stroke, int strokeSoft, int shadow, int title, int label, + int muted, int mutedLabel, int surface, int surfaceHover, int accent, int accentSoft) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java index 4ae488f..dedf4c9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java @@ -1,9 +1,4 @@ -/* - * 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.module.core; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java index fc05b92..1268590 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java @@ -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.module.core; public enum ModuleCategory { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java index 9044d48..8473e86 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java @@ -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.module.core; import dev.loki.lovisual.events.meta.UsedImplicitly; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java index 93f0d88..bd2b6e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java @@ -1,152 +1,69 @@ -/* - * 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.module.core; import com.mojang.blaze3d.vertex.PoseStack; -import dev.loki.lovisual.addon.manager.ModuleExtensionManager; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.module.phase.ModuleStateListener; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.logging.DebugLog; import dev.loki.lovisual.util.screen.ClientScreen; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Setter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.renderer.SubmitNodeCollector; -import dev.loki.lovisual.events.Events; -import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; -import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler2D; -import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler3D; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.util.input.KeyManager; -import dev.loki.lovisual.util.logging.DebugLog; -import java.util.*; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.phase.ModuleStateListener; -import dev.loki.lovisual.features.module.phase.WorldPhase; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +/** + * Singleton entry point for module registration, lookup and per-frame dispatch. + * + *

    Delegates the heavy work to {@link ModuleRegistrar} (validate + insert + + * rebuild array snapshots) and {@link ModuleDispatcher} (tick / HUD / world / + * keybind loops), keeping the public static API consumed by the ClickGUI, + * commands, HUD, add-on bridge and {@code LoVisualClientApi} unchanged.

    + */ public enum ModuleManager { ; - private static final List modules = new ArrayList<>(); + // Indexes + snapshots (package-private so Registrar / Dispatcher can share them) -------- + static final List modules = new ArrayList<>(); private static final List modulesView = Collections.unmodifiableList(modules); - private static final Map byId = new LinkedHashMap<>(); - private static final Map, Module> byType = new IdentityHashMap<>(); - private static final EnumMap> HUD_PHASE_MODULES = new EnumMap<>(HudPhase.class); - private static final EnumMap HUD_PHASE_SNAPSHOTS = new EnumMap<>(HudPhase.class); - private static final EnumMap> WORLD_PHASE_MODULES = new EnumMap<>(WorldPhase.class); - private static final EnumMap WORLD_PHASE_SNAPSHOTS = new EnumMap<>(WorldPhase.class); + static final Map byId = new LinkedHashMap<>(); + static final Map, Module> byType = new IdentityHashMap<>(); + static final EnumMap> HUD_PHASE_MODULES = new EnumMap<>(HudPhase.class); + static final EnumMap HUD_PHASE_SNAPSHOTS = new EnumMap<>(HudPhase.class); + static final EnumMap> WORLD_PHASE_MODULES = new EnumMap<>(WorldPhase.class); + static final EnumMap WORLD_PHASE_SNAPSHOTS = new EnumMap<>(WorldPhase.class); private static final List listeners = new ArrayList<>(); - private static Module[] modulesSnapshot = new Module[0]; + static Module[] modulesSnapshot = new Module[0]; private static ModuleStateListener[] listenerSnapshot = new ModuleStateListener[0]; - @Setter - private static boolean suppressToggleSound = false; - @Setter - private static boolean suppressToggleNotifications = false; + @Setter private static boolean suppressToggleSound = false; + @Setter private static boolean suppressToggleNotifications = false; static { - for (HudPhase phase : HudPhase.values()) { - HUD_PHASE_MODULES.put(phase, new ArrayList<>()); - HUD_PHASE_SNAPSHOTS.put(phase, new Module[0]); - } - for (WorldPhase phase : WorldPhase.values()) { - WORLD_PHASE_MODULES.put(phase, new ArrayList<>()); - WORLD_PHASE_SNAPSHOTS.put(phase, new Module[0]); - } + for (HudPhase phase : HudPhase.values()) { HUD_PHASE_MODULES.put(phase, new ArrayList<>()); HUD_PHASE_SNAPSHOTS.put(phase, new Module[0]); } + for (WorldPhase phase : WorldPhase.values()) { WORLD_PHASE_MODULES.put(phase, new ArrayList<>()); WORLD_PHASE_SNAPSHOTS.put(phase, new Module[0]); } } - public static void register(Module module) { - if (module == null) { - DebugLog.error("Skipping null module registration"); - return; - } - - String id = module.name(); - if (id == null || id.isBlank()) { - DebugLog.error("Skipping module with blank id: %s", null, module.getClass().getName()); - return; - } - - Class type = module.getClass(); - if (!hasModuleMetadata(type)) { - DebugLog.error("Skipping module without metadata annotation: %s", null, type.getName()); - return; - } - - String normalizedId = normalizeName(id); - Module existingById = byId.get(normalizedId); - if (existingById != null) { - DebugLog.error("Skipping duplicate module id '%s': existing=%s duplicate=%s", - null, normalizedId, existingById.getClass().getName(), type.getName()); - return; - } - - Module existingByType = byType.get(type); - if (existingByType != null) { - DebugLog.error("Skipping duplicate module class: %s", null, type.getName()); - return; - } - - if (modules.contains(module)) { - DebugLog.error("Skipping duplicate module instance: %s", null, type.getName()); - return; - } - - modules.add(module); - byId.put(normalizedId, module); - byType.put(type, module); - cacheHudPhase(module); - cacheWorldPhase(module); - rebuildSnapshots(); - Events.BUS.register(module); - } - - private static boolean hasModuleMetadata(Class type) { - return type.isAnnotationPresent(ModuleInfo.class); - } - - private static void cacheHudPhase(Module module) { - if (module == null) return; - HudPhase phase = module.getHudPhase(); - if (phase == null || phase == HudPhase.NONE) return; - HUD_PHASE_MODULES.get(phase).add(module); - } - - private static void cacheWorldPhase(Module module) { - if (module == null) return; - WorldPhase phase = module.getWorldPhase(); - if (phase == null || phase == WorldPhase.NONE) return; - WORLD_PHASE_MODULES.get(phase).add(module); - } - - private static void rebuildSnapshots() { - modulesSnapshot = modules.toArray(Module[]::new); - - for (HudPhase phase : HudPhase.values()) { - List list = HUD_PHASE_MODULES.get(phase); - HUD_PHASE_SNAPSHOTS.put(phase, list == null || list.isEmpty() ? new Module[0] : list.toArray(Module[]::new)); - } - - for (WorldPhase phase : WorldPhase.values()) { - List list = WORLD_PHASE_MODULES.get(phase); - WORLD_PHASE_SNAPSHOTS.put(phase, list == null || list.isEmpty() ? new Module[0] : list.toArray(Module[]::new)); - } - } + /** Validate + insert {@code module}; delegates to {@link ModuleRegistrar}. */ + public static void register(Module module) { ModuleRegistrar.register(module); } + // Lookup / toggle ---------------------------------------------------------------------- public static Module get(String name) { if (name == null || name.isBlank()) return null; - Module module = byId.get(normalizeName(name)); - return module; + return byId.get(ModuleRegistrar.normalizeName(name)); } public static T get(Class type) { @@ -155,374 +72,123 @@ public enum ModuleManager { return module == null ? null : type.cast(module); } + /** Same as {@link #get(Class)} but throws when the module is not registered. */ public static T require(Class type) { T module = get(type); - if (module == null) { - String typeName = type == null ? "" : type.getName(); - throw new NoSuchElementException("Module is not registered: " + typeName); - } + if (module == null) throw new NoSuchElementException("Module is not registered: " + (type == null ? "" : type.getName())); return module; } - public static void toggle(String name) { - Module m = get(name); - if (m != null) m.toggle(); - } - - public static void toggle(String name, ModuleActivationSource source) { - Module m = get(name); - if (m != null) m.setEnabled(!m.isEnabled(), source); - } - - public static void tickAll() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) return; - if (!Lifecycle.isActive()) return; - - Module[] snapshot = modulesSnapshot; - for (Module m : snapshot) { - if (m.isEnabled() && ModuleExtensionManager.beforeTick(m)) { - m.onTick(); - ModuleExtensionManager.afterTick(m); - } - } - } - - public static void frameAll(float frameDeltaTicks) { - if (!Lifecycle.isActive()) return; - Module[] snapshot = modulesSnapshot; - for (Module m : snapshot) { - if (m.isEnabled() && ModuleExtensionManager.beforeFrame(m, frameDeltaTicks)) { - m.onFrame(frameDeltaTicks); - ModuleExtensionManager.afterFrame(m, frameDeltaTicks); - } - } - } - - public static void renderHud(HudPhase phase, - GuiGraphicsExtractor ctx, - float tickDelta) { - - if (!Lifecycle.canRunHud()) return; - - Module[] phaseModules = HUD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return; - - for (Module m : phaseModules) { - if (m.isEnabled()) { - m.onRender2D(ctx, tickDelta); - } - } - } - - public static void renderHudEngine(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - if (!Lifecycle.canRunHud()) return; - - Module[] phaseModules = HUD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return; - - for (Module m : phaseModules) { - if (m.isEnabled()) { - try (ProfilerPhase.Scope _ = ProfilerPhase.scope("2d:module:", m.name()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("module:", m.name())) { - if (ModuleExtensionManager.beforeHudRender(m, renderer, textRenderer, ctx, tickDelta)) { - m.onRenderHudEngine(renderer, textRenderer, ctx, tickDelta); - ModuleExtensionManager.afterHudRender(m, renderer, textRenderer, ctx, tickDelta); - } - } - } - } - } - - public static boolean hasHudEngineWork(HudPhase phase) { - return hasEnabledHudPhaseModule(phase); - } - - public static void renderHudEngineForeground(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - if (!Lifecycle.canRunHud()) return; - - Module[] phaseModules = HUD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return; - - for (Module m : phaseModules) { - if (m.isEnabled()) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("2d:module_fg:", m.name()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("module_fg:", m.name())) { - m.onRenderHudEngineForeground(renderer, textRenderer, ctx, tickDelta); - } - } - } - } - - public static boolean hasHudEngineForegroundWork(HudPhase phase) { - return hasEnabledHudPhaseModule(phase); - } - - private static boolean hasEnabledHudPhaseModule(HudPhase phase) { - if (!Lifecycle.canRunHud()) return false; - Module[] phaseModules = HUD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return false; - - for (Module m : phaseModules) { - if (m.isEnabled()) return true; - } - return false; - } - - // --------------------------- - // 3D WORLD рендер - // --------------------------- - - public static void renderWorld(WorldPhase phase, PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) { - - if (!Lifecycle.canRunRender()) return; - - Module[] phaseModules = WORLD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return; - - for (Module m : phaseModules) { - if (m.isEnabled()) { - m.onRenderWorld(matrices, consumers, tickDelta); - } - } - } - - public static void renderWorldEngine(WorldPhase phase, Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - if (!Lifecycle.canRunRender()) return; - - Module[] phaseModules = WORLD_PHASE_SNAPSHOTS.get(phase); - if (phaseModules == null) return; - - for (Module m : phaseModules) { - if (m.isEnabled()) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:module:", m.name()); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("module:", m.name())) { - if (ModuleExtensionManager.beforeWorldRender(m, renderer, depthRenderer, tickDelta)) { - m.onRenderWorldEngine(renderer, depthRenderer, tickDelta); - ModuleExtensionManager.afterWorldRender(m, renderer, depthRenderer, tickDelta); - } - } - } - } - } - - // --------------------------- - // KEY HANDLING - // --------------------------- - - public static void handleModuleKeybinds() { - if (!Lifecycle.isActive()) return; - boolean soundSuppressedThisPass = false; - Module[] snapshot = modulesSnapshot; - for (Module m : snapshot) { - if (KeyManager.wasPressed(m.name())) { - setSuppressToggleSound(soundSuppressedThisPass); - m.toggleFromKeybind(); - soundSuppressedThisPass = true; - } - } - setSuppressToggleSound(false); - } - - public static List getEnabledModulesNames() { - List list = new ArrayList<>(); - Module[] snapshot = modulesSnapshot; - for (int i = 0; i < snapshot.length; i++) { - Module m = snapshot[i]; - if (m.isEnabled()) list.add(m.name()); - } - return list; - } - - public static List getKeybindEnabledModules() { - List list = new ArrayList<>(); - Module[] snapshot = modulesSnapshot; - for (int i = 0; i < snapshot.length; i++) { - Module m = snapshot[i]; - if (m.isEnabledFromKeybind()) list.add(m); - } - return list; - } - - /** - * View of all registered modules (do not modify). - */ - public static List getModules() { - return modulesView; - } - - public static void setEnabledModules(List names) { - if (!Lifecycle.isActive()) return; - boolean prev = suppressToggleNotifications; - suppressToggleNotifications = true; - try { - ObjectOpenHashSet enabledNames = new ObjectOpenHashSet<>(); - if (names != null) { - for (String name : names) { - if (name != null && !name.isBlank()) { - enabledNames.add(normalizeName(name)); - } - } - } - - Module[] snapshot = modulesSnapshot; - for (int i = 0; i < snapshot.length; i++) { - Module m = snapshot[i]; - boolean shouldEnable = enabledNames.contains(normalizeName(m.name())); - if (m.isEnabled() != shouldEnable) { - m.setEnabled(shouldEnable, ModuleActivationSource.INTERNAL); - } - } - } finally { - suppressToggleNotifications = prev; - } - } - - public static boolean isEnabled(String name) { - Module m = get(name); - return m != null && m.isEnabled(); - } - - public static void setEnabled(String name, boolean enabled) { - setEnabled(name, enabled, ModuleActivationSource.MANUAL); - } - - public static void setEnabled(String name, boolean enabled, ModuleActivationSource source) { - Module m = get(name); - if (m == null) return; - if (enabled && !Lifecycle.isActive()) return; - - if (enabled && name.equals("clickgui")) { - if (ClientScreen.current(Minecraft.getInstance()) != null) { - return; - } - } - - m.setEnabled(enabled, source); - } - - public static boolean setRuntimeEnabledTransient(String name, boolean enabled) { - Module m = get(name); - if (m == null) return false; - m.setRuntimeEnabledTransient(enabled); - return true; - } - public static ModuleCategory getCategory(String name) { Module m = get(name); return m != null ? m.getCategory() : null; } - /** - * ClickGUI expects list of module names - */ - public static List getAllModules() { - List out = new ArrayList<>(); - Module[] snapshot = modulesSnapshot; - for (int i = 0; i < snapshot.length; i++) { - out.add(snapshot[i].name()); - } + public static void toggle(String name) { Module m = get(name); if (m != null) m.toggle(); } + public static void toggle(String name, ModuleActivationSource source) { Module m = get(name); if (m != null) m.setEnabled(!m.isEnabled(), source); } + + // Per-frame dispatch (delegates to ModuleDispatcher) ----------------------------------- + public static void tickAll() { ModuleDispatcher.tickAll(); } + public static void frameAll(float frameDeltaTicks) { ModuleDispatcher.frameAll(frameDeltaTicks); } + public static void renderHud(HudPhase phase, GuiGraphicsExtractor ctx, float tickDelta) { ModuleDispatcher.renderHud(phase, ctx, tickDelta); } + public static void renderHudEngine(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { ModuleDispatcher.renderHudEngine(phase, renderer, textRenderer, ctx, tickDelta); } + public static void renderHudEngineForeground(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { ModuleDispatcher.renderHudEngineForeground(phase, renderer, textRenderer, ctx, tickDelta); } + public static void renderWorld(WorldPhase phase, PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) { ModuleDispatcher.renderWorld(phase, matrices, consumers, tickDelta); } + public static void renderWorldEngine(WorldPhase phase, Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { ModuleDispatcher.renderWorldEngine(phase, renderer, depthRenderer, tickDelta); } + public static boolean hasHudEngineWork(HudPhase phase) { return ModuleDispatcher.hasEnabledHudPhaseModule(phase); } + public static boolean hasHudEngineForegroundWork(HudPhase phase) { return ModuleDispatcher.hasEnabledHudPhaseModule(phase); } + public static void handleModuleKeybinds() { ModuleDispatcher.handleModuleKeybinds(); } + + // Enable / disable --------------------------------------------------------------------- + public static boolean isEnabled(String name) { Module m = get(name); return m != null && m.isEnabled(); } + + public static void setEnabled(String name, boolean enabled) { setEnabled(name, enabled, ModuleActivationSource.MANUAL); } + + public static void setEnabled(String name, boolean enabled, ModuleActivationSource source) { + Module m = get(name); + if (m == null) return; + if (enabled && !Lifecycle.isActive()) return; + if (enabled && name.equals("clickgui") && ClientScreen.current(Minecraft.getInstance()) != null) return; + m.setEnabled(enabled, source); + } + + /** Enable or disable a module without firing persistence or keybind hooks. */ + public static boolean setRuntimeEnabledTransient(String name, boolean enabled) { + Module m = get(name); + if (m == null) return false; + m.setRuntimeEnabledTransient(enabled); + return true; + } + + /** Disable every currently-enabled module; toggle notifications are suppressed for the whole pass. */ + public static void disableAll() { + boolean prev = suppressToggleNotifications; suppressToggleNotifications = true; + try { for (Module m : modulesSnapshot) if (m != null && m.isEnabled()) m.setEnabled(false, ModuleActivationSource.INTERNAL); } + finally { suppressToggleNotifications = prev; } + } + + /** Bulk-apply a config profile: enable exactly the modules named, disable everything else, without firing per-toggle notifications. */ + public static void setEnabledModules(List names) { + if (!Lifecycle.isActive()) return; + boolean prev = suppressToggleNotifications; + suppressToggleNotifications = true; + try { + ObjectOpenHashSet enabledNames = new ObjectOpenHashSet<>(); + if (names != null) for (String name : names) if (name != null && !name.isBlank()) enabledNames.add(ModuleRegistrar.normalizeName(name)); + for (Module m : modulesSnapshot) { + boolean shouldEnable = enabledNames.contains(ModuleRegistrar.normalizeName(m.name())); + if (m.isEnabled() != shouldEnable) m.setEnabled(shouldEnable, ModuleActivationSource.INTERNAL); + } + } finally { suppressToggleNotifications = prev; } + } + + // Bulk queries ------------------------------------------------------------------------- + /** Unmodifiable insertion-order view of every registered module. */ + public static List getModules() { return modulesView; } + + /** Names of every registered module; consumed by the ClickGUI module list. */ + public static List getAllModules() { return listMatched(m -> true, Module::name); } + public static List getEnabledModulesNames() { return listMatched(Module::isEnabled, Module::name); } + public static List getKeybindEnabledModules() { return listMatched(Module::isEnabledFromKeybind, m -> m); } + public static int registeredCount() { return modulesSnapshot.length; } + private static List listMatched(java.util.function.Predicate filter, java.util.function.Function mapper) { + List out = new ArrayList<>(); + for (Module m : modulesSnapshot) if (filter.test(m)) out.add(mapper.apply(m)); return out; } + // State listeners ---------------------------------------------------------------------- public static void addListener(ModuleStateListener listener) { if (listener == null) return; listeners.add(listener); listenerSnapshot = listeners.toArray(ModuleStateListener[]::new); } + /** Fan out a toggle event to every listener; suppressed inside a batched config apply. */ public static void notifyListeners(String name, boolean enabled) { - if (suppressToggleNotifications) { - // DebugLog.config("[ModuleManager] notifyListeners suppressed: %s -> %s (listeners=%d)", name, enabled, listeners.size()); - return; - } - ModuleStateListener[] snapshot = listenerSnapshot; - //DebugLog.config("[ModuleManager] notifyListeners: %s -> %s (listeners=%d)", name, enabled, snapshot.length); - for (int i = 0; i < snapshot.length; i++) { - snapshot[i].onModuleStateChanged(name, enabled); - } + if (suppressToggleNotifications) return; + for (ModuleStateListener l : listenerSnapshot) l.onModuleStateChanged(name, enabled); } - // ==================================== - // Sound suppression for batched toggles - // ==================================== - public static boolean isToggleSoundSuppressed() { - return suppressToggleSound; - } + // Toggle-sound / notification suppression --------------------------------------------- + public static boolean isToggleSoundSuppressed() { return suppressToggleSound; } + public static boolean isToggleNotificationSuppressed() { return suppressToggleNotifications; } - public static boolean isToggleNotificationSuppressed() { - return suppressToggleNotifications; - } - - /** - * Runs config profile application without toggle sounds or module-state notifications. - */ + /** Runs {@code action} with both toggle sounds and module-state notifications suppressed. */ public static void runSilentConfigApply(Runnable action) { if (action == null) return; - boolean prevSound = suppressToggleSound; - boolean prevNotifications = suppressToggleNotifications; - suppressToggleSound = true; - suppressToggleNotifications = true; - try { - action.run(); - } finally { - suppressToggleSound = prevSound; - suppressToggleNotifications = prevNotifications; - } + boolean ps = suppressToggleSound, pn = suppressToggleNotifications; + suppressToggleSound = true; suppressToggleNotifications = true; + try { action.run(); } finally { suppressToggleSound = ps; suppressToggleNotifications = pn; } } - // ============================ - // CONFIG ORCHESTRATION - // ============================ - - /** - * Loads and applies config for all modules (enabled state + settings). - */ + // Config orchestration ---------------------------------------------------------------- + /** Load + apply config for every registered module; logs per-module failures without aborting. */ public static void loadAllModuleConfigs() { - Module[] snapshot = modulesSnapshot; - DebugLog.config("loadAllModuleConfigs: %d modules", snapshot.length); - for (int i = 0; i < snapshot.length; i++) { - Module m = snapshot[i]; - try { - DebugLog.config("Loading module config -> %s", m.name()); - m.loadAndApplyConfig(); - } catch (Throwable ignored) { - DebugLog.error("Failed to load config for module %s", ignored, m.name()); - } - } + DebugLog.config("loadAllModuleConfigs: %d modules", modulesSnapshot.length); + for (Module m : modulesSnapshot) try { DebugLog.config("Loading module config -> %s", m.name()); m.loadAndApplyConfig(); } + catch (Throwable ignored) { DebugLog.error("Failed to load config for module %s", ignored, m.name()); } } - public static void saveAllModuleConfigs() { - Module[] snapshot = modulesSnapshot; - for (int i = 0; i < snapshot.length; i++) { - Module m = snapshot[i]; - try { - m.saveConfig(); - } catch (Throwable ignored) { - } - } - } - - public static int registeredCount() { - return modulesSnapshot.length; - } - - public static void disableAll() { - Module[] snapshot = modulesSnapshot; - boolean prevNotifications = suppressToggleNotifications; - suppressToggleNotifications = true; - try { - for (Module module : snapshot) { - if (module != null && module.isEnabled()) { - module.setEnabled(false, ModuleActivationSource.INTERNAL); - } - } - } finally { - suppressToggleNotifications = prevNotifications; - } - } - - private static String normalizeName(String name) { - return name == null ? "" : name.toLowerCase(Locale.ROOT); - } + public static void saveAllModuleConfigs() { for (Module m : modulesSnapshot) { try { m.saveConfig(); } catch (Throwable ignored) { } } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleDispatcher.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleDispatcher.java new file mode 100644 index 0000000..cf72875 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleDispatcher.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.features.module.core; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.addon.manager.ModuleExtensionManager; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; +import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler2D; +import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler3D; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.input.KeyManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.SubmitNodeCollector; + +/** + * Per-frame dispatch loops backing {@link ModuleManager}. Iterates the array + * snapshots maintained by {@link ModuleRegistrar}, gated by {@link Lifecycle} + * and the add-on {@link ModuleExtensionManager} hooks, and wraps engine + * render calls in 2D / 3D profiler scopes. Sibling folder sharing the parent's + * package so {@link ModuleManager}'s package-private snapshots stay reachable. + */ +final class ModuleDispatcher { + + private ModuleDispatcher() { } + + /** Game tick: runs {@link Module#onTick()} on every enabled module when a player is loaded. */ + static void tickAll() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || !Lifecycle.isActive()) return; + for (Module m : ModuleManager.modulesSnapshot) { + if (m.isEnabled() && ModuleExtensionManager.beforeTick(m)) { + m.onTick(); + ModuleExtensionManager.afterTick(m); + } + } + } + + /** Interpolation-only pass that runs regardless of world state; drives animation clocks. */ + static void frameAll(float frameDeltaTicks) { + if (!Lifecycle.isActive()) return; + for (Module m : ModuleManager.modulesSnapshot) { + if (m.isEnabled() && ModuleExtensionManager.beforeFrame(m, frameDeltaTicks)) { + m.onFrame(frameDeltaTicks); + ModuleExtensionManager.afterFrame(m, frameDeltaTicks); + } + } + } + + /** Legacy 2D HUD pass (Renderer2D not used); iterates modules bound to {@code phase}. */ + static void renderHud(HudPhase phase, GuiGraphicsExtractor ctx, float tickDelta) { + if (!Lifecycle.canRunHud()) return; + Module[] phaseModules = ModuleManager.HUD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return; + for (Module m : phaseModules) if (m.isEnabled()) m.onRender2D(ctx, tickDelta); + } + + /** Engine 2D HUD pass with per-module profiler scopes and add-on extension hooks. */ + static void renderHudEngine(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + if (!Lifecycle.canRunHud()) return; + Module[] phaseModules = ModuleManager.HUD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return; + for (Module m : phaseModules) { + if (!m.isEnabled()) continue; + try (ProfilerPhase.Scope _ = ProfilerPhase.scope("2d:module:", m.name()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("module:", m.name())) { + if (ModuleExtensionManager.beforeHudRender(m, renderer, textRenderer, ctx, tickDelta)) { + m.onRenderHudEngine(renderer, textRenderer, ctx, tickDelta); + ModuleExtensionManager.afterHudRender(m, renderer, textRenderer, ctx, tickDelta); + } + } + } + } + + /** Foreground (post-HUD) engine pass; runs unconditionally on every enabled module in {@code phase}. */ + static void renderHudEngineForeground(HudPhase phase, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + if (!Lifecycle.canRunHud()) return; + Module[] phaseModules = ModuleManager.HUD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return; + for (Module m : phaseModules) { + if (!m.isEnabled()) continue; + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("2d:module_fg:", m.name()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("module_fg:", m.name())) { + m.onRenderHudEngineForeground(renderer, textRenderer, ctx, tickDelta); + } + } + } + + /** Vanilla 3D world pass; iterates modules bound to {@code phase}. */ + static void renderWorld(WorldPhase phase, PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) { + if (!Lifecycle.canRunRender()) return; + Module[] phaseModules = ModuleManager.WORLD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return; + for (Module m : phaseModules) if (m.isEnabled()) m.onRenderWorld(matrices, consumers, tickDelta); + } + + /** Engine 3D world pass with per-module profiler scopes and add-on extension hooks. */ + static void renderWorldEngine(WorldPhase phase, Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + if (!Lifecycle.canRunRender()) return; + Module[] phaseModules = ModuleManager.WORLD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return; + for (Module m : phaseModules) { + if (!m.isEnabled()) continue; + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:module:", m.name()); + RenderProfiler3D.Section ignored = RenderProfiler3D.section("module:", m.name())) { + if (ModuleExtensionManager.beforeWorldRender(m, renderer, depthRenderer, tickDelta)) { + m.onRenderWorldEngine(renderer, depthRenderer, tickDelta); + ModuleExtensionManager.afterWorldRender(m, renderer, depthRenderer, tickDelta); + } + } + } + } + + /** True when at least one enabled module is bound to {@code phase}; used to skip the whole pass. */ + static boolean hasEnabledHudPhaseModule(HudPhase phase) { + if (!Lifecycle.canRunHud()) return false; + Module[] phaseModules = ModuleManager.HUD_PHASE_SNAPSHOTS.get(phase); + if (phaseModules == null) return false; + for (Module m : phaseModules) if (m.isEnabled()) return true; + return false; + } + + /** Toggle every module whose keybind was pressed this tick; suppresses the click sound on all but the first. */ + static void handleModuleKeybinds() { + if (!Lifecycle.isActive()) return; + boolean soundSuppressedThisPass = false; + for (Module m : ModuleManager.modulesSnapshot) { + if (KeyManager.wasPressed(m.name())) { + ModuleManager.setSuppressToggleSound(soundSuppressedThisPass); + m.toggleFromKeybind(); + soundSuppressedThisPass = true; + } + } + ModuleManager.setSuppressToggleSound(false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleRegistrar.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleRegistrar.java new file mode 100644 index 0000000..41042ba --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/impl/ModuleRegistrar.java @@ -0,0 +1,73 @@ +package dev.loki.lovisual.features.module.core; + +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.List; +import java.util.Locale; + +/** + * Registration pipeline backing {@link ModuleManager}. Validates a module's + * identity (annotation, id, class, instance), inserts it into the three + * lookup indexes ({@code byId}, {@code byType}, insertion-order list), + * buckets it into the HUD / world phase caches and rebuilds the array views + * consumed by the per-frame render loops. Sibling folder sharing the parent's + * package so {@link ModuleManager}'s package-private indexes stay reachable. + */ +final class ModuleRegistrar { + + private ModuleRegistrar() { } + + /** Validate + insert {@code module} into every index, then rebuild snapshots; logs and skips on any conflict. */ + static void register(Module module) { + if (module == null) { DebugLog.error("Skipping null module registration"); return; } + String id = module.name(); + if (id == null || id.isBlank()) { DebugLog.error("Skipping module with blank id: %s", null, module.getClass().getName()); return; } + Class type = module.getClass(); + if (!type.isAnnotationPresent(ModuleInfo.class)) { DebugLog.error("Skipping module without metadata annotation: %s", null, type.getName()); return; } + String normalizedId = normalizeName(id); + Module existingById = ModuleManager.byId.get(normalizedId); + if (existingById != null) { DebugLog.error("Skipping duplicate module id '%s': existing=%s duplicate=%s", null, normalizedId, existingById.getClass().getName(), type.getName()); return; } + if (ModuleManager.byType.get(type) != null) { DebugLog.error("Skipping duplicate module class: %s", null, type.getName()); return; } + if (ModuleManager.modules.contains(module)) { DebugLog.error("Skipping duplicate module instance: %s", null, type.getName()); return; } + ModuleManager.modules.add(module); + ModuleManager.byId.put(normalizedId, module); + ModuleManager.byType.put(type, module); + cacheHudPhase(module); + cacheWorldPhase(module); + rebuildSnapshots(); + Events.BUS.register(module); + } + + /** Locale-independent lowercase key used for id lookups; also consumed by the facade. */ + static String normalizeName(String name) { + return name == null ? "" : name.toLowerCase(Locale.ROOT); + } + + private static void cacheHudPhase(Module module) { + HudPhase phase = module.getHudPhase(); + if (phase == null || phase == HudPhase.NONE) return; + ModuleManager.HUD_PHASE_MODULES.get(phase).add(module); + } + + private static void cacheWorldPhase(Module module) { + WorldPhase phase = module.getWorldPhase(); + if (phase == null || phase == WorldPhase.NONE) return; + ModuleManager.WORLD_PHASE_MODULES.get(phase).add(module); + } + + /** Flatten every lookup list into the array views the render loops iterate on the client thread. */ + static void rebuildSnapshots() { + ModuleManager.modulesSnapshot = ModuleManager.modules.toArray(Module[]::new); + for (HudPhase phase : HudPhase.values()) { + List list = ModuleManager.HUD_PHASE_MODULES.get(phase); + ModuleManager.HUD_PHASE_SNAPSHOTS.put(phase, list == null || list.isEmpty() ? new Module[0] : list.toArray(Module[]::new)); + } + for (WorldPhase phase : WorldPhase.values()) { + List list = ModuleManager.WORLD_PHASE_MODULES.get(phase); + ModuleManager.WORLD_PHASE_SNAPSHOTS.put(phase, list == null || list.isEmpty() ? new Module[0] : list.toArray(Module[]::new)); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java index 8f39e31..40cba73 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java @@ -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.module.lifecycle; public enum ModuleActivationSource { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java index fd33765..8a47824 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java @@ -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.module.lifecycle; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java index c4a6f9c..a8588a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java @@ -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.module.lifecycle; import dev.loki.lovisual.events.meta.UsedImplicitly; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java index 6a02bc1..c38a764 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java @@ -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.module.lifecycle; import dev.loki.lovisual.Lifecycle; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java index 8a18ca1..9c66f36 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java @@ -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.module.module; import dev.loki.lovisual.config.values.mode.BindMode; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java index 6468177..408da70 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java @@ -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.module.module; import de.marhali.json5.Json5Object; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java index acf0de9..77162ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java @@ -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.module.module; import net.minecraft.client.resources.language.I18n; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java index 2b9df4a..4bf8979 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java @@ -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.module.module; import dev.loki.lovisual.config.definition.SettingDef; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java index 391a11e..9209046 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java @@ -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.module.modules.combat.damage; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java index 75d6763..ccd4b68 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java @@ -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.module.modules.combat.damage; import dev.loki.lovisual.features.module.core.Module; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java index b4203dd..d638ecf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java @@ -1,19 +1,8 @@ -/* - * 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.module.modules.combat.prediction; import net.minecraft.client.Minecraft; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.phys.Vec3; import dev.loki.lovisual.config.common.CommonSettingSchemas; import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.mode.ModeValue; @@ -23,24 +12,16 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; -import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals; import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects; import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes; -import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals.CrystalInstance; import dev.loki.lovisual.features.relations.category.CategoryService; -import dev.loki.lovisual.mixininterface.entity.IEntity; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.renderer.Renderer3DCullOptions; +import dev.loki.lovisual.render.engine.renderer.Renderer3DCulling; import dev.loki.lovisual.util.target.TargetManager; -import java.util.ArrayList; -import java.util.List; import java.util.function.IntFunction; import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects.renderBlackhole; @@ -76,7 +57,6 @@ public class TargetESP extends Module { private static final String SETTING_BLACKHOLE_SPIN_SPEED = "blackhole_spin_speed"; private static final String SETTING_BLACKHOLE_STYLE = "blackhole_style"; private static final String SETTING_VORTEX_SPEED = "vortex_speed"; - private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM; private static final float ALPHA_TICK_DT = 1.0f / 20.0f; private static final float ALPHA_SNAP_EPSILON = 0.02f; private final ModeValue espMode = modeSetting( @@ -128,20 +108,17 @@ public class TargetESP extends Module { visibleWhen(modeSetting("targetEspBlackholeStyle", SETTING_BLACKHOLE_STYLE, "Single", "Single", "Body"), this::isBlackholeMode); private final NumberValue vortexSpeed = visibleWhen(num("targetEspVortexSpeed", SETTING_VORTEX_SPEED, 1.2f, 0.2f, 4.0f), this::isVortexMode); - private final List crystalList = new ArrayList<>(); + private final TargetEspCrystalRenderer crystalRenderer = new TargetEspCrystalRenderer(); private LivingEntity target; private LivingEntity renderTarget; private float targetAlpha; - private Entity lastCrystalTarget; - private float crystalRotationAngle; @Override public void onDisable() { target = null; renderTarget = null; targetAlpha = 0.0f; - crystalList.clear(); - lastCrystalTarget = null; + crystalRenderer.clear(); } @Override @@ -150,15 +127,13 @@ public class TargetESP extends Module { if (!isEnabled() || mc.level == null) { target = null; updateRenderTarget(null); - crystalList.clear(); - lastCrystalTarget = null; + crystalRenderer.clear(); return; } target = TargetManager.getTarget(crosshairTargets.get()); updateRenderTarget(target); if (renderTarget == null) { - crystalList.clear(); - lastCrystalTarget = null; + crystalRenderer.clear(); } setCaptureSpeedScale(isCaptureMarkMode() ? captureSpinSpeed.get() : 1.0f); @@ -176,8 +151,8 @@ public class TargetESP extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { if (!isEnabled() || renderTarget == null || targetAlpha <= 0.001f) return; - Renderer3D.CullOptions cull = resolveCullOptions(renderTarget); - if (!Renderer3D.Culling.shouldRender(renderTarget, tickDelta, cull)) return; + Renderer3DCullOptions cull = resolveCullOptions(renderTarget); + if (!Renderer3DCulling.shouldRender(renderTarget, tickDelta, cull)) return; IntFunction palette = index -> AnimatedRenderColors.scaleAlpha(getColorArgb(index), targetAlpha); @@ -198,7 +173,7 @@ public class TargetESP extends Module { } else if ("capture_mark".equals(mode)) { renderCaptureMark(renderer, renderTarget, tickDelta, palette); } else if ("crystals".equals(mode)) { - renderCrystals(renderer, renderTarget, tickDelta); + crystalRenderer.render(renderer, renderTarget, tickDelta, crystalHitRed.get(), targetAlpha, this::getColorArgb); } else if ("blackhole".equals(mode)) { renderBlackhole(renderer, renderTarget, tickDelta, palette, blackholeSize.get(), blackholeSpinSpeed.get(), targetAlpha, blackholeStyle.get()); @@ -223,8 +198,7 @@ public class TargetESP extends Module { if (renderTarget != next) { renderTarget = next; targetAlpha = 1.0f; - crystalList.clear(); - lastCrystalTarget = null; + crystalRenderer.clear(); return; } targetAlpha = AnimationUtility.snap( @@ -242,123 +216,21 @@ public class TargetESP extends Module { ); if (targetAlpha <= 0.0f) { renderTarget = null; - crystalList.clear(); - lastCrystalTarget = null; + crystalRenderer.clear(); } } - private Renderer3D.CullOptions resolveCullOptions(LivingEntity target) { + private Renderer3DCullOptions resolveCullOptions(LivingEntity target) { Minecraft mc = Minecraft.getInstance(); if (target == null || mc.player == null) { - return Renderer3D.CullOptions.SMART_TARGET; + return Renderer3DCullOptions.SMART_TARGET; } // Allow rendering through walls if the current target is already selected, // so raycast-based targeting doesn't get culled by line-of-sight checks. if (!mc.player.hasLineOfSight(target)) { - return Renderer3D.CullOptions.FRUSTUM_LOOK; + return Renderer3DCullOptions.FRUSTUM_LOOK; } - return Renderer3D.CullOptions.SMART_TARGET; - } - - private Vec3 obtainEntityLerpedPos(Entity e, float f) { - try { - return e.getPosition(f); - } catch (NoSuchMethodError ex) { - if (e instanceof IEntity access) { - Vec3 last = access.get$InstantRenderPos(); - return last.lerp(e.position(), f); - } - return e.position(); - } - } - - private void renderCrystals(Renderer3D renderer, LivingEntity target, float tickDelta) { - if (renderer == null || target == null) { - return; - } - if (crystalList.isEmpty() || target != lastCrystalTarget) { - createCrystals(target); - lastCrystalTarget = target; - } - if (crystalList.isEmpty()) { - return; - } - - crystalRotationAngle = (crystalRotationAngle + 0.5f) % 360.0f; - float orbitYawRad = (float) Math.toRadians(crystalRotationAngle); - float red = crystalHitRed.get() - ? Mth.clamp((target.hurtTime - tickDelta) / 20.0f, 0.0f, 1.0f) - : 0.0f; - - Renderer3D.DepthMode depthMode = Renderer3D.DepthMode.PRE_DEPTH; - MeshBuilder additiveMesh = renderer.batch( - LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, - depthMode - ); - MeshBuilder fillMesh = renderer.batch( - LoVisualRenderPipelines.WORLD_COLORED_DEPTH, - depthMode - ); - MeshBuilder lineMesh = renderer.batch( - LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, - depthMode - ); - MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() - ? null - : renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_DEPTH, - CRYSTAL_BLOOM, - depthMode - ); - if (additiveMesh == null || fillMesh == null || lineMesh == null) { - return; - } - - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = 1.0f; - try { - Vec3 targetPos = obtainEntityLerpedPos(target, tickDelta); - for (int i = 0; i < crystalList.size(); i++) { - CrystalInstance crystal = crystalList.get(i); - int baseColor = getCrystalColor(i, red); - crystal.append(additiveMesh, fillMesh, lineMesh, bloomMesh, targetPos, orbitYawRad, baseColor); - } - } finally { - RenderState.lineWidth = prevWidth; - } - } - - private int getCrystalColor(int index, float redFactor) { - int baseArgb = getColorArgb(index); - int out; - if (redFactor <= 0.0f) { - out = baseArgb; - } else { - int redArgb = (baseArgb & 0xFF000000) | 0x00FF0000; - out = AnimatedRenderColors.mixArgb(baseArgb, redArgb, redFactor); - } - return AnimatedRenderColors.scaleAlpha(out, targetAlpha); - } - - private void createCrystals(Entity target) { - crystalList.clear(); - if (target == null) { - return; - } - crystalList.add(new CrystalInstance(new Vec3(0, 0.85, 0.8), new Vec3(-49, 0, 40))); - crystalList.add(new CrystalInstance(new Vec3(0.2, 0.85, -0.675), new Vec3(35, 0, -30))); - crystalList.add(new CrystalInstance(new Vec3(0.6, 1.35, 0.6), new Vec3(-30, 0, 35))); - crystalList.add(new CrystalInstance(new Vec3(-0.74, 1.05, 0.4), new Vec3(-25, 0, -30))); - crystalList.add(new CrystalInstance(new Vec3(0.74, 0.95, -0.4), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(-0.475, 0.85, -0.375), new Vec3(30, 0, -25))); - crystalList.add(new CrystalInstance(new Vec3(0, 1.35, -0.6), new Vec3(45, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(0.85, 0.7, 0.1), new Vec3(-30, 0, 30))); - crystalList.add(new CrystalInstance(new Vec3(-0.7, 1.35, -0.3), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(-0.3, 1.35, 0.55), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(-0.5, 0.7, 0.7), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(0.5, 0.7, 0.7), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(-0.7, 0.75, 0), new Vec3(0, 0, 0))); - crystalList.add(new CrystalInstance(new Vec3(-0.2, 0.65, -0.7), new Vec3(0, 0, 0))); + return Renderer3DCullOptions.SMART_TARGET; } private boolean isGhostMode() { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/impl/TargetEspCrystalRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/impl/TargetEspCrystalRenderer.java new file mode 100644 index 0000000..a56d72a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/impl/TargetEspCrystalRenderer.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals.CrystalInstance; +import dev.loki.lovisual.mixininterface.entity.IEntity; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.IntFunction; + +/** + * Crystal-orbit rendering subsystem for {@link TargetESP}: owns the per-instance crystal list, + * rotation angle and last-target tracking, and bakes the additive/fill/line/bloom meshes for the + * "crystals" mode. Colors are supplied by the caller so the palette stays in the module. + */ +final class TargetEspCrystalRenderer { + private static final net.minecraft.resources.Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM; + + private final List crystalList = new ArrayList<>(); + private Entity lastCrystalTarget; + private float crystalRotationAngle; + + void clear() { + crystalList.clear(); + lastCrystalTarget = null; + } + + void render(Renderer3D renderer, LivingEntity target, float tickDelta, + boolean hitRed, float targetAlpha, IntFunction argbColorAt) { + if (renderer == null || target == null) { + return; + } + if (crystalList.isEmpty() || target != lastCrystalTarget) { + createCrystals(target); + lastCrystalTarget = target; + } + if (crystalList.isEmpty()) { + return; + } + + crystalRotationAngle = (crystalRotationAngle + 0.5f) % 360.0f; + float orbitYawRad = (float) Math.toRadians(crystalRotationAngle); + float red = hitRed + ? Mth.clamp((target.hurtTime - tickDelta) / 20.0f, 0.0f, 1.0f) + : 0.0f; + + Renderer3D.DepthMode depthMode = Renderer3D.DepthMode.PRE_DEPTH; + MeshBuilder additiveMesh = renderer.batch( + LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, + depthMode + ); + MeshBuilder fillMesh = renderer.batch( + LoVisualRenderPipelines.WORLD_COLORED_DEPTH, + depthMode + ); + MeshBuilder lineMesh = renderer.batch( + LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, + depthMode + ); + MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() + ? null + : renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_DEPTH, + CRYSTAL_BLOOM, + depthMode + ); + if (additiveMesh == null || fillMesh == null || lineMesh == null) { + return; + } + + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = 1.0f; + try { + Vec3 targetPos = obtainEntityLerpedPos(target, tickDelta); + for (int i = 0; i < crystalList.size(); i++) { + CrystalInstance crystal = crystalList.get(i); + int baseColor = crystalColor(argbColorAt, i, red, targetAlpha); + crystal.append(additiveMesh, fillMesh, lineMesh, bloomMesh, targetPos, orbitYawRad, baseColor); + } + } finally { + RenderState.lineWidth = prevWidth; + } + } + + private int crystalColor(IntFunction argbColorAt, int index, float redFactor, float targetAlpha) { + int baseArgb = argbColorAt.apply(index); + int out; + if (redFactor <= 0.0f) { + out = baseArgb; + } else { + int redArgb = (baseArgb & 0xFF000000) | 0x00FF0000; + out = AnimatedRenderColors.mixArgb(baseArgb, redArgb, redFactor); + } + return AnimatedRenderColors.scaleAlpha(out, targetAlpha); + } + + private void createCrystals(Entity target) { + crystalList.clear(); + if (target == null) { + return; + } + crystalList.add(new CrystalInstance(new Vec3(0, 0.85, 0.8), new Vec3(-49, 0, 40))); + crystalList.add(new CrystalInstance(new Vec3(0.2, 0.85, -0.675), new Vec3(35, 0, -30))); + crystalList.add(new CrystalInstance(new Vec3(0.6, 1.35, 0.6), new Vec3(-30, 0, 35))); + crystalList.add(new CrystalInstance(new Vec3(-0.74, 1.05, 0.4), new Vec3(-25, 0, -30))); + crystalList.add(new CrystalInstance(new Vec3(0.74, 0.95, -0.4), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(-0.475, 0.85, -0.375), new Vec3(30, 0, -25))); + crystalList.add(new CrystalInstance(new Vec3(0, 1.35, -0.6), new Vec3(45, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(0.85, 0.7, 0.1), new Vec3(-30, 0, 30))); + crystalList.add(new CrystalInstance(new Vec3(-0.7, 1.35, -0.3), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(-0.3, 1.35, 0.55), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(-0.5, 0.7, 0.7), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(0.5, 0.7, 0.7), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(-0.7, 0.75, 0), new Vec3(0, 0, 0))); + crystalList.add(new CrystalInstance(new Vec3(-0.2, 0.65, -0.7), new Vec3(0, 0, 0))); + } + + private Vec3 obtainEntityLerpedPos(Entity e, float f) { + try { + return e.getPosition(f); + } catch (NoSuchMethodError ex) { + if (e instanceof IEntity access) { + Vec3 last = access.get$InstantRenderPos(); + return last.lerp(e.position(), f); + } + return e.position(); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java index 99622b3..3e26cbb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java @@ -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.module.modules.combat.predictions; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java index 89322d6..edc363a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java @@ -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.module.modules.combat.predictions; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java index cb988ff..8b71047 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java @@ -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.module.modules.combat.predictions; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java index 4f92991..2ec258d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java @@ -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.module.modules.combat.predictions; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java index 8b6d9eb..d07648e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java @@ -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.module.modules.combat.pvp; import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; @@ -13,17 +6,13 @@ import dev.loki.lovisual.config.values.primitive.BooleanMapValue; import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.relations.PlayerRelations; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; import net.minecraft.world.item.Item; -import net.minecraft.world.item.Items; import dev.loki.lovisual.config.definition.SettingDef; import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.util.pvp.cooldown.ItemCooldownSnapshot; import dev.loki.lovisual.util.pvp.parser.PvpChatParser; import dev.loki.lovisual.util.pvp.parser.PvpOverlayParser; import dev.loki.lovisual.util.pvp.state.PvpState; @@ -43,8 +32,6 @@ import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.CooldownRulesSe ) public class PvpCooldowns extends Module { - private static final long LOCAL_RULE_GRACE_MS = 10_000L; - private final BooleanValue renderSlots = bool("pvpcd_render_slots", "render_slots", true); private final BooleanValue renderWidget = bool("pvpcd_widget", "show_in_hud", true); private final NumberValue targetGlowTtlMs = @@ -60,7 +47,7 @@ public class PvpCooldowns extends Module { private final BooleanMapValue stateSources = group( "pvpcd_state_sources", "state_sources", - defaultStateSources() + PvpCooldownRules.defaultStateSources() ); private final SetValue chatGivePatterns = visibleWhen(textList( @@ -130,54 +117,16 @@ public class PvpCooldowns extends Module { */ private final ItemCooldownRulesValue itemRules = new ItemCooldownRulesValue( "pvpcd_item_rules", - defaultItemRules() + PvpCooldownRules.defaultItemRules() ); + private final PvpCooldownRules localRules = + new PvpCooldownRules(this::isEnabled, localRulesEnabled, itemRules); + { setting(SettingDef.cooldownRules("cooldown_rules", itemRules).visibleWhen(localRulesEnabled::get)); } - private static Map defaultStateSources() { - Map defaults = new LinkedHashMap<>(); - defaults.put("chat", true); - defaults.put("game_message", true); - defaults.put("overlay", true); - defaults.put("tab", true); - return defaults; - } - - private static Map defaultItemRules() { - Map rules = new LinkedHashMap<>(); - rules.put("minecraft:golden_apple", new ItemCooldownRulesValue.Rule( - false, - 30, - 2, - 10, - ItemCooldownRulesValue.Scope.PVP_GRACE, - ItemCooldownRulesValue.Trigger.CONSUME_FINISH, - false - )); - rules.put("minecraft:enchanted_golden_apple", new ItemCooldownRulesValue.Rule( - false, - 60, - 2, - 10, - ItemCooldownRulesValue.Scope.PVP_GRACE, - ItemCooldownRulesValue.Trigger.CONSUME_FINISH, - false - )); - rules.put("minecraft:totem_of_undying", new ItemCooldownRulesValue.Rule( - false, - 0, - 1, - 0, - ItemCooldownRulesValue.Scope.PVP_GRACE, - ItemCooldownRulesValue.Trigger.TOTEM_POP, - false - )); - return rules; - } - @Override public void onTick() { if (!isEnabled()) { @@ -254,29 +203,7 @@ public class PvpCooldowns extends Module { } public boolean shouldBlockItemUse(Item item) { - if (!isEnabled()) return false; - if (item == null || item == Items.AIR) return false; - - Identifier id = BuiltInRegistries.ITEM.getKey(item); - if (id == null) return false; - - if (!localRulesEnabled.get()) return false; - - ItemCooldownRulesValue.Rule rule = itemRules.getRule(id.toString()); - if (rule == null || !rule.enabled() || !scopeAllows(rule.scope())) return false; - - ItemCooldownRulesValue.UseBlockMode blockMode = rule.blockMode() != null - ? rule.blockMode() - : ItemCooldownRulesValue.UseBlockMode.NONE; - if (blockMode == ItemCooldownRulesValue.UseBlockMode.NONE) return false; - - ItemCooldownSnapshot snapshot = CooldownsState.MANAGER.snapshot(item); - return switch (blockMode) { - case NONE -> false; - case COOLDOWN -> snapshot.cooling(); - case WINDOW -> snapshot.useWindowActive(); - case ANY -> snapshot.visible(); - }; + return localRules.shouldBlockItemUse(item); } public boolean shouldReadChatMessages() { @@ -328,72 +255,27 @@ public class PvpCooldowns extends Module { } public int getLocalCooldownSeconds(Item item) { - ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); - return rule != null ? rule.seconds() : 0; + return localRules.cooldownSeconds(item); } public int getLocalRuleUses(Item item) { - ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); - return rule != null ? rule.uses() : 1; + return localRules.ruleUses(item); } public int getLocalRuleWindowSeconds(Item item) { - ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); - return rule != null ? rule.windowSeconds() : 0; + return localRules.windowSeconds(item); } public ItemCooldownRulesValue.Rule getLocalRule(Item item) { - return configuredRuleFor(item); + return localRules.rule(item); } public Set getLocalCooldownItems() { - Set out = new LinkedHashSet<>(); - if (!isEnabled() || !localRulesEnabled.get()) return out; - - for (var entry : itemRules.getRules().entrySet()) { - ItemCooldownRulesValue.Rule rule = entry.getValue(); - if (rule == null || !rule.enabled()) continue; - if (rule.seconds() <= 0 && rule.uses() <= 1) continue; - try { - Item item = BuiltInRegistries.ITEM.getValue(Identifier.parse(entry.getKey())); - if (item != null && item != Items.AIR) { - out.add(item); - } - } catch (RuntimeException ignored) { - // Invalid custom item id in config: ignore this rule instead of breaking the module. - } - } - - return out; + return localRules.cooldownItems(); } public boolean tryStartLocalCooldown(Item item, ItemCooldownRulesValue.Trigger trigger) { - ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); - if (rule == null || rule.trigger() != trigger) return false; - if (!scopeAllows(rule.scope())) return false; - CooldownsState.MANAGER.recordRuleUse(item, rule); - return true; - } - - private ItemCooldownRulesValue.Rule configuredRuleFor(Item item) { - if (!isEnabled() || !localRulesEnabled.get()) return null; - if (item == null || item == Items.AIR) return null; - - Identifier id = BuiltInRegistries.ITEM.getKey(item); - if (id == null) return null; - - ItemCooldownRulesValue.Rule rule = itemRules.getRule(id.toString()); - if (rule == null || !rule.enabled()) return null; - if (rule.seconds() <= 0 && rule.uses() <= 1) return null; - return rule; - } - - private boolean scopeAllows(ItemCooldownRulesValue.Scope scope) { - return switch (scope != null ? scope : ItemCooldownRulesValue.Scope.PVP_GRACE) { - case ALWAYS -> true; - case PVP_ONLY -> CooldownsState.MANAGER.isInPvp(); - case PVP_GRACE -> CooldownsState.MANAGER.isPvpGraceActive(LOCAL_RULE_GRACE_MS); - }; + return localRules.tryStart(item, trigger); } private void syncDynamicEnemySettings() { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java index 28dc3b0..00988e4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java @@ -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.module.modules.combat.pvp; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java index 0df2c13..0706c62 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java @@ -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.module.modules.combat.pvp; import net.minecraft.core.component.DataComponents; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java index 83e452b..d916617 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java @@ -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.module.modules.combat.pvp; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/impl/PvpCooldownRules.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/impl/PvpCooldownRules.java new file mode 100644 index 0000000..90305a3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/impl/PvpCooldownRules.java @@ -0,0 +1,172 @@ +package dev.loki.lovisual.features.module.modules.combat.pvp; + +import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.util.pvp.client.CooldownsState; +import dev.loki.lovisual.util.pvp.cooldown.ItemCooldownSnapshot; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BooleanSupplier; + +/** + * Local per-item cooldown-rule engine for {@link PvpCooldowns}: default state-source and item-rule + * tables plus the enable/scope-gated evaluation of blocking, snapshot lookup and use recording. + * Reads the module's enablement through a {@link BooleanSupplier} and the live configuration values + * so behaviour stays identical to the inlined implementation. + */ +final class PvpCooldownRules { + private static final long LOCAL_RULE_GRACE_MS = 10_000L; + + private final BooleanSupplier moduleEnabled; + private final BooleanValue localRulesEnabled; + private final ItemCooldownRulesValue itemRules; + + PvpCooldownRules(BooleanSupplier moduleEnabled, BooleanValue localRulesEnabled, ItemCooldownRulesValue itemRules) { + this.moduleEnabled = moduleEnabled; + this.localRulesEnabled = localRulesEnabled; + this.itemRules = itemRules; + } + + static Map defaultStateSources() { + Map defaults = new LinkedHashMap<>(); + defaults.put("chat", true); + defaults.put("game_message", true); + defaults.put("overlay", true); + defaults.put("tab", true); + return defaults; + } + + static Map defaultItemRules() { + Map rules = new LinkedHashMap<>(); + rules.put("minecraft:golden_apple", new ItemCooldownRulesValue.Rule( + false, + 30, + 2, + 10, + ItemCooldownRulesValue.Scope.PVP_GRACE, + ItemCooldownRulesValue.Trigger.CONSUME_FINISH, + false + )); + rules.put("minecraft:enchanted_golden_apple", new ItemCooldownRulesValue.Rule( + false, + 60, + 2, + 10, + ItemCooldownRulesValue.Scope.PVP_GRACE, + ItemCooldownRulesValue.Trigger.CONSUME_FINISH, + false + )); + rules.put("minecraft:totem_of_undying", new ItemCooldownRulesValue.Rule( + false, + 0, + 1, + 0, + ItemCooldownRulesValue.Scope.PVP_GRACE, + ItemCooldownRulesValue.Trigger.TOTEM_POP, + false + )); + return rules; + } + + boolean shouldBlockItemUse(Item item) { + if (!moduleEnabled.getAsBoolean()) return false; + if (item == null || item == Items.AIR) return false; + + Identifier id = BuiltInRegistries.ITEM.getKey(item); + if (id == null) return false; + + if (!localRulesEnabled.get()) return false; + + ItemCooldownRulesValue.Rule rule = itemRules.getRule(id.toString()); + if (rule == null || !rule.enabled() || !scopeAllows(rule.scope())) return false; + + ItemCooldownRulesValue.UseBlockMode blockMode = rule.blockMode() != null + ? rule.blockMode() + : ItemCooldownRulesValue.UseBlockMode.NONE; + if (blockMode == ItemCooldownRulesValue.UseBlockMode.NONE) return false; + + ItemCooldownSnapshot snapshot = CooldownsState.MANAGER.snapshot(item); + return switch (blockMode) { + case NONE -> false; + case COOLDOWN -> snapshot.cooling(); + case WINDOW -> snapshot.useWindowActive(); + case ANY -> snapshot.visible(); + }; + } + + int cooldownSeconds(Item item) { + ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); + return rule != null ? rule.seconds() : 0; + } + + int ruleUses(Item item) { + ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); + return rule != null ? rule.uses() : 1; + } + + int windowSeconds(Item item) { + ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); + return rule != null ? rule.windowSeconds() : 0; + } + + ItemCooldownRulesValue.Rule rule(Item item) { + return configuredRuleFor(item); + } + + Set cooldownItems() { + Set out = new LinkedHashSet<>(); + if (!moduleEnabled.getAsBoolean() || !localRulesEnabled.get()) return out; + + for (var entry : itemRules.getRules().entrySet()) { + ItemCooldownRulesValue.Rule rule = entry.getValue(); + if (rule == null || !rule.enabled()) continue; + if (rule.seconds() <= 0 && rule.uses() <= 1) continue; + try { + Item item = BuiltInRegistries.ITEM.getValue(Identifier.parse(entry.getKey())); + if (item != null && item != Items.AIR) { + out.add(item); + } + } catch (RuntimeException ignored) { + // Invalid custom item id in config: ignore this rule instead of breaking the module. + } + } + + return out; + } + + boolean tryStart(Item item, ItemCooldownRulesValue.Trigger trigger) { + ItemCooldownRulesValue.Rule rule = configuredRuleFor(item); + if (rule == null || rule.trigger() != trigger) return false; + if (!scopeAllows(rule.scope())) return false; + CooldownsState.MANAGER.recordRuleUse(item, rule); + return true; + } + + private ItemCooldownRulesValue.Rule configuredRuleFor(Item item) { + if (!moduleEnabled.getAsBoolean() || !localRulesEnabled.get()) return null; + if (item == null || item == Items.AIR) return null; + + Identifier id = BuiltInRegistries.ITEM.getKey(item); + if (id == null) return null; + + ItemCooldownRulesValue.Rule rule = itemRules.getRule(id.toString()); + if (rule == null || !rule.enabled()) return null; + if (rule.seconds() <= 0 && rule.uses() <= 1) return null; + return rule; + } + + private boolean scopeAllows(ItemCooldownRulesValue.Scope scope) { + return switch (scope != null ? scope : ItemCooldownRulesValue.Scope.PVP_GRACE) { + case ALWAYS -> true; + case PVP_ONLY -> CooldownsState.MANAGER.isInPvp(); + case PVP_GRACE -> CooldownsState.MANAGER.isPvpGraceActive(LOCAL_RULE_GRACE_MS); + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java index 618f841..12ae766 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java @@ -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.module.modules.combat.targetesp; import dev.loki.lovisual.render.engine.RenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java index 603614e..2babeb9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java @@ -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.module.modules.combat.targetesp; import dev.loki.lovisual.render.engine.texture.TextureStorage; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java index 41a1bf4..a0260c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java @@ -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.module.modules.combat.targetesp; import dev.loki.lovisual.render.engine.RenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java index 5db1cf1..4bd2f16 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java @@ -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.module.modules.combat.targetesp; import dev.loki.lovisual.render.engine.RenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java index 3a60e04..966f760 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java @@ -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.module.modules.misc; import dev.loki.lovisual.config.values.mode.ModeValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java index 2612165..0171427 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java @@ -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.module.modules.misc; import dev.loki.lovisual.config.i18n.DisableSettingI18n; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java index 50261d7..1d7bed5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java @@ -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.module.modules.misc; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java index 090c744..ce9e04f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java @@ -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.module.modules.misc; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java index dd92542..5e91e37 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java @@ -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.module.modules.misc.esp; import net.minecraft.core.registries.BuiltInRegistries; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java index 66216b6..cf6d3ed 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java @@ -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.module.modules.misc.items; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java index c0ffcf9..7aeba87 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java @@ -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.module.modules.misc.items; import net.minecraft.world.entity.player.Inventory; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java index 5d3ca04..f789afc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java @@ -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.module.modules.misc.protect; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java index 5c1c729..1dec259 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java @@ -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.module.modules.misc.protect; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java index 9ed6f41..79d6766 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java @@ -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.module.modules.misc.protect; import dev.loki.lovisual.config.values.mode.ModeValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java index d304777..0cb0f35 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java @@ -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.module.modules.misc.protect; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java index 2850d94..24bd033 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java @@ -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.module.modules.misc.sound; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java index b1be141..3b2d698 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java @@ -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.module.modules.misc.sound; import dev.loki.lovisual.config.values.mode.ModeValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java index 61b4080..e7e76ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java @@ -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.module.modules.misc.vanilla; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java index 8bc7a2c..764ef85 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java @@ -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.module.modules.player; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java index 25ece98..6c78b68 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java @@ -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.module.modules.player; import dev.loki.lovisual.config.common.CommonSettingSchemas; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java index 758488f..ca38d09 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java @@ -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.module.modules.player; import dev.loki.lovisual.config.values.mode.BindMode; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java index e69a302..9005819 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java @@ -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.module.modules.player; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java index 64032b3..9bba90f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java @@ -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.module.modules.player.auto; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java index 533c738..f43b06e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java @@ -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.module.modules.player.auto; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java index 49e3fab..4d5567c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java @@ -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.module.modules.player.auto; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java index 4f20abe..7417ef9 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java @@ -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.module.modules.player.interact; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java index 2a7d35a..c64a437 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java @@ -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.module.modules.player.interact; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java index add0131..b276281 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java @@ -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.module.modules.player.interact; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java index 227d58f..0449269 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java @@ -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.module.modules.player.interact; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java index 1732a09..59373c7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java @@ -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.module.modules.player.inventory; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java index e648d63..5c37820 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java @@ -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.module.modules.player.inventory; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java index 02e1e57..11d35fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java @@ -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.module.modules.visuals; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java index a7d639c..4d57de7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java @@ -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.module.modules.visuals.camera; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java index a26019a..da1e77f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java @@ -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.module.modules.visuals.camera; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java index 19c6b4a..27069f0 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java @@ -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.module.modules.visuals.camera; import net.minecraft.client.CameraType; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java index 8f916ec..cd1297c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java @@ -1,14 +1,5 @@ -/* - * 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.module.modules.visuals.camera; -import dev.loki.lovisual.util.time.GameClock; - import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Axis; import net.minecraft.client.Minecraft; @@ -24,6 +15,7 @@ import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSettin import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.camera.swing.SwingAnimator; /** * ViewModel: mini items + custom swing animations @@ -164,7 +156,6 @@ public class ViewModel extends Module { float sin1 = Mth.sin(animProgress * animProgress * (float) Math.PI); float sin2 = Mth.sin(Mth.sqrt(animProgress) * (float) Math.PI); float power = animStrength.get(); - float scale = 1.0f; // Apply user offsets ms.translate(swingX.get(), swingY.get(), swingZ.get()); @@ -175,314 +166,7 @@ public class ViewModel extends Module { if (currentSpin > 360.0f) currentSpin -= 360.0f; } - switch (animMode.get()) { - case "SmoothVanilla": { - ms.scale(scale, scale, scale); - ms.translate(0.0, -0.2f * anim, 0.0); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); - break; - } - - case "SwipeBackDown": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.1, -0.1); - ms.mulPose(Axis.YP.rotationDegrees(60.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); - ms.translate(0.0, -0.3f * anim, 0.0); - break; - } - - case "Swipe Back": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.1, -0.1); - ms.mulPose(Axis.YP.rotationDegrees(60.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); - ms.mulPose(Axis.YP.rotationDegrees(sin2 * sin1 * -5.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-10.0f - power * 10.0f * anim)); - ms.mulPose(Axis.XP.rotationDegrees(-60.0f)); - break; - } - - case "New": { - ms.scale(scale * (1.0f + 0.1f * sin2), scale, scale); - ms.translate(0.2, 0.1, -0.2); - ms.mulPose(Axis.XP.rotationDegrees(60.0f)); - float swingAngle = -120.0f - power * 10.0f * anim; - ms.mulPose(Axis.XP.rotationDegrees(swingAngle)); - ms.mulPose(Axis.YP.rotationDegrees(30.0f * sin2)); - ms.mulPose(Axis.ZP.rotationDegrees(15.0f * sin2)); - if (progress > 0.8f) { - ms.mulPose(Axis.XP.rotationDegrees(10.0f * (progress - 0.8f) * 5.0f)); - } - break; - } - - case "Block Down": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.1, -0.1); - ms.translate(0.5, -0.1, 0.0); - ms.mulPose(Axis.XP.rotationDegrees(sin2 * -9.0f)); - ms.translate(-0.5, 0.1, 0.0); - ms.translate(0.5, -0.1, 0.0); - ms.mulPose(Axis.YP.rotationDegrees(sin2 * -18.0f)); - ms.translate(-0.5, 0.1, 0.0); - ms.mulPose(Axis.YP.rotationDegrees(50.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - ms.mulPose(Axis.YP.rotationDegrees(50.0f)); - break; - } - - case "Back": { - ms.scale(scale, scale, scale); - ms.translate(0.4, 0.1, -0.5); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f - power * 10.0f * anim)); - break; - } - - case "7": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.YP.rotationDegrees(15.0f * anim)); - ms.mulPose(Axis.ZP.rotationDegrees(-50.0f * anim)); - ms.mulPose(Axis.XP.rotationDegrees((-10.0f - power) * anim)); - break; - } - - case "Block": { - ms.scale(scale, scale, scale); - ms.translate(0.4, 0.0, -0.5); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(-30.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f - power * 10.0f * anim)); - break; - } - - case "Forward": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(50.0f)); - ms.mulPose(Axis.YP.rotationDegrees(-30.0f)); - ms.translate(0.2, 0.5, 1.3); - ms.translate(0.9f * anim, 0.2f + 0.35f * anim, 0.3f * anim); - ms.mulPose(Axis.YP.rotationDegrees(anim * 50.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(anim * -20.0f)); - break; - } - - case "Glide": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(50.0f)); - ms.mulPose(Axis.YP.rotationDegrees(-30.0f)); - ms.translate(0.2, 0.5, 1.3); - ms.translate(0.0, 0.7f * anim + 0.2, -0.7f * anim); - ms.mulPose(Axis.XP.rotationDegrees(anim * -70.0f)); - break; - } - - case "Tap": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-60.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(60.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-0.9f)); - ms.mulPose(Axis.YP.rotationDegrees(60.0f)); - ms.translate(0.6, 0.0, 1.0); - ms.translate(0.4f * anim, 0.0, 0.0); - break; - } - - case "Blocking": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); - ms.mulPose(Axis.YP.rotationDegrees(40.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(-20.0f)); - ms.translate(0.0, 0.3, 0.9); - ms.translate(0.0, 0.6f * anim, -0.6f * anim); - ms.mulPose(Axis.XP.rotationDegrees(anim * -45.0f)); - break; - } - - case "Touch": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.translate(-0.3, 0.0, 1.3); - ms.translate(0.0, 0.0, 0.2f * anim); - ms.mulPose(Axis.XP.rotationDegrees(anim * 15.0f)); - break; - } - - case "Slant": { - ms.scale(scale, scale, scale); - float rotate = 35.0f; - ms.translate(0.0, 0.0, -0.3f * anim); - ms.mulPose(Axis.XP.rotationDegrees(anim * -rotate)); - ms.mulPose(Axis.ZP.rotationDegrees(anim * rotate)); - break; - } - - case "Spin": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.1, -0.2); - ms.mulPose(Axis.YP.rotationDegrees(anim * 360.0f * power)); - ms.mulPose(Axis.XP.rotationDegrees(45.0f)); - break; - } - - case "Helicopter": { - ms.scale(scale, scale, scale); - ms.translate(0.0, -0.2, 0.0); - float rotation = (GameClock.millis() % 2000L) / 2000.0f * 360.0f * power; - ms.mulPose(Axis.ZP.rotationDegrees(rotation)); - ms.translate(0.0, 0.3, 0.0); - break; - } - - case "Stab": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.0, -0.5f * anim * power); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); - break; - } - - case "Circle": { - ms.scale(scale, scale, scale); - float circleProgress = progress * 2.0f * (float) Math.PI; - ms.translate(Math.sin(circleProgress) * 0.5f * power, Math.cos(circleProgress) * 0.3 * power, 0.0); - ms.mulPose(Axis.ZP.rotationDegrees(anim * 180.0f)); - break; - } - - case "Wave": { - ms.scale(scale, scale, scale); - ms.translate(0.0, Math.sin(progress * Math.PI * 2.0f) * 0.3 * power, 0.0); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f + anim * 45.0f * power)); - break; - } - - case "360 Swing": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.1, -0.1); - ms.mulPose(Axis.XP.rotationDegrees(anim * 360.0f * power)); - break; - } - - case "Vanilla": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); - break; - } - - case "Flip": { - ms.scale(scale, scale, scale); - float angle = anim * 180.0f * power; - ms.mulPose(Axis.ZP.rotationDegrees(angle)); - ms.translate(0.0, 0.0, -0.3f * anim); - break; - } - - case "Side": { - ms.scale(scale, scale, scale); - ms.translate(0.4f * anim, 0.0, -0.2f * anim); - ms.mulPose(Axis.YP.rotationDegrees(-60.0f * anim)); - break; - } - - case "Overhead": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.5, 0.0); - ms.mulPose(Axis.XP.rotationDegrees(-150.0f * anim)); - break; - } - - case "Hammer": { - ms.scale(scale, scale, scale); - ms.translate(0.3, 0.3, -0.3); - ms.mulPose(Axis.ZP.rotationDegrees(30.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-120.0f * anim)); - break; - } - - case "Chop": { - ms.scale(scale, scale, scale); - ms.translate(0.0, -0.2f * anim, -0.3f * anim); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); - break; - } - - case "Arc": { - ms.scale(scale, scale, scale); - float a = anim * 90.0f; - ms.mulPose(Axis.YP.rotationDegrees(a)); - ms.mulPose(Axis.XP.rotationDegrees(-a)); - break; - } - - case "Double": { - ms.scale(scale, scale, scale); - float d = anim * 720.0f; - ms.mulPose(Axis.XP.rotationDegrees(d)); - break; - } - - case "Shake": { - ms.scale(scale, scale, scale); - float s = (float) Math.sin((GameClock.millis() % 200L) / 200.0f * Math.PI * 2.0f) * 5.0f; - ms.mulPose(Axis.ZP.rotationDegrees(s)); - break; - } - - case "Shove": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.0, -0.6f * anim); - break; - } - - case "Slash": { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.YP.rotationDegrees(90.0f)); - ms.mulPose(Axis.XP.rotationDegrees(-120.0f * anim)); - break; - } - - case "Thrust": { - ms.scale(scale, scale, scale); - ms.translate(0.0, 0.0, -1.0f * anim); - break; - } - - case "Smooth Down": { - ms.scale(scale, scale, scale); - ms.translate(0.0, -0.1f * anim, 0.0); - float down = Mth.sin(progress * (float) Math.PI) * -65.0f * power; - ms.mulPose(Axis.XP.rotationDegrees(down)); - break; - } - - case "Smooth": { - ms.scale(scale, scale, scale); - float s = Mth.sqrt(progress); - float back = Mth.sin(s * (float) Math.PI) * 25.0f * power; - float down = Mth.sin(progress * (float) Math.PI) * -55.0f * power; - ms.mulPose(Axis.XP.rotationDegrees(back + down)); - break; - } - - default: { - ms.scale(scale, scale, scale); - ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim * power * 0.35f)); - } - } + SwingAnimator.apply(animMode.get(), ms, new SwingAnimator.Params(progress, anim, sin1, sin2, power)); if (applySpin) { switch (spinAxis.get()) { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingAnimator.java new file mode 100644 index 0000000..c3c3d96 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingAnimator.java @@ -0,0 +1,46 @@ +package dev.loki.lovisual.features.module.modules.visuals.camera.swing; + +import java.util.HashMap; +import java.util.Map; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +/** + * Baked per-mode hand swing animation transforms for {@code ViewModel}. + * Pure geometry: given the animation progress scalars and the user power it applies the + * translate / rotate / scale matrix for the selected swing mode. The concrete per-mode + * transforms live in {@link SwingStylesA} and {@link SwingStylesB}; this class only dispatches + * and provides the fallback used for unknown modes. Extracted so the module stays under the cap. + */ +public final class SwingAnimator { + + private SwingAnimator() { } + + /** A single swing animation mode transform. */ + @FunctionalInterface + public interface Style { + void apply(PoseStack ms, Params p); + } + + /** Pre-computed animation scalars shared by every swing mode. */ + public record Params(float progress, float anim, float sin1, float sin2, float power) { } + + private static final Map REGISTRY = new HashMap<>(); + + static { + SwingStylesA.register(REGISTRY); + SwingStylesB.register(REGISTRY); + SwingStylesC.register(REGISTRY); + } + + /** Apply the transform stack for the given swing {@code mode} onto {@code ms}. */ + public static void apply(String mode, PoseStack ms, Params p) { + REGISTRY.getOrDefault(mode, SwingAnimator::defaultStyle).apply(ms, p); + } + + private static void defaultStyle(PoseStack ms, Params p) { + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f * p.anim() * p.power() * 0.35f)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesA.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesA.java new file mode 100644 index 0000000..05183e1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesA.java @@ -0,0 +1,174 @@ +package dev.loki.lovisual.features.module.modules.visuals.camera.swing; + +import java.util.Map; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +/** + * Swing animation modes, group A: vanilla-like, block and directional styles. + * Each mode is a pure transform over the pre-computed {@link SwingAnimator.Params}; registered + * into the {@link SwingAnimator} dispatcher. See {@link SwingStylesB} / {@link SwingStylesC}. + */ +final class SwingStylesA { + + private SwingStylesA() { } + + static void register(Map m) { + m.put("SmoothVanilla", SwingStylesA::smoothVanilla); + m.put("SwipeBackDown", SwingStylesA::swipeBackDown); + m.put("Swipe Back", SwingStylesA::swipeBack); + m.put("New", SwingStylesA::neo); + m.put("Block Down", SwingStylesA::blockDown); + m.put("Back", SwingStylesA::back); + m.put("7", SwingStylesA::seven); + m.put("Block", SwingStylesA::block); + m.put("Forward", SwingStylesA::forward); + m.put("Glide", SwingStylesA::glide); + m.put("Tap", SwingStylesA::tap); + m.put("Blocking", SwingStylesA::blocking); + } + + private static void smoothVanilla(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, -0.2f * anim, 0.0); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); + } + + private static void swipeBackDown(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.1, -0.1); + ms.mulPose(Axis.YP.rotationDegrees(60.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); + ms.translate(0.0, -0.3f * anim, 0.0); + } + + private static void swipeBack(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float sin1 = p.sin1(); + float sin2 = p.sin2(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.1, -0.1); + ms.mulPose(Axis.YP.rotationDegrees(60.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); + ms.mulPose(Axis.YP.rotationDegrees(sin2 * sin1 * -5.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-10.0f - power * 10.0f * anim)); + ms.mulPose(Axis.XP.rotationDegrees(-60.0f)); + } + + private static void neo(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float sin2 = p.sin2(); + float power = p.power(); + float progress = p.progress(); + ms.scale(1.0f + 0.1f * sin2, 1.0f, 1.0f); + ms.translate(0.2, 0.1, -0.2); + ms.mulPose(Axis.XP.rotationDegrees(60.0f)); + float swingAngle = -120.0f - power * 10.0f * anim; + ms.mulPose(Axis.XP.rotationDegrees(swingAngle)); + ms.mulPose(Axis.YP.rotationDegrees(30.0f * sin2)); + ms.mulPose(Axis.ZP.rotationDegrees(15.0f * sin2)); + if (progress > 0.8f) { + ms.mulPose(Axis.XP.rotationDegrees(10.0f * (progress - 0.8f) * 5.0f)); + } + } + + private static void blockDown(PoseStack ms, SwingAnimator.Params p) { + float sin2 = p.sin2(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.1, -0.1); + ms.translate(0.5, -0.1, 0.0); + ms.mulPose(Axis.XP.rotationDegrees(sin2 * -9.0f)); + ms.translate(-0.5, 0.1, 0.0); + ms.translate(0.5, -0.1, 0.0); + ms.mulPose(Axis.YP.rotationDegrees(sin2 * -18.0f)); + ms.translate(-0.5, 0.1, 0.0); + ms.mulPose(Axis.YP.rotationDegrees(50.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + ms.mulPose(Axis.YP.rotationDegrees(50.0f)); + } + + private static void back(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.4, 0.1, -0.5); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(-60.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f - power * 10.0f * anim)); + } + + private static void seven(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.YP.rotationDegrees(15.0f * anim)); + ms.mulPose(Axis.ZP.rotationDegrees(-50.0f * anim)); + ms.mulPose(Axis.XP.rotationDegrees((-10.0f - power) * anim)); + } + + private static void block(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.4, 0.0, -0.5); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(-30.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f - power * 10.0f * anim)); + } + + private static void forward(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(50.0f)); + ms.mulPose(Axis.YP.rotationDegrees(-30.0f)); + ms.translate(0.2, 0.5, 1.3); + ms.translate(0.9f * anim, 0.2f + 0.35f * anim, 0.3f * anim); + ms.mulPose(Axis.YP.rotationDegrees(anim * 50.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(anim * -20.0f)); + } + + private static void glide(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(50.0f)); + ms.mulPose(Axis.YP.rotationDegrees(-30.0f)); + ms.translate(0.2, 0.5, 1.3); + ms.translate(0.0, 0.7f * anim + 0.2, -0.7f * anim); + ms.mulPose(Axis.XP.rotationDegrees(anim * -70.0f)); + } + + private static void tap(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-60.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(60.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-0.9f)); + ms.mulPose(Axis.YP.rotationDegrees(60.0f)); + ms.translate(0.6, 0.0, 1.0); + ms.translate(0.4f * anim, 0.0, 0.0); + } + + private static void blocking(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); + ms.mulPose(Axis.YP.rotationDegrees(40.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(-20.0f)); + ms.translate(0.0, 0.3, 0.9); + ms.translate(0.0, 0.6f * anim, -0.6f * anim); + ms.mulPose(Axis.XP.rotationDegrees(anim * -45.0f)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesB.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesB.java new file mode 100644 index 0000000..719d9c4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesB.java @@ -0,0 +1,127 @@ +package dev.loki.lovisual.features.module.modules.visuals.camera.swing; + +import java.util.Map; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import dev.loki.lovisual.util.time.GameClock; + +/** + * Swing animation modes, group B: touch, spin, circular and flip styles. + * Each mode is a pure transform over the pre-computed {@link SwingAnimator.Params}; registered + * into the {@link SwingAnimator} dispatcher. See {@link SwingStylesA} / {@link SwingStylesC}. + */ +final class SwingStylesB { + + private SwingStylesB() { } + + static void register(Map m) { + m.put("Touch", SwingStylesB::touch); + m.put("Slant", SwingStylesB::slant); + m.put("Spin", SwingStylesB::spin); + m.put("Helicopter", SwingStylesB::helicopter); + m.put("Stab", SwingStylesB::stab); + m.put("Circle", SwingStylesB::circle); + m.put("Wave", SwingStylesB::wave); + m.put("360 Swing", SwingStylesB::swing360); + m.put("Vanilla", SwingStylesB::vanilla); + m.put("Flip", SwingStylesB::flip); + m.put("Side", SwingStylesB::side); + } + + private static void touch(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + ms.mulPose(Axis.ZP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-1.35f)); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.translate(-0.3, 0.0, 1.3); + ms.translate(0.0, 0.0, 0.2f * anim); + ms.mulPose(Axis.XP.rotationDegrees(anim * 15.0f)); + } + + private static void slant(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + float rotate = 35.0f; + ms.translate(0.0, 0.0, -0.3f * anim); + ms.mulPose(Axis.XP.rotationDegrees(anim * -rotate)); + ms.mulPose(Axis.ZP.rotationDegrees(anim * rotate)); + } + + private static void spin(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.1, -0.2); + ms.mulPose(Axis.YP.rotationDegrees(anim * 360.0f * power)); + ms.mulPose(Axis.XP.rotationDegrees(45.0f)); + } + + private static void helicopter(PoseStack ms, SwingAnimator.Params p) { + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, -0.2, 0.0); + float rotation = (GameClock.millis() % 2000L) / 2000.0f * 360.0f * power; + ms.mulPose(Axis.ZP.rotationDegrees(rotation)); + ms.translate(0.0, 0.3, 0.0); + } + + private static void stab(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.0, -0.5f * anim * power); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f)); + } + + private static void circle(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + float progress = p.progress(); + ms.scale(1.0f, 1.0f, 1.0f); + float circleProgress = progress * 2.0f * (float) Math.PI; + ms.translate(Math.sin(circleProgress) * 0.5f * power, Math.cos(circleProgress) * 0.3 * power, 0.0); + ms.mulPose(Axis.ZP.rotationDegrees(anim * 180.0f)); + } + + private static void wave(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + float progress = p.progress(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, Math.sin(progress * Math.PI * 2.0f) * 0.3 * power, 0.0); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f + anim * 45.0f * power)); + } + + private static void swing360(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.1, -0.1); + ms.mulPose(Axis.XP.rotationDegrees(anim * 360.0f * power)); + } + + private static void vanilla(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); + } + + private static void flip(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + ms.scale(1.0f, 1.0f, 1.0f); + float angle = anim * 180.0f * power; + ms.mulPose(Axis.ZP.rotationDegrees(angle)); + ms.translate(0.0, 0.0, -0.3f * anim); + } + + private static void side(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.4f * anim, 0.0, -0.2f * anim); + ms.mulPose(Axis.YP.rotationDegrees(-60.0f * anim)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesC.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesC.java new file mode 100644 index 0000000..ca06b9d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/swing/SwingStylesC.java @@ -0,0 +1,114 @@ +package dev.loki.lovisual.features.module.modules.visuals.camera.swing; + +import java.util.Map; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; +import net.minecraft.util.Mth; +import dev.loki.lovisual.util.time.GameClock; + +/** + * Swing animation modes, group C: strike, chop and smooth styles. + * Each mode is a pure transform over the pre-computed {@link SwingAnimator.Params}; registered + * into the {@link SwingAnimator} dispatcher. See {@link SwingStylesA} / {@link SwingStylesB}. + */ +final class SwingStylesC { + + private SwingStylesC() { } + + static void register(Map m) { + m.put("Overhead", SwingStylesC::overhead); + m.put("Hammer", SwingStylesC::hammer); + m.put("Chop", SwingStylesC::chop); + m.put("Arc", SwingStylesC::arc); + m.put("Double", SwingStylesC::dbl); + m.put("Shake", SwingStylesC::shake); + m.put("Shove", SwingStylesC::shove); + m.put("Slash", SwingStylesC::slash); + m.put("Thrust", SwingStylesC::thrust); + m.put("Smooth Down", SwingStylesC::smoothDown); + m.put("Smooth", SwingStylesC::smooth); + } + + private static void overhead(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.5, 0.0); + ms.mulPose(Axis.XP.rotationDegrees(-150.0f * anim)); + } + + private static void hammer(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.3, 0.3, -0.3); + ms.mulPose(Axis.ZP.rotationDegrees(30.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-120.0f * anim)); + } + + private static void chop(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, -0.2f * anim, -0.3f * anim); + ms.mulPose(Axis.XP.rotationDegrees(-90.0f * anim)); + } + + private static void arc(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + float a = anim * 90.0f; + ms.mulPose(Axis.YP.rotationDegrees(a)); + ms.mulPose(Axis.XP.rotationDegrees(-a)); + } + + private static void dbl(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + float d = anim * 720.0f; + ms.mulPose(Axis.XP.rotationDegrees(d)); + } + + private static void shake(PoseStack ms, SwingAnimator.Params p) { + ms.scale(1.0f, 1.0f, 1.0f); + float s = (float) Math.sin((GameClock.millis() % 200L) / 200.0f * Math.PI * 2.0f) * 5.0f; + ms.mulPose(Axis.ZP.rotationDegrees(s)); + } + + private static void shove(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.0, -0.6f * anim); + } + + private static void slash(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.mulPose(Axis.YP.rotationDegrees(90.0f)); + ms.mulPose(Axis.XP.rotationDegrees(-120.0f * anim)); + } + + private static void thrust(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, 0.0, -1.0f * anim); + } + + private static void smoothDown(PoseStack ms, SwingAnimator.Params p) { + float anim = p.anim(); + float power = p.power(); + float progress = p.progress(); + ms.scale(1.0f, 1.0f, 1.0f); + ms.translate(0.0, -0.1f * anim, 0.0); + float down = Mth.sin(progress * (float) Math.PI) * -65.0f * power; + ms.mulPose(Axis.XP.rotationDegrees(down)); + } + + private static void smooth(PoseStack ms, SwingAnimator.Params p) { + float power = p.power(); + float progress = p.progress(); + ms.scale(1.0f, 1.0f, 1.0f); + float s = Mth.sqrt(progress); + float back = Mth.sin(s * (float) Math.PI) * 25.0f * power; + float down = Mth.sin(progress * (float) Math.PI) * -55.0f * power; + ms.mulPose(Axis.XP.rotationDegrees(back + down)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java index d289b2e..27e1709 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java @@ -1,687 +1,130 @@ -/* - * 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.module.modules.visuals.effects.ambient; -import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.config.values.color.RGBAColorValue; import dev.loki.lovisual.config.values.mode.EnumValue; import dev.loki.lovisual.config.values.mode.ModeValue; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.entity.projectile.Projectile; -import net.minecraft.world.entity.projectile.arrow.AbstractArrow; -import net.minecraft.world.phys.Vec3; -import org.joml.Quaternionf; -import org.joml.Vector3f; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.mixininterface.entity.IEntity; -import dev.loki.lovisual.mixins.accessors.projectile.PersistentProjectileEntityAccessor; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.helpers.player.Particle3D; -import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; import dev.loki.lovisual.render.helpers.player.TrailPoint; - -import java.awt.*; -import java.util.*; +import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; -//todo Description @ModuleInfo(id = "trails", displayName = "Trails", category = ModuleCategory.VISUALS) public class Trails extends Module { - private static final String SETTING_ONLY_SELF = "only_self"; - private static final String SETTING_HIDE_FIRST_PERSON = "hide_first_person"; - private static final String SETTING_DEPTH_TEST = "depth_test"; - private static final String SETTING_TRAIL_MODE = "trail_mode"; - private static final String SETTING_LIFE_TIME_TICKS = "life_time_ticks"; - private static final String SETTING_SCALE = "scale"; - private static final String SETTING_TRAIL_DOWN = "trail_down"; - private static final String SETTING_TRAIL_HEIGHT = "trail_height"; - private static final String SETTING_TRAIL_STROKE = "trail_stroke"; - private static final String SETTING_TRAIL_STROKE_WIDTH = "trail_stroke_width"; - private static final String SETTING_TRAIL_STROKE_ALPHA = "trail_stroke_alpha"; - private static final String SETTING_COLOR = "color"; - private static final String SETTING_COLOR_MODE = "color_mode"; - private static final String SETTING_COLOR_SPEED = "color_speed"; - private static final String SETTING_COLOR_PHASE = "color_phase"; - private static final String SETTING_COLOR_LENGTH_SPREAD = "color_length_spread"; - private static final String SETTING_COLOR_AGE_SPREAD = "color_age_spread"; - private static final String SETTING_COLOR_2 = "color2"; - private static final String SETTING_PARTICLES_ENABLED = "particles_enabled"; - private static final String SETTING_PARTICLES_HIDE_FIRST_PERSON = "particles_hide_first_person"; - private static final String SETTING_PARTICLE_TRIGGERS = "particle_triggers"; - private static final String SETTING_PARTICLE_TEXTURE = "particle_texture"; - private static final String SETTING_PARTICLE_RANDOM_COLOR = "particle_random_color"; - private static final String SETTING_PARTICLE_SIZE = "particle_size"; - private static final String SETTING_PARTICLE_LIFE = "particle_life_ms"; - private static final String SETTING_PARTICLE_WALK_COUNT = "particle_walk_count"; - private static final String SETTING_PARTICLE_JUMP_COUNT = "particle_jump_count"; - private static final String SETTING_PARTICLE_PROJECTILE_COUNT = "particle_projectile_count"; - private static final String SETTING_CONNECTED_PATH = "connected_path"; - private static final String SETTING_CONNECT_STEP = "connected_step"; - private static final float PARTICLE_GRAVITY = 0.01f; - private static final float PARTICLE_DRAG = 0.96f; - private static final int PARTICLE_MAX = 600; - private static final int PROJECTILE_SCAN_RADIUS = 48; - private static final int PROJECTILE_MAX_SPAWN = 140; - private static final int CONNECT_MAX_STEPS = 6; private final Random random = new Random(); - private final BooleanValue onlySelf = bool("trailsOnlySelf", SETTING_ONLY_SELF, false); - private final BooleanValue hideFirstPerson = bool("trailsHideFirstPerson", SETTING_HIDE_FIRST_PERSON, true); - private final BooleanValue depthTest = bool("trailsDepthTest", SETTING_DEPTH_TEST, true); - private final ModeValue trailMode = - modeSetting("trailsTrailMode", SETTING_TRAIL_MODE, "Tail", "Tail", "Trail"); - private final RGBAColorValue colorValue = color("trailsColor", SETTING_COLOR, "#8800FF00"); - private final ModeValue colorMode = - modeSetting("trailsColorMode", SETTING_COLOR_MODE, "Static", - "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); - private final RGBAColorValue colorValue2 = - visibleWhen(color("trailsColor2", SETTING_COLOR_2, "#FF55FFFF"), this::usesSecondaryColor); - private final NumberValue colorSpeed = - num("trailsColorSpeed", SETTING_COLOR_SPEED, 18, 2, 54); - private final NumberValue lifeTimeTicks = - num("trailsLifeTimeTicks", SETTING_LIFE_TIME_TICKS, 10, 1, 40); - private final NumberValue trailScale = - visibleWhen(num("trailsScale", SETTING_SCALE, 0.6f, 0.2f, 1.2f), this::isTailMode); - private final NumberValue trailDown = - visibleWhen(num("trailsTrailDown", SETTING_TRAIL_DOWN, 0.5f, 0.0f, 2.0f), this::isTrailDownVisible); - private final NumberValue trailHeight = - visibleWhen(num("trailsTrailHeight", SETTING_TRAIL_HEIGHT, 1.3f, 0.1f, 2.0f), this::isTrailDownVisible); - private final BooleanValue trailStroke = - visibleWhen(bool("trailsTrailStroke", SETTING_TRAIL_STROKE, true), this::isTrailMode); - private final NumberValue trailStrokeWidth = - visibleWhen(num("trailsTrailStrokeWidth", SETTING_TRAIL_STROKE_WIDTH, 2.0f, 0.5f, 8.0f), - () -> isTrailMode() && trailStroke.get()); - private final NumberValue trailStrokeAlpha = - visibleWhen(num("trailsTrailStrokeAlpha", SETTING_TRAIL_STROKE_ALPHA, 0.75f, 0.05f, 1.0f), - () -> isTrailMode() && trailStroke.get()); - private final NumberValue colorPhase = - num("trailsColorPhase", SETTING_COLOR_PHASE, 0, 0, 360); - private final NumberValue colorLengthSpread = - num("trailsColorLengthSpread", SETTING_COLOR_LENGTH_SPREAD, 180, -720, 720); - private final NumberValue colorAgeSpread = - num("trailsColorAgeSpread", SETTING_COLOR_AGE_SPREAD, 90, -720, 720); - private final BooleanValue particlesEnabled = - bool("trailsParticlesEnabled", SETTING_PARTICLES_ENABLED, false); - private final BooleanValue particlesHideFirstPerson = - visibleWhen(bool("trailsParticlesHideFirstPerson", SETTING_PARTICLES_HIDE_FIRST_PERSON, false), particlesEnabled::get); - private final BooleanMapValue particleTriggers = visibleWhen(group("trailsParticleTriggers", SETTING_PARTICLE_TRIGGERS, Map.of( - "walk", true, - "jump", true, - "projectile", true - )), particlesEnabled::get); - private final NumberValue particleWalkCount = - visibleWhen(num("trailsParticleWalkCount", SETTING_PARTICLE_WALK_COUNT, 3, 1, 8), - () -> particlesEnabled.get() && particleTriggers.get("walk")); - private final NumberValue particleJumpCount = - visibleWhen(num("trailsParticleJumpCount", SETTING_PARTICLE_JUMP_COUNT, 12, 1, 30), - () -> particlesEnabled.get() && particleTriggers.get("jump")); - private final NumberValue particleProjectileCount = - visibleWhen(num("trailsParticleProjectileCount", SETTING_PARTICLE_PROJECTILE_COUNT, 3, 1, 10), - () -> particlesEnabled.get() && particleTriggers.get("projectile")); - private final EnumValue particleTexture = - visibleWhen(enumSetting("trailsParticleTexture", SETTING_PARTICLE_TEXTURE, ParticleTextureMode.BLOOM, ParticleTextureMode.values()), - particlesEnabled::get); - private final BooleanValue particleRandomColor = - visibleWhen(bool("trailsParticleRandomColor", SETTING_PARTICLE_RANDOM_COLOR, false), particlesEnabled::get); - private final NumberValue particleSize = - visibleWhen(num("trailsParticleSize", SETTING_PARTICLE_SIZE, 0.12f, 0.04f, 0.4f), particlesEnabled::get); - private final NumberValue particleLifeMs = - visibleWhen(num("trailsParticleLifeMs", SETTING_PARTICLE_LIFE, 1600, 200, 5000), particlesEnabled::get); - private final BooleanValue connectedPath = - visibleWhen(bool("trailsConnectedPath", SETTING_CONNECTED_PATH, true), this::isTailMode); - private final NumberValue connectStep = - visibleWhen(num("trailsConnectedStep", SETTING_CONNECT_STEP, 0.35f, 0.1f, 1.0f), - () -> isTailMode() && connectedPath.get()); + private final TrailsColor trailColor = new TrailsColor(); + private final TrailsGeometry trailGeometry = new TrailsGeometry(); + private final TrailsParticles trailParticles = new TrailsParticles(); + private final TrailsRenderer trailRenderer = new TrailsRenderer(); + private final BooleanValue onlySelf = bool("trailsOnlySelf", "only_self", false); + private final BooleanValue hideFirstPerson = bool("trailsHideFirstPerson", "hide_first_person", true); + private final BooleanValue depthTest = bool("trailsDepthTest", "depth_test", true); + private final ModeValue trailMode = modeSetting("trailsTrailMode", "trail_mode", "Tail", "Tail", "Trail"); + private final RGBAColorValue colorValue = color("trailsColor", "color", "#8800FF00"); + private final ModeValue colorMode = modeSetting("trailsColorMode", "color_mode", "Static", "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); + private final RGBAColorValue colorValue2 = color("trailsColor2", "color2", "#FF55FFFF"); + private final NumberValue colorSpeed = num("trailsColorSpeed", "color_speed", 18, 2, 54); + private final NumberValue lifeTimeTicks = num("trailsLifeTimeTicks", "life_time_ticks", 10, 1, 40); + private final NumberValue trailScale = num("trailsScale", "scale", 0.6f, 0.2f, 1.2f); + private final NumberValue trailDown = num("trailsTrailDown", "trail_down", 0.5f, 0.0f, 2.0f); + private final NumberValue trailHeight = num("trailsTrailHeight", "trail_height", 1.3f, 0.1f, 2.0f); + private final BooleanValue trailStroke = bool("trailsTrailStroke", "trail_stroke", true); + private final NumberValue trailStrokeWidth = num("trailsTrailStrokeWidth", "trail_stroke_width", 2.0f, 0.5f, 8.0f); + private final NumberValue trailStrokeAlpha = num("trailsTrailStrokeAlpha", "trail_stroke_alpha", 0.75f, 0.05f, 1.0f); + private final NumberValue colorPhase = num("trailsColorPhase", "color_phase", 0, 0, 360); + private final NumberValue colorLengthSpread = num("trailsColorLengthSpread", "color_length_spread", 180, -720, 720); + private final NumberValue colorAgeSpread = num("trailsColorAgeSpread", "color_age_spread", 90, -720, 720); + private final BooleanValue particlesEnabled = bool("trailsParticlesEnabled", "particles_enabled", false); + private final BooleanValue particlesHideFirstPerson = bool("trailsParticlesHideFirstPerson", "particles_hide_first_person", false); + private final BooleanMapValue particleTriggers = group("trailsParticleTriggers", "particle_triggers", Map.of("walk", true, "jump", true, "projectile", true)); + private final NumberValue particleWalkCount = num("trailsParticleWalkCount", "particle_walk_count", 3, 1, 8); + private final NumberValue particleJumpCount = num("trailsParticleJumpCount", "particle_jump_count", 12, 1, 30); + private final NumberValue particleProjectileCount = num("trailsParticleProjectileCount", "particle_projectile_count", 3, 1, 10); + private final EnumValue particleTexture = enumSetting("trailsParticleTexture", "particle_texture", ParticleTextureMode.BLOOM, ParticleTextureMode.values()); + private final BooleanValue particleRandomColor = bool("trailsParticleRandomColor", "particle_random_color", false); + private final NumberValue particleSize = num("trailsParticleSize", "particle_size", 0.12f, 0.04f, 0.4f); + private final NumberValue particleLifeMs = num("trailsParticleLifeMs", "particle_life_ms", 1600, 200, 5000); + private final BooleanValue connectedPath = bool("trailsConnectedPath", "connected_path", true); + private final NumberValue connectStep = num("trailsConnectedStep", "connected_step", 0.35f, 0.1f, 1.0f); private final Map groundState = new HashMap<>(); private final List extraParticles = new ArrayList<>(); - private final List[] particleBuckets = createBuckets(); + private final List[] particleBuckets = trailParticles.createBuckets(); - @SuppressWarnings("unchecked") - private static List[] createBuckets() { - List[] buckets = new List[TextureStorage.RANDOM_PARTICLES.length]; - for (int i = 0; i < buckets.length; i++) { - buckets[i] = new ArrayList<>(); - } - return buckets; + @Override public WorldPhase getWorldPhase() { return WorldPhase.END_MAIN; } + + @Override public void onDisable() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return; + for (Player p : mc.level.players()) ((IEntity) p).lovisual$getTrails().clear(); + extraParticles.clear(); groundState.clear(); } - private static void clearBuckets(List[] buckets) { - for (List bucket : buckets) { - bucket.clear(); - } - } - - private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, - Vector3f right, Vector3f up, int argb) { - double p1x = cx - right.x() - up.x(); - double p1y = cy - right.y() - up.y(); - double p1z = cz - right.z() - up.z(); - double p2x = cx + right.x() - up.x(); - double p2y = cy + right.y() - up.y(); - double p2z = cz + right.z() - up.z(); - double p3x = cx + right.x() + up.x(); - double p3y = cy + right.y() + up.y(); - double p3z = cz + right.z() + up.z(); - double p4x = cx - right.x() + up.x(); - double p4y = cy - right.y() + up.y(); - double p4z = cz - right.z() + up.z(); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, - float size, Quaternionf camRot, int argb) { - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); - addBillboardQuad(mesh, cx, cy, cz, right, up, argb); - } - - private static void addVerticalQuad(MeshBuilder mesh, Vec3 p0, Vec3 p1, - float down, float height, int argb0, int argb1) { - double x1 = p0.x; - double z1 = p0.z; - double y1 = p0.y + down; - double y2 = p0.y + down + height; - - double x2 = p1.x; - double z2 = p1.z; - double y3 = p1.y + down + height; - double y4 = p1.y + down; - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(x1, y1, z1).color(new RenderColor(argb0)).next(); - int i2 = mesh.vec3(x1, y2, z1).color(new RenderColor(argb0)).next(); - int i3 = mesh.vec3(x2, y3, z2).color(new RenderColor(argb1)).next(); - int i4 = mesh.vec3(x2, y4, z2).color(new RenderColor(argb1)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void addLine(MeshBuilder mesh, double x0, double y0, double z0, - double x1, double y1, double z1, int argb0, int argb1) { - mesh.ensureLineCapacity(); - int i1 = mesh.vec3(x0, y0, z0).color(new RenderColor(argb0)).next(); - int i2 = mesh.vec3(x1, y1, z1).color(new RenderColor(argb1)).next(); - mesh.line(i1, i2); - } - - private static int withAlpha(int argb, int alpha) { - int a = Math.max(0, Math.min(255, alpha)); - return (a << 24) | (argb & 0x00FFFFFF); - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public void onTick() { + @Override public void onTick() { Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; - int colorIndex = mc.player.tickCount % 360; - boolean particlesOn = particlesEnabled.get(); - boolean triggerWalk = particlesOn && particleTriggers.get("walk"); - boolean triggerJump = particlesOn && particleTriggers.get("jump"); - boolean triggerProjectile = particlesOn && particleTriggers.get("projectile"); - - if (!extraParticles.isEmpty()) { - extraParticles.removeIf(Particle3D::update); - } - - java.util.Set active = new java.util.HashSet<>(); - + boolean on = particlesEnabled.get(); + boolean walk = on && particleTriggers.get("walk"); + boolean jump = on && particleTriggers.get("jump"); + boolean proj = on && particleTriggers.get("projectile"); + trailParticles.updateExtra(extraParticles); + Set active = new HashSet<>(); for (Player player : mc.level.players()) { if (onlySelf.get() && player != mc.player) continue; active.add(player.getUUID()); - List trails = ((IEntity) player).lovisual$getTrails(); Vec3 last = ((IEntity) player).get$InstantRenderPos(); - - boolean skipSelfInFirstPerson = hideFirstPerson.get() - && player == mc.player - && mc.options.getCameraType().isFirstPerson(); + boolean skip = hideFirstPerson.get() && player == mc.player && mc.options.getCameraType().isFirstPerson(); boolean moved = player.getX() != last.x || player.getZ() != last.z; - if (!skipSelfInFirstPerson && moved) { - int argb = getColorArgb(colorIndex); - trails.add(new TrailPoint( - last, - player.position(), - argb, - lifeTimeTicks.get() - )); + if (!skip && moved) { + int argb = trailColor.getColorArgb(colorMode.get(), colorSpeed.get(), colorIndex, colorValue.getArgb(), colorValue2.getArgb()); + trails.add(new TrailPoint(last, player.position(), argb, lifeTimeTicks.get())); } - trails.removeIf(TrailPoint::update); - - boolean skipParticlesFirstPerson = particlesHideFirstPerson.get() - && player == mc.player - && mc.options.getCameraType().isFirstPerson(); - if (particlesOn && !skipParticlesFirstPerson) { - if (triggerWalk && moved) { - spawnWalkParticles(player, colorIndex); - } - - boolean prevGround = groundState.getOrDefault(player.getUUID(), player.onGround()); - boolean nowGround = player.onGround(); - if (triggerJump && prevGround && !nowGround) { - spawnJumpParticles(player, colorIndex); - } - groundState.put(player.getUUID(), nowGround); + boolean skipP = particlesHideFirstPerson.get() && player == mc.player && mc.options.getCameraType().isFirstPerson(); + if (on && !skipP) { + if (walk && moved) trailParticles.spawnWalk(player, colorIndex, random, extraParticles, particleWalkCount.get(), trailColor, colorMode.get(), colorSpeed.get(), colorValue.getArgb(), colorValue2.getArgb(), particleSize.get(), particleLifeMs.get(), particleTexture.get(), particleRandomColor.get()); + boolean prev = groundState.getOrDefault(player.getUUID(), player.onGround()); + boolean now = player.onGround(); + if (jump && prev && !now) trailParticles.spawnJump(player, colorIndex, random, extraParticles, particleJumpCount.get(), trailColor, colorMode.get(), colorSpeed.get(), colorValue.getArgb(), colorValue2.getArgb(), particleSize.get(), particleLifeMs.get(), particleTexture.get(), particleRandomColor.get()); + groundState.put(player.getUUID(), now); } } - - if (particlesOn && triggerProjectile) { - spawnProjectileParticles(colorIndex); - } - + if (on && proj) trailParticles.spawnProjectile(colorIndex, extraParticles, random, mc, particleProjectileCount.get(), trailColor, colorMode.get(), colorSpeed.get(), colorValue.getArgb(), colorValue2.getArgb(), particleSize.get(), particleLifeMs.get(), particleTexture.get(), particleRandomColor.get()); groundState.keySet().removeIf(id -> !active.contains(id)); } - @Override - public void onDisable() { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return; - for (Player player : mc.level.players()) { - ((IEntity) player).lovisual$getTrails().clear(); - } - extraParticles.clear(); - groundState.clear(); - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; - boolean useDepth = depthTest.get(); - if (particlesEnabled.get() && !extraParticles.isEmpty()) { - renderExtraParticles(renderer, tickDelta); - } - + if (particlesEnabled.get() && !extraParticles.isEmpty()) trailParticles.renderExtra(renderer, tickDelta, useDepth, particleTexture.get(), extraParticles, particleBuckets, trailGeometry); if (isTrailMode()) { - renderRibbonTrails(renderer, tickDelta, useDepth); + int phase = trailColor.currentColorPhase(tickDelta, colorPhase.get()); + trailRenderer.renderRibbon(renderer, tickDelta, useDepth, trailDown.get(), trailHeight.get(), phase, trailColor, trailGeometry, colorMode.get(), colorSpeed.get(), colorValue.getArgb(), colorValue2.getArgb(), colorLengthSpread.get(), colorAgeSpread.get(), onlySelf.get(), hideFirstPerson.get(), trailStroke.get(), trailStrokeWidth.get(), trailStrokeAlpha.get()); return; } - - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); - if (mesh == null) return; - - Quaternionf camRot = RenderState.cameraRotation; - float scale = trailScale.get(); - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(scale); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(scale); - boolean connect = connectedPath.get(); - float step = Math.max(0.05f, connectStep.get()); - int phaseBase = currentColorPhase(tickDelta); - - for (Player player : mc.level.players()) { - if (onlySelf.get() && player != mc.player) continue; - if (hideFirstPerson.get() - && player == mc.player - && mc.options.getCameraType().isFirstPerson()) { - continue; - } - - List trails = ((IEntity) player).lovisual$getTrails(); - int size = trails.size(); - if (size == 0) continue; - - for (int i = 0; i < size; i++) { - TrailPoint ctx = trails.get(i); - - int col = trailPointColor(ctx, i, size, tickDelta, phaseBase); - float anim = ctx.animation(tickDelta); - int alpha = (int) (((col >>> 24) & 0xFF) * anim); - int argb = withAlpha(col, alpha); - - if (connect) { - Vec3 from = ctx.from(); - Vec3 to = ctx.to(); - double dist = from.distanceTo(to); - int steps = Math.min(CONNECT_MAX_STEPS, (int) Math.ceil(dist / step)); - if (steps <= 1) { - Vec3 pos = ctx.interpolate(tickDelta); - addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); - } else { - for (int s = 0; s <= steps; s++) { - float t = s / (float) steps; - Vec3 pos = from.lerp(to, t); - addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); - } - } - } else { - Vec3 pos = ctx.interpolate(tickDelta); - addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); - } - } - } - + int phase = trailColor.currentColorPhase(tickDelta, colorPhase.get()); + trailRenderer.renderTail(renderer, tickDelta, useDepth, trailScale.get(), connectedPath.get(), connectStep.get(), phase, trailColor, trailGeometry, colorMode.get(), colorSpeed.get(), colorValue.getArgb(), colorValue2.getArgb(), colorLengthSpread.get(), colorAgeSpread.get(), onlySelf.get(), hideFirstPerson.get()); } - private void renderRibbonTrails(Renderer3D renderer, float tickDelta, boolean useDepth) { - Minecraft mc = Minecraft.getInstance(); - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_COLORED; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - MeshBuilder mesh = renderer.batch(pipeline, depthMode); - if (mesh == null) return; - - MeshBuilder strokeMesh = null; - if (trailStroke.get()) { - float previousLineWidth = RenderState.lineWidth; - try { - RenderState.lineWidth = Math.max(0.5f, trailStrokeWidth.get()); - strokeMesh = renderer.batch( - useDepth - ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_COLORED_LINES, - depthMode - ); - } finally { - RenderState.lineWidth = previousLineWidth; - } - } - - float down = trailDown.get(); - float height = trailHeight.get(); - int phaseBase = currentColorPhase(tickDelta); - - for (Player player : mc.level.players()) { - if (onlySelf.get() && player != mc.player) continue; - if (hideFirstPerson.get() - && player == mc.player - && mc.options.getCameraType().isFirstPerson()) { - continue; - } - - List trails = ((IEntity) player).lovisual$getTrails(); - int size = trails.size(); - if (size < 2) continue; - - for (int i = 1; i < size; i++) { - TrailPoint prev = trails.get(i - 1); - TrailPoint cur = trails.get(i); - - Vec3 p0 = prev.interpolate(tickDelta); - Vec3 p1 = cur.interpolate(tickDelta); - - int col0 = trailPointColor(prev, i - 1, size, tickDelta, phaseBase); - int col1 = trailPointColor(cur, i, size, tickDelta, phaseBase); - - int alpha0 = (int) (((col0 >>> 24) & 0xFF) * prev.animation(tickDelta)); - int alpha1 = (int) (((col1 >>> 24) & 0xFF) * cur.animation(tickDelta)); - - int argb0 = withAlpha(col0, alpha0); - int argb1 = withAlpha(col1, alpha1); - addVerticalQuad(mesh, p0, p1, down, height, argb0, argb1); - if (strokeMesh != null) { - int stroke0 = AnimatedRenderColors.scaleAlpha(argb0, trailStrokeAlpha.get()); - int stroke1 = AnimatedRenderColors.scaleAlpha(argb1, trailStrokeAlpha.get()); - addLine(strokeMesh, p0.x, p0.y + down, p0.z, p1.x, p1.y + down, p1.z, stroke0, stroke1); - addLine(strokeMesh, p0.x, p0.y + down + height, p0.z, p1.x, p1.y + down + height, p1.z, stroke0, stroke1); - } - } - } - } - - private void spawnWalkParticles(Player player, int colorIndex) { - int count = particleWalkCount.get(); - if (count <= 0) return; - for (int i = 0; i < count; i++) { - spawnParticleAround(player, colorIndex, 0.5, 0.4, 0.1); - } - } - - private void spawnJumpParticles(Player player, int colorIndex) { - int count = particleJumpCount.get(); - if (count <= 0) return; - for (int i = 0; i < count; i++) { - spawnParticleAround(player, colorIndex, 0.4, 0.1, 0.2); - } - } - - private void spawnProjectileParticles(int colorIndex) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) return; - int count = particleProjectileCount.get(); - if (count <= 0) return; - - int spawned = 0; - var box = mc.player.getBoundingBox().inflate(PROJECTILE_SCAN_RADIUS); - List projectiles = mc.level.getEntitiesOfClass( - Projectile.class, - box, - e -> true - ); - - for (Projectile p : projectiles) { - if (p instanceof AbstractArrow persistent && isInGround(persistent)) { - continue; - } - if (spawned >= PROJECTILE_MAX_SPAWN) break; - for (int i = 0; i < count && spawned < PROJECTILE_MAX_SPAWN; i++) { - spawnParticleAt( - p.getX() + random.nextDouble() * 0.6 - 0.3, - p.getY() + random.nextDouble() * Math.max(0.2, p.getBbHeight()), - p.getZ() + random.nextDouble() * 0.6 - 0.3, - colorIndex - ); - spawned++; - } - } - } - - private boolean isInGround(AbstractArrow projectile) { - if (projectile == null) return false; - try { - return ((PersistentProjectileEntityAccessor) projectile).lovisual$isInGround(); - } catch (Throwable t) { - return false; - } - } - - private void spawnParticleAround(Player player, int colorIndex, double radius, double height, double motionScale) { - double px = player.getX() + random.nextDouble() * radius - radius * 0.5; - double py = player.getY() + random.nextDouble() * Math.max(0.1, height); - double pz = player.getZ() + random.nextDouble() * radius - radius * 0.5; - spawnParticleAt(px, py, pz, colorIndex, motionScale); - } - - private void spawnParticleAt(double x, double y, double z, int colorIndex) { - spawnParticleAt(x, y, z, colorIndex, 0.12); - } - - private void spawnParticleAt(double x, double y, double z, int colorIndex, double motionScale) { - if (extraParticles.size() >= PARTICLE_MAX) { - extraParticles.remove(0); - } - - double motion = 0.06 * motionScale; - Vec3 vel = new Vec3( - random.nextDouble() * motion * 2 - motion, - random.nextDouble() * 0.03 - 0.015, - random.nextDouble() * motion * 2 - motion - ); - - int color = particleRandomColor.get() ? randomColor() : getColorArgb(colorIndex); - ParticleTexture tex = resolveParticleTexture(); - long life = particleLifeMs.get(); - long fadeIn = Math.min(200L, life / 4L); - long fadeOut = Math.min(700L, life / 2L); - - extraParticles.add(new Particle3D( - new Vec3(x, y, z), - vel, - life, - fadeIn, - fadeOut, - particleSize.get(), - color, - tex.texture(), - tex.index(), - PARTICLE_DRAG, - PARTICLE_GRAVITY, - (random.nextFloat() - 0.5f) * 8f - )); - } - - private void renderExtraParticles(Renderer3D renderer, float tickDelta) { - boolean useDepth = depthTest.get(); - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - Quaternionf camRot = RenderState.cameraRotation; - - if (particleTexture.get() == ParticleTextureMode.BLOOM) { - if (FpsOptimizer.bloomDisabled()) return; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); - if (mesh == null) return; - for (Particle3D p : extraParticles) { - renderParticle(mesh, p, tickDelta, camRot); - } - return; - } - - clearBuckets(particleBuckets); - for (Particle3D p : extraParticles) { - int idx = p.textureIndex(); - if (idx >= 0 && idx < particleBuckets.length) { - particleBuckets[idx].add(p); - } - } - - for (int i = 0; i < particleBuckets.length; i++) { - List bucket = particleBuckets[i]; - if (bucket.isEmpty()) continue; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.RANDOM_PARTICLES[i], depthMode); - if (mesh == null) continue; - for (Particle3D p : bucket) { - renderParticle(mesh, p, tickDelta, camRot); - } - } - } - - private void renderParticle(MeshBuilder mesh, Particle3D particle, float tickDelta, Quaternionf camRot) { - Vec3 pos = particle.interpolate(tickDelta); - float alpha = particle.alpha(); - if (alpha <= 0.01f) return; - int argb = withAlpha(particle.color(), (int) (((particle.color() >>> 24) & 0xFF) * alpha)); - addBillboardQuad(mesh, pos.x, pos.y, pos.z, particle.size(), camRot, argb); - } - - private int randomColor() { - float h = random.nextFloat(); - float s = 0.7f + random.nextFloat() * 0.3f; - float v = 0.8f + random.nextFloat() * 0.2f; - return Color.HSBtoRGB(h, s, v) | 0xFF000000; - } - - private ParticleTexture resolveParticleTexture() { - if (particleTexture.get() == ParticleTextureMode.BLOOM) { - return new ParticleTexture(TextureStorage.FIRE_FLY, -1); - } - int idx = random.nextInt(TextureStorage.RANDOM_PARTICLES.length); - return new ParticleTexture(TextureStorage.RANDOM_PARTICLES[idx], idx); - } - - private boolean usesSecondaryColor() { - return AnimatedRenderColors.usesSecondary(animatedColorMode()); - } - - private boolean isTailMode() { - return "Tail".equals(trailMode.get()); - } - - private boolean isTrailMode() { - return "Trail".equals(trailMode.get()); - } - - private boolean isTrailDownVisible() { - return isTrailMode(); - } - - private int getColorArgb(int count) { - return AnimatedRenderColors.resolve( - animatedColorMode(), - colorSpeed.get(), - count, - colorValue.getArgb(), - colorValue2.getArgb(), - true - ); - } - - private int currentColorPhase(float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) return colorPhase.get(); - return Math.round((mc.player.tickCount + tickDelta) * 4.0f) + colorPhase.get(); - } - - private int trailPointColor(TrailPoint point, int index, int size, float tickDelta, int phaseBase) { - float position = size <= 1 ? 0.0f : index / (float) (size - 1); - float age = 1.0f - point.animation(tickDelta); - int phase = phaseBase - + Math.round(position * colorLengthSpread.get()) - + Math.round(age * colorAgeSpread.get()); - - int animated = getColorArgb(phase); - if (animatedColorMode() == AnimatedRenderColors.Mode.STATIC) { - animated = point.color(); - } - - float lift = (float) Math.pow(1.0f - position, 2.0f) * 0.45f; - return AnimatedRenderColors.mixArgb( - animated, - (animated & 0xFF000000) | 0x00FFFFFF, - lift - ); - } - - private AnimatedRenderColors.Mode animatedColorMode() { - return switch (colorMode.get()) { - case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW; - case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; - case "Sky" -> AnimatedRenderColors.Mode.SKY; - case "Fade" -> AnimatedRenderColors.Mode.FADE; - case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR; - case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS; - case "Theme" -> AnimatedRenderColors.Mode.THEME; - default -> AnimatedRenderColors.Mode.STATIC; - }; - } - - private record ParticleTexture(net.minecraft.resources.Identifier texture, int index) { - } - - // textured rendering handled by Renderer3D batching + private boolean isTrailMode() { return "Trail".equals(trailMode.get()); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java index ce6382a..3f03472 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java @@ -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.module.modules.visuals.effects.ambient; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java index ab4ab2b..4684ebb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java @@ -4,6 +4,7 @@ import net.minecraft.resources.Identifier; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.texture.TextureStorage; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.TextureTintUniforms; @@ -53,7 +54,7 @@ final class WorldParticlesBatches { ? LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE, depthMode, - Renderer3D.BatchBindings.none() + Renderer3DBatchBindings.none() .withSampler("u_Texture", texture) .withUniform("TextureTint", TextureTintUniforms.get()) ); diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesRender.java new file mode 100644 index 0000000..99be9ea --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesRender.java @@ -0,0 +1,94 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import java.util.List; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesGeometry; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesSphere; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +/** + * Рендер-фасад для WorldParticles: ветвление FUNNEL/BUBBLE/QUADS и подготовка батчей. + * Вынесено из {@link WorldParticles} для соблюдения лимита ≤200 строк. + */ +final class WorldParticlesRender { + private static final float SPRITE_SIZE_MULTIPLIER = 4.0f; + private static final float SPRITE_ALPHA_MULTIPLIER = 0.40f; + private static final float DIAGONAL_ALPHA_MULTIPLIER = 0.40f; + private static final float OUTLINE_ALPHA_MULTIPLIER = 205.0f / 255.0f; + + private WorldParticlesRender() {} + + static void render(Renderer3D renderer, + float tickDelta, + List particles, + boolean useDepth, + Renderer3D.DepthMode depthMode, + boolean funnelMode, + boolean bubbleMode, + float lineWidth, + WorldParticlesFunnel.Tint funnelTint, + WorldParticlesFunnel.ColorProvider colors) { + if (funnelMode) { + WorldParticlesFunnel.render(renderer, useDepth, depthMode, tickDelta, particles, funnelTint, colors); + return; + } + + MeshBuilder spriteMesh = WorldParticlesBatches.sprite(renderer, useDepth, depthMode); + MeshBuilder lineMesh = bubbleMode + ? null + : WorldParticlesBatches.lines(renderer, useDepth, depthMode, lineWidth); + MeshBuilder bubbleShellMesh = bubbleMode + ? WorldParticlesBatches.bubbleShell(renderer, useDepth, depthMode) + : null; + + int particleCount = particles.size(); + if (bubbleShellMesh != null) { + bubbleShellMesh.ensureCapacity( + particleCount * (WorldParticlesSphere.STACKS + 1) * (WorldParticlesSphere.SLICES + 1), + particleCount * WorldParticlesSphere.STACKS * WorldParticlesSphere.SLICES * 6 + ); + } + if (spriteMesh != null) { + spriteMesh.ensureCapacity( + bubbleMode ? particleCount * 20 : particleCount * 4, + bubbleMode ? particleCount * 30 : particleCount * 6 + ); + } + if (lineMesh != null) { + lineMesh.ensureCapacity(particleCount * 32, particleCount * 32); + } + + Quaternionf camRot = RenderState.cameraRotation; + for (int i = 0; i < particles.size(); i++) { + WorldParticlesParticle particle = particles.get(i); + float alpha = particle.alpha(); + if (alpha <= 0.003f) { + continue; + } + + Vec3 pos = particle.interpolatePos(tickDelta); + int baseColor = colors.colorFor(particle, i); + + if (bubbleMode) { + WorldParticlesBubble.renderParticle(bubbleShellMesh, spriteMesh, particle, pos, camRot, alpha, baseColor); + continue; + } + + if (spriteMesh != null) { + int spriteArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * SPRITE_ALPHA_MULTIPLIER); + WorldParticlesGeometry.addBillboardQuad(spriteMesh, pos.x, pos.y, pos.z, + particle.size() * SPRITE_SIZE_MULTIPLIER, camRot, spriteArgb); + } + + if (lineMesh != null) { + Vec3 rotation = particle.interpolateRotation(tickDelta); + int diagonalArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * DIAGONAL_ALPHA_MULTIPLIER); + int outlineArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * OUTLINE_ALPHA_MULTIPLIER); + WorldParticlesGeometry.addCube(lineMesh, pos, rotation, particle.size(), diagonalArgb, outlineArgb); + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsColor.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsColor.java new file mode 100644 index 0000000..ca1c8cc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsColor.java @@ -0,0 +1,46 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.client.Minecraft; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.helpers.player.TrailPoint; + +final class TrailsColor { + boolean usesSecondary(String mode) { + return AnimatedRenderColors.usesSecondary(parse(mode)); + } + + int getColorArgb(String mode, int speed, int phase, int primary, int secondary) { + return AnimatedRenderColors.resolve(parse(mode), speed, phase, primary, secondary, true); + } + + int currentColorPhase(float tickDelta, int phaseSetting) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return phaseSetting; + return Math.round((mc.player.tickCount + tickDelta) * 4.0f) + phaseSetting; + } + + int trailPointColor(TrailPoint point, int index, int size, float tickDelta, + int phaseBase, String mode, int speed, int primary, int secondary, + int lengthSpread, int ageSpread) { + float pos = size <= 1 ? 0.0f : index / (float) (size - 1); + float age = 1.0f - point.animation(tickDelta); + int phase = phaseBase + Math.round(pos * lengthSpread) + Math.round(age * ageSpread); + int animated = getColorArgb(mode, speed, phase, primary, secondary); + if (parse(mode) == AnimatedRenderColors.Mode.STATIC) animated = point.color(); + float lift = (float) Math.pow(1.0f - pos, 2.0f) * 0.45f; + return AnimatedRenderColors.mixArgb(animated, (animated & 0xFF000000) | 0x00FFFFFF, lift); + } + + AnimatedRenderColors.Mode parse(String mode) { + return switch (mode) { + case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW; + case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; + case "Sky" -> AnimatedRenderColors.Mode.SKY; + case "Fade" -> AnimatedRenderColors.Mode.FADE; + case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR; + case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS; + case "Theme" -> AnimatedRenderColors.Mode.THEME; + default -> AnimatedRenderColors.Mode.STATIC; + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsGeometry.java new file mode 100644 index 0000000..458084a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsGeometry.java @@ -0,0 +1,63 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +final class TrailsGeometry { + int withAlpha(int argb, int alpha) { + int a = Math.max(0, Math.min(255, alpha)); + return (a << 24) | (argb & 0x00FFFFFF); + } + + void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, + Vector3f right, Vector3f up, int argb) { + double p1x = cx - right.x() - up.x(); + double p1y = cy - right.y() - up.y(); + double p1z = cz - right.z() - up.z(); + double p2x = cx + right.x() - up.x(); + double p2y = cy + right.y() - up.y(); + double p2z = cz + right.z() - up.z(); + double p3x = cx + right.x() + up.x(); + double p3y = cy + right.y() + up.y(); + double p3z = cz + right.z() + up.z(); + double p4x = cx - right.x() + up.x(); + double p4y = cy - right.y() + up.y(); + double p4z = cz - right.z() + up.z(); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(argb)).next(); + int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(argb)).next(); + int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(argb)).next(); + int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(argb)).next(); + mesh.quad(i1, i2, i3, i4); + } + + void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, + float size, Quaternionf camRot, int argb) { + Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); + Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); + addBillboardQuad(mesh, cx, cy, cz, right, up, argb); + } + + void addVerticalQuad(MeshBuilder mesh, Vec3 p0, Vec3 p1, + float down, float height, int argb0, int argb1) { + double x1 = p0.x; double z1 = p0.z; double y1 = p0.y + down; double y2 = p0.y + down + height; + double x2 = p1.x; double z2 = p1.z; double y3 = p1.y + down + height; double y4 = p1.y + down; + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(x1, y1, z1).color(new RenderColor(argb0)).next(); + int i2 = mesh.vec3(x1, y2, z1).color(new RenderColor(argb0)).next(); + int i3 = mesh.vec3(x2, y3, z2).color(new RenderColor(argb1)).next(); + int i4 = mesh.vec3(x2, y4, z2).color(new RenderColor(argb1)).next(); + mesh.quad(i1, i2, i3, i4); + } + + void addLine(MeshBuilder mesh, double x0, double y0, double z0, + double x1, double y1, double z1, int argb0, int argb1) { + mesh.ensureLineCapacity(); + int i1 = mesh.vec3(x0, y0, z0).color(new RenderColor(argb0)).next(); + int i2 = mesh.vec3(x1, y1, z1).color(new RenderColor(argb1)).next(); + mesh.line(i1, i2); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsParticles.java new file mode 100644 index 0000000..9b75e8e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsParticles.java @@ -0,0 +1,149 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import java.awt.Color; +import java.util.List; +import java.util.Random; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.projectile.Projectile; +import net.minecraft.world.entity.projectile.arrow.AbstractArrow; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; +import dev.loki.lovisual.mixins.accessors.projectile.PersistentProjectileEntityAccessor; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.player.Particle3D; +import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; + +final class TrailsParticles { + private static final float PARTICLE_GRAVITY = 0.01f; + private static final float PARTICLE_DRAG = 0.96f; + private static final int PARTICLE_MAX = 600; + private static final int PROJECTILE_SCAN_RADIUS = 48; + private static final int PROJECTILE_MAX_SPAWN = 140; + + @SuppressWarnings("unchecked") + List[] createBuckets() { + List[] b = new List[TextureStorage.RANDOM_PARTICLES.length]; + for (int i = 0; i < b.length; i++) b[i] = new java.util.ArrayList<>(); + return b; + } + + void clearBuckets(List[] buckets) { + for (List l : buckets) l.clear(); + } + + void updateExtra(List extra) { + if (!extra.isEmpty()) extra.removeIf(Particle3D::update); + } + + void spawnWalk(net.minecraft.world.entity.player.Player player, int colorIndex, Random rnd, List extra, + int walkCount, TrailsColor colors, String mode, int speed, int primary, int secondary, + float size, long life, ParticleTextureMode texMode, boolean randomColor) { + if (walkCount <= 0) return; + for (int i = 0; i < walkCount; i++) spawnAround(player, colorIndex, rnd, extra, 0.5, 0.4, 0.1, + colors, mode, speed, primary, secondary, size, life, texMode, randomColor); + } + + void spawnJump(net.minecraft.world.entity.player.Player player, int colorIndex, Random rnd, List extra, + int jumpCount, TrailsColor colors, String mode, int speed, int primary, int secondary, + float size, long life, ParticleTextureMode texMode, boolean randomColor) { + if (jumpCount <= 0) return; + for (int i = 0; i < jumpCount; i++) spawnAround(player, colorIndex, rnd, extra, 0.4, 0.1, 0.2, + colors, mode, speed, primary, secondary, size, life, texMode, randomColor); + } + + void spawnProjectile(int colorIndex, List extra, Random rnd, Minecraft mc, + int projectileCount, TrailsColor colors, String mode, int speed, int primary, int secondary, + float size, long life, ParticleTextureMode texMode, boolean randomColor) { + if (mc.player == null || projectileCount <= 0) return; + int spawned = 0; + var box = mc.player.getBoundingBox().inflate(PROJECTILE_SCAN_RADIUS); + List list = mc.level.getEntitiesOfClass(Projectile.class, box, e -> true); + for (Projectile p : list) { + if (p instanceof AbstractArrow a && isInGround(a)) continue; + if (spawned >= PROJECTILE_MAX_SPAWN) break; + for (int i = 0; i < projectileCount && spawned < PROJECTILE_MAX_SPAWN; i++) { + spawnAt(p.getX() + rnd.nextDouble() * 0.6 - 0.3, + p.getY() + rnd.nextDouble() * Math.max(0.2, p.getBbHeight()), + p.getZ() + rnd.nextDouble() * 0.6 - 0.3, + colorIndex, rnd, extra, 0.12, colors, mode, speed, primary, secondary, size, life, texMode, randomColor); + spawned++; + } + } + } + + private boolean isInGround(AbstractArrow p) { + if (p == null) return false; + try { return ((PersistentProjectileEntityAccessor) p).lovisual$isInGround(); } + catch (Throwable t) { return false; } + } + + private void spawnAround(net.minecraft.world.entity.player.Player player, int colorIndex, Random rnd, List extra, + double radius, double height, double motionScale, + TrailsColor colors, String mode, int speed, int primary, int secondary, + float size, long life, ParticleTextureMode texMode, boolean randomColor) { + double px = player.getX() + rnd.nextDouble() * radius - radius * 0.5; + double py = player.getY() + rnd.nextDouble() * Math.max(0.1, height); + double pz = player.getZ() + rnd.nextDouble() * radius - radius * 0.5; + spawnAt(px, py, pz, colorIndex, rnd, extra, motionScale, colors, mode, speed, primary, secondary, size, life, texMode, randomColor); + } + + private void spawnAt(double x, double y, double z, int colorIndex, Random rnd, List extra, + double motionScale, TrailsColor colors, String mode, int speed, int primary, int secondary, + float size, long life, ParticleTextureMode texMode, boolean randomColor) { + if (extra.size() >= PARTICLE_MAX) extra.remove(0); + double motion = 0.06 * motionScale; + Vec3 vel = new Vec3(rnd.nextDouble() * motion * 2 - motion, rnd.nextDouble() * 0.03 - 0.015, rnd.nextDouble() * motion * 2 - motion); + int col = randomColor ? randomColor(rnd) : colors.getColorArgb(mode, speed, colorIndex, primary, secondary); + ParticleTexture tex = resolve(texMode, rnd); + long fadeIn = Math.min(200L, life / 4L); + long fadeOut = Math.min(700L, life / 2L); + extra.add(new Particle3D(new Vec3(x, y, z), vel, life, fadeIn, fadeOut, size, col, tex.texture(), tex.index(), PARTICLE_DRAG, PARTICLE_GRAVITY, (rnd.nextFloat() - 0.5f) * 8f)); + } + + private int randomColor(Random rnd) { + float h = rnd.nextFloat(); float s = 0.7f + rnd.nextFloat() * 0.3f; float v = 0.8f + rnd.nextFloat() * 0.2f; + return Color.HSBtoRGB(h, s, v) | 0xFF000000; + } + + private ParticleTexture resolve(ParticleTextureMode mode, Random rnd) { + if (mode == ParticleTextureMode.BLOOM) return new ParticleTexture(TextureStorage.FIRE_FLY, -1); + int idx = rnd.nextInt(TextureStorage.RANDOM_PARTICLES.length); + return new ParticleTexture(TextureStorage.RANDOM_PARTICLES[idx], idx); + } + + void renderExtra(Renderer3D renderer, float tickDelta, boolean useDepth, ParticleTextureMode mode, + List extra, List[] buckets, TrailsGeometry geo) { + RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; + Renderer3D.DepthMode dm = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + Quaternionf camRot = RenderState.cameraRotation; + if (mode == ParticleTextureMode.BLOOM) { + if (FpsOptimizer.bloomDisabled()) return; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, dm); + if (mesh == null) return; + for (Particle3D p : extra) renderOne(mesh, p, tickDelta, camRot, geo); + return; + } + clearBuckets(buckets); + for (Particle3D p : extra) { int idx = p.textureIndex(); if (idx >= 0 && idx < buckets.length) buckets[idx].add(p); } + for (int i = 0; i < buckets.length; i++) { + List b = buckets[i]; if (b.isEmpty()) continue; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.RANDOM_PARTICLES[i], dm); + if (mesh == null) continue; + for (Particle3D p : b) renderOne(mesh, p, tickDelta, camRot, geo); + } + } + + private void renderOne(MeshBuilder mesh, Particle3D p, float tickDelta, Quaternionf camRot, TrailsGeometry geo) { + Vec3 pos = p.interpolate(tickDelta); float a = p.alpha(); if (a <= 0.01f) return; + int argb = geo.withAlpha(p.color(), (int) (((p.color() >>> 24) & 0xFF) * a)); + geo.addBillboardQuad(mesh, pos.x, pos.y, pos.z, p.size(), camRot, argb); + } + + record ParticleTexture(net.minecraft.resources.Identifier texture, int index) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsRenderer.java new file mode 100644 index 0000000..be99d17 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/trails/TrailsRenderer.java @@ -0,0 +1,112 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import dev.loki.lovisual.mixininterface.entity.IEntity; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.player.TrailPoint; + +final class TrailsRenderer { + private static final int CONNECT_MAX_STEPS = 6; + + void renderTail(Renderer3D renderer, float tickDelta, boolean useDepth, + float scale, boolean connected, float step, int phaseBase, + TrailsColor colors, TrailsGeometry geo, + String colorMode, int colorSpeed, int primary, int secondary, + int lengthSpread, int ageSpread, + boolean onlySelf, boolean hideFirstPerson) { + Minecraft mc = Minecraft.getInstance(); + RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; + Renderer3D.DepthMode dm = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, dm); + if (mesh == null) return; + Quaternionf camRot = RenderState.cameraRotation; + float s = scale; + Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(s); + Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(s); + float st = Math.max(0.05f, step); + for (Player player : mc.level.players()) { + if (onlySelf && player != mc.player) continue; + if (hideFirstPerson && player == mc.player && mc.options.getCameraType().isFirstPerson()) continue; + List trails = ((IEntity) player).lovisual$getTrails(); + int size = trails.size(); if (size == 0) continue; + for (int i = 0; i < size; i++) { + TrailPoint ctx = trails.get(i); + int col = colors.trailPointColor(ctx, i, size, tickDelta, phaseBase, colorMode, colorSpeed, primary, secondary, lengthSpread, ageSpread); + float anim = ctx.animation(tickDelta); + int argb = geo.withAlpha(col, (int) (((col >>> 24) & 0xFF) * anim)); + if (connected) { + Vec3 from = ctx.from(); Vec3 to = ctx.to(); + double dist = from.distanceTo(to); + int steps = Math.min(CONNECT_MAX_STEPS, (int) Math.ceil(dist / st)); + if (steps <= 1) { + Vec3 pos = ctx.interpolate(tickDelta); + geo.addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); + } else { + for (int si = 0; si <= steps; si++) { + float t = si / (float) steps; + Vec3 pos = from.lerp(to, t); + geo.addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); + } + } + } else { + Vec3 pos = ctx.interpolate(tickDelta); + geo.addBillboardQuad(mesh, pos.x, pos.y + 0.9, pos.z, right, up, argb); + } + } + } + } + + void renderRibbon(Renderer3D renderer, float tickDelta, boolean useDepth, + float down, float height, int phaseBase, + TrailsColor colors, TrailsGeometry geo, + String colorMode, int colorSpeed, int primary, int secondary, + int lengthSpread, int ageSpread, + boolean onlySelf, boolean hideFirstPerson, + boolean stroke, float strokeWidth, float strokeAlpha) { + Minecraft mc = Minecraft.getInstance(); + RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED; + Renderer3D.DepthMode dm = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + MeshBuilder mesh = renderer.batch(pipeline, dm); + if (mesh == null) return; + MeshBuilder strokeMesh = null; + if (stroke) { + float prev = RenderState.lineWidth; + try { + RenderState.lineWidth = Math.max(0.5f, strokeWidth); + strokeMesh = renderer.batch(useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED_LINES, dm); + } finally { RenderState.lineWidth = prev; } + } + for (Player player : mc.level.players()) { + if (onlySelf && player != mc.player) continue; + if (hideFirstPerson && player == mc.player && mc.options.getCameraType().isFirstPerson()) continue; + List trails = ((IEntity) player).lovisual$getTrails(); + int size = trails.size(); if (size < 2) continue; + for (int i = 1; i < size; i++) { + TrailPoint prev = trails.get(i - 1); TrailPoint cur = trails.get(i); + Vec3 p0 = prev.interpolate(tickDelta); Vec3 p1 = cur.interpolate(tickDelta); + int col0 = colors.trailPointColor(prev, i - 1, size, tickDelta, phaseBase, colorMode, colorSpeed, primary, secondary, lengthSpread, ageSpread); + int col1 = colors.trailPointColor(cur, i, size, tickDelta, phaseBase, colorMode, colorSpeed, primary, secondary, lengthSpread, ageSpread); + int argb0 = geo.withAlpha(col0, (int) (((col0 >>> 24) & 0xFF) * prev.animation(tickDelta))); + int argb1 = geo.withAlpha(col1, (int) (((col1 >>> 24) & 0xFF) * cur.animation(tickDelta))); + geo.addVerticalQuad(mesh, p0, p1, down, height, argb0, argb1); + if (strokeMesh != null) { + int s0 = AnimatedRenderColors.scaleAlpha(argb0, strokeAlpha); + int s1 = AnimatedRenderColors.scaleAlpha(argb1, strokeAlpha); + geo.addLine(strokeMesh, p0.x, p0.y + down, p0.z, p1.x, p1.y + down, p1.z, s0, s1); + geo.addLine(strokeMesh, p0.x, p0.y + down + height, p0.z, p1.x, p1.y + down + height, p1.z, s0, s1); + } + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java index 90fc682..23b8ef6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java @@ -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.module.modules.visuals.effects.hits; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java index 5f9d6c7..f2f5b66 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java @@ -1,62 +1,30 @@ -/* - * 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.module.modules.visuals.effects.hits; import dev.loki.lovisual.util.time.GameClock; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.core.particles.ParticleTypes; -import net.minecraft.resources.Identifier; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.phys.Vec3; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.mixins.accessors.world.ClientLevelAccessor; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; -import dev.loki.lovisual.render.engine.color.ColorUtils; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.world.KillEffectBeamUniforms; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import dev.loki.lovisual.util.time.Timer; import dev.loki.lovisual.util.wav.engine.CustomSoundEngine; +import net.minecraft.client.Minecraft; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; -import java.awt.*; -import java.util.*; -import java.util.List; +import java.util.HashMap; +import java.util.Map; //todo Description @ModuleInfo(id = "killeffect", displayName = "KillEffect", category = ModuleCategory.VISUALS) public class KillEffect extends Module { - private static final int MAX_EMBERS = 26; - private static final int MIN_EMBERS = 16; - private static final int LIGHTNING_POINTS = 200; - private static final double LIGHTNING_STEP_Y = 0.25; - private static final float LIGHTNING_STEP_SPREAD = 0.25f; - private static final long LIGHTNING_APPEAR_MS = 1200L; - private static final long LIGHTNING_FADE_MS = 250L; - private static final long LIGHTNING_LIFE_MS = LIGHTNING_APPEAR_MS + LIGHTNING_FADE_MS; - private static final float LIGHTNING_MIN_SIZE = 0.26f; - private static final float LIGHTNING_MAX_SIZE = 0.78f; - private static final float LIGHTNING_ALPHA = 0.40f; private static final String SETTING_MODE = "mode"; private static final String SETTING_Y_SPEED = "y_speed"; private static final String SETTING_PLAY_SOUND = "play_sound"; @@ -65,98 +33,42 @@ public class KillEffect extends Module { private static final String SETTING_MOBS = "mobs"; private static final String SETTING_BEAM_INTENSITY = "beam_intensity"; private static final String SETTING_BEAM_SIZE = "beam_size"; - private static final long BEAM_LIFE_MS = 2500L; private static final Identifier ORTHODOX_SOUND = Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav"); - private static final long EMBER_GROUND_EXTRA_MS = 0; - private final Random random = new Random(); - private final ModeValue mode = - modeSetting("killEffectMode", SETTING_MODE, "Orthodox", - "Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado"); - private final NumberValue ySpeed = - visibleWhen(num("killEffectYSpeed", SETTING_Y_SPEED, 0, -10, 10), () -> "Orthodox".equals(mode.get())); - private final BooleanValue playSound = - visibleWhen(bool("killEffectPlaySound", SETTING_PLAY_SOUND, true), () -> "Orthodox".equals(mode.get())); - private final NumberValue soundVolume = - visibleWhen(num("killEffectSoundVolume", SETTING_SOUND_VOLUME, 1.0, 0.0, 2.0), - () -> "Orthodox".equals(mode.get()) && playSound.get()); - private final RGBAColorValue color = - visibleWhen(color("killEffectColor", SETTING_COLOR, "#FFFF9600"), this::usesColorSetting); + + private final ModeValue mode = modeSetting("killEffectMode", SETTING_MODE, "Orthodox", + "Orthodox", "FallingLava", "EmbersArc", "Lightning", "Portal", "Tornado"); + private final NumberValue ySpeed = visibleWhen( + num("killEffectYSpeed", SETTING_Y_SPEED, 0, -10, 10), () -> isMode("Orthodox")); + private final BooleanValue playSound = visibleWhen( + bool("killEffectPlaySound", SETTING_PLAY_SOUND, true), () -> isMode("Orthodox")); + private final NumberValue soundVolume = visibleWhen( + num("killEffectSoundVolume", SETTING_SOUND_VOLUME, 1.0, 0.0, 2.0), + () -> isMode("Orthodox") && playSound.get()); + private final RGBAColorValue color = visibleWhen( + color("killEffectColor", SETTING_COLOR, "#FFFF9600"), this::usesColorSetting); private final BooleanValue mobs = bool("killEffectMobs", SETTING_MOBS, false); - private final NumberValue beamIntensity = - visibleWhen(num("killEffectBeamIntensity", SETTING_BEAM_INTENSITY, 1.2f, 0.2f, 3.0f), this::usesBeamSettings); - private final NumberValue beamSize = - visibleWhen(num("killEffectBeamSize", SETTING_BEAM_SIZE, 2.0f, 0.5f, 5.0f), this::usesBeamSettings); + private final NumberValue beamIntensity = visibleWhen( + num("killEffectBeamIntensity", SETTING_BEAM_INTENSITY, 1.2f, 0.2f, 3.0f), this::usesBeamSettings); + private final NumberValue beamSize = visibleWhen( + num("killEffectBeamSize", SETTING_BEAM_SIZE, 2.0f, 0.5f, 5.0f), this::usesBeamSettings); + private final Map handled = new HashMap<>(); - private final List orthodoxMarks = new ArrayList<>(); - private final List embers = new ArrayList<>(); - private final List flashes = new ArrayList<>(); - private final List lightningStrikes = new ArrayList<>(); - private final List beamEffects = new ArrayList<>(); + private final KillOrthodoxHelper orthodoxHelper = new KillOrthodoxHelper(); + private final KillEmberHelper emberHelper = new KillEmberHelper(); + private final KillLightningHelper lightningHelper = new KillLightningHelper(); + private final KillBeamHelper beamHelper = new KillBeamHelper(); + + private boolean isMode(String value) { + return value.equals(mode.get()); + } private boolean usesColorSetting() { - return "Orthodox".equals(mode.get()) || "Lightning".equals(mode.get()) || usesBeamSettings(); + return isMode("Orthodox") || isMode("Lightning") || usesBeamSettings(); } private boolean usesBeamSettings() { - return "Portal".equals(mode.get()) || "Tornado".equals(mode.get()); - } - - private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, - float size, Quaternionf camRot, int argb) { - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); - - double p1x = cx - right.x() - up.x(); - double p1y = cy - right.y() - up.y(); - double p1z = cz - right.z() - up.z(); - double p2x = cx + right.x() - up.x(); - double p2y = cy + right.y() - up.y(); - double p2z = cz + right.z() - up.z(); - double p3x = cx + right.x() + up.x(); - double p3y = cy + right.y() + up.y(); - double p3z = cz + right.z() + up.z(); - double p4x = cx - right.x() + up.x(); - double p4y = cy - right.y() + up.y(); - double p4z = cz - right.z() + up.z(); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void addGroundQuad(MeshBuilder mesh, Vec3 center, float size, float angleDeg, int argb) { - double radians = Math.toRadians(angleDeg); - float cos = (float) Math.cos(radians); - float sin = (float) Math.sin(radians); - - Vec3 right = new Vec3(cos, 0.0, -sin); - Vec3 forward = new Vec3(sin, 0.0, cos); - - Vec3 p1 = center.add(right.scale(-size)).add(forward.scale(size)); - Vec3 p2 = center.add(right.scale(size)).add(forward.scale(size)); - Vec3 p3 = center.add(right.scale(size)).add(forward.scale(-size)); - Vec3 p4 = center.add(right.scale(-size)).add(forward.scale(-size)); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 1).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 1).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 0).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 0).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static int withAlpha(int argb, int alpha) { - int a = Math.max(0, Math.min(255, alpha)); - return (a << 24) | (argb & 0x00FFFFFF); - } - - private static float smoothProgress(float value) { - float clamped = Math.max(0f, Math.min(1f, value)); - return clamped * clamped * (3f - 2f * clamped); + return isMode("Portal") || isMode("Tornado"); } @Override @@ -167,408 +79,100 @@ public class KillEffect extends Module { @Override public void onTick() { Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - + if (!isEnabled() || mc.level == null || mc.player == null) { + return; + } long now = GameClock.millis(); handled.entrySet().removeIf(e -> now - e.getValue() > 6000); - - for (LivingEntity entity : mc.level.getEntitiesOfClass(LivingEntity.class, mc.player.getBoundingBox().inflate(128), e -> true)) { - if (entity == mc.player) continue; - if (!mobs.get() && !(entity instanceof Player)) continue; - if (entity.isAlive() || entity.getHealth() > 0) continue; - - int id = entity.getId(); - if (handled.containsKey(id)) continue; - handled.put(id, now); - - Vec3 pos = entity.position(); - - if ("Orthodox".equals(mode.get())) { - orthodoxMarks.add(new OrthodoxMark(pos, now)); - if (playSound.get()) { - CustomSoundEngine.get().play( - ORTHODOX_SOUND, - soundVolume.get(), - 1.0, - false, - true - ); - } - } else if ("FallingLava".equals(mode.get())) { - spawnFallingLava(entity); - } else if ("EmbersArc".equals(mode.get())) { - spawnEmbers(pos); - } else if ("Lightning".equals(mode.get())) { - lightningStrikes.add(new LightningStrike(pos, color.getArgb(), now)); - } else if ("Portal".equals(mode.get())) { - beamEffects.add(new BeamEffect(pos, color.getArgb(), now, BeamKind.PORTAL)); - } else if ("Tornado".equals(mode.get())) { - beamEffects.add(new BeamEffect(pos, color.getArgb(), now, BeamKind.TORNADO)); + for (LivingEntity entity : mc.level.getEntitiesOfClass(LivingEntity.class, + mc.player.getBoundingBox().inflate(128), e -> true)) { + if (entity == mc.player) { + continue; } + if (!mobs.get() && !(entity instanceof Player)) { + continue; + } + if (entity.isAlive() || entity.getHealth() > 0) { + continue; + } + int id = entity.getId(); + if (handled.containsKey(id)) { + continue; + } + handled.put(id, now); + Vec3 pos = entity.position(); + handleKill(pos, entity, now); } + emberHelper.tick(mc.level); + beamHelper.tick(); + lightningHelper.tick(); + orthodoxHelper.tick(now); + } - if (!embers.isEmpty()) { - embers.removeIf(e -> e.update(mc.level)); - } - - if (!beamEffects.isEmpty()) { - beamEffects.removeIf(b -> now - b.createdAtMs > BEAM_LIFE_MS); - } - - if (!flashes.isEmpty()) { - flashes.removeIf(f -> f.timer.passedMs(f.lifeMs)); - } - - if (!lightningStrikes.isEmpty()) { - lightningStrikes.removeIf(LightningStrike::isExpired); - } - - if (!orthodoxMarks.isEmpty()) { - orthodoxMarks.removeIf(m -> now - m.createdAtMs > 3000); + private void handleKill(Vec3 pos, LivingEntity entity, long now) { + String current = mode.get(); + if ("Orthodox".equals(current)) { + orthodoxHelper.add(pos, now); + if (playSound.get()) { + CustomSoundEngine.get().play(ORTHODOX_SOUND, soundVolume.get(), 1.0, false, true); + } + } else if ("FallingLava".equals(current)) { + spawnFallingLava(entity); + } else if ("EmbersArc".equals(current)) { + emberHelper.spawn(pos); + } else if ("Lightning".equals(current)) { + lightningHelper.add(pos, color.getArgb()); + } else if ("Portal".equals(current)) { + beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.PORTAL); + } else if ("Tornado".equals(current)) { + beamHelper.add(pos, color.getArgb(), KillBeamHelper.BeamKind.TORNADO); } } @Override public void onDisable() { handled.clear(); - orthodoxMarks.clear(); - embers.clear(); - flashes.clear(); - lightningStrikes.clear(); - beamEffects.clear(); + orthodoxHelper.clear(); + emberHelper.clear(); + lightningHelper.clear(); + beamHelper.clear(); } @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - - if ("Orthodox".equals(mode.get())) { - renderOrthodox(renderer); - } else if ("Lightning".equals(mode.get())) { - renderLightning(renderer); - } else if ("EmbersArc".equals(mode.get())) { - renderEmbers(renderer, tickDelta); - renderFlashRings(renderer); + if (!isEnabled() || mc.level == null || mc.player == null) { + return; } - - renderBeams(renderer, mc, BeamKind.PORTAL, LoVisualRenderPipelines.WORLD_KILL_EFFECT_PORTAL); - renderBeams(renderer, mc, BeamKind.TORNADO, LoVisualRenderPipelines.WORLD_KILL_EFFECT_TORNADO); - } - - private void renderBeams(Renderer3D renderer, Minecraft mc, BeamKind kind, RenderPipeline pipeline) { - if (beamEffects.isEmpty()) return; - - Vec3 camPos = mc.gameRenderer.mainCamera().position(); - Quaternionf camRot = RenderState.cameraRotation; - long now = GameClock.millis(); - - for (BeamEffect beam : beamEffects) { - if (beam.kind != kind) continue; - - float progress = Math.min(1f, (now - beam.createdAtMs) / (float) BEAM_LIFE_MS); - float time = (now - beam.createdAtMs) / 1000f; - - int argb = beam.colorArgb; - float cr = ((argb >> 16) & 255) / 255f; - float cg = ((argb >> 8) & 255) / 255f; - float cb = (argb & 255) / 255f; - float ca = ((argb >> 24) & 255) / 255f; - - KillEffectBeamUniforms.update(time, progress, beamIntensity.get(), beamSize.get(), cr, cg, cb, ca); - - Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() - .withUniform("KillEffectBeam", KillEffectBeamUniforms.get()); - MeshBuilder mesh = renderer.batch(pipeline, Renderer3D.DepthMode.NONE, bindings); - if (mesh == null) continue; - - if (kind == BeamKind.PORTAL) { - addBillboardQuad(mesh, beam.pos.x, beam.pos.y + 1.0, beam.pos.z, beamSize.get(), camRot, 0xFFFFFFFF); - } else { - addVerticalQuad(mesh, beam.pos, beamSize.get(), beamSize.get() * 2f, camPos, 0xFFFFFFFF); - } + String current = mode.get(); + if ("Orthodox".equals(current)) { + orthodoxHelper.render(renderer, color.getArgb(), ySpeed.get()); + } else if ("Lightning".equals(current)) { + lightningHelper.render(renderer); + } else if ("EmbersArc".equals(current)) { + emberHelper.render(renderer, tickDelta); } - } - - private static void addVerticalQuad(MeshBuilder mesh, Vec3 basePos, float width, float height, Vec3 cameraPos, int argb) { - Vec3 toCam = new Vec3(cameraPos.x - basePos.x, 0.0, cameraPos.z - basePos.z); - if (toCam.lengthSqr() < 1.0E-6) toCam = new Vec3(1.0, 0.0, 0.0); - Vec3 forward = toCam.normalize(); - Vec3 right = new Vec3(-forward.z, 0.0, forward.x); - double halfWidth = width / 2.0; - - Vec3 p1 = basePos.add(right.scale(-halfWidth)); - Vec3 p2 = basePos.add(right.scale(halfWidth)); - Vec3 p3 = p2.add(0.0, height, 0.0); - Vec3 p4 = p1.add(0.0, height, 0.0); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 0).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 0).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 1).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 1).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private void renderOrthodox(Renderer3D renderer) { - int argb = color.getArgb(); - int r = (argb >> 16) & 255; - int g = (argb >> 8) & 255; - int b = argb & 255; - int a = (argb >> 24) & 255; - - double offset = ySpeed.get() / 100.0; - - for (OrthodoxMark mark : orthodoxMarks) { - Vec3 pos = mark.pos; - renderer.line(pos.x, pos.y + offset, pos.z, - pos.x, pos.y + 3.0 + offset, pos.z, r, g, b, a); - renderer.line(pos.x + 1.0, pos.y + 2.3 + offset, pos.z, - pos.x - 1.0, pos.y + 2.3 + offset, pos.z, r, g, b, a); - renderer.line(pos.x + 0.5, pos.y + 1.2 + offset, pos.z, - pos.x - 0.5, pos.y + 0.8 + offset, pos.z, r, g, b, a); - } - } - - private void renderEmbers(Renderer3D renderer, float tickDelta) { - if (embers.isEmpty()) return; - - MeshBuilder mesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - TextureStorage.FIRE_FLY, - Renderer3D.DepthMode.MAIN - ); - if (mesh == null) return; - - Quaternionf camRot = RenderState.cameraRotation; - - for (int i = 0; i < embers.size(); i++) { - Ember e = embers.get(i); - Vec3 pos = e.interpolate(tickDelta); - - float lifeT = e.getLifeProgress(); - Color base = new Color(255, 20, 0, 255); - Color hot = new Color(255, 220, 80, 255); - Color col = ColorUtils.interpolateColorC(base, hot, 1f - lifeT); - - int alpha = (int) (255f * (1f - lifeT)); - int argb = withAlpha(col.getRGB(), alpha); - - float size = e.size * (1f - lifeT); - if (size <= 0.01f) continue; - - addBillboardQuad(mesh, pos.x, pos.y, pos.z, size, camRot, argb); - } - - } - - private void renderFlashRings(Renderer3D renderer) { - if (flashes.isEmpty()) return; - - MeshBuilder mesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - TextureStorage.DEFAULT_CIRCLE, - Renderer3D.DepthMode.MAIN - ); - if (mesh == null) return; - - for (FlashRing ring : flashes) { - float t = Math.min(1f, ring.timer.getPassedTimeMs() / (float) ring.lifeMs); - float size = 0.4f + t * 1.8f; - float alpha = (float) Math.pow(1f - t, 1.6); - - Color base = new Color(255, 60, 0, 255); - Color hot = new Color(255, 220, 120, 255); - Color col = ColorUtils.interpolateColorC(hot, base, t); - int argb = withAlpha(col.getRGB(), (int) (255f * alpha)); - - float angle = t * 180f; - addGroundQuad(mesh, ring.pos, size, angle, argb); - } - - } - - private void renderLightning(Renderer3D renderer) { - if (lightningStrikes.isEmpty()) return; - - MeshBuilder mesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - TextureStorage.FIRE_FLY, - Renderer3D.DepthMode.MAIN - ); - if (mesh == null) return; - - Quaternionf camRot = RenderState.cameraRotation; - - for (LightningStrike strike : lightningStrikes) { - float alpha = strike.getAlpha(); - if (alpha <= 0.01f) continue; - - int colorArgb = withAlpha(strike.colorArgb, (int) (255f * alpha * LIGHTNING_ALPHA)); - int pointCount = Math.max(1, strike.points.size() - 1); - - for (int i = 0; i < strike.points.size(); i++) { - float progress = i / (float) pointCount; - float size = LIGHTNING_MIN_SIZE + (LIGHTNING_MAX_SIZE - LIGHTNING_MIN_SIZE) * progress; - Vec3 point = strike.points.get(i); - addBillboardQuad(mesh, point.x, point.y, point.z, size, camRot, colorArgb); - } - } - } - - private void spawnEmbers(Vec3 pos) { - int count = MIN_EMBERS + random.nextInt(MAX_EMBERS - MIN_EMBERS + 1); - for (int i = 0; i < count; i++) { - double angle = random.nextDouble() * Math.PI * 2.0; - double speed = 0.18 + random.nextDouble() * 0.18; - double vx = Math.cos(angle) * speed; - double vz = Math.sin(angle) * speed; - double vy = 0.28 + random.nextDouble() * 0.20; - - long life = 4000 + random.nextInt(401); - float size = 0.24f + random.nextFloat() * 0.18f; - embers.add(new Ember(pos, new Vec3(vx, vy, vz), life, size)); - } - flashes.add(new FlashRing(pos, 240)); + beamHelper.render(renderer, mc, beamIntensity.get(), beamSize.get()); } private void spawnFallingLava(LivingEntity entity) { Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return; + if (mc.level == null) { + return; + } int height = (int) (entity.getBbHeight() * 10); int width = (int) (entity.getBbWidth() * 10); - for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { for (int k = 0; k < width; k++) { ((ClientLevelAccessor) mc.level).invokeAddParticle( - ParticleTypes.FALLING_LAVA, - false, - true, + ParticleTypes.FALLING_LAVA, false, true, entity.getX() + j * 0.1, entity.getY() + i * 0.1, entity.getZ() + k * 0.1, - 0, 0, 0 - ); + 0, 0, 0); } } } } - - private float randomRange(float min, float max) { - return min + random.nextFloat() * (max - min); - } - - // textured rendering handled by Renderer3D batching - - private record OrthodoxMark(Vec3 pos, long createdAtMs) { - } - - private enum BeamKind { - PORTAL, TORNADO - } - - private record BeamEffect(Vec3 pos, int colorArgb, long createdAtMs, BeamKind kind) { - } - - private static class Ember { - private final Timer timer; - private final long lifeMs; - private final float size; - private Vec3 pos; - private Vec3 prevPos; - private Vec3 velocity; - private boolean grounded; - private long groundExtraMs; - - private Ember(Vec3 pos, Vec3 velocity, long lifeMs, float size) { - this.pos = pos; - this.prevPos = pos; - this.velocity = velocity; - this.timer = new Timer(); - this.lifeMs = lifeMs; - this.size = size; - } - - private boolean update(ClientLevel world) { - prevPos = pos; - pos = pos.add(velocity); - - if (!grounded && world != null) { - var blockPos = net.minecraft.core.BlockPos.containing(pos.x, pos.y - 0.02, pos.z); - boolean solid = !world.getBlockState(blockPos).isAir(); - if (solid) { - grounded = true; - velocity = new Vec3(0.0, 0.0, 0.0); - groundExtraMs = EMBER_GROUND_EXTRA_MS; - pos = new Vec3(pos.x, blockPos.getY() + 1.001, pos.z); - } - } - - if (!grounded) { - velocity = velocity.multiply(0.97, 0.97, 0.97).add(0.0, -0.055, 0.0); - } - - long totalLife = lifeMs + groundExtraMs; - return timer.passedMs(totalLife); - } - - private Vec3 interpolate(float tickDelta) { - return prevPos.lerp(pos, tickDelta); - } - - private float getLifeProgress() { - long totalLife = lifeMs + groundExtraMs; - return Math.min(1f, timer.getPassedTimeMs() / (float) totalLife); - } - } - - private static class FlashRing { - private final Vec3 pos; - private final Timer timer; - private final long lifeMs; - - private FlashRing(Vec3 pos, long lifeMs) { - this.pos = pos; - this.timer = new Timer(); - this.lifeMs = lifeMs; - } - } - - private class LightningStrike { - private final List points = new ArrayList<>(LIGHTNING_POINTS); - private final int colorArgb; - private final long createdAtMs; - - private LightningStrike(Vec3 startPos, int colorArgb, long createdAtMs) { - this.colorArgb = colorArgb; - this.createdAtMs = createdAtMs; - - Vec3 current = startPos; - for (int i = 0; i < LIGHTNING_POINTS; i++) { - current = current.add( - randomRange(-LIGHTNING_STEP_SPREAD, LIGHTNING_STEP_SPREAD), - LIGHTNING_STEP_Y, - randomRange(-LIGHTNING_STEP_SPREAD, LIGHTNING_STEP_SPREAD) - ); - points.add(current); - } - } - - private boolean isExpired() { - return GameClock.millis() - createdAtMs > LIGHTNING_LIFE_MS; - } - - private float getAlpha() { - long age = GameClock.millis() - createdAtMs; - float appear = smoothProgress(Math.min(1f, age / (float) LIGHTNING_APPEAR_MS)); - float fade = 1f; - if (age > LIGHTNING_APPEAR_MS) { - float fadeProgress = Math.min(1f, (age - LIGHTNING_APPEAR_MS) / (float) LIGHTNING_FADE_MS); - fade = 1f - smoothProgress(fadeProgress); - } - return Math.max(0f, Math.min(1f, appear * fade)); - } - } } - diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillBeamHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillBeamHelper.java new file mode 100644 index 0000000..72244ad --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillBeamHelper.java @@ -0,0 +1,113 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.world.KillEffectBeamUniforms; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.util.ArrayList; +import java.util.List; + +final class KillBeamHelper { + private static final long BEAM_LIFE_MS = 2500L; + + private final List beamEffects = new ArrayList<>(); + + void add(Vec3 pos, int colorArgb, BeamKind kind) { + beamEffects.add(new BeamEffect(pos, colorArgb, GameClock.millis(), kind)); + } + + void tick() { + if (beamEffects.isEmpty()) { + return; + } + long now = GameClock.millis(); + beamEffects.removeIf(b -> now - b.createdAtMs > BEAM_LIFE_MS); + } + + void render(Renderer3D renderer, Minecraft mc, float beamIntensity, float beamSize) { + renderBeams(renderer, mc, BeamKind.PORTAL, + LoVisualRenderPipelines.WORLD_KILL_EFFECT_PORTAL, beamIntensity, beamSize); + renderBeams(renderer, mc, BeamKind.TORNADO, + LoVisualRenderPipelines.WORLD_KILL_EFFECT_TORNADO, beamIntensity, beamSize); + } + + void clear() { + beamEffects.clear(); + } + + boolean isEmpty() { + return beamEffects.isEmpty(); + } + + private void renderBeams(Renderer3D renderer, Minecraft mc, + BeamKind kind, RenderPipeline pipeline, + float beamIntensity, float beamSize) { + if (beamEffects.isEmpty()) { + return; + } + Vec3 camPos = mc.gameRenderer.mainCamera().position(); + Quaternionf camRot = RenderState.cameraRotation; + long now = GameClock.millis(); + for (BeamEffect beam : beamEffects) { + if (beam.kind != kind) { + continue; + } + float progress = Math.min(1f, (now - beam.createdAtMs) / (float) BEAM_LIFE_MS); + float time = (now - beam.createdAtMs) / 1000f; + int argb = beam.colorArgb; + float cr = ((argb >> 16) & 255) / 255f; + float cg = ((argb >> 8) & 255) / 255f; + float cb = (argb & 255) / 255f; + float ca = ((argb >> 24) & 255) / 255f; + KillEffectBeamUniforms.update(time, progress, beamIntensity, beamSize, cr, cg, cb, ca); + Renderer3DBatchBindings bindings = Renderer3DBatchBindings.none() + .withUniform("KillEffectBeam", KillEffectBeamUniforms.get()); + MeshBuilder mesh = renderer.batch(pipeline, Renderer3D.DepthMode.NONE, bindings); + if (mesh == null) { + continue; + } + if (kind == BeamKind.PORTAL) { + HitRenderUtil.addBillboardQuad(mesh, beam.pos.x, beam.pos.y + 1.0, + beam.pos.z, beamSize, camRot, 0xFFFFFFFF); + } else { + addVerticalQuad(mesh, beam.pos, beamSize, beamSize * 2f, camPos, 0xFFFFFFFF); + } + } + } + + private static void addVerticalQuad(MeshBuilder mesh, Vec3 basePos, + float width, float height, Vec3 cameraPos, int argb) { + Vec3 toCam = new Vec3(cameraPos.x - basePos.x, 0.0, cameraPos.z - basePos.z); + if (toCam.lengthSqr() < 1.0E-6) { + toCam = new Vec3(1.0, 0.0, 0.0); + } + Vec3 forward = toCam.normalize(); + Vec3 right = new Vec3(-forward.z, 0.0, forward.x); + double halfWidth = width / 2.0; + Vec3 p1 = basePos.add(right.scale(-halfWidth)); + Vec3 p2 = basePos.add(right.scale(halfWidth)); + Vec3 p3 = p2.add(0.0, height, 0.0); + Vec3 p4 = p1.add(0.0, height, 0.0); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 0).color(new RenderColor(argb)).next(); + int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 0).color(new RenderColor(argb)).next(); + int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 1).color(new RenderColor(argb)).next(); + int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 1).color(new RenderColor(argb)).next(); + mesh.quad(i1, i2, i3, i4); + } + + enum BeamKind { + PORTAL, TORNADO + } + + private record BeamEffect(Vec3 pos, int colorArgb, long createdAtMs, BeamKind kind) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillEmberHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillEmberHelper.java new file mode 100644 index 0000000..0b4d740 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillEmberHelper.java @@ -0,0 +1,200 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.color.ColorUtils; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.Timer; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +final class KillEmberHelper { + private static final int MAX_EMBERS = 26; + private static final int MIN_EMBERS = 16; + private static final long EMBER_GROUND_EXTRA_MS = 0; + + private final Random random = new Random(); + private final List embers = new ArrayList<>(); + private final List flashes = new ArrayList<>(); + + void spawn(Vec3 pos) { + int count = MIN_EMBERS + random.nextInt(MAX_EMBERS - MIN_EMBERS + 1); + for (int i = 0; i < count; i++) { + double angle = random.nextDouble() * Math.PI * 2.0; + double speed = 0.18 + random.nextDouble() * 0.18; + double vx = Math.cos(angle) * speed; + double vz = Math.sin(angle) * speed; + double vy = 0.28 + random.nextDouble() * 0.20; + long life = 4000 + random.nextInt(401); + float size = 0.24f + random.nextFloat() * 0.18f; + embers.add(new Ember(pos, new Vec3(vx, vy, vz), life, size)); + } + flashes.add(new FlashRing(pos, 240)); + } + + void tick(ClientLevel level) { + if (!embers.isEmpty()) { + embers.removeIf(e -> e.update(level)); + } + if (!flashes.isEmpty()) { + flashes.removeIf(f -> f.timer.passedMs(f.lifeMs)); + } + } + + void render(Renderer3D renderer, float tickDelta) { + renderEmbers(renderer, tickDelta); + renderFlashRings(renderer); + } + + void clear() { + embers.clear(); + flashes.clear(); + } + + private void renderEmbers(Renderer3D renderer, float tickDelta) { + if (embers.isEmpty()) { + return; + } + MeshBuilder mesh = renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + TextureStorage.FIRE_FLY, + Renderer3D.DepthMode.MAIN + ); + if (mesh == null) { + return; + } + Quaternionf camRot = RenderState.cameraRotation; + for (Ember e : embers) { + Vec3 pos = e.interpolate(tickDelta); + float lifeT = e.getLifeProgress(); + Color base = new Color(255, 20, 0, 255); + Color hot = new Color(255, 220, 80, 255); + Color col = ColorUtils.interpolateColorC(base, hot, 1f - lifeT); + int alpha = (int) (255f * (1f - lifeT)); + int argb = withAlpha(col.getRGB(), alpha); + float size = e.size * (1f - lifeT); + if (size <= 0.01f) { + continue; + } + HitRenderUtil.addBillboardQuad(mesh, pos.x, pos.y, pos.z, size, camRot, argb); + } + } + + private void renderFlashRings(Renderer3D renderer) { + if (flashes.isEmpty()) { + return; + } + MeshBuilder mesh = renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + TextureStorage.DEFAULT_CIRCLE, + Renderer3D.DepthMode.MAIN + ); + if (mesh == null) { + return; + } + for (FlashRing ring : flashes) { + float t = Math.min(1f, ring.timer.getPassedTimeMs() / (float) ring.lifeMs); + float size = 0.4f + t * 1.8f; + float alpha = (float) Math.pow(1f - t, 1.6); + Color base = new Color(255, 60, 0, 255); + Color hot = new Color(255, 220, 120, 255); + Color col = ColorUtils.interpolateColorC(hot, base, t); + int argb = withAlpha(col.getRGB(), (int) (255f * alpha)); + float angle = t * 180f; + addGroundQuad(mesh, ring.pos, size, angle, argb); + } + } + + private static void addGroundQuad(MeshBuilder mesh, Vec3 center, float size, float angleDeg, int argb) { + double radians = Math.toRadians(angleDeg); + float cos = (float) Math.cos(radians); + float sin = (float) Math.sin(radians); + Vec3 right = new Vec3(cos, 0.0, -sin); + Vec3 forward = new Vec3(sin, 0.0, cos); + Vec3 p1 = center.add(right.scale(-size)).add(forward.scale(size)); + Vec3 p2 = center.add(right.scale(size)).add(forward.scale(size)); + Vec3 p3 = center.add(right.scale(size)).add(forward.scale(-size)); + Vec3 p4 = center.add(right.scale(-size)).add(forward.scale(-size)); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(p1.x, p1.y, p1.z).vec2(0, 1).color(new RenderColor(argb)).next(); + int i2 = mesh.vec3(p2.x, p2.y, p2.z).vec2(1, 1).color(new RenderColor(argb)).next(); + int i3 = mesh.vec3(p3.x, p3.y, p3.z).vec2(1, 0).color(new RenderColor(argb)).next(); + int i4 = mesh.vec3(p4.x, p4.y, p4.z).vec2(0, 0).color(new RenderColor(argb)).next(); + mesh.quad(i1, i2, i3, i4); + } + + private static int withAlpha(int argb, int alpha) { + int a = Math.max(0, Math.min(255, alpha)); + return (a << 24) | (argb & 0x00FFFFFF); + } + + private static class Ember { + private final Timer timer; + private final long lifeMs; + private final float size; + private Vec3 pos; + private Vec3 prevPos; + private Vec3 velocity; + private boolean grounded; + private long groundExtraMs; + + private Ember(Vec3 pos, Vec3 velocity, long lifeMs, float size) { + this.pos = pos; + this.prevPos = pos; + this.velocity = velocity; + this.timer = new Timer(); + this.lifeMs = lifeMs; + this.size = size; + } + + private boolean update(ClientLevel world) { + prevPos = pos; + pos = pos.add(velocity); + if (!grounded && world != null) { + var blockPos = net.minecraft.core.BlockPos.containing(pos.x, pos.y - 0.02, pos.z); + boolean solid = !world.getBlockState(blockPos).isAir(); + if (solid) { + grounded = true; + velocity = new Vec3(0.0, 0.0, 0.0); + groundExtraMs = EMBER_GROUND_EXTRA_MS; + pos = new Vec3(pos.x, blockPos.getY() + 1.001, pos.z); + } + } + if (!grounded) { + velocity = velocity.multiply(0.97, 0.97, 0.97).add(0.0, -0.055, 0.0); + } + long totalLife = lifeMs + groundExtraMs; + return timer.passedMs(totalLife); + } + + private Vec3 interpolate(float tickDelta) { + return prevPos.lerp(pos, tickDelta); + } + + private float getLifeProgress() { + long totalLife = lifeMs + groundExtraMs; + return Math.min(1f, timer.getPassedTimeMs() / (float) totalLife); + } + } + + private static class FlashRing { + private final Vec3 pos; + private final Timer timer; + private final long lifeMs; + + private FlashRing(Vec3 pos, long lifeMs) { + this.pos = pos; + this.timer = new Timer(); + this.lifeMs = lifeMs; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillLightningHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillLightningHelper.java new file mode 100644 index 0000000..66c89d8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillLightningHelper.java @@ -0,0 +1,121 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +final class KillLightningHelper { + private static final int LIGHTNING_POINTS = 200; + private static final double LIGHTNING_STEP_Y = 0.25; + private static final float LIGHTNING_STEP_SPREAD = 0.25f; + private static final long LIGHTNING_APPEAR_MS = 1200L; + private static final long LIGHTNING_FADE_MS = 250L; + private static final long LIGHTNING_LIFE_MS = LIGHTNING_APPEAR_MS + LIGHTNING_FADE_MS; + private static final float LIGHTNING_MIN_SIZE = 0.26f; + private static final float LIGHTNING_MAX_SIZE = 0.78f; + private static final float LIGHTNING_ALPHA = 0.40f; + + private final Random random = new Random(); + private final List strikes = new ArrayList<>(); + + void add(Vec3 pos, int colorArgb) { + strikes.add(new LightningStrike(pos, colorArgb, GameClock.millis())); + } + + void tick() { + if (!strikes.isEmpty()) { + strikes.removeIf(LightningStrike::isExpired); + } + } + + void render(Renderer3D renderer) { + if (strikes.isEmpty()) { + return; + } + MeshBuilder mesh = renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + TextureStorage.FIRE_FLY, + Renderer3D.DepthMode.MAIN + ); + if (mesh == null) { + return; + } + Quaternionf camRot = RenderState.cameraRotation; + for (LightningStrike strike : strikes) { + float alpha = strike.getAlpha(); + if (alpha <= 0.01f) { + continue; + } + int colorArgb = withAlpha(strike.colorArgb, (int) (255f * alpha * LIGHTNING_ALPHA)); + int pointCount = Math.max(1, strike.points.size() - 1); + for (int i = 0; i < strike.points.size(); i++) { + float progress = i / (float) pointCount; + float size = LIGHTNING_MIN_SIZE + (LIGHTNING_MAX_SIZE - LIGHTNING_MIN_SIZE) * progress; + Vec3 point = strike.points.get(i); + HitRenderUtil.addBillboardQuad(mesh, point.x, point.y, point.z, size, camRot, colorArgb); + } + } + } + + void clear() { + strikes.clear(); + } + + private static int withAlpha(int argb, int alpha) { + int a = Math.max(0, Math.min(255, alpha)); + return (a << 24) | (argb & 0x00FFFFFF); + } + + private static float smoothProgress(float value) { + float clamped = Math.max(0f, Math.min(1f, value)); + return clamped * clamped * (3f - 2f * clamped); + } + + private float randomRange(float min, float max) { + return min + random.nextFloat() * (max - min); + } + + private class LightningStrike { + private final List points = new ArrayList<>(LIGHTNING_POINTS); + private final int colorArgb; + private final long createdAtMs; + + private LightningStrike(Vec3 startPos, int colorArgb, long createdAtMs) { + this.colorArgb = colorArgb; + this.createdAtMs = createdAtMs; + Vec3 current = startPos; + for (int i = 0; i < LIGHTNING_POINTS; i++) { + current = current.add( + randomRange(-LIGHTNING_STEP_SPREAD, LIGHTNING_STEP_SPREAD), + LIGHTNING_STEP_Y, + randomRange(-LIGHTNING_STEP_SPREAD, LIGHTNING_STEP_SPREAD) + ); + points.add(current); + } + } + + private boolean isExpired() { + return GameClock.millis() - createdAtMs > LIGHTNING_LIFE_MS; + } + + private float getAlpha() { + long age = GameClock.millis() - createdAtMs; + float appear = smoothProgress(Math.min(1f, age / (float) LIGHTNING_APPEAR_MS)); + float fade = 1f; + if (age > LIGHTNING_APPEAR_MS) { + float fadeProgress = Math.min(1f, (age - LIGHTNING_APPEAR_MS) / (float) LIGHTNING_FADE_MS); + fade = 1f - smoothProgress(fadeProgress); + } + return Math.max(0f, Math.min(1f, appear * fade)); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillOrthodoxHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillOrthodoxHelper.java new file mode 100644 index 0000000..8e4a2ad --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/kill/KillOrthodoxHelper.java @@ -0,0 +1,46 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +final class KillOrthodoxHelper { + private final List marks = new ArrayList<>(); + + void add(Vec3 pos, long now) { + marks.add(new OrthodoxMark(pos, now)); + } + + void tick(long now) { + marks.removeIf(m -> now - m.createdAtMs > 3000); + } + + void render(Renderer3D renderer, int colorArgb, int ySpeed) { + int r = (colorArgb >> 16) & 255; + int g = (colorArgb >> 8) & 255; + int b = colorArgb & 255; + int a = (colorArgb >> 24) & 255; + double offset = ySpeed / 100.0; + for (OrthodoxMark mark : marks) { + Vec3 pos = mark.pos; + renderer.line(pos.x, pos.y + offset, pos.z, + pos.x, pos.y + 3.0 + offset, pos.z, r, g, b, a); + renderer.line(pos.x + 1.0, pos.y + 2.3 + offset, pos.z, + pos.x - 1.0, pos.y + 2.3 + offset, pos.z, r, g, b, a); + renderer.line(pos.x + 0.5, pos.y + 1.2 + offset, pos.z, + pos.x - 0.5, pos.y + 0.8 + offset, pos.z, r, g, b, a); + } + } + + void clear() { + marks.clear(); + } + + boolean isEmpty() { + return marks.isEmpty(); + } + + private record OrthodoxMark(Vec3 pos, long createdAtMs) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java index c601281..a4267be 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java @@ -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.module.modules.visuals.esp; import com.mojang.blaze3d.pipeline.TextureTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java index c884159..29c6db8 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java @@ -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.module.modules.visuals.esp; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java index 7564cac..30c097e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java @@ -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.module.modules.visuals.esp; import dev.loki.lovisual.features.relations.category.CategoryService; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java index 4b0926d..cf6665e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java @@ -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.module.modules.visuals.esp; import dev.loki.lovisual.render.helpers.clip.ScreenSpaceOverlay2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java index a88a70f..84453be 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java @@ -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.module.modules.visuals.esp.audio; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java index 385ca09..a17279d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java @@ -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.module.modules.visuals.esp.audio; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/CrystalHoles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/CrystalHoles.java new file mode 100644 index 0000000..2ee4f4c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/CrystalHoles.java @@ -0,0 +1,102 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay; + +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal.Hole; +import dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal.HoleRenderer; +import dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal.HoleScanner; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import net.minecraft.client.Minecraft; + +import java.util.List; + +/** + * Highlights blast-safe "holes" (single cells and two-block / 2x2 shapes fully surrounded by + * bedrock or at least one indestructible block) around the player. Detection lives in + * {@link HoleScanner}/{@link dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal.BlockSafety}, + * drawing in {@link HoleRenderer}; this class is the module facade holding settings, the periodic + * rescan tick and the world-render hook. + */ +@ModuleInfo( + id = "holeesp", + displayName = "CrystalHoles", + aliases = {"holes", "crystalholes", "crystal"}, + category = ModuleCategory.VISUALS, + description = "module.holeesp.description" +) +public final class CrystalHoles extends Module { + private static final int RESCAN_TICKS = 10; + + private final NumberValue rangeXZ = + num("holeesp_range_xz", "range_xz", 10, 1, 128); + private final NumberValue rangeY = + num("holeesp_range_y", "range_y", 5, 1, 128); + private final NumberValue height = + num("holeesp_height", "height", 1.0f, 0.01f, 5.0f); + private final NumberValue lineWidth = + num("holeesp_line_width", "line_width", 0.5f, 0.01f, 5.0f); + private final BooleanValue culling = + bool("holeesp_culling", "culling", true); + private final RGBAColorValue indestructibleColor = + color("holeesp_indestructible_color", "indestructible_color", "#FF7A00FF"); + private final RGBAColorValue bedrockColor = + color("holeesp_bedrock_color", "bedrock_color", "#FF00FF51"); + + private final HoleScanner scanner = + new HoleScanner(rangeXZ, rangeY, height, indestructibleColor, bedrockColor); + private final HoleRenderer holeRenderer = new HoleRenderer(rangeXZ); + + private List holes = List.of(); + private int ticks; + + @Override + public void onDisable() { + holes = List.of(); + ticks = 0; + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || mc.player == null) { + holes = List.of(); + return; + } + + if (ticks++ % RESCAN_TICKS == 0) { + holes = scanner.scan(mc.level, mc.player.position()); + } + } + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.END_MAIN; + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || renderer == null || mc.level == null || mc.player == null || holes.isEmpty()) { + return; + } + + float previousLineWidth = RenderState.lineWidth; + RenderState.lineWidth = Math.max(0.01f, lineWidth.get()); + try { + for (Hole hole : holes) { + if (culling.get() && !HoleRenderer.shouldRender(hole.box())) { + continue; + } + holeRenderer.render(renderer, hole); + } + } finally { + RenderState.lineWidth = previousLineWidth; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java index 89e14f0..ab014e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java @@ -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.module.modules.visuals.esp.overlay; import dev.loki.lovisual.config.values.collection.ConfigValue; @@ -30,11 +23,9 @@ import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.mixininterface.entity.IEntity; -import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; import dev.loki.lovisual.render.engine.text.glyph.VanillaTextRenderer; -import dev.loki.lovisual.render.helpers.material.MatteHudStyle; import dev.loki.lovisual.render.helpers.clip.ScreenProjection; import dev.loki.lovisual.render.helpers.clip.ScreenSpaceOverlay2D; import dev.loki.lovisual.util.item.food.IllegalItemUtil; @@ -57,11 +48,6 @@ public class DropESP extends Module { private static final String SETTING_ITEM_ICON = "item_icon"; private static final double ITEM_BOX_EXPAND_XZ = 0.05; private static final double ITEM_BOX_EXPAND_TOP = 0.15; - private static final double MATTE_LABEL_PAD_X = 3.5; - private static final double MATTE_LABEL_PAD_Y = 2.0; - private static final double MATTE_ICON_SIZE = 14.0; - private static final double MATTE_ICON_GAP = 4.0; - private static final float MATTE_ICON_SCALE = (float) (MATTE_ICON_SIZE / 16.0); private final ModeValue modeValue = modeSetting("dropEspMode", SETTING_MODE, "Matte", "Vanilla", "New", "Matte"); private final BooleanValue limitCommonDistanceValue = bool("dropEspLimitCommonDistance", SETTING_LIMIT_COMMON_DISTANCE, true); private final NumberValue commonMaxDistanceValue = @@ -82,18 +68,6 @@ public class DropESP extends Module { setting(SettingDef.textList(SETTING_TOP_ENCHANT_IGNORE_LIST, topIgnore, TextListSetting.PickerMode.ENCHANTMENTS)); } - private static int compareDropRenderOrder(DropOverlayEntry a, DropOverlayEntry b) { - int rarity = Integer.compare(a.sortPriority(), b.sortPriority()); - if (rarity != 0) return rarity; - return Double.compare(b.distSq(), a.distSq()); - } - - private static int compareDropRenderOrder(DropVanillaEntry a, DropVanillaEntry b) { - int rarity = Integer.compare(a.sortPriority(), b.sortPriority()); - if (rarity != 0) return rarity; - return Double.compare(b.distSq(), a.distSq()); - } - @Override public HudPhase getHudPhase() { return HudPhase.FIRST; @@ -108,8 +82,8 @@ public class DropESP extends Module { boolean overlayMode = isOverlayMode(); TextRenderer labelRenderer = overlayMode ? ScreenSpaceOverlay2D.labelRenderer(textRenderer) : VanillaTextRenderer.INSTANCE; boolean measureStarted = false; - java.util.List overlayEntries = new java.util.ArrayList<>(); - java.util.List vanillaEntries = new java.util.ArrayList<>(); + java.util.List overlayEntries = new java.util.ArrayList<>(); + java.util.List vanillaEntries = new java.util.ArrayList<>(); double textScale = overlayMode ? ScreenSpaceOverlay2D.TEXT_SCALE : ScreenSpaceOverlay2D.VANILLA_TEXT_SCALE; if (!labelRenderer.isBuilding()) { @@ -145,13 +119,13 @@ public class DropESP extends Module { ); if (rect == null) continue; - DropLabelEntry matteLabel = matteMode - ? createMatteDropLabel(labelRenderer, stack, text, color, rect, itemIconValue.get()) + DropEspOverlayRenderer.DropLabelEntry matteLabel = matteMode + ? DropEspOverlayRenderer.createMatteDropLabel(labelRenderer, stack, text, color, rect, itemIconValue.get()) : null; ScreenSpaceOverlay2D.LabelEntry label = matteMode ? null : ScreenSpaceOverlay2D.createCenteredLabel(labelRenderer, text, color, rect); - overlayEntries.add(new DropOverlayEntry(sortPriority, distSq, rect, color, matteLabel, label)); + overlayEntries.add(new DropEspOverlayRenderer.DropOverlayEntry(sortPriority, distSq, rect, color, matteLabel, label)); } else { AABB box = item.getBoundingBox().move( pos.x - item.getX(), @@ -170,7 +144,7 @@ public class DropESP extends Module { double x = screen.x - (labelRenderer.getWidth(text, true) / 2.0); double y = screen.y; - vanillaEntries.add(new DropVanillaEntry(sortPriority, distSq, ScreenSpaceOverlay2D.labelAt(text, x, y, color))); + vanillaEntries.add(new DropEspOverlayRenderer.DropVanillaEntry(sortPriority, distSq, ScreenSpaceOverlay2D.labelAt(text, x, y, color))); } } @@ -179,106 +153,21 @@ public class DropESP extends Module { } if (overlayEntries.isEmpty() && vanillaEntries.isEmpty()) return; - overlayEntries.sort(DropESP::compareDropRenderOrder); - vanillaEntries.sort(DropESP::compareDropRenderOrder); + overlayEntries.sort(DropEspOverlayRenderer::compareDropRenderOrder); + vanillaEntries.sort(DropEspOverlayRenderer::compareDropRenderOrder); if (overlayMode) { - for (DropOverlayEntry entry : overlayEntries) { - renderOverlayEntryPass(renderer, labelRenderer, entry, matteMode, textScale); + for (DropEspOverlayRenderer.DropOverlayEntry entry : overlayEntries) { + DropEspOverlayRenderer.renderOverlayEntryPass(renderer, labelRenderer, entry, matteMode, textScale, frameValue.get(), textShadowValue.get()); } } else { - for (DropVanillaEntry entry : vanillaEntries) { - renderSingleLabel(labelRenderer, entry.label(), true, textScale); + for (DropEspOverlayRenderer.DropVanillaEntry entry : vanillaEntries) { + DropEspOverlayRenderer.renderSingleLabel(labelRenderer, entry.label(), true, textScale); Renderer2D.flushBatch(Renderer2D.FlushReason.EXPLICIT); } } } - private void renderOverlayEntryPass(Renderer2D renderer, - TextRenderer labelRenderer, - DropOverlayEntry entry, - boolean matteMode, - double textScale) { - ScreenSpaceOverlay2D.ScreenRect rect = entry.rect(); - if (frameValue.get()) { - if (matteMode) { - MatteHudStyle.drawFrame(renderer, rect.minX(), rect.minY(), rect.width(), rect.height(), entry.color(), 1.0f); - } else { - ScreenSpaceOverlay2D.drawFrame(renderer, rect, entry.color()); - } - } - - if (matteMode) { - DropLabelEntry label = entry.matteLabel(); - if (label == null) return; - MatteHudStyle.drawPlate(renderer, label.x(), label.y(), label.width(), label.height(), 2.0f, 1.0f); - if (label.icon()) { - renderer.item(label.stack(), label.iconX(), label.iconY(), MATTE_ICON_SCALE, 0, Renderer2D.ITEM_OVERLAY_NONE, null); - } - renderSingleDropLabel(labelRenderer, label, textScale); - } else { - ScreenSpaceOverlay2D.LabelEntry label = entry.label(); - if (label == null) return; - if (textShadowValue.get()) { - ScreenSpaceOverlay2D.renderLabelBackplate(renderer, label); - } - renderSingleLabel(labelRenderer, label, false, textScale); - } - - Renderer2D.flushBatch(Renderer2D.FlushReason.EXPLICIT); - } - - private void renderSingleDropLabel(TextRenderer labelRenderer, DropLabelEntry label, double textScale) { - boolean renderStarted = false; - if (!labelRenderer.isBuilding()) { - labelRenderer.begin(textScale); - renderStarted = true; - } - try { - labelRenderer.render(label.text(), label.textX(), label.textY(), new RenderColor(label.color()), false); - } finally { - if (renderStarted) labelRenderer.end(); - } - } - - private void renderSingleLabel(TextRenderer labelRenderer, - ScreenSpaceOverlay2D.LabelEntry label, - boolean shadow, - double textScale) { - boolean renderStarted = false; - if (!labelRenderer.isBuilding()) { - labelRenderer.begin(textScale); - renderStarted = true; - } - try { - ScreenSpaceOverlay2D.renderLabels(labelRenderer, java.util.List.of(label), shadow); - } finally { - if (renderStarted) labelRenderer.end(); - } - } - - private DropLabelEntry createMatteDropLabel(TextRenderer labelRenderer, - ItemStack stack, - String text, - int color, - ScreenSpaceOverlay2D.ScreenRect rect, - boolean icon) { - double textWidth = labelRenderer.getWidth(text, false); - double textHeight = labelRenderer.getHeight(false); - double iconBlock = icon ? MATTE_ICON_SIZE + MATTE_ICON_GAP : 0.0; - double width = textWidth + iconBlock + MATTE_LABEL_PAD_X * 2.0; - double height = Math.max(textHeight + MATTE_LABEL_PAD_Y * 2.0, icon ? MATTE_ICON_SIZE + MATTE_LABEL_PAD_Y * 2.0 : 0.0); - double x = rect.minX() + (rect.width() - width) * 0.5; - double y = rect.minY() - height - 4.0; - x = Math.floor(x + 0.5); - y = Math.floor(y + 0.5); - double iconX = x + MATTE_LABEL_PAD_X; - double iconY = y + (height - MATTE_ICON_SIZE) * 0.5; - double textX = x + MATTE_LABEL_PAD_X + iconBlock; - double textY = y + (height - textHeight) * 0.5; - return new DropLabelEntry(stack, text, x, y, width, height, icon, iconX, iconY, textX, textY, color); - } - private boolean passesDistanceFilter(ItemEntity item, double dist) { String id = BuiltInRegistries.ITEM.getKey(item.getItem().getItem()).toString().toLowerCase(); @@ -381,15 +270,4 @@ public class DropESP extends Module { return e.position(); } } - - private record DropOverlayEntry(int sortPriority, double distSq, ScreenSpaceOverlay2D.ScreenRect rect, int color, - DropLabelEntry matteLabel, ScreenSpaceOverlay2D.LabelEntry label) { - } - - private record DropVanillaEntry(int sortPriority, double distSq, ScreenSpaceOverlay2D.LabelEntry label) { - } - - private record DropLabelEntry(ItemStack stack, String text, double x, double y, double width, double height, - boolean icon, double iconX, double iconY, double textX, double textY, int color) { - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java deleted file mode 100644 index 69c2a02..0000000 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.esp.overlay; - -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.util.block.scan.BlockSearchUtil; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.util.Mth; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -@ModuleInfo( - id = "holeesp", - displayName = "HoleESP", - aliases = {"holes"}, - category = ModuleCategory.VISUALS, - description = "module.holeesp.description" -) -public final class HoleESP extends Module { - private static final Direction[] HORIZONTAL = { - Direction.NORTH, - Direction.SOUTH, - Direction.EAST, - Direction.WEST - }; - private static final int RESCAN_TICKS = 10; - private static final int BASE_FILL_ALPHA = 60; - - private final NumberValue rangeXZ = - num("holeesp_range_xz", "range_xz", 10, 1, 128); - private final NumberValue rangeY = - num("holeesp_range_y", "range_y", 5, 1, 128); - private final NumberValue height = - num("holeesp_height", "height", 1.0f, 0.01f, 5.0f); - private final NumberValue lineWidth = - num("holeesp_line_width", "line_width", 0.5f, 0.01f, 5.0f); - private final BooleanValue culling = - bool("holeesp_culling", "culling", true); - private final RGBAColorValue indestructibleColor = - color("holeesp_indestructible_color", "indestructible_color", "#FF7A00FF"); - private final RGBAColorValue bedrockColor = - color("holeesp_bedrock_color", "bedrock_color", "#FF00FF51"); - - private List holes = List.of(); - private int ticks; - - @Override - public void onDisable() { - holes = List.of(); - ticks = 0; - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) { - holes = List.of(); - return; - } - - if (ticks++ % RESCAN_TICKS == 0) { - holes = scanHoles(mc.level, mc.player.position()); - } - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || renderer == null || mc.level == null || mc.player == null || holes.isEmpty()) { - return; - } - - float previousLineWidth = RenderState.lineWidth; - RenderState.lineWidth = Math.max(0.01f, lineWidth.get()); - try { - for (Hole hole : holes) { - if (culling.get() && !shouldRender(hole.box())) { - continue; - } - renderFade(renderer, hole); - } - } finally { - RenderState.lineWidth = previousLineWidth; - } - } - - private List scanHoles(ClientLevel level, Vec3 center) { - int xz = rangeXZ.get(); - int y = rangeY.get(); - List found = new ArrayList<>(); - List acceptedBoxes = new ArrayList<>(); - Set consumed = new HashSet<>(); - - for (BlockSearchUtil.BlockSearchEntry entry : BlockSearchUtil.searchBlocksInCuboid( - level, - center, - xz, - y, - (pos, state) -> isReplaceable(level, pos) - && Math.abs((pos.getX() + 0.5) - center.x) <= xz - && Math.abs((pos.getY() + 0.5) - center.y) <= y - && Math.abs((pos.getZ() + 0.5) - center.z) <= xz - )) { - BlockPos pos = entry.pos(); - if (consumed.contains(pos)) { - continue; - } - - Hole hole = resolveHole(level, pos); - if (hole == null || intersectsAny(hole.box(), acceptedBoxes)) { - continue; - } - - found.add(hole); - acceptedBoxes.add(hole.box()); - consumed.addAll(hole.positions()); - } - - return List.copyOf(found); - } - - private Hole resolveHole(ClientLevel level, BlockPos pos) { - if (validIndestructible(level, pos)) { - return new Hole(box(pos, 1, 1), indestructibleColor.getArgb(), List.of(pos)); - } - if (validBedrock(level, pos)) { - return new Hole(box(pos, 1, 1), bedrockColor.getArgb(), List.of(pos)); - } - - List two = twoBlockShape(level, pos); - if (two != null) { - if (validBedrockShape(level, two)) { - return new Hole(box(two), bedrockColor.getArgb(), two); - } - if (validIndestructibleShape(level, two)) { - return new Hole(box(two), indestructibleColor.getArgb(), two); - } - } - - List quad = quadShape(level, pos); - if (quad != null) { - if (validBedrockShape(level, quad)) { - return new Hole(box(quad), bedrockColor.getArgb(), quad); - } - if (validIndestructibleShape(level, quad)) { - return new Hole(box(quad), indestructibleColor.getArgb(), quad); - } - } - - return null; - } - - private void renderFade(Renderer3D renderer, Hole hole) { - AABB box = hole.box(); - int fillBottom = colorForDistance(box, hole.argb(), BASE_FILL_ALPHA); - int fillTop = colorForDistance(box, hole.argb(), 0); - int line = colorForDistance(box, hole.argb(), (hole.argb() >>> 24) & 0xFF); - - addVerticalFadeBox(renderer, box, fillBottom, fillTop); - addBottomOutline(renderer, box, line); - } - - private int colorForDistance(AABB box, int argb, int alpha) { - Minecraft mc = Minecraft.getInstance(); - Vec3 center = box.getCenter(); - double dx = center.x - mc.player.getX(); - double dz = center.z - mc.player.getZ(); - double distSqr = dx * dx + dz * dz; - double maxSqr = Math.max(1.0, (double) rangeXZ.get() * rangeXZ.get()); - float factor = (float) (distSqr / maxSqr); - factor = 1.0f - easeOutExpo(factor); - factor = Mth.clamp(factor, 0.0f, 1.0f); - - int baseAlpha = (argb >>> 24) & 0xFF; - int outAlpha = Mth.clamp((int) (factor * Math.min(alpha, baseAlpha)), 0, 255); - return (outAlpha << 24) | (argb & 0x00FFFFFF); - } - - private static float easeOutExpo(float x) { - return x >= 1.0f ? 1.0f : (float) (1.0f - Math.pow(2.0, -10.0f * x)); - } - - private static boolean shouldRender(AABB box) { - return Renderer3D.Culling.isInFrustum(box) && Renderer3D.Culling.isSectionVisible(box); - } - - private static boolean intersectsAny(AABB box, List boxes) { - for (AABB other : boxes) { - if (other.intersects(box)) { - return true; - } - } - return false; - } - - private AABB box(BlockPos pos, int widthX, int widthZ) { - return new AABB( - pos.getX(), - pos.getY(), - pos.getZ(), - pos.getX() + widthX, - pos.getY() + height.get(), - pos.getZ() + widthZ - ); - } - - private AABB box(List positions) { - int minX = Integer.MAX_VALUE; - int minY = Integer.MAX_VALUE; - int minZ = Integer.MAX_VALUE; - int maxX = Integer.MIN_VALUE; - int maxZ = Integer.MIN_VALUE; - - for (BlockPos pos : positions) { - minX = Math.min(minX, pos.getX()); - minY = Math.min(minY, pos.getY()); - minZ = Math.min(minZ, pos.getZ()); - maxX = Math.max(maxX, pos.getX()); - maxZ = Math.max(maxZ, pos.getZ()); - } - - return new AABB(minX, minY, minZ, maxX + 1, minY + height.get(), maxZ + 1); - } - - private static List twoBlockShape(ClientLevel level, BlockPos pos) { - if (!isReplaceable(level, pos)) { - return null; - } - for (Direction direction : HORIZONTAL) { - BlockPos other = pos.relative(direction); - if (isReplaceable(level, other)) { - return List.of(pos, other); - } - } - return null; - } - - private static List quadShape(ClientLevel level, BlockPos pos) { - if (!isReplaceable(level, pos)) { - return null; - } - - List eastSouth = quadIfReplaceable(level, pos, pos.east(), pos.south(), pos.east().south()); - if (eastSouth != null) return eastSouth; - - List westNorth = quadIfReplaceable(level, pos, pos.west(), pos.north(), pos.west().north()); - if (westNorth != null) return westNorth; - - List eastNorth = quadIfReplaceable(level, pos, pos.east(), pos.north(), pos.east().north()); - if (eastNorth != null) return eastNorth; - - return quadIfReplaceable(level, pos, pos.west(), pos.south(), pos.west().south()); - } - - private static List quadIfReplaceable(ClientLevel level, BlockPos a, BlockPos b, BlockPos c, BlockPos d) { - if (isReplaceable(level, b) && isReplaceable(level, c) && isReplaceable(level, d)) { - return List.of(a, b, c, d); - } - return null; - } - - private static boolean validBedrock(ClientLevel level, BlockPos pos) { - return isBedrock(level, pos.below()) - && isBedrock(level, pos.east()) - && isBedrock(level, pos.west()) - && isBedrock(level, pos.south()) - && isBedrock(level, pos.north()) - && isReplaceable(level, pos) - && isReplaceable(level, pos.above()) - && isReplaceable(level, pos.above(2)); - } - - private static boolean validIndestructible(ClientLevel level, BlockPos pos) { - return !validBedrock(level, pos) - && isSafeBlock(level, pos.below()) - && isSafeBlock(level, pos.east()) - && isSafeBlock(level, pos.west()) - && isSafeBlock(level, pos.south()) - && isSafeBlock(level, pos.north()) - && isReplaceable(level, pos) - && isReplaceable(level, pos.above()) - && isReplaceable(level, pos.above(2)); - } - - private static boolean validBedrockShape(ClientLevel level, List positions) { - Set inside = new HashSet<>(positions); - for (BlockPos check : positions) { - if (!isReplaceable(level, check) || !isReplaceable(level, check.above()) || !isReplaceable(level, check.above(2))) { - return false; - } - if (!isBedrock(level, check.below())) { - return false; - } - for (Direction direction : HORIZONTAL) { - BlockPos surround = check.relative(direction); - if (!inside.contains(surround) && !isBedrock(level, surround)) { - return false; - } - } - } - return true; - } - - private static boolean validIndestructibleShape(ClientLevel level, List positions) { - Set inside = new HashSet<>(positions); - boolean hasIndestructible = false; - for (BlockPos check : positions) { - if (!isReplaceable(level, check) || !isReplaceable(level, check.above()) || !isReplaceable(level, check.above(2))) { - return false; - } - - BlockPos below = check.below(); - if (isIndestructible(level, below)) { - hasIndestructible = true; - } else if (!isBedrock(level, below)) { - return false; - } - - for (Direction direction : HORIZONTAL) { - BlockPos surround = check.relative(direction); - if (inside.contains(surround)) { - continue; - } - if (isIndestructible(level, surround)) { - hasIndestructible = true; - } else if (!isBedrock(level, surround)) { - return false; - } - } - } - return hasIndestructible; - } - - private static boolean isSafeBlock(ClientLevel level, BlockPos pos) { - return isBedrock(level, pos) || isIndestructible(level, pos); - } - - private static boolean isBedrock(ClientLevel level, BlockPos pos) { - return level != null && block(level, pos) == Blocks.BEDROCK; - } - - private static boolean isIndestructible(ClientLevel level, BlockPos pos) { - Block block = block(level, pos); - return block == Blocks.OBSIDIAN - || block == Blocks.NETHERITE_BLOCK - || block == Blocks.CRYING_OBSIDIAN - || block == Blocks.RESPAWN_ANCHOR; - } - - private static boolean isReplaceable(ClientLevel level, BlockPos pos) { - if (level == null || pos == null || pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) { - return false; - } - BlockState state = level.getBlockState(pos); - return state.isAir() || state.canBeReplaced() || !state.getFluidState().isEmpty(); - } - - private static Block block(ClientLevel level, BlockPos pos) { - if (level == null || pos == null || pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) { - return Blocks.AIR; - } - return level.getBlockState(pos).getBlock(); - } - - private static void addVerticalFadeBox(Renderer3D renderer, AABB box, int bottomArgb, int topArgb) { - MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.MAIN); - if (mesh == null) { - return; - } - - int bottomA = (bottomArgb >>> 24) & 0xFF; - int bottomR = (bottomArgb >>> 16) & 0xFF; - int bottomG = (bottomArgb >>> 8) & 0xFF; - int bottomB = bottomArgb & 0xFF; - int topA = (topArgb >>> 24) & 0xFF; - int topR = (topArgb >>> 16) & 0xFF; - int topG = (topArgb >>> 8) & 0xFF; - int topB = topArgb & 0xFF; - - addGradientQuad(mesh, box.minX, box.minY, box.maxZ, box.maxX, box.minY, box.maxZ, - box.maxX, box.maxY, box.maxZ, box.minX, box.maxY, box.maxZ, - bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - addGradientQuad(mesh, box.minX, box.minY, box.minZ, box.minX, box.maxY, box.minZ, - box.maxX, box.maxY, box.minZ, box.maxX, box.minY, box.minZ, - bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - addGradientQuad(mesh, box.maxX, box.minY, box.minZ, box.maxX, box.maxY, box.minZ, - box.maxX, box.maxY, box.maxZ, box.maxX, box.minY, box.maxZ, - bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - addGradientQuad(mesh, box.minX, box.minY, box.minZ, box.minX, box.minY, box.maxZ, - box.minX, box.maxY, box.maxZ, box.minX, box.maxY, box.minZ, - bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - } - - private static void addGradientQuad(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2, - double x3, double y3, double z3, - double x4, double y4, double z4, - int bottomR, int bottomG, int bottomB, int bottomA, - int topR, int topG, int topB, int topA) { - mesh.ensureQuadCapacity(); - double minY = Math.min(Math.min(y1, y2), Math.min(y3, y4)); - double maxY = Math.max(Math.max(y1, y2), Math.max(y3, y4)); - double midY = (minY + maxY) * 0.5; - int i1 = fadeVertex(mesh, x1, y1, z1, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - int i2 = fadeVertex(mesh, x2, y2, z2, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - int i3 = fadeVertex(mesh, x3, y3, z3, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - int i4 = fadeVertex(mesh, x4, y4, z4, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); - mesh.quad(i1, i2, i3, i4); - } - - private static int fadeVertex(MeshBuilder mesh, - double x, double y, double z, double midY, - int bottomR, int bottomG, int bottomB, int bottomA, - int topR, int topG, int topB, int topA) { - boolean bottom = y <= midY; - return mesh.vec3(x, y, z) - .color( - bottom ? bottomR : topR, - bottom ? bottomG : topG, - bottom ? bottomB : topB, - bottom ? bottomA : topA - ) - .next(); - } - - private static void addBottomOutline(Renderer3D renderer, AABB box, int argb) { - int a = (argb >>> 24) & 0xFF; - if (a <= 0) { - return; - } - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - renderer.line(box.minX, box.minY, box.minZ, box.maxX, box.minY, box.minZ, r, g, b, a); - renderer.line(box.maxX, box.minY, box.minZ, box.maxX, box.minY, box.maxZ, r, g, b, a); - renderer.line(box.maxX, box.minY, box.maxZ, box.minX, box.minY, box.maxZ, r, g, b, a); - renderer.line(box.minX, box.minY, box.maxZ, box.minX, box.minY, box.minZ, r, g, b, a); - } - - private record Hole(AABB box, int argb, List positions) { - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java index 14c5c4b..a68d5c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java @@ -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.module.modules.visuals.esp.overlay; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/BlockSafety.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/BlockSafety.java new file mode 100644 index 0000000..fdd5ba7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/BlockSafety.java @@ -0,0 +1,133 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Blast-safety predicates for {@link HoleScanner}: which blocks are replaceable / bedrock / + * indestructible, and whether a single cell or a multi-cell shape is a valid safe hole (fully + * surrounded by bedrock, or by at least one indestructible with the rest no weaker). Stateless + * static helpers over the client world. Extracted so the module stays under the file-size cap. + */ +final class BlockSafety { + + static final Direction[] HORIZONTAL = { + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.WEST + }; + + private BlockSafety() { } + + static boolean validBedrock(ClientLevel level, BlockPos pos) { + return isBedrock(level, pos.below()) + && isBedrock(level, pos.east()) + && isBedrock(level, pos.west()) + && isBedrock(level, pos.south()) + && isBedrock(level, pos.north()) + && isReplaceable(level, pos) + && isReplaceable(level, pos.above()) + && isReplaceable(level, pos.above(2)); + } + + static boolean validIndestructible(ClientLevel level, BlockPos pos) { + return !validBedrock(level, pos) + && isSafeBlock(level, pos.below()) + && isSafeBlock(level, pos.east()) + && isSafeBlock(level, pos.west()) + && isSafeBlock(level, pos.south()) + && isSafeBlock(level, pos.north()) + && isReplaceable(level, pos) + && isReplaceable(level, pos.above()) + && isReplaceable(level, pos.above(2)); + } + + static boolean validBedrockShape(ClientLevel level, List positions) { + Set inside = new HashSet<>(positions); + for (BlockPos check : positions) { + if (!isReplaceable(level, check) || !isReplaceable(level, check.above()) || !isReplaceable(level, check.above(2))) { + return false; + } + if (!isBedrock(level, check.below())) { + return false; + } + for (Direction direction : HORIZONTAL) { + BlockPos surround = check.relative(direction); + if (!inside.contains(surround) && !isBedrock(level, surround)) { + return false; + } + } + } + return true; + } + + static boolean validIndestructibleShape(ClientLevel level, List positions) { + Set inside = new HashSet<>(positions); + boolean hasIndestructible = false; + for (BlockPos check : positions) { + if (!isReplaceable(level, check) || !isReplaceable(level, check.above()) || !isReplaceable(level, check.above(2))) { + return false; + } + + BlockPos below = check.below(); + if (isIndestructible(level, below)) { + hasIndestructible = true; + } else if (!isBedrock(level, below)) { + return false; + } + + for (Direction direction : HORIZONTAL) { + BlockPos surround = check.relative(direction); + if (inside.contains(surround)) { + continue; + } + if (isIndestructible(level, surround)) { + hasIndestructible = true; + } else if (!isBedrock(level, surround)) { + return false; + } + } + } + return hasIndestructible; + } + + static boolean isSafeBlock(ClientLevel level, BlockPos pos) { + return isBedrock(level, pos) || isIndestructible(level, pos); + } + + static boolean isBedrock(ClientLevel level, BlockPos pos) { + return level != null && block(level, pos) == Blocks.BEDROCK; + } + + static boolean isIndestructible(ClientLevel level, BlockPos pos) { + Block block = block(level, pos); + return block == Blocks.OBSIDIAN + || block == Blocks.NETHERITE_BLOCK + || block == Blocks.CRYING_OBSIDIAN + || block == Blocks.RESPAWN_ANCHOR; + } + + static boolean isReplaceable(ClientLevel level, BlockPos pos) { + if (level == null || pos == null || pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) { + return false; + } + BlockState state = level.getBlockState(pos); + return state.isAir() || state.canBeReplaced() || !state.getFluidState().isEmpty(); + } + + private static Block block(ClientLevel level, BlockPos pos) { + if (level == null || pos == null || pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) { + return Blocks.AIR; + } + return level.getBlockState(pos).getBlock(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/Hole.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/Hole.java new file mode 100644 index 0000000..f32d687 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/Hole.java @@ -0,0 +1,14 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal; + +import java.util.List; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.AABB; + +/** + * A detected safe hole: its world-space bounding {@code box}, the resolved ARGB color for the + * protection type and the set of {@code positions} it consumes (used to avoid overlapping + * duplicates during scanning). Produced by {@link HoleScanner}, drawn by {@link HoleRenderer}. + */ +public record Hole(AABB box, int argb, List positions) { +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleRenderer.java new file mode 100644 index 0000000..6b00ed6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleRenderer.java @@ -0,0 +1,143 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal; + +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DCulling; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * Draws a detected {@link Hole} for {@code CrystalHoles}: a vertical fade-filled box (opaque at + * the base, transparent at the top) plus a solid bottom outline, with the whole marker's alpha + * eased down by the player's horizontal distance so far holes fade out. Static geometry helpers; + * the distance falloff reads the module's {@code rangeXZ} setting. Extracted so the module class + * stays under the file-size cap. + */ +public final class HoleRenderer { + + private static final int BASE_FILL_ALPHA = 60; + + private final NumberValue rangeXZ; + + public HoleRenderer(NumberValue rangeXZ) { + this.rangeXZ = rangeXZ; + } + + /** Whether the hole is inside the camera frustum and its chunk section is visible. */ + public static boolean shouldRender(AABB box) { + return Renderer3DCulling.isInFrustum(box) && Renderer3DCulling.isSectionVisible(box); + } + + /** Render one hole: distance-faded gradient fill plus a bottom outline. */ + public void render(Renderer3D renderer, Hole hole) { + AABB box = hole.box(); + int fillBottom = colorForDistance(box, hole.argb(), BASE_FILL_ALPHA); + int fillTop = colorForDistance(box, hole.argb(), 0); + int line = colorForDistance(box, hole.argb(), (hole.argb() >>> 24) & 0xFF); + + addVerticalFadeBox(renderer, box, fillBottom, fillTop); + addBottomOutline(renderer, box, line); + } + + private int colorForDistance(AABB box, int argb, int alpha) { + Minecraft mc = Minecraft.getInstance(); + Vec3 center = box.getCenter(); + double dx = center.x - mc.player.getX(); + double dz = center.z - mc.player.getZ(); + double distSqr = dx * dx + dz * dz; + double maxSqr = Math.max(1.0, (double) rangeXZ.get() * rangeXZ.get()); + float factor = (float) (distSqr / maxSqr); + factor = 1.0f - easeOutExpo(factor); + factor = Mth.clamp(factor, 0.0f, 1.0f); + + int baseAlpha = (argb >>> 24) & 0xFF; + int outAlpha = Mth.clamp((int) (factor * Math.min(alpha, baseAlpha)), 0, 255); + return (outAlpha << 24) | (argb & 0x00FFFFFF); + } + + private static float easeOutExpo(float x) { + return x >= 1.0f ? 1.0f : (float) (1.0f - Math.pow(2.0, -10.0f * x)); + } + + private static void addVerticalFadeBox(Renderer3D renderer, AABB box, int bottomArgb, int topArgb) { + MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.MAIN); + if (mesh == null) { + return; + } + + int bottomA = (bottomArgb >>> 24) & 0xFF; + int bottomR = (bottomArgb >>> 16) & 0xFF; + int bottomG = (bottomArgb >>> 8) & 0xFF; + int bottomB = bottomArgb & 0xFF; + int topA = (topArgb >>> 24) & 0xFF; + int topR = (topArgb >>> 16) & 0xFF; + int topG = (topArgb >>> 8) & 0xFF; + int topB = topArgb & 0xFF; + + addGradientQuad(mesh, box.minX, box.minY, box.maxZ, box.maxX, box.minY, box.maxZ, + box.maxX, box.maxY, box.maxZ, box.minX, box.maxY, box.maxZ, + bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + addGradientQuad(mesh, box.minX, box.minY, box.minZ, box.minX, box.maxY, box.minZ, + box.maxX, box.maxY, box.minZ, box.maxX, box.minY, box.minZ, + bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + addGradientQuad(mesh, box.maxX, box.minY, box.minZ, box.maxX, box.maxY, box.minZ, + box.maxX, box.maxY, box.maxZ, box.maxX, box.minY, box.maxZ, + bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + addGradientQuad(mesh, box.minX, box.minY, box.minZ, box.minX, box.minY, box.maxZ, + box.minX, box.maxY, box.maxZ, box.minX, box.maxY, box.minZ, + bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + } + + private static void addGradientQuad(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2, + double x3, double y3, double z3, + double x4, double y4, double z4, + int bottomR, int bottomG, int bottomB, int bottomA, + int topR, int topG, int topB, int topA) { + mesh.ensureQuadCapacity(); + double minY = Math.min(Math.min(y1, y2), Math.min(y3, y4)); + double maxY = Math.max(Math.max(y1, y2), Math.max(y3, y4)); + double midY = (minY + maxY) * 0.5; + int i1 = fadeVertex(mesh, x1, y1, z1, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + int i2 = fadeVertex(mesh, x2, y2, z2, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + int i3 = fadeVertex(mesh, x3, y3, z3, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + int i4 = fadeVertex(mesh, x4, y4, z4, midY, bottomR, bottomG, bottomB, bottomA, topR, topG, topB, topA); + mesh.quad(i1, i2, i3, i4); + } + + private static int fadeVertex(MeshBuilder mesh, + double x, double y, double z, double midY, + int bottomR, int bottomG, int bottomB, int bottomA, + int topR, int topG, int topB, int topA) { + boolean bottom = y <= midY; + return mesh.vec3(x, y, z) + .color( + bottom ? bottomR : topR, + bottom ? bottomG : topG, + bottom ? bottomB : topB, + bottom ? bottomA : topA + ) + .next(); + } + + private static void addBottomOutline(Renderer3D renderer, AABB box, int argb) { + int a = (argb >>> 24) & 0xFF; + if (a <= 0) { + return; + } + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + + renderer.line(box.minX, box.minY, box.minZ, box.maxX, box.minY, box.minZ, r, g, b, a); + renderer.line(box.maxX, box.minY, box.minZ, box.maxX, box.minY, box.maxZ, r, g, b, a); + renderer.line(box.maxX, box.minY, box.maxZ, box.minX, box.minY, box.maxZ, r, g, b, a); + renderer.line(box.minX, box.minY, box.maxZ, box.minX, box.minY, box.minZ, r, g, b, a); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleScanner.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleScanner.java new file mode 100644 index 0000000..99c81e0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/crystal/HoleScanner.java @@ -0,0 +1,185 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay.crystal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.util.block.scan.BlockSearchUtil; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * Discovers safe {@link Hole}s around the player for {@code CrystalHoles}. Scans replaceable + * cells within the configured range and, per seed position, resolves the largest matching shape + * (single cell, two-block or 2x2 quad) whose surroundings are blast-safe (see {@link BlockSafety}); + * already-consumed positions and overlapping boxes are skipped so each hole is reported once. + * Box height comes from the module's {@code height} setting. Extracted so the module class stays + * under the file-size cap. + */ +public final class HoleScanner { + + private final NumberValue rangeXZ; + private final NumberValue rangeY; + private final NumberValue height; + private final RGBAColorValue indestructibleColor; + private final RGBAColorValue bedrockColor; + + public HoleScanner(NumberValue rangeXZ, NumberValue rangeY, NumberValue height, + RGBAColorValue indestructibleColor, RGBAColorValue bedrockColor) { + this.rangeXZ = rangeXZ; + this.rangeY = rangeY; + this.height = height; + this.indestructibleColor = indestructibleColor; + this.bedrockColor = bedrockColor; + } + + public List scan(ClientLevel level, Vec3 center) { + int xz = rangeXZ.get(); + int y = rangeY.get(); + List found = new ArrayList<>(); + List acceptedBoxes = new ArrayList<>(); + Set consumed = new HashSet<>(); + + for (BlockSearchUtil.BlockSearchEntry entry : BlockSearchUtil.searchBlocksInCuboid( + level, + center, + xz, + y, + (pos, state) -> BlockSafety.isReplaceable(level, pos) + && Math.abs((pos.getX() + 0.5) - center.x) <= xz + && Math.abs((pos.getY() + 0.5) - center.y) <= y + && Math.abs((pos.getZ() + 0.5) - center.z) <= xz + )) { + BlockPos pos = entry.pos(); + if (consumed.contains(pos)) { + continue; + } + + Hole hole = resolveHole(level, pos); + if (hole == null || intersectsAny(hole.box(), acceptedBoxes)) { + continue; + } + + found.add(hole); + acceptedBoxes.add(hole.box()); + consumed.addAll(hole.positions()); + } + + return List.copyOf(found); + } + + private Hole resolveHole(ClientLevel level, BlockPos pos) { + if (BlockSafety.validIndestructible(level, pos)) { + return new Hole(box(pos, 1, 1), indestructibleColor.getArgb(), List.of(pos)); + } + if (BlockSafety.validBedrock(level, pos)) { + return new Hole(box(pos, 1, 1), bedrockColor.getArgb(), List.of(pos)); + } + + List two = twoBlockShape(level, pos); + if (two != null) { + Hole hole = shapeHole(level, two); + if (hole != null) return hole; + } + + List quad = quadShape(level, pos); + if (quad != null) { + Hole hole = shapeHole(level, quad); + if (hole != null) return hole; + } + + return null; + } + + /** Resolve a multi-cell shape's protection type: bedrock takes priority, else indestructible. */ + private Hole shapeHole(ClientLevel level, List positions) { + if (BlockSafety.validBedrockShape(level, positions)) { + return new Hole(box(positions), bedrockColor.getArgb(), positions); + } + if (BlockSafety.validIndestructibleShape(level, positions)) { + return new Hole(box(positions), indestructibleColor.getArgb(), positions); + } + return null; + } + + private static List twoBlockShape(ClientLevel level, BlockPos pos) { + if (!BlockSafety.isReplaceable(level, pos)) { + return null; + } + for (Direction direction : BlockSafety.HORIZONTAL) { + BlockPos other = pos.relative(direction); + if (BlockSafety.isReplaceable(level, other)) { + return List.of(pos, other); + } + } + return null; + } + + private static List quadShape(ClientLevel level, BlockPos pos) { + if (!BlockSafety.isReplaceable(level, pos)) { + return null; + } + + List eastSouth = quadIfReplaceable(level, pos, pos.east(), pos.south(), pos.east().south()); + if (eastSouth != null) return eastSouth; + + List westNorth = quadIfReplaceable(level, pos, pos.west(), pos.north(), pos.west().north()); + if (westNorth != null) return westNorth; + + List eastNorth = quadIfReplaceable(level, pos, pos.east(), pos.north(), pos.east().north()); + if (eastNorth != null) return eastNorth; + + return quadIfReplaceable(level, pos, pos.west(), pos.south(), pos.west().south()); + } + + private static List quadIfReplaceable(ClientLevel level, BlockPos a, BlockPos b, BlockPos c, BlockPos d) { + if (BlockSafety.isReplaceable(level, b) && BlockSafety.isReplaceable(level, c) && BlockSafety.isReplaceable(level, d)) { + return List.of(a, b, c, d); + } + return null; + } + + private AABB box(BlockPos pos, int widthX, int widthZ) { + return new AABB( + pos.getX(), + pos.getY(), + pos.getZ(), + pos.getX() + widthX, + pos.getY() + height.get(), + pos.getZ() + widthZ + ); + } + + private AABB box(List positions) { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + + for (BlockPos pos : positions) { + minX = Math.min(minX, pos.getX()); + minY = Math.min(minY, pos.getY()); + minZ = Math.min(minZ, pos.getZ()); + maxX = Math.max(maxX, pos.getX()); + maxZ = Math.max(maxZ, pos.getZ()); + } + + return new AABB(minX, minY, minZ, maxX + 1, minY + height.get(), maxZ + 1); + } + + private static boolean intersectsAny(AABB box, List boxes) { + for (AABB other : boxes) { + if (other.intersects(box)) { + return true; + } + } + return false; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/impl/DropEspOverlayRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/impl/DropEspOverlayRenderer.java new file mode 100644 index 0000000..5828d9c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/impl/DropEspOverlayRenderer.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.module.modules.visuals.esp.overlay; + +import net.minecraft.world.item.ItemStack; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.material.MatteHudStyle; +import dev.loki.lovisual.render.helpers.clip.ScreenSpaceOverlay2D; + +import java.util.List; + +/** + * Screen-space label/overlay rendering subsystem for {@link DropESP}. Owns the collected entry + * models, their render-order comparators, the matte label layout and the per-entry frame/plate/icon/ + * text drawing passes. All module settings that influenced the old inlined code are passed in as + * plain arguments so behaviour stays identical to the original implementation. + */ +final class DropEspOverlayRenderer { + private static final double MATTE_LABEL_PAD_X = 3.5; + private static final double MATTE_LABEL_PAD_Y = 2.0; + private static final double MATTE_ICON_SIZE = 14.0; + private static final double MATTE_ICON_GAP = 4.0; + private static final float MATTE_ICON_SCALE = (float) (MATTE_ICON_SIZE / 16.0); + + private DropEspOverlayRenderer() { + } + + static int compareDropRenderOrder(DropOverlayEntry a, DropOverlayEntry b) { + int rarity = Integer.compare(a.sortPriority(), b.sortPriority()); + if (rarity != 0) return rarity; + return Double.compare(b.distSq(), a.distSq()); + } + + static int compareDropRenderOrder(DropVanillaEntry a, DropVanillaEntry b) { + int rarity = Integer.compare(a.sortPriority(), b.sortPriority()); + if (rarity != 0) return rarity; + return Double.compare(b.distSq(), a.distSq()); + } + + static void renderOverlayEntryPass(Renderer2D renderer, + TextRenderer labelRenderer, + DropOverlayEntry entry, + boolean matteMode, + double textScale, + boolean frame, + boolean textShadow) { + ScreenSpaceOverlay2D.ScreenRect rect = entry.rect(); + if (frame) { + if (matteMode) { + MatteHudStyle.drawFrame(renderer, rect.minX(), rect.minY(), rect.width(), rect.height(), entry.color(), 1.0f); + } else { + ScreenSpaceOverlay2D.drawFrame(renderer, rect, entry.color()); + } + } + + if (matteMode) { + DropLabelEntry label = entry.matteLabel(); + if (label == null) return; + MatteHudStyle.drawPlate(renderer, label.x(), label.y(), label.width(), label.height(), 2.0f, 1.0f); + if (label.icon()) { + renderer.item(label.stack(), label.iconX(), label.iconY(), MATTE_ICON_SCALE, 0, Renderer2D.ITEM_OVERLAY_NONE, null); + } + renderSingleDropLabel(labelRenderer, label, textScale); + } else { + ScreenSpaceOverlay2D.LabelEntry label = entry.label(); + if (label == null) return; + if (textShadow) { + ScreenSpaceOverlay2D.renderLabelBackplate(renderer, label); + } + renderSingleLabel(labelRenderer, label, false, textScale); + } + + Renderer2D.flushBatch(Renderer2D.FlushReason.EXPLICIT); + } + + static void renderSingleDropLabel(TextRenderer labelRenderer, DropLabelEntry label, double textScale) { + boolean renderStarted = false; + if (!labelRenderer.isBuilding()) { + labelRenderer.begin(textScale); + renderStarted = true; + } + try { + labelRenderer.render(label.text(), label.textX(), label.textY(), new RenderColor(label.color()), false); + } finally { + if (renderStarted) labelRenderer.end(); + } + } + + static void renderSingleLabel(TextRenderer labelRenderer, + ScreenSpaceOverlay2D.LabelEntry label, + boolean shadow, + double textScale) { + boolean renderStarted = false; + if (!labelRenderer.isBuilding()) { + labelRenderer.begin(textScale); + renderStarted = true; + } + try { + ScreenSpaceOverlay2D.renderLabels(labelRenderer, List.of(label), shadow); + } finally { + if (renderStarted) labelRenderer.end(); + } + } + + static DropLabelEntry createMatteDropLabel(TextRenderer labelRenderer, + ItemStack stack, + String text, + int color, + ScreenSpaceOverlay2D.ScreenRect rect, + boolean icon) { + double textWidth = labelRenderer.getWidth(text, false); + double textHeight = labelRenderer.getHeight(false); + double iconBlock = icon ? MATTE_ICON_SIZE + MATTE_ICON_GAP : 0.0; + double width = textWidth + iconBlock + MATTE_LABEL_PAD_X * 2.0; + double height = Math.max(textHeight + MATTE_LABEL_PAD_Y * 2.0, icon ? MATTE_ICON_SIZE + MATTE_LABEL_PAD_Y * 2.0 : 0.0); + double x = rect.minX() + (rect.width() - width) * 0.5; + double y = rect.minY() - height - 4.0; + x = Math.floor(x + 0.5); + y = Math.floor(y + 0.5); + double iconX = x + MATTE_LABEL_PAD_X; + double iconY = y + (height - MATTE_ICON_SIZE) * 0.5; + double textX = x + MATTE_LABEL_PAD_X + iconBlock; + double textY = y + (height - textHeight) * 0.5; + return new DropLabelEntry(stack, text, x, y, width, height, icon, iconX, iconY, textX, textY, color); + } + + record DropOverlayEntry(int sortPriority, double distSq, ScreenSpaceOverlay2D.ScreenRect rect, int color, + DropLabelEntry matteLabel, ScreenSpaceOverlay2D.LabelEntry label) { + } + + record DropVanillaEntry(int sortPriority, double distSq, ScreenSpaceOverlay2D.LabelEntry label) { + } + + record DropLabelEntry(ItemStack stack, String text, double x, double y, double width, double height, + boolean icon, double iconX, double iconY, double textX, double textY, int color) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java index 3989ff0..fc16f43 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java @@ -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.module.modules.visuals.esp.render; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java index 7bb0145..b98e10a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java @@ -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.module.modules.visuals.esp.render; import dev.loki.lovisual.render.engine.RenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java index eca4995..1ae76f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java @@ -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.module.modules.visuals.esp.render; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java index 33b8c5e..6ba3c80 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java @@ -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.module.modules.visuals.esp.render; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java index 0a6ded4..ffe3b95 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java @@ -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.module.modules.visuals.esp.shader; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java index b259061..42652e5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java @@ -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.module.modules.visuals.esp.shader; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java index de5bf07..4b2a160 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java @@ -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.module.modules.visuals.esp.shader; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java index 72e902c..3417e43 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java @@ -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.module.modules.visuals.esp.shader; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java index 4c7e38f..80e0de1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java @@ -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.module.modules.visuals.fx; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java index 981b0e0..d996de7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java @@ -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.module.modules.visuals.fx; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java index 392d975..41b98c6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java @@ -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.module.modules.visuals.fx; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java index a2835b0..1d12cff 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java @@ -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.module.modules.visuals.fx; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java index d5df60f..2bf7070 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java @@ -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.module.modules.visuals.hats; import dev.loki.lovisual.config.common.CommonSettingSchemas; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java index 74cf30a..01a9cd2 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java @@ -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.module.modules.visuals.hats; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java index 9af0816..bc31db4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java @@ -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.module.modules.visuals.hats; import dev.loki.lovisual.features.relations.category.CategoryService; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java deleted file mode 100644 index 8f69839..0000000 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java +++ /dev/null @@ -1,603 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.hats; - -import dev.loki.lovisual.util.time.GameClock; - -import net.minecraft.client.Minecraft; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.EquipmentSlot; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.config.common.CommonSettingSchemas; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.relations.category.CategoryService; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.helpers.player.HeadAnchor; - -//todo Description -@ModuleInfo( - id = "tazikhat", - displayName = "TazikHat", - category = ModuleCategory.VISUALS -) -public final class TazikHat extends Module { - - private static final int SEGMENTS = 56; - private static final double RAINBOW_SPATIAL_SCALE = 12.0; - - private static final float BOWL_RIM_RADIUS = 0.42f; - private static final float BOWL_TOP_RADIUS = 0.245f; - private static final float BOWL_HEIGHT = 0.18f; - private static final float BOWL_RIM_WIDTH = 0.052f; - private final EnumValue renderTarget = - enumMode( - "render_target", - RenderTarget.BOTH, - RenderTarget.values() - ); - private final BooleanValue useRelations = - boolCommon( - "use_relations", - "use_relations", - CommonSettingSchemas.PLAYER_USE_RELATIONS, - true - ); - private final RGBAColorValue primaryColor = - visibleWhen(common( - color( - "primary_color", - "#CC17C765" - ), - CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey() - ), () -> !useRelations.get()); - private final EnumValue colorMode = - enumCommon( - "color_mode", - "color_mode", - CommonSettingSchemas.RENDER_COLOR_MODE, - ColorMode.STATIC, - ColorMode.values() - ); - private final RGBAColorValue secondaryColor = - visibleWhen(common( - color( - "secondary_color", - "#AA079C46" - ), - CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey() - ), this::usesSecondaryManualColor); - private final NumberValue colorSpeed = - numCommon( - "color_speed", - "color_speed", - CommonSettingSchemas.RENDER_COLOR_SPEED, - 18, - 2, - 54 - ); - private final NumberValue scale = - numCommon( - "scale", - "scale", - CommonSettingSchemas.RENDER_SCALE, - 1.15f, - 0.45f, - 2.0f - ); - private final BooleanValue depthTest = - boolCommon( - "depth_test", - "depth_test", - CommonSettingSchemas.RENDER_DEPTH_TEST, - true - ); - private final RGBAColorValue rimColor = - visibleWhen(color( - "rim_color", - "#EE52FFA0" - ), () -> !useRelations.get()); - private final NumberValue helmetScale = - num( - "helmet_scale", - 1.16f, - 1.0f, - 1.45f - ); - private final NumberValue headTopOffset = - num( - "head_top_offset", - 0.255f, - 0.05f, - 0.45f - ); - private final NumberValue headForwardOffset = - num( - "head_forward_offset", - 0.0f, - -0.25f, - 0.25f - ); - private final NumberValue headRightOffset = - num( - "head_right_offset", - 0.0f, - -0.25f, - 0.25f - ); - private final NumberValue wobbleStrength = - num( - "wobble_strength", - 1.0f, - 0.0f, - 2.5f - ); - private static void quadGradient(MeshBuilder mesh, - Vec3 p1, int c1, - Vec3 p2, int c2, - Vec3 p3, int c3, - Vec3 p4, int c4) { - mesh.ensureQuadCapacity(); - - putColorVertex(mesh, p1, c1); - int i1 = mesh.next(); - - putColorVertex(mesh, p2, c2); - int i2 = mesh.next(); - - putColorVertex(mesh, p3, c3); - int i3 = mesh.next(); - - putColorVertex(mesh, p4, c4); - int i4 = mesh.next(); - - mesh.quad(i1, i2, i3, i4); - } - - private static void triangle(MeshBuilder mesh, - Vec3 p1, - Vec3 p2, - Vec3 p3, - int argb) { - mesh.ensureTriCapacity(); - - putColorVertex(mesh, p1, argb); - int i1 = mesh.next(); - - putColorVertex(mesh, p2, argb); - int i2 = mesh.next(); - - putColorVertex(mesh, p3, argb); - int i3 = mesh.next(); - - mesh.triangle(i1, i2, i3); - } - - private static void lineGradient(MeshBuilder mesh, - Vec3 p1, - Vec3 p2, - int c1, - int c2) { - mesh.ensureLineCapacity(); - - putColorVertex(mesh, p1, c1); - int i1 = mesh.next(); - - putColorVertex(mesh, p2, c2); - int i2 = mesh.next(); - - mesh.line(i1, i2); - } - - private static void putColorVertex(MeshBuilder mesh, Vec3 p, int argb) { - mesh.vec3(p.x, p.y, p.z).color(red(argb), green(argb), blue(argb), alpha(argb)); - } - - private static int mix(int a, int b, float t) { - float clamped = AnimationUtility.clamp01(t); - - int aa = alpha(a); - int ar = red(a); - int ag = green(a); - int ab = blue(a); - - int ba = alpha(b); - int br = red(b); - int bg = green(b); - int bb = blue(b); - - int ca = Math.round(AnimationUtility.lerp(aa, ba, clamped)); - int cr = Math.round(AnimationUtility.lerp(ar, br, clamped)); - int cg = Math.round(AnimationUtility.lerp(ag, bg, clamped)); - int cb = Math.round(AnimationUtility.lerp(ab, bb, clamped)); - - return (ca << 24) | (cr << 16) | (cg << 8) | cb; - } - - private static int multiplyRgb(int argb, float factor) { - int a = alpha(argb); - int r = Mth.clamp(Math.round(red(argb) * factor), 0, 255); - int g = Mth.clamp(Math.round(green(argb) * factor), 0, 255); - int b = Mth.clamp(Math.round(blue(argb) * factor), 0, 255); - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int brighten(int argb, float factor) { - return multiplyRgb(argb, factor); - } - - private static int withAlpha(int argb, int alpha) { - int a = Mth.clamp(alpha, 0, 255); - return (a << 24) | (argb & 0x00FFFFFF); - } - - private static int red(int argb) { - return (argb >>> 16) & 255; - } - - private static int green(int argb) { - return (argb >>> 8) & 255; - } - - private static int blue(int argb) { - return argb & 255; - } - - private static int alpha(int argb) { - return (argb >>> 24) & 255; - } - - private static int alphaOr(int argb, int fallback) { - int alpha = alpha(argb); - return alpha <= 0 ? Mth.clamp(fallback, 0, 255) : alpha; - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.BEFORE_TRANSLUCENT; - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - - MeshBuilder tris = renderer.batch( - depthTest.get() - ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH - : LoVisualRenderPipelines.WORLD_COLORED, - depthTest.get() - ? Renderer3D.DepthMode.PRE_DEPTH - : Renderer3D.DepthMode.NONE - ); - - MeshBuilder lines = renderer.batch( - depthTest.get() - ? LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH - : LoVisualRenderPipelines.WORLD_COLORED_LINES, - depthTest.get() - ? Renderer3D.DepthMode.PRE_DEPTH - : Renderer3D.DepthMode.NONE - ); - - if (tris == null || lines == null) return; - - RenderBuffers buffers = new RenderBuffers(tris, lines); - - if (shouldRenderSelf() && !mc.options.getCameraType().isFirstPerson()) { - renderAttached(buffers, mc.player, tickDelta); - } - - if (shouldRenderPlayers()) { - for (Player player : mc.level.players()) { - if (player == null || player == mc.player || player.isRemoved()) continue; - if (!player.isAlive() || player.getHealth() <= 0.0f) continue; - - renderAttached(buffers, player, tickDelta); - } - } - } - - private boolean shouldRenderSelf() { - return renderTarget.get() == RenderTarget.SELF || renderTarget.get() == RenderTarget.BOTH; - } - - private boolean shouldRenderPlayers() { - return renderTarget.get() == RenderTarget.PLAYERS || renderTarget.get() == RenderTarget.BOTH; - } - - private boolean usesSecondaryManualColor() { - if (useRelations.get()) return false; - return AnimatedRenderColors.usesSecondary(animatedColorMode()); - } - - private void renderAttached(RenderBuffers buffers, Player player, float tickDelta) { - boolean helmet = hasHelmet(player); - float size = scale.get() * (helmet ? helmetScale.get() : 1.0f); - - HeadAnchor anchor = HeadAnchor.of( - player, - tickDelta, - headTopOffset.get(), - headForwardOffset.get(), - headRightOffset.get() - ); - - Palette palette = resolvePalette(player); - Wobble wobble = resolveWobble(player, tickDelta); - - buildTazik( - buffers, - anchor, - size, - 0.0f, - wobble.pitchDeg, - wobble.rollDeg, - palette, - 1.0f, - player.getId() - ); - } - - private Wobble resolveWobble(Player player, float tickDelta) { - float hurt = Math.max(0.0f, player.hurtTime - tickDelta); - float hit = AnimationUtility.easeOutBack(AnimationUtility.clamp01(hurt / 10.0f), 1.25f); - - float strength = wobbleStrength.get(); - float time = (player.tickCount + tickDelta) * 0.35f + player.getId() * 0.17f; - - float hitAmp = hit * 5.0f * strength; - float idleAmp = 1.3f * strength; - - float pitch = Mth.sin(time * 3.7f) * hitAmp - + Mth.sin(time * 1.2f) * idleAmp; - - float roll = Mth.cos(time * 3.1f) * hitAmp - + Mth.cos(time * 0.9f) * idleAmp; - - return new Wobble(pitch, roll); - } - - private void buildTazik(RenderBuffers buffers, - HeadAnchor anchor, - float size, - float yawDeg, - float pitchDeg, - float rollDeg, - Palette palette, - float alphaMul, - int seed) { - float rimRadius = BOWL_RIM_RADIUS * size; - float topRadius = BOWL_TOP_RADIUS * size; - float height = BOWL_HEIGHT * size; - float rimWidth = BOWL_RIM_WIDTH * size; - - double rimOuterY = 0.055 * size; - double rimInnerY = 0.073 * size; - double topOuterY = height * 0.82; - double topInnerY = height; - - Vec3[] rimOuter = new Vec3[SEGMENTS]; - Vec3[] rimInner = new Vec3[SEGMENTS]; - Vec3[] topOuter = new Vec3[SEGMENTS]; - Vec3[] topInner = new Vec3[SEGMENTS]; - - for (int i = 0; i < SEGMENTS; i++) { - float angle = (float) ((Math.PI * 2.0 * i) / SEGMENTS); - - float sx = Mth.sin(angle); - float sz = Mth.cos(angle); - - float rimWave = 1.0f + 0.010f * Mth.sin(angle * 4.0f + seed * 0.013f); - float topWave = 1.0f + 0.006f * Mth.cos(angle * 3.0f + seed * 0.011f); - - float outerRim = rimRadius * rimWave; - float innerRim = Math.max(0.02f, outerRim - rimWidth); - - float outerTop = topRadius * topWave; - float innerTop = Math.max(0.02f, outerTop - rimWidth * 0.35f); - - rimOuter[i] = anchor.localRotated(sx * outerRim, rimOuterY, sz * outerRim, yawDeg, pitchDeg, rollDeg); - rimInner[i] = anchor.localRotated(sx * innerRim, rimInnerY, sz * innerRim, yawDeg, pitchDeg, rollDeg); - topOuter[i] = anchor.localRotated(sx * outerTop, topOuterY, sz * outerTop, yawDeg, pitchDeg, rollDeg); - topInner[i] = anchor.localRotated(sx * innerTop, topInnerY, sz * innerTop, yawDeg, pitchDeg, rollDeg); - } - - Vec3 topCenter = anchor.localRotated( - 0.0, - topInnerY + 0.002 * size, - 0.0, - yawDeg, - pitchDeg, - rollDeg - ); - - drawTazikMesh(buffers, rimOuter, rimInner, topOuter, topInner, topCenter, palette, alphaMul); - } - - private void drawTazikMesh(RenderBuffers buffers, - Vec3[] rimOuter, - Vec3[] rimInner, - Vec3[] topOuter, - Vec3[] topInner, - Vec3 topCenter, - Palette palette, - float alphaMul) { - for (int i = 0; i < SEGMENTS; i++) { - int next = (i + 1) % SEGMENTS; - - int rimLow1 = colorAt(palette, i, 0.76f, alphaMul); - int rimLow2 = colorAt(palette, next, 0.76f, alphaMul); - - int wallTop1 = colorAt(palette, i, 1.02f, alphaMul); - int wallTop2 = colorAt(palette, next, 1.02f, alphaMul); - - int innerTop1 = colorAt(palette, i, 0.66f, alphaMul * 0.90f); - int innerTop2 = colorAt(palette, next, 0.66f, alphaMul * 0.90f); - - int innerLow1 = colorAt(palette, i, 0.50f, alphaMul * 0.82f); - int innerLow2 = colorAt(palette, next, 0.50f, alphaMul * 0.82f); - - int rim1 = rimColorAt(palette, i, alphaMul); - int rim2 = rimColorAt(palette, next, alphaMul); - - int topCenterColor = colorAt(palette, i, 1.10f, alphaMul * 0.95f); - - quadGradient(buffers.tris, rimOuter[i], rimLow1, rimOuter[next], rimLow2, topOuter[next], wallTop2, topOuter[i], wallTop1); - quadGradient(buffers.tris, topInner[i], innerTop1, topInner[next], innerTop2, rimInner[next], innerLow2, rimInner[i], innerLow1); - quadGradient(buffers.tris, rimInner[i], rim1, rimInner[next], rim2, rimOuter[next], rim2, rimOuter[i], rim1); - - triangle(buffers.tris, topCenter, topInner[i], topInner[next], topCenterColor); - - lineGradient(buffers.lines, rimOuter[i], rimOuter[next], rim1, rim2); - lineGradient(buffers.lines, topOuter[i], topOuter[next], wallTop1, wallTop2); - } - } - - private Palette resolvePalette(Player player) { - if (useRelations.get()) { - int relation = CategoryService.getColor(player); - int primary = withAlpha(relation, alphaOr(primaryColor.getArgb(), 255)); - int secondary = withAlpha(AnimatedRenderColors.analogousColor(relation), alphaOr(secondaryColor.getArgb(), 170)); - int rim = withAlpha(brighten(relation, 1.35f), alphaOr(rimColor.getArgb(), 238)); - - return new Palette(primary, secondary, rim); - } - - return new Palette( - primaryColor.getArgb(), - secondaryColor.getArgb(), - rimColor.getArgb() - ); - } - - private int colorAt(Palette palette, int index, float verticalLight, float alphaMul) { - int dynamic = getDynamicColor( - palette.primary, - palette.secondary, - animatedColorMode(), - colorSpeed.get(), - index - ); - - float circumference = 0.5f + 0.5f * Mth.sin(index * 0.47f + GameClock.millis() * 0.0007f); - int mixed = multiplyRgb(dynamic, verticalLight + circumference * 0.16f); - - return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul)); - } - - private int rimColorAt(Palette palette, int index, float alphaMul) { - int dynamic = getDynamicColor( - palette.rim, - palette.secondary, - animatedColorMode(), - colorSpeed.get(), - index + 120 - ); - - float t = 0.5f + 0.5f * Mth.sin(index * 0.71f + GameClock.millis() * 0.0011f); - int mixed = mix(dynamic, brighten(palette.primary, 1.35f), t); - - return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul)); - } - - private AnimatedRenderColors.Mode animatedColorMode() { - return switch (colorMode.get()) { - case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW; - case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; - case SKY -> AnimatedRenderColors.Mode.SKY; - case FADE -> AnimatedRenderColors.Mode.FADE; - case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR; - case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS; - case THEME -> AnimatedRenderColors.Mode.THEME; - case STATIC -> AnimatedRenderColors.Mode.STATIC; - }; - } - - private int getDynamicColor(int primary, - int secondary, - AnimatedRenderColors.Mode mode, - int speed, - int index) { - int spatialIndex = Math.floorMod((int) Math.round(index * RAINBOW_SPATIAL_SCALE), 3600); - return AnimatedRenderColors.resolve(mode, speed, spatialIndex, primary, secondary, true); - } - - private boolean hasHelmet(Player player) { - ItemStack stack = player.getItemBySlot(EquipmentSlot.HEAD); - return !stack.isEmpty() && ( - stack.is(Items.LEATHER_HELMET) - || stack.is(Items.CHAINMAIL_HELMET) - || stack.is(Items.IRON_HELMET) - || stack.is(Items.GOLDEN_HELMET) - || stack.is(Items.DIAMOND_HELMET) - || stack.is(Items.NETHERITE_HELMET) - ); - } - - public enum RenderTarget implements EnumValue.IdProvider { - SELF("self"), - PLAYERS("players"), - BOTH("both"); - - private final String id; - - RenderTarget(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum ColorMode implements EnumValue.IdProvider { - STATIC("static"), - RAINBOW("rainbow"), - LIGHT_RAINBOW("light_rainbow"), - SKY("sky"), - FADE("fade"), - DOUBLE_COLOR("double_color"), - ANALOGOUS("analogous"), - THEME("theme"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private record RenderBuffers(MeshBuilder tris, MeshBuilder lines) { - } - - private record Palette(int primary, int secondary, int rim) { - } - - private record Wobble(float pitchDeg, float rollDeg) { - } -} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHat.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHat.java new file mode 100644 index 0000000..667e4fa --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHat.java @@ -0,0 +1,179 @@ +package dev.loki.lovisual.features.module.modules.visuals.hats.tazik; + +import dev.loki.lovisual.config.common.CommonSettingSchemas; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.player.HeadAnchor; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Player; + +/** + * Module facade for the Tazik bowl hat. Owns the ClickGui settings, decides + * which players should wear the hat each frame and adapts settings to + * {@link TazikHatModel} (geometry) and {@link TazikHatPalette} (colour math). + */ +@ModuleInfo( + id = "tazikhat", + displayName = "TazikHat", + category = ModuleCategory.VISUALS +) +public final class TazikHat extends Module { + + /** Which entities receive the hat. */ + private final EnumValue renderTarget = enumMode("render_target", RenderTarget.BOTH, RenderTarget.values()); + /** Tint by player relationship category instead of manual colors. */ + private final BooleanValue useRelations = boolCommon("use_relations", "use_relations", CommonSettingSchemas.PLAYER_USE_RELATIONS, true); + /** Manual primary bowl color (only when relations are off). */ + private final RGBAColorValue primaryColor = visibleWhen(common(color("primary_color", "#CC17C765"), CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()), () -> !useRelations.get()); + /** Animated colour mode applied around the bowl. */ + private final EnumValue colorMode = enumCommon("color_mode", "color_mode", CommonSettingSchemas.RENDER_COLOR_MODE, ColorMode.STATIC, ColorMode.values()); + /** Manual secondary color used by animated modes that need two stops. */ + private final RGBAColorValue secondaryColor = visibleWhen(common(color("secondary_color", "#AA079C46"), CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()), this::usesSecondaryManualColor); + /** Speed multiplier for animated colour modes. */ + private final NumberValue colorSpeed = numCommon("color_speed", "color_speed", CommonSettingSchemas.RENDER_COLOR_SPEED, 18, 2, 54); + /** Overall hat size (multiplied by helmet scale if the target wears a helmet). */ + private final NumberValue scale = numCommon("scale", "scale", CommonSettingSchemas.RENDER_SCALE, 1.15f, 0.45f, 2.0f); + /** Toggle depth-tested rendering vs. always-on-top. */ + private final BooleanValue depthTest = boolCommon("depth_test", "depth_test", CommonSettingSchemas.RENDER_DEPTH_TEST, true); + /** Rim highlight colour. */ + private final RGBAColorValue rimColor = visibleWhen(color("rim_color", "#EE52FFA0"), () -> !useRelations.get()); + /** Size multiplier when the target is wearing a helmet. */ + private final NumberValue helmetScale = num("helmet_scale", 1.16f, 1.0f, 1.45f); + /** Vertical offset from the head anchor to the bowl base. */ + private final NumberValue headTopOffset = num("head_top_offset", 0.255f, 0.05f, 0.45f); + /** Forward offset from the head anchor. */ + private final NumberValue headForwardOffset = num("head_forward_offset", 0.0f, -0.25f, 0.25f); + /** Right offset from the head anchor. */ + private final NumberValue headRightOffset = num("head_right_offset", 0.0f, -0.25f, 0.25f); + /** Amplitude of the idle / hit wobble. */ + private final NumberValue wobbleStrength = num("wobble_strength", 1.0f, 0.0f, 2.5f); + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.BEFORE_TRANSLUCENT; + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || mc.player == null) return; + + MeshBuilder tris = renderer.batch( + depthTest.get() ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH : LoVisualRenderPipelines.WORLD_COLORED, + depthTest.get() ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.NONE + ); + MeshBuilder lines = renderer.batch( + depthTest.get() ? LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH : LoVisualRenderPipelines.WORLD_COLORED_LINES, + depthTest.get() ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.NONE + ); + if (tris == null || lines == null) return; + + if (shouldRenderSelf() && !mc.options.getCameraType().isFirstPerson()) { + renderAttached(tris, lines, mc.player, tickDelta); + } + if (shouldRenderPlayers()) { + for (Player player : mc.level.players()) { + if (player == null || player == mc.player || player.isRemoved()) continue; + if (!player.isAlive() || player.getHealth() <= 0.0f) continue; + renderAttached(tris, lines, player, tickDelta); + } + } + } + + /** Position, colour and draw a single hat for one player. */ + private void renderAttached(MeshBuilder tris, MeshBuilder lines, Player player, float tickDelta) { + float size = scale.get() * (TazikHatModel.hasHelmet(player) ? helmetScale.get() : 1.0f); + HeadAnchor anchor = HeadAnchor.of(player, tickDelta, headTopOffset.get(), headForwardOffset.get(), headRightOffset.get()); + TazikHatPalette.Palette palette = TazikHatPalette.resolve( + player, useRelations.get(), + primaryColor.getArgb(), secondaryColor.getArgb(), rimColor.getArgb() + ); + AnimatedRenderColors.Mode mode = animatedColorMode(); + int speed = colorSpeed.get(); + TazikHatModel.Wobble wobble = TazikHatModel.resolveWobble(player, tickDelta, wobbleStrength.get()); + TazikHatModel.build( + tris, lines, anchor, size, 0.0f, wobble.pitchDeg(), wobble.rollDeg(), + (i, light, alphaMul) -> TazikHatPalette.colorAt(palette, i, light, alphaMul, mode, speed), + (i, light, alphaMul) -> TazikHatPalette.rimColorAt(palette, i, alphaMul, mode, speed), + 1.0f, player.getId() + ); + } + + private boolean shouldRenderSelf() { + return renderTarget.get() == RenderTarget.SELF || renderTarget.get() == RenderTarget.BOTH; + } + + private boolean shouldRenderPlayers() { + return renderTarget.get() == RenderTarget.PLAYERS || renderTarget.get() == RenderTarget.BOTH; + } + + private boolean usesSecondaryManualColor() { + if (useRelations.get()) return false; + return AnimatedRenderColors.usesSecondary(animatedColorMode()); + } + + /** Bridge from the module-local {@link ColorMode} to the shared animated-colour enum. */ + private AnimatedRenderColors.Mode animatedColorMode() { + return switch (colorMode.get()) { + case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW; + case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; + case SKY -> AnimatedRenderColors.Mode.SKY; + case FADE -> AnimatedRenderColors.Mode.FADE; + case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR; + case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS; + case THEME -> AnimatedRenderColors.Mode.THEME; + case STATIC -> AnimatedRenderColors.Mode.STATIC; + }; + } + + /** Which entities should wear the hat. */ + public enum RenderTarget implements EnumValue.IdProvider { + SELF("self"), + PLAYERS("players"), + BOTH("both"); + + private final String id; + + RenderTarget(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + /** Colour animation style applied around the bowl. */ + public enum ColorMode implements EnumValue.IdProvider { + STATIC("static"), + RAINBOW("rainbow"), + LIGHT_RAINBOW("light_rainbow"), + SKY("sky"), + FADE("fade"), + DOUBLE_COLOR("double_color"), + ANALOGOUS("analogous"), + THEME("theme"); + + private final String id; + + ColorMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatModel.java new file mode 100644 index 0000000..abc4f5f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatModel.java @@ -0,0 +1,179 @@ +package dev.loki.lovisual.features.module.modules.visuals.hats.tazik; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.player.HeadAnchor; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.Vec3; + +/** + * Head-mounted bowl ("tazik") geometry: rim ellipse + inner top ellipse joined + * by a wall, with a filled top disc and a highlighted rim ring. Colour is + * supplied by the caller through {@link Shade} callbacks so this class stays + * free of config state. Also hosts the two small motion / equipment helpers the + * facade needs per player. + */ +public final class TazikHatModel { + + private TazikHatModel() { + } + + /** Number of quads around the bowl; higher values cost more vertices. */ + static final int SEGMENTS = 56; + + static final float BOWL_RIM_RADIUS = 0.42f; + static final float BOWL_TOP_RADIUS = 0.245f; + static final float BOWL_HEIGHT = 0.18f; + static final float BOWL_RIM_WIDTH = 0.052f; + + /** Per-vertex colour provider: {@code (segmentIndex, verticalLight, alphaMul) -> ARGB}. */ + public interface Shade { + int color(int index, float verticalLight, float alphaMul); + } + + /** Idle / hit-driven pitch-and-roll wobble applied to the hat. */ + public record Wobble(float pitchDeg, float rollDeg) { + } + + /** Sum of an idle sway and an easeOutBack hit impulse, both scaled by {@code strength}. */ + public static Wobble resolveWobble(Player player, float tickDelta, float strength) { + float hurt = Math.max(0.0f, player.hurtTime - tickDelta); + float hit = AnimationUtility.easeOutBack(AnimationUtility.clamp01(hurt / 10.0f), 1.25f); + float time = (player.tickCount + tickDelta) * 0.35f + player.getId() * 0.17f; + float hitAmp = hit * 5.0f * strength; + float idleAmp = 1.3f * strength; + float pitch = Mth.sin(time * 3.7f) * hitAmp + Mth.sin(time * 1.2f) * idleAmp; + float roll = Mth.cos(time * 3.1f) * hitAmp + Mth.cos(time * 0.9f) * idleAmp; + return new Wobble(pitch, roll); + } + + /** Any real helmet (leather..netherite) — used to slightly enlarge the hat on top. */ + public static boolean hasHelmet(Player player) { + ItemStack stack = player.getItemBySlot(EquipmentSlot.HEAD); + return !stack.isEmpty() && ( + stack.is(Items.LEATHER_HELMET) + || stack.is(Items.CHAINMAIL_HELMET) + || stack.is(Items.IRON_HELMET) + || stack.is(Items.GOLDEN_HELMET) + || stack.is(Items.DIAMOND_HELMET) + || stack.is(Items.NETHERITE_HELMET) + ); + } + + /** Emit triangles + outline lines for one bowl, anchored to the player's head. */ + public static void build(MeshBuilder tris, + MeshBuilder lines, + HeadAnchor anchor, + float size, + float yawDeg, + float pitchDeg, + float rollDeg, + Shade bowl, + Shade rim, + float alphaMul, + int seed) { + float rimRadius = BOWL_RIM_RADIUS * size; + float topRadius = BOWL_TOP_RADIUS * size; + float height = BOWL_HEIGHT * size; + float rimWidth = BOWL_RIM_WIDTH * size; + double rimOuterY = 0.055 * size; + double rimInnerY = 0.073 * size; + double topOuterY = height * 0.82; + double topInnerY = height; + + Vec3[] rimOuter = new Vec3[SEGMENTS]; + Vec3[] rimInner = new Vec3[SEGMENTS]; + Vec3[] topOuter = new Vec3[SEGMENTS]; + Vec3[] topInner = new Vec3[SEGMENTS]; + + for (int i = 0; i < SEGMENTS; i++) { + float angle = (float) ((Math.PI * 2.0 * i) / SEGMENTS); + float sx = Mth.sin(angle); + float sz = Mth.cos(angle); + float rimWave = 1.0f + 0.010f * Mth.sin(angle * 4.0f + seed * 0.013f); + float topWave = 1.0f + 0.006f * Mth.cos(angle * 3.0f + seed * 0.011f); + float outerRim = rimRadius * rimWave; + float innerRim = Math.max(0.02f, outerRim - rimWidth); + float outerTop = topRadius * topWave; + float innerTop = Math.max(0.02f, outerTop - rimWidth * 0.35f); + rimOuter[i] = anchor.localRotated(sx * outerRim, rimOuterY, sz * outerRim, yawDeg, pitchDeg, rollDeg); + rimInner[i] = anchor.localRotated(sx * innerRim, rimInnerY, sz * innerRim, yawDeg, pitchDeg, rollDeg); + topOuter[i] = anchor.localRotated(sx * outerTop, topOuterY, sz * outerTop, yawDeg, pitchDeg, rollDeg); + topInner[i] = anchor.localRotated(sx * innerTop, topInnerY, sz * innerTop, yawDeg, pitchDeg, rollDeg); + } + + Vec3 topCenter = anchor.localRotated(0.0, topInnerY + 0.002 * size, 0.0, yawDeg, pitchDeg, rollDeg); + draw(tris, lines, rimOuter, rimInner, topOuter, topInner, topCenter, bowl, rim, alphaMul); + } + + private static void draw(MeshBuilder tris, + MeshBuilder lines, + Vec3[] rimOuter, + Vec3[] rimInner, + Vec3[] topOuter, + Vec3[] topInner, + Vec3 topCenter, + Shade bowl, + Shade rim, + float alphaMul) { + for (int i = 0; i < SEGMENTS; i++) { + int next = (i + 1) % SEGMENTS; + int rimLow1 = bowl.color(i, 0.76f, alphaMul); + int rimLow2 = bowl.color(next, 0.76f, alphaMul); + int wallTop1 = bowl.color(i, 1.02f, alphaMul); + int wallTop2 = bowl.color(next, 1.02f, alphaMul); + int innerTop1 = bowl.color(i, 0.66f, alphaMul * 0.90f); + int innerTop2 = bowl.color(next, 0.66f, alphaMul * 0.90f); + int innerLow1 = bowl.color(i, 0.50f, alphaMul * 0.82f); + int innerLow2 = bowl.color(next, 0.50f, alphaMul * 0.82f); + int rim1 = rim.color(i, 1.0f, alphaMul); + int rim2 = rim.color(next, 1.0f, alphaMul); + int topCenterColor = bowl.color(i, 1.10f, alphaMul * 0.95f); + + quadGradient(tris, rimOuter[i], rimLow1, rimOuter[next], rimLow2, topOuter[next], wallTop2, topOuter[i], wallTop1); + quadGradient(tris, topInner[i], innerTop1, topInner[next], innerTop2, rimInner[next], innerLow2, rimInner[i], innerLow1); + quadGradient(tris, rimInner[i], rim1, rimInner[next], rim2, rimOuter[next], rim2, rimOuter[i], rim1); + triangle(tris, topCenter, topInner[i], topInner[next], topCenterColor); + lineGradient(lines, rimOuter[i], rimOuter[next], rim1, rim2); + lineGradient(lines, topOuter[i], topOuter[next], wallTop1, wallTop2); + } + } + + private static void quadGradient(MeshBuilder mesh, + Vec3 p1, int c1, + Vec3 p2, int c2, + Vec3 p3, int c3, + Vec3 p4, int c4) { + mesh.ensureQuadCapacity(); + int i1 = putColorVertex(mesh, p1, c1); + int i2 = putColorVertex(mesh, p2, c2); + int i3 = putColorVertex(mesh, p3, c3); + int i4 = putColorVertex(mesh, p4, c4); + mesh.quad(i1, i2, i3, i4); + } + + private static void triangle(MeshBuilder mesh, Vec3 p1, Vec3 p2, Vec3 p3, int argb) { + mesh.ensureTriCapacity(); + int i1 = putColorVertex(mesh, p1, argb); + int i2 = putColorVertex(mesh, p2, argb); + int i3 = putColorVertex(mesh, p3, argb); + mesh.triangle(i1, i2, i3); + } + + private static void lineGradient(MeshBuilder mesh, Vec3 p1, Vec3 p2, int c1, int c2) { + mesh.ensureLineCapacity(); + int i1 = putColorVertex(mesh, p1, c1); + int i2 = putColorVertex(mesh, p2, c2); + mesh.line(i1, i2); + } + + private static int putColorVertex(MeshBuilder mesh, Vec3 p, int argb) { + mesh.vec3(p.x, p.y, p.z) + .color((argb >>> 16) & 255, (argb >>> 8) & 255, argb & 255, (argb >>> 24) & 255); + return mesh.next(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatPalette.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatPalette.java new file mode 100644 index 0000000..be462f0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/tazik/TazikHatPalette.java @@ -0,0 +1,127 @@ +package dev.loki.lovisual.features.module.modules.visuals.hats.tazik; + +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; + +/** + * Colour math for the Tazik hat: palette resolution (relations vs. manual) plus + * per-vertex shading (bowl and rim). All state is passed in explicitly — the + * facade reads its settings and hands the unwrapped values to these statics. + */ +public final class TazikHatPalette { + + private TazikHatPalette() { + } + + /** Spatial frequency of the rainbow cycle around the bowl circumference. */ + private static final double RAINBOW_SPATIAL_SCALE = 12.0; + + /** Base ARGB triple used to shade every vertex of the hat. */ + public record Palette(int primary, int secondary, int rim) { + } + + /** Relations-driven palette if {@code useRelations}; otherwise straight from settings. */ + public static Palette resolve(Player player, + boolean useRelations, + int primaryArgb, + int secondaryArgb, + int rimArgb) { + if (useRelations) { + int relation = CategoryService.getColor(player); + int primary = withAlpha(relation, alphaOr(primaryArgb, 255)); + int secondary = withAlpha(AnimatedRenderColors.analogousColor(relation), alphaOr(secondaryArgb, 170)); + int rim = withAlpha(brighten(relation, 1.35f), alphaOr(rimArgb, 238)); + return new Palette(primary, secondary, rim); + } + return new Palette(primaryArgb, secondaryArgb, rimArgb); + } + + /** Bowl vertex shade: animated base, then vertical + circumference light, then alpha. */ + public static int colorAt(Palette palette, + int index, + float verticalLight, + float alphaMul, + AnimatedRenderColors.Mode mode, + int speed) { + int dynamic = getDynamicColor(palette.primary, palette.secondary, mode, speed, index); + float circumference = 0.5f + 0.5f * Mth.sin(index * 0.47f + GameClock.millis() * 0.0007f); + int mixed = multiplyRgb(dynamic, verticalLight + circumference * 0.16f); + return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul)); + } + + /** Rim vertex shade: animated base offset by 120°, then lerp toward a brightened primary. */ + public static int rimColorAt(Palette palette, + int index, + float alphaMul, + AnimatedRenderColors.Mode mode, + int speed) { + int dynamic = getDynamicColor(palette.rim, palette.secondary, mode, speed, index + 120); + float t = 0.5f + 0.5f * Mth.sin(index * 0.71f + GameClock.millis() * 0.0011f); + int mixed = mix(dynamic, brighten(palette.primary, 1.35f), t); + return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul)); + } + + /** Maps a vertex index around the bowl to a stable 0..3600 angle for AnimatedRenderColors. */ + private static int getDynamicColor(int primary, + int secondary, + AnimatedRenderColors.Mode mode, + int speed, + int index) { + int spatialIndex = Math.floorMod((int) Math.round(index * RAINBOW_SPATIAL_SCALE), 3600); + return AnimatedRenderColors.resolve(mode, speed, spatialIndex, primary, secondary, true); + } + + /** ARGB-safe linear blend of two colours, alpha included. */ + static int mix(int a, int b, float t) { + float clamped = AnimationUtility.clamp01(t); + int ca = Math.round(AnimationUtility.lerp(alpha(a), alpha(b), clamped)); + int cr = Math.round(AnimationUtility.lerp(red(a), red(b), clamped)); + int cg = Math.round(AnimationUtility.lerp(green(a), green(b), clamped)); + int cb = Math.round(AnimationUtility.lerp(blue(a), blue(b), clamped)); + return (ca << 24) | (cr << 16) | (cg << 8) | cb; + } + + /** Scale RGB channels by {@code factor}, keeping alpha intact. */ + static int multiplyRgb(int argb, float factor) { + int a = alpha(argb); + int r = Mth.clamp(Math.round(red(argb) * factor), 0, 255); + int g = Mth.clamp(Math.round(green(argb) * factor), 0, 255); + int b = Mth.clamp(Math.round(blue(argb) * factor), 0, 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + static int brighten(int argb, float factor) { + return multiplyRgb(argb, factor); + } + + static int withAlpha(int argb, int alpha) { + int a = Mth.clamp(alpha, 0, 255); + return (a << 24) | (argb & 0x00FFFFFF); + } + + static int red(int argb) { + return (argb >>> 16) & 255; + } + + static int green(int argb) { + return (argb >>> 8) & 255; + } + + static int blue(int argb) { + return argb & 255; + } + + static int alpha(int argb) { + return (argb >>> 24) & 255; + } + + /** Fall back to {@code fallback} when the source colour is fully transparent. */ + static int alphaOr(int argb, int fallback) { + int a = alpha(argb); + return a <= 0 ? Mth.clamp(fallback, 0, 255) : a; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java index d1d3cb7..ac3cae3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java @@ -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.module.modules.visuals.items; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java index a6d33fd..449e711 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java @@ -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.module.modules.visuals.items; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java index 635cf1e..a1e0064 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java @@ -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.module.modules.visuals.items.held; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java index 6c841fd..f3d07a3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java @@ -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.module.modules.visuals.items.held; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java index c4192c0..3a41c47 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java @@ -1,35 +1,32 @@ package dev.loki.lovisual.features.module.modules.visuals.lyrics; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.mode.LyricModes.AnimationMode; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.mode.LyricModes.ColorMode; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.mode.LyricModes.FontMode; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.mode.LyricModes.SplitMode; import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.theme.Theme; import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.engine.text.font.info.FontInfo; import dev.loki.lovisual.render.engine.text.font.Fonts; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.media.session.MediaSessionService; -import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LrcLibClient; -import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLine; -import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLineSplitter; -import net.minecraft.client.Camera; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.world.phys.Vec3; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ThreadLocalRandom; -import dev.loki.lovisual.features.module.modules.visuals.lyrics.LyricParticle3D; +/** + * KineticLyrics: renders the current song's lyrics as animated 3D particles floating in the + * world around the player. The module itself only declares settings and forwards lifecycle / + * render callbacks; playback tracking lives in {@link LyricPlayback}, particle placement and + * styling in {@link LyricStage}, and the selectable modes in {@code LyricModes}. + */ @ModuleInfo( id = "kineticlyrics", displayName = "KineticLyrics", @@ -39,322 +36,49 @@ import dev.loki.lovisual.features.module.modules.visuals.lyrics.LyricParticle3D; ) public final class KineticLyrics extends Module { - /** Прыжок вперёд больше этого — seek: переиндексировать очередь, не спавнить пропущенные строки залпом. */ - private static final long RESYNC_FORWARD_MS = 1500L; - /** Регрессия больше этого — пауза/откат: строки после индекса уже не догнать, нужен resync. */ - private static final long RESYNC_BACKWARD_MS = 750L; - private final EnumValue animation = enumMode("animation", AnimationMode.LYRIC_FLOW, AnimationMode.values()); - private final EnumValue colorMode = + final EnumValue colorMode = enumMode("color_mode", ColorMode.THEME, ColorMode.values()); - private final RGBAColorValue staticColor = + final RGBAColorValue staticColor = visibleWhen(color("static_color", "#FFFFFFFF"), () -> colorMode.get() == ColorMode.STATIC); - private final EnumValue fontMode = + final EnumValue fontMode = enumMode("font", FontMode.ONEST_BOLD, FontMode.values()); - private final EnumValue splitMode = + final EnumValue splitMode = enumMode("split_mode", SplitMode.SMART_SPLIT, SplitMode.values()); - private final NumberValue maxLines = + final NumberValue maxLines = num("max_lines", 3, 1, 6); - private final NumberValue minDistance = + final NumberValue minDistance = num("min_distance", 2.0f, 1.2f, 4.0f); - private final NumberValue maxDistance = + final NumberValue maxDistance = num("max_distance", 4.5f, 2.0f, 7.0f); private final NumberValue textScale = num("text_scale", 2.5f, 0.5f, 5.0f); - private final NumberValue arcSpread = + final NumberValue arcSpread = num("arc_spread", 70.0f, 20.0f, 85.0f); - private final NumberValue floatHeight = - num("float_height", 0.5f, 0.1f, 2.0f); - private final NumberValue timeOffsetMs = + final NumberValue timeOffsetMs = num("time_offset_ms", -1000, -5000, 5000); - private final BooleanValue debugMode = + final BooleanValue debugMode = bool("debug", false); - private final List rawLyrics = new ArrayList<>(); - private final List lyricsQueue = new ArrayList<>(); - private final List activeParticles = new CopyOnWriteArrayList<>(); - private int currentLyricIndex = 0; - - private String lastTrackKey = ""; - private String currentTrackTitle = ""; - private String currentTrackArtist = ""; - private long lastAudioTimeMs = -1L; - private boolean isPlaying = false; + private final LyricStage stage = new LyricStage(this); + private final LyricPlayback playback = new LyricPlayback(this, stage); @Override public void onEnable() { - resetPlaybackState(); + playback.reset(); } @Override public void onDisable() { - resetPlaybackState(); - } - - private void resetPlaybackState() { - activeParticles.clear(); - rawLyrics.clear(); - lyricsQueue.clear(); - lastTrackKey = ""; - currentTrackTitle = ""; - currentTrackArtist = ""; - currentLyricIndex = 0; - lastAudioTimeMs = -1L; - isPlaying = false; + playback.reset(); } @Override public void onTick() { Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null) return; - syncPlaybackTime(); - } - - private void syncPlaybackTime() { - MediaSessionService.Snapshot snapshot = MediaSessionService.get().snapshot(); - if (!snapshot.hasSession() || snapshot.title().isBlank()) { - if (isPlaying) { - isPlaying = false; - activeParticles.clear(); - } - return; - } - - String title = snapshot.title(); - String artist = snapshot.artist(); - String key = (title + " - " + artist).toLowerCase(); - - if (!key.equalsIgnoreCase(lastTrackKey)) { - loadTrack(title, artist, key); - return; - } - - if (!isPlaying && !lyricsQueue.isEmpty()) { - isPlaying = true; - } - if (lyricsQueue.isEmpty()) return; - - // Позиция плеера (MPRIS/SMTC) — это внутренний таймер декодера: он - // опережает слышимый вывод на латентность аудио-тракта (0.3-1с+, до - // нескольких секунд через Bluetooth), а LRC-таймкоды привязаны к - // слышимому звуку. Дефолтный сдвиг назад компенсирует это (решение - // совпадает с lrcsnc: у автора ~0.75с задержки для mpd). - long audioTimeMs = Math.max(0L, snapshot.predictedPositionMs() + timeOffsetMs.get()); - - // Любая ощутимая регрессия (pause через truncate-в-секунды MPRIS, короткий seek) — - // иначе индекс остаётся впереди звука и строки молча скипаются до догонки. - if (lastAudioTimeMs >= 0 && audioTimeMs < lastAudioTimeMs - RESYNC_BACKWARD_MS) { - resyncQueue(audioTimeMs); - } else if (lastAudioTimeMs >= 0 && audioTimeMs - lastAudioTimeMs > RESYNC_FORWARD_MS) { - resyncQueue(audioTimeMs); - } - lastAudioTimeMs = audioTimeMs; - - advanceQueue(audioTimeMs); - } - - private void loadTrack(String title, String artist, String key) { - this.currentTrackTitle = title; - this.currentTrackArtist = artist; - this.lastTrackKey = key; - - activeParticles.clear(); - rawLyrics.clear(); - lyricsQueue.clear(); - currentLyricIndex = 0; - lastAudioTimeMs = -1L; - isPlaying = false; - - LrcLibClient.fetchLyricsAsync(title, artist, debugMode.get()).thenAccept(lines -> - Minecraft.getInstance().execute(() -> { - if (!this.lastTrackKey.equalsIgnoreCase(key)) return; - - activeParticles.clear(); - rawLyrics.clear(); - lyricsQueue.clear(); - rawLyrics.addAll(lines); - rebuildLyricsQueue(); - currentLyricIndex = 0; - isPlaying = !lyricsQueue.isEmpty(); - }) - ); - } - - private void rebuildLyricsQueue() { - lyricsQueue.clear(); - if (splitMode.get() == SplitMode.SMART_SPLIT) { - lyricsQueue.addAll(LyricLineSplitter.splitLongLines(rawLyrics)); - } else { - lyricsQueue.addAll(rawLyrics); - } - } - - private void resyncQueue(long audioTimeMs) { - activeParticles.clear(); - currentLyricIndex = 0; - long now = System.currentTimeMillis(); - - for (int i = 0; i < lyricsQueue.size(); i++) { - LyricLine line = lyricsQueue.get(i); - long nextStartMs = (i + 1 < lyricsQueue.size()) ? lyricsQueue.get(i + 1).timestampMs() : line.timestampMs() + 3500L; - long durationMs = Math.max(1200L, Math.min(5000L, nextStartMs - line.timestampMs())); - - if (audioTimeMs >= line.timestampMs()) { - long timeSinceLine = audioTimeMs - line.timestampMs(); - if (timeSinceLine < durationMs) { - spawnLyricParticle(line.text(), now - timeSinceLine, durationMs); - } - currentLyricIndex = i + 1; - } else { - break; - } - } - } - - private void advanceQueue(long audioTimeMs) { - Minecraft mc = Minecraft.getInstance(); - if (!isPlaying || lyricsQueue.isEmpty() || mc.player == null) return; - - long now = System.currentTimeMillis(); - - while (currentLyricIndex < lyricsQueue.size()) { - LyricLine line = lyricsQueue.get(currentLyricIndex); - long nextStartMs = (currentLyricIndex + 1 < lyricsQueue.size()) ? lyricsQueue.get(currentLyricIndex + 1).timestampMs() : line.timestampMs() + 3500L; - long durationMs = Math.max(1400L, Math.min(5500L, nextStartMs - line.timestampMs())); - - if (audioTimeMs >= line.timestampMs()) { - long timeSinceLine = audioTimeMs - line.timestampMs(); - if (timeSinceLine < durationMs) { - spawnLyricParticle(line.text(), now - timeSinceLine, durationMs); - } - currentLyricIndex++; - } else { - break; - } - } - - activeParticles.removeIf(particle -> particle.isDead(now)); - } - - /** - * arcSpread is a user-facing "how wide" preference, but it must never exceed what the - * player's actual camera FOV can show, or particles spawn off-screen. Clamp it to the - * real horizontal FOV (derived from vertical FOV + window aspect), with a safety margin - * so text doesn't pop in right at the frustum edge. - */ - private double resolveSpawnSpreadDeg() { - double configured = arcSpread.get(); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null || mc.getWindow() == null) { - return configured; - } - - float verticalFovDeg = mc.gameRenderer.mainCamera().getFov(); - double aspect = (double) mc.getWindow().getWidth() / Math.max(1, mc.getWindow().getHeight()); - double halfVerticalRad = Math.toRadians(verticalFovDeg) / 2.0; - double halfHorizontalDeg = Math.toDegrees(Math.atan(Math.tan(halfVerticalRad) * aspect)); - - return Math.min(configured, halfHorizontalDeg * 0.85); - } - - private Vec3 findNonOverlappingSpawnPos(Vec3 headPos, float cameraYaw) { - double spread = resolveSpawnSpreadDeg(); - double minD = minDistance.get(); - double maxD = Math.max(minD + 0.5, maxDistance.get()); - - Vec3 bestPos = null; - double maxMinDistance = -1.0; - - for (int attempt = 0; attempt < 16; attempt++) { - double randomAngleOffset = ThreadLocalRandom.current().nextDouble(-spread, spread); - double targetYawDeg = cameraYaw + randomAngleOffset; - double distance = ThreadLocalRandom.current().nextDouble(minD, maxD); - double yOffset = ThreadLocalRandom.current().nextDouble(-0.25, 0.55); - - double yawRad = Math.toRadians(targetYawDeg); - double dirX = -Math.sin(yawRad); - double dirZ = Math.cos(yawRad); - - Vec3 candidate = new Vec3( - headPos.x + (dirX * distance), - headPos.y + yOffset, - headPos.z + (dirZ * distance) - ); - - if (activeParticles.isEmpty()) { - return candidate; - } - - double minDistanceToOthers = Double.MAX_VALUE; - for (LyricParticle3D active : activeParticles) { - double d = candidate.distanceTo(active.getBasePosition()); - if (d < minDistanceToOthers) { - minDistanceToOthers = d; - } - } - - if (minDistanceToOthers >= 1.35) { - return candidate; - } - - if (minDistanceToOthers > maxMinDistance) { - maxMinDistance = minDistanceToOthers; - bestPos = candidate; - } - } - - return bestPos; - } - - private void spawnLyricParticle(String text, long spawnTimeMs, long durationMs) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || text == null || text.isBlank()) return; - if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return; - - Camera camera = mc.gameRenderer.mainCamera(); - Vec3 headPos = mc.player.getEyePosition(); - float cameraYaw = camera.yRot(); - - int maxAllowed = maxLines.get(); - while (activeParticles.size() >= maxAllowed) { - activeParticles.remove(0); - } - - Vec3 spawnPos = findNonOverlappingSpawnPos(headPos, cameraYaw); - if (spawnPos == null) return; - - activeParticles.add(new LyricParticle3D(text, spawnPos, spawnTimeMs, durationMs)); - } - - private TextRenderer getSelectedFont() { - return Fonts.renderer(fontMode.get().family, FontInfo.Type.Regular, TextRenderer.get()); - } - - private int getActiveColorRgb() { - return switch (colorMode.get()) { - case THEME -> Theme.theme().accent(); - case THEME_GRADIENT -> { - float ratio = (float) (Math.sin(System.currentTimeMillis() / 450.0) * 0.5 + 0.5); - int c1 = Theme.theme().accent(); - int c2 = Theme.theme().accentSoft(); - yield mixArgb(c1, c2, ratio); - } - case WHITE -> 0xFFFFFF; - case STATIC -> staticColor.getArgb(); - }; - } - - private static int mixArgb(int from, int to, float t) { - t = Math.max(0.0f, Math.min(1.0f, t)); - int r = mixChannel((from >>> 16) & 0xFF, (to >>> 16) & 0xFF, t); - int g = mixChannel((from >>> 8) & 0xFF, (to >>> 8) & 0xFF, t); - int b = mixChannel(from & 0xFF, to & 0xFF, t); - return (r << 16) | (g << 8) | b; - } - - private static int mixChannel(int from, int to, float t) { - return from + Math.round((to - from) * t); + playback.sync(); } @Override @@ -366,14 +90,14 @@ public final class KineticLyrics extends Module { public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; - if (activeParticles.isEmpty()) return; + if (stage.isEmpty()) return; - TextRenderer font = getSelectedFont(); + TextRenderer font = stage.selectedFont(); String animMode = animation.get().name(); - int colorRgb = getActiveColorRgb(); + int colorRgb = stage.activeColorRgb(); long now = System.currentTimeMillis(); - for (LyricParticle3D particle : activeParticles) { + for (LyricParticle3D particle : stage.particles()) { particle.render(renderer, font, colorRgb, animMode, now, textScale.get()); } } @@ -384,16 +108,17 @@ public final class KineticLyrics extends Module { if (!isEnabled() || !debugMode.get() || mc.player == null) return; TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); - String trackLine = "Track: " + (currentTrackTitle.isEmpty() ? "None" : currentTrackTitle + " - " + currentTrackArtist); - long displayTimeMs = Math.max(0L, lastAudioTimeMs); + String title = playback.trackTitle(); + String trackLine = "Track: " + (title.isEmpty() ? "None" : title + " - " + playback.trackArtist()); + long displayTimeMs = Math.max(0L, playback.lastAudioTimeMs()); String timerLine = String.format("Audio time: %02d:%02d.%03d (%d ms)", displayTimeMs / 60_000, (displayTimeMs % 60_000) / 1000, displayTimeMs % 1000, displayTimeMs ); - String particlesLine = "Active particles: " + activeParticles.size(); - String queueLine = "Queue: " + currentLyricIndex + " / " + lyricsQueue.size() + " lines"; + String particlesLine = "Active particles: " + stage.size(); + String queueLine = "Queue: " + playback.currentLyricIndex() + " / " + playback.queueSize() + " lines"; tr.begin(0.85); tr.render(trackLine, 10.0, 70.0, new RenderColor(0xFFDCDCDC), false); @@ -402,78 +127,4 @@ public final class KineticLyrics extends Module { tr.render(queueLine, 10.0, 106.0, new RenderColor(0xFFB4B4FF), false); tr.end(); } - - public enum AnimationMode implements EnumValue.IdProvider { - LYRIC_FLOW("lyric_flow"), - TYPEWRITER("typewriter"), - POP_SCALE("pop_scale"), - KINETIC_SLIDE("kinetic_slide"), - LETTER_FALL("letter_fall"), - FADE("fade"); - - private final String id; - - AnimationMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum ColorMode implements EnumValue.IdProvider { - THEME("theme"), - THEME_GRADIENT("theme_gradient"), - WHITE("white"), - STATIC("static"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum FontMode implements EnumValue.IdProvider { - ONEST_BOLD("onest_bold", "OnestBold"), - ONEST_MEDIUM("onest_medium", "OnestMedium"), - ONEST("onest", "Onest"), - INTER_MEDIUM("inter_medium", "InterMedium"); - - private final String id; - private final String family; - - FontMode(String id, String family) { - this.id = id; - this.family = family; - } - - @Override - public String id() { - return id; - } - } - - public enum SplitMode implements EnumValue.IdProvider { - SMART_SPLIT("smart_split"), - FULL_LINE("full_line"); - - private final String id; - - SplitMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricPlayback.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricPlayback.java new file mode 100644 index 0000000..7f87cf9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricPlayback.java @@ -0,0 +1,198 @@ +package dev.loki.lovisual.features.module.modules.visuals.lyrics; + +import java.util.ArrayList; +import java.util.List; + +import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LrcLibClient; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLine; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.lrc.LyricLineSplitter; +import dev.loki.lovisual.features.module.modules.visuals.lyrics.mode.LyricModes; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import net.minecraft.client.Minecraft; + +/** + * Playback state machine for {@link KineticLyrics}: polls the active media session, tracks the + * current song, keeps the raw / split lyric queues and the read index, decides which lines become + * due as the audio clock advances and hands the actual text to {@link LyricStage} to spawn. + * Handles track switches (async lyrics fetch), seek/rewind resyncs and the pause guard. Extracted + * so the module class stays under the file-size cap. + */ +final class LyricPlayback { + + /** Прыжок вперёд больше этого — seek: переиндексировать очередь, не спавнить пропущенные строки залпом. */ + private static final long RESYNC_FORWARD_MS = 1500L; + /** Регрессия больше этого — пауза/откат: строки после индекса уже не догнать, нужен resync. */ + private static final long RESYNC_BACKWARD_MS = 750L; + + private final KineticLyrics owner; + private final LyricStage stage; + + private final List rawLyrics = new ArrayList<>(); + private final List lyricsQueue = new ArrayList<>(); + private int currentLyricIndex = 0; + + private String lastTrackKey = ""; + private String currentTrackTitle = ""; + private String currentTrackArtist = ""; + private long lastAudioTimeMs = -1L; + private boolean isPlaying = false; + + LyricPlayback(KineticLyrics owner, LyricStage stage) { + this.owner = owner; + this.stage = stage; + } + + String trackTitle() { + return currentTrackTitle; + } + String trackArtist() { + return currentTrackArtist; + } + int currentLyricIndex() { + return currentLyricIndex; + } + int queueSize() { + return lyricsQueue.size(); + } + long lastAudioTimeMs() { + return lastAudioTimeMs; + } + + /** Clear all playback state: called on module enable / disable. */ + void reset() { + stage.clear(); + rawLyrics.clear(); + lyricsQueue.clear(); + lastTrackKey = ""; + currentTrackTitle = ""; + currentTrackArtist = ""; + currentLyricIndex = 0; + lastAudioTimeMs = -1L; + isPlaying = false; + } + + /** Poll the media session once: switch tracks, resync on seek/pause and spawn due lines. */ + void sync() { + MediaSessionService.Snapshot snapshot = MediaSessionService.get().snapshot(); + if (!snapshot.hasSession() || snapshot.title().isBlank()) { + if (isPlaying) { + isPlaying = false; + stage.clear(); + } + return; + } + + String title = snapshot.title(); + String artist = snapshot.artist(); + String key = (title + " - " + artist).toLowerCase(); + + if (!key.equalsIgnoreCase(lastTrackKey)) { + loadTrack(title, artist, key); + return; + } + + if (!isPlaying && !lyricsQueue.isEmpty()) { + isPlaying = true; + } + if (lyricsQueue.isEmpty()) return; + + // Позиция плеера (MPRIS/SMTC) — это внутренний таймер декодера: он + // опережает слышимый вывод на латентность аудио-тракта (0.3-1с+, до + // нескольких секунд через Bluetooth), а LRC-таймкоды привязаны к + // слышимому звуку. Дефолтный сдвиг назад компенсирует это (решение + // совпадает с lrcsnc: у автора ~0.75с задержки для mpd). + long audioTimeMs = Math.max(0L, snapshot.predictedPositionMs() + owner.timeOffsetMs.get()); + resyncIfNeeded(audioTimeMs); + lastAudioTimeMs = audioTimeMs; + + advance(audioTimeMs); + } + + // Любая ощутимая регрессия (pause через truncate-в-секунды MPRIS, короткий seek) — + // иначе индекс остаётся впереди звука и строки молча скипаются до догонки. + private void resyncIfNeeded(long audioTimeMs) { + boolean backward = lastAudioTimeMs >= 0 && audioTimeMs < lastAudioTimeMs - RESYNC_BACKWARD_MS; + boolean forward = lastAudioTimeMs >= 0 && audioTimeMs - lastAudioTimeMs > RESYNC_FORWARD_MS; + if (backward || forward) { + resync(audioTimeMs); + } + } + + private void loadTrack(String title, String artist, String key) { + this.currentTrackTitle = title; + this.currentTrackArtist = artist; + this.lastTrackKey = key; + + reset(); + + LrcLibClient.fetchLyricsAsync(title, artist, owner.debugMode.get()).thenAccept(lines -> + Minecraft.getInstance().execute(() -> { + if (!this.lastTrackKey.equalsIgnoreCase(key)) return; + + stage.clear(); + rawLyrics.clear(); + lyricsQueue.clear(); + rawLyrics.addAll(lines); + rebuildLyricsQueue(); + currentLyricIndex = 0; + isPlaying = !lyricsQueue.isEmpty(); + }) + ); + } + + private void rebuildLyricsQueue() { + lyricsQueue.clear(); + if (owner.splitMode.get() == LyricModes.SplitMode.SMART_SPLIT) { + lyricsQueue.addAll(LyricLineSplitter.splitLongLines(rawLyrics)); + } else { + lyricsQueue.addAll(rawLyrics); + } + } + + private void resync(long audioTimeMs) { + stage.clear(); + currentLyricIndex = 0; + long now = System.currentTimeMillis(); + + for (int i = 0; i < lyricsQueue.size(); i++) { + LyricLine line = lyricsQueue.get(i); + long nextStartMs = (i + 1 < lyricsQueue.size()) ? lyricsQueue.get(i + 1).timestampMs() : line.timestampMs() + 3500L; + long durationMs = Math.max(1200L, Math.min(5000L, nextStartMs - line.timestampMs())); + + if (audioTimeMs >= line.timestampMs()) { + long timeSinceLine = audioTimeMs - line.timestampMs(); + if (timeSinceLine < durationMs) { + stage.spawn(line.text(), now - timeSinceLine, durationMs); + } + currentLyricIndex = i + 1; + } else { + break; + } + } + } + + private void advance(long audioTimeMs) { + Minecraft mc = Minecraft.getInstance(); + if (!isPlaying || lyricsQueue.isEmpty() || mc.player == null) return; + + long now = System.currentTimeMillis(); + + while (currentLyricIndex < lyricsQueue.size()) { + LyricLine line = lyricsQueue.get(currentLyricIndex); + long nextStartMs = (currentLyricIndex + 1 < lyricsQueue.size()) ? lyricsQueue.get(currentLyricIndex + 1).timestampMs() : line.timestampMs() + 3500L; + long durationMs = Math.max(1400L, Math.min(5500L, nextStartMs - line.timestampMs())); + + if (audioTimeMs >= line.timestampMs()) { + long timeSinceLine = audioTimeMs - line.timestampMs(); + if (timeSinceLine < durationMs) { + stage.spawn(line.text(), now - timeSinceLine, durationMs); + } + currentLyricIndex++; + } else { + break; + } + } + + stage.pruneDead(now); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricStage.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricStage.java new file mode 100644 index 0000000..bc1490c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricStage.java @@ -0,0 +1,176 @@ +package dev.loki.lovisual.features.module.modules.visuals.lyrics; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ThreadLocalRandom; + +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; + +/** + * Owns the set of live {@link LyricParticle3D}s for {@link KineticLyrics} and everything about + * placing and coloring them: FOV-clamped spawn spread, non-overlapping spawn position search, + * theme / gradient / static color resolution and the selected font renderer. Reads the module's + * settings through its owner; lifecycle (reset) and the queue that decides *what* to spawn live + * in {@link LyricPlayback}. Extracted so the module class stays under the file-size cap. + */ +final class LyricStage { + + private final KineticLyrics owner; + private final List activeParticles = new CopyOnWriteArrayList<>(); + + LyricStage(KineticLyrics owner) { + this.owner = owner; + } + + List particles() { + return activeParticles; + } + + boolean isEmpty() { + return activeParticles.isEmpty(); + } + + int size() { + return activeParticles.size(); + } + + void clear() { + activeParticles.clear(); + } + + /** Spawn a lyric particle for {@code text}, trimming the oldest when over the line cap. */ + void spawn(String text, long spawnTimeMs, long durationMs) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || text == null || text.isBlank()) return; + if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return; + + Camera camera = mc.gameRenderer.mainCamera(); + Vec3 headPos = mc.player.getEyePosition(); + float cameraYaw = camera.yRot(); + + int maxAllowed = owner.maxLines.get(); + while (activeParticles.size() >= maxAllowed) { + activeParticles.remove(0); + } + + Vec3 spawnPos = findSpawnPos(headPos, cameraYaw); + if (spawnPos == null) return; + + activeParticles.add(new LyricParticle3D(text, spawnPos, spawnTimeMs, durationMs)); + } + + void pruneDead(long now) { + activeParticles.removeIf(particle -> particle.isDead(now)); + } + + /** + * arcSpread is a user-facing "how wide" preference, but it must never exceed what the + * player's actual camera FOV can show, or particles spawn off-screen. Clamp it to the + * real horizontal FOV (derived from vertical FOV + window aspect), with a safety margin + * so text doesn't pop in right at the frustum edge. + */ + private double resolveSpawnSpreadDeg() { + double configured = owner.arcSpread.get(); + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null || mc.getWindow() == null) { + return configured; + } + + float verticalFovDeg = mc.gameRenderer.mainCamera().getFov(); + double aspect = (double) mc.getWindow().getWidth() / Math.max(1, mc.getWindow().getHeight()); + double halfVerticalRad = Math.toRadians(verticalFovDeg) / 2.0; + double halfHorizontalDeg = Math.toDegrees(Math.atan(Math.tan(halfVerticalRad) * aspect)); + + return Math.min(configured, halfHorizontalDeg * 0.85); + } + + private Vec3 findSpawnPos(Vec3 headPos, float cameraYaw) { + double spread = resolveSpawnSpreadDeg(); + double minD = owner.minDistance.get(); + double maxD = Math.max(minD + 0.5, owner.maxDistance.get()); + + Vec3 bestPos = null; + double maxMinDistance = -1.0; + + for (int attempt = 0; attempt < 16; attempt++) { + Vec3 candidate = randomCandidate(headPos, cameraYaw, spread, minD, maxD); + if (activeParticles.isEmpty()) { + return candidate; + } + double minDistanceToOthers = minDistanceToOthers(candidate); + if (minDistanceToOthers >= 1.35) { + return candidate; + } + if (minDistanceToOthers > maxMinDistance) { + maxMinDistance = minDistanceToOthers; + bestPos = candidate; + } + } + + return bestPos; + } + + private Vec3 randomCandidate(Vec3 headPos, float cameraYaw, double spread, double minD, double maxD) { + double randomAngleOffset = ThreadLocalRandom.current().nextDouble(-spread, spread); + double targetYawDeg = cameraYaw + randomAngleOffset; + double distance = ThreadLocalRandom.current().nextDouble(minD, maxD); + double yOffset = ThreadLocalRandom.current().nextDouble(-0.25, 0.55); + + double yawRad = Math.toRadians(targetYawDeg); + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + return new Vec3( + headPos.x + (dirX * distance), + headPos.y + yOffset, + headPos.z + (dirZ * distance) + ); + } + + private double minDistanceToOthers(Vec3 candidate) { + double minDistanceToOthers = Double.MAX_VALUE; + for (LyricParticle3D active : activeParticles) { + double d = candidate.distanceTo(active.getBasePosition()); + if (d < minDistanceToOthers) { + minDistanceToOthers = d; + } + } + return minDistanceToOthers; + } + + TextRenderer selectedFont() { + return Fonts.renderer(owner.fontMode.get().family(), FontInfo.Type.Regular, TextRenderer.get()); + } + + int activeColorRgb() { + return switch (owner.colorMode.get()) { + case THEME -> Theme.theme().accent(); + case THEME_GRADIENT -> { + float ratio = (float) (Math.sin(System.currentTimeMillis() / 450.0) * 0.5 + 0.5); + int c1 = Theme.theme().accent(); + int c2 = Theme.theme().accentSoft(); + yield mixArgb(c1, c2, ratio); + } + case WHITE -> 0xFFFFFF; + case STATIC -> owner.staticColor.getArgb(); + }; + } + + private static int mixArgb(int from, int to, float t) { + t = Math.max(0.0f, Math.min(1.0f, t)); + int r = mixChannel((from >>> 16) & 0xFF, (to >>> 16) & 0xFF, t); + int g = mixChannel((from >>> 8) & 0xFF, (to >>> 8) & 0xFF, t); + int b = mixChannel(from & 0xFF, to & 0xFF, t); + return (r << 16) | (g << 8) | b; + } + + private static int mixChannel(int from, int to, float t) { + return from + Math.round((to - from) * t); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/mode/LyricModes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/mode/LyricModes.java new file mode 100644 index 0000000..3b2d453 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/mode/LyricModes.java @@ -0,0 +1,91 @@ +package dev.loki.lovisual.features.module.modules.visuals.lyrics.mode; + +import dev.loki.lovisual.config.values.mode.EnumValue; + +/** + * Selection enums for the KineticLyrics module: animation style, coloring mode, + * font family and line-splitting strategy. Grouped in one file so the module class stays + * under the project file-size cap. + */ +public final class LyricModes { + + private LyricModes() { } + + public enum AnimationMode implements EnumValue.IdProvider { + LYRIC_FLOW("lyric_flow"), + TYPEWRITER("typewriter"), + POP_SCALE("pop_scale"), + KINETIC_SLIDE("kinetic_slide"), + LETTER_FALL("letter_fall"), + FADE("fade"); + + private final String id; + + AnimationMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public enum ColorMode implements EnumValue.IdProvider { + THEME("theme"), + THEME_GRADIENT("theme_gradient"), + WHITE("white"), + STATIC("static"); + + private final String id; + + ColorMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public enum FontMode implements EnumValue.IdProvider { + ONEST_BOLD("onest_bold", "OnestBold"), + ONEST_MEDIUM("onest_medium", "OnestMedium"), + ONEST("onest", "Onest"), + INTER_MEDIUM("inter_medium", "InterMedium"); + + private final String id; + private final String family; + + FontMode(String id, String family) { + this.id = id; + this.family = family; + } + + @Override + public String id() { + return id; + } + + public String family() { + return family; + } + } + + public enum SplitMode implements EnumValue.IdProvider { + SMART_SPLIT("smart_split"), + FULL_LINE("full_line"); + + private final String id; + + SplitMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java index 4926b1e..016eced 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java @@ -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.module.modules.visuals.nametags; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java index 76dfd23..8f8aa42 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java @@ -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.module.modules.visuals.nametags; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java index 8890a53..794c1cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java @@ -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.module.modules.visuals.nametags; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java index 574e6d7..b8cec6e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java @@ -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.module.modules.visuals.nametags; import dev.loki.lovisual.config.definition.SettingDef; @@ -20,82 +13,32 @@ import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSettin import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.visuals.nametags.NameTagCache; -import dev.loki.lovisual.features.module.modules.visuals.nametags.NameTagLayoutEngine; -import dev.loki.lovisual.features.module.modules.visuals.nametags.NameTagModel; -import dev.loki.lovisual.features.module.modules.visuals.nametags.NameTags; -import dev.loki.lovisual.features.module.modules.visuals.nametags.parts.NameTagOpponentRenderer; -import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagLabelBuilder; import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagRenderer; import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.mixininterface.entity.IEntity; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScreenProjection; import dev.loki.lovisual.util.item.food.IllegalItemUtil; import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; -import dev.loki.lovisual.util.player.effect.tracker.StatusEffectView; import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; -import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot; -import dev.loki.lovisual.features.module.modules.visuals.nametags.parts.NameTagOpponentRenderer; -import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagLabelBuilder; -import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagRenderer; import net.minecraft.client.CameraType; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; import java.util.*; -import dev.loki.lovisual.features.gui.hud.draggable.impl.stats.Ping; + //todo Description @ModuleInfo(id = "nametags", displayName = "NameTags", aliases = {"nametag", "tags", "names"}, category = ModuleCategory.VISUALS) public class NameTags extends Module { public static final int OPPONENT_DISTANCE = 80; - private static final int INFO_COLOR_DEFAULT = 0xFFFF8A8A; - private static final float NAMEPLATE_EXTRA_LIFT = 8f; - private static final ItemStack[] EMPTY_SLOTS = new ItemStack[0]; - - private static final String SETTING_TOGGLES = "toggles"; - private static final String SETTING_DURABILITY_START = "durability_start_percent"; - private static final String SETTING_DISTANCE_TOGGLES = "distance_toggles"; - private static final String SETTING_NAMEPLATE_CUTOFF = "nameplate_cutoff"; - private static final String SETTING_SLOTS_CUTOFF = "slots_cutoff"; - private static final String SETTING_EFFECTS_CUTOFF = "effects_cutoff"; - private static final String SETTING_ARMOR_ENCHANTS = "armor_enchants"; - private static final String SETTING_MELEE_ENCHANTS = "melee_enchants"; - private static final String SETTING_TOOLS_ENCHANTS = "tools_enchants"; - private static final String SETTING_BOW_ENCHANTS = "bow_enchants"; - private static final String SETTING_FISHING_ENCHANTS = "fishing_enchants"; - private static final String SETTING_TRIDENT_ENCHANTS = "trident_enchants"; - private static final String SETTING_CROSSBOW_ENCHANTS = "crossbow_enchants"; - private static final String SETTING_MACE_ENCHANTS = "mace_enchants"; - private static final String SETTING_OTHER_ENCHANTS = "other_enchants"; - private static final String SETTING_ILLEGAL_ENCHANT_COLOR = "illegal_enchant_color"; - private static final String SETTING_ILLEGAL_ENCHANTS_ALWAYS = "illegal_enchants_always"; - private static final String SETTING_TOP_ENCHANT_COLOR = "top_enchant_color"; - private static final String SETTING_TOP_ENCHANT_IGNORE_LIST = "top_enchant_ignore_list"; - private static final String SETTING_HP_DECIMALS = "hp_decimals"; - private static final String SETTING_HP_COLOR_MODE = "hp_color_mode"; - private static final String SETTING_NAMEPLATE_ALPHA = "nameplate_alpha"; - private static final String SETTING_NAMEPLATE_STYLE = "nameplate_style"; - private static final String SETTING_PLAYER_HEAD = "player_head"; - private static final String SETTING_NAMEPLATE_EXTRAS = "nameplate_extras"; - private static final String SETTING_OPPONENT_APPLE_COOLDOWNS = "opponent_apple_cooldowns"; - private static final String SETTING_PVP_PREFIX = "pvp_prefix"; - private static final String SETTING_PVP_PREFIX_COLOR = "pvp_prefix_color"; private final ItemIdSetValue topIgnore = TopEnchantUtil.ignoreValue(); - private final List renderQueue = new ArrayList<>(); - private final List playerBuffer = new ArrayList<>(); + + final List renderQueue = new ArrayList<>(); + final List playerBuffer = new ArrayList<>(); final Map slotCache = new HashMap<>(); final Map effectCache = new HashMap<>(); final Map enchantCache = new HashMap<>(); @@ -103,7 +46,7 @@ public class NameTags extends Module { final Map totemPopLastCounts = new HashMap<>(); final HashSet seenIds = new HashSet<>(); - public final BooleanMapValue toggles = group("nameTagsToggles", SETTING_TOGGLES, Map.of( + public final BooleanMapValue toggles = group("nameTagsToggles", NameTagKeys.TOGGLES, Map.of( "Show nameplate", true, "Show distance", true, "Show armor row", true, @@ -112,37 +55,37 @@ public class NameTags extends Module { "Show self in 3rd person", false )); public final NumberValue durabilityStartPercent = - visibleWhen(num("nameTagsDurabilityStartPercent", SETTING_DURABILITY_START, 99, 0, 100), + visibleWhen(num("nameTagsDurabilityStartPercent", NameTagKeys.DURABILITY_START, 99, 0, 100), () -> toggles.get("Item durability bar")); - private final BooleanMapValue distanceToggles = - visibleWhen(group("nameTagsDistanceToggles", SETTING_DISTANCE_TOGGLES, Map.of( + final BooleanMapValue distanceToggles = + visibleWhen(group("nameTagsDistanceToggles", NameTagKeys.DISTANCE_TOGGLES, Map.of( "nameplate", true, "slots", true, "effects", true )), () -> toggles.get("Show nameplate") || toggles.get("Show armor row") || toggles.get("Show effects")); - private final NumberValue plateDistance = - visibleWhen(num("nameTagsPlateDistance", SETTING_NAMEPLATE_CUTOFF, 80, 5, 256), + final NumberValue plateDistance = + visibleWhen(num("nameTagsPlateDistance", NameTagKeys.NAMEPLATE_CUTOFF, 80, 5, 256), () -> toggles.get("Show nameplate")); - private final NumberValue slotsDistance = - visibleWhen(num("nameTagsSlotsDistance", SETTING_SLOTS_CUTOFF, 70, 5, 256), + final NumberValue slotsDistance = + visibleWhen(num("nameTagsSlotsDistance", NameTagKeys.SLOTS_CUTOFF, 70, 5, 256), () -> toggles.get("Show armor row")); - private final NumberValue effectsDistance = - visibleWhen(num("nameTagsEffectsDistance", SETTING_EFFECTS_CUTOFF, 90, 5, 256), + final NumberValue effectsDistance = + visibleWhen(num("nameTagsEffectsDistance", NameTagKeys.EFFECTS_CUTOFF, 90, 5, 256), () -> toggles.get("Show effects")); public final NumberValue nameplateAlpha = - visibleWhen(num("nameTagsNameplateAlpha", SETTING_NAMEPLATE_ALPHA, 180, 0, 255), + visibleWhen(num("nameTagsNameplateAlpha", NameTagKeys.NAMEPLATE_ALPHA, 180, 0, 255), () -> toggles.get("Show nameplate")); public final EnumValue nameplateStyle = - visibleWhen(enumSetting("nameTagsNameplateStyle", SETTING_NAMEPLATE_STYLE, NameTagModel.NameplateStyle.LIQUID_GLASS, NameTagModel.NameplateStyle.values()), + visibleWhen(enumSetting("nameTagsNameplateStyle", NameTagKeys.NAMEPLATE_STYLE, NameTagModel.NameplateStyle.LIQUID_GLASS, NameTagModel.NameplateStyle.values()), () -> toggles.get("Show nameplate")); public final BooleanValue playerHead = - visibleWhen(bool("nameTagsPlayerHead", SETTING_PLAYER_HEAD, false), + visibleWhen(bool("nameTagsPlayerHead", NameTagKeys.PLAYER_HEAD, false), () -> toggles.get("Show nameplate")); - public final BooleanMapValue nameplateExtras = visibleWhen(group("nameTagsNameplateExtras", SETTING_NAMEPLATE_EXTRAS, new LinkedHashMap<>() {{ + public final BooleanMapValue nameplateExtras = visibleWhen(group("nameTagsNameplateExtras", NameTagKeys.NAMEPLATE_EXTRAS, new LinkedHashMap<>() {{ put("Tab names", false); put("Ping", false); }}), () -> toggles.get("Show nameplate")); - public final BooleanMapValue enchantArmor = group("nameTagsEnchantArmor", SETTING_ARMOR_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantArmor = group("nameTagsEnchantArmor", NameTagKeys.ARMOR_ENCHANTS, Map.ofEntries( Map.entry("protection", true), Map.entry("fire_protection", false), Map.entry("feather_falling", false), @@ -156,7 +99,7 @@ public class NameTags extends Module { Map.entry("soul_speed", false), Map.entry("swift_sneak", false) )); - public final BooleanMapValue enchantMelee = group("nameTagsEnchantMelee", SETTING_MELEE_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantMelee = group("nameTagsEnchantMelee", NameTagKeys.MELEE_ENCHANTS, Map.ofEntries( Map.entry("sharpness", true), Map.entry("smite", false), Map.entry("bane_of_arthropods", false), @@ -165,55 +108,55 @@ public class NameTags extends Module { Map.entry("looting", true), Map.entry("sweeping_edge", false) )); - public final BooleanMapValue enchantTools = group("nameTagsEnchantTools", SETTING_TOOLS_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantTools = group("nameTagsEnchantTools", NameTagKeys.TOOLS_ENCHANTS, Map.ofEntries( Map.entry("efficiency", true), Map.entry("silk_touch", true), Map.entry("fortune", true) )); - public final BooleanMapValue enchantBow = group("nameTagsEnchantBow", SETTING_BOW_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantBow = group("nameTagsEnchantBow", NameTagKeys.BOW_ENCHANTS, Map.ofEntries( Map.entry("power", true), Map.entry("punch", true), Map.entry("flame", false), Map.entry("infinity", false) )); - public final BooleanMapValue enchantFishing = group("nameTagsEnchantFishing", SETTING_FISHING_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantFishing = group("nameTagsEnchantFishing", NameTagKeys.FISHING_ENCHANTS, Map.ofEntries( Map.entry("luck_of_the_sea", false), Map.entry("lure", false) )); - public final BooleanMapValue enchantTrident = group("nameTagsEnchantTrident", SETTING_TRIDENT_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantTrident = group("nameTagsEnchantTrident", NameTagKeys.TRIDENT_ENCHANTS, Map.ofEntries( Map.entry("loyalty", false), Map.entry("impaling", false), Map.entry("riptide", false), Map.entry("channeling", false) )); - public final BooleanMapValue enchantCrossbow = group("nameTagsEnchantCrossbow", SETTING_CROSSBOW_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantCrossbow = group("nameTagsEnchantCrossbow", NameTagKeys.CROSSBOW_ENCHANTS, Map.ofEntries( Map.entry("multishot", false), Map.entry("quick_charge", false), Map.entry("piercing", true) )); - public final BooleanMapValue enchantMace = group("nameTagsEnchantMace", SETTING_MACE_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantMace = group("nameTagsEnchantMace", NameTagKeys.MACE_ENCHANTS, Map.ofEntries( Map.entry("density", true), Map.entry("breach", false), Map.entry("wind_burst", true) )); - public final BooleanMapValue enchantOther = group("nameTagsEnchantOther", SETTING_OTHER_ENCHANTS, Map.ofEntries( + public final BooleanMapValue enchantOther = group("nameTagsEnchantOther", NameTagKeys.OTHER_ENCHANTS, Map.ofEntries( Map.entry("mending", false), Map.entry("unbreaking", false), Map.entry("vanishing_curse", false), Map.entry("lunge", true) )); - public final BooleanValue illegalEnchantsAlways = bool("nameTagsIllegalEnchantsAlways", SETTING_ILLEGAL_ENCHANTS_ALWAYS, true); - public final BooleanValue opponentAppleCooldowns = bool("nameTagsOpponentAppleCooldowns", SETTING_OPPONENT_APPLE_COOLDOWNS, true); + public final BooleanValue illegalEnchantsAlways = bool("nameTagsIllegalEnchantsAlways", NameTagKeys.ILLEGAL_ENCHANTS_ALWAYS, true); + public final BooleanValue opponentAppleCooldowns = bool("nameTagsOpponentAppleCooldowns", NameTagKeys.OPPONENT_APPLE_COOLDOWNS, true); final BooleanValue totemPopCounter = TotemPopCounter.enabledValue(); private final NumberValue totemPopResetSeconds = TotemPopCounter.resetAfterSecondsValue(); - public final BooleanValue pvpPrefix = bool("nameTagsPvpPrefix", SETTING_PVP_PREFIX, false); + public final BooleanValue pvpPrefix = bool("nameTagsPvpPrefix", NameTagKeys.PVP_PREFIX, false); public final RGBColorValue pvpPrefixColor = - visibleWhen(colorNoAlpha("nameTagsPvpPrefixColor", SETTING_PVP_PREFIX_COLOR, "#FF0000"), pvpPrefix::get); + visibleWhen(colorNoAlpha("nameTagsPvpPrefixColor", NameTagKeys.PVP_PREFIX_COLOR, "#FF0000"), pvpPrefix::get); public final BooleanValue hpDecimals = - visibleWhen(bool("nameTagsHpDecimals", SETTING_HP_DECIMALS, false), + visibleWhen(bool("nameTagsHpDecimals", NameTagKeys.HP_DECIMALS, false), () -> toggles.get("Show nameplate")); public final ModeValue hpColorMode = - visibleWhen(modeSetting("nameTagsHpColorMode", SETTING_HP_COLOR_MODE, "Static", "Static", "Gradient"), + visibleWhen(modeSetting("nameTagsHpColorMode", NameTagKeys.HP_COLOR_MODE, "Static", "Static", "Gradient"), () -> toggles.get("Show nameplate")); public transient TextRenderer cachedNameTr; @@ -227,9 +170,9 @@ public class NameTags extends Module { setting(SettingDef.number(totemPopResetSeconds) .common(CommonSettingSchemas.TOTEM_POP_RESET_SECONDS.commonI18nKey()) .visibleWhen(totemPopCounter::get)); - setting(SettingDef.colorNoAlpha(SETTING_ILLEGAL_ENCHANT_COLOR, IllegalItemUtil.illegalColorValue())); - setting(SettingDef.colorNoAlpha(SETTING_TOP_ENCHANT_COLOR, TopEnchantUtil.topColorValue())); - setting(SettingDef.textList(SETTING_TOP_ENCHANT_IGNORE_LIST, topIgnore, TextListSetting.PickerMode.ENCHANTMENTS)); + setting(SettingDef.colorNoAlpha(NameTagKeys.ILLEGAL_ENCHANT_COLOR, IllegalItemUtil.illegalColorValue())); + setting(SettingDef.colorNoAlpha(NameTagKeys.TOP_ENCHANT_COLOR, TopEnchantUtil.topColorValue())); + setting(SettingDef.textList(NameTagKeys.TOP_ENCHANT_IGNORE_LIST, topIgnore, TextListSetting.PickerMode.ENCHANTMENTS)); } @Override @@ -243,152 +186,7 @@ public class NameTags extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - - NameTagRenderer.ensureFontCache(this, textRenderer); - - StatusEffectView.sync(mc); - TotemPopCounter.tick(); - float dt = AnimationUtility.deltaTime(); - - renderQueue.clear(); - playerBuffer.clear(); - seenIds.clear(); - - boolean wantNameplate = toggles.get("Show nameplate"); - boolean wantSlots = toggles.get("Show armor row"); - boolean wantEffects = toggles.get("Show effects"); - boolean wantOpponent = opponentAppleCooldowns.get() && NameTagOpponentRenderer.isOpponentAppleTrackingEnabled(); - if (!wantNameplate && !wantSlots && !wantEffects && !wantOpponent) return; - - boolean hasUnlimited = false; - double maxCutoff = 0.0; - if (wantNameplate) { - if (distanceToggles.get("nameplate")) { - maxCutoff = Math.max(maxCutoff, plateDistance.get()); - } else { - hasUnlimited = true; - } - } - if (wantSlots) { - if (distanceToggles.get("slots")) { - maxCutoff = Math.max(maxCutoff, slotsDistance.get()); - } else { - hasUnlimited = true; - } - } - if (wantEffects) { - if (distanceToggles.get("effects")) { - maxCutoff = Math.max(maxCutoff, effectsDistance.get()); - } else { - hasUnlimited = true; - } - } - if (wantOpponent) { - double oppCutoff = OPPONENT_DISTANCE; - if (oppCutoff > 0) { - maxCutoff = Math.max(maxCutoff, oppCutoff); - } else { - hasUnlimited = true; - } - } - - Vec3 cameraPos = null; - if (mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { - cameraPos = mc.gameRenderer.mainCamera().position(); - } - if (cameraPos == null && mc.player != null) { - cameraPos = mc.player.position(); - } - - for (Player player : mc.level.players()) { - if (!shouldRender(player)) continue; - playerBuffer.add(player); - seenIds.add(player.getUUID()); - } - - if (cameraPos != null && playerBuffer.size() > 1) { - double camX = cameraPos.x; - double camY = cameraPos.y; - double camZ = cameraPos.z; - playerBuffer.sort(Comparator.comparingDouble((Player p) -> p.distanceToSqr(camX, camY, camZ)).reversed()); - } - - for (Player player : playerBuffer) { - Vec3 pos = obtainEntityLerpedPos(player, tickDelta); - double dist = (cameraPos == null) ? 0.0 : cameraPos.distanceTo(pos); - if (!hasUnlimited && maxCutoff > 0.0 && dist > maxCutoff) continue; - - AABB box = player.getBoundingBox().move(pos.subtract(player.position())); - Vec3 anchor = new Vec3(box.getCenter().x, box.maxY + 0.5, box.getCenter().z); - NameTagModel.ScreenRect rect = projectBoxScreen(box, tickDelta); - int centerX; - int anchorY; - if (rect != null) { - centerX = (int) Math.round((rect.minX() + rect.maxX()) * 0.5); - anchorY = (int) Math.round(rect.minY()); - } else { - Vec3 screen = ScreenProjection.worldToScreen(anchor, tickDelta); - if (screen == null) continue; - centerX = (int) screen.x; - anchorY = (int) screen.y; - } - float distOffset = NameTagLayoutEngine.computeDistanceOffset(dist); - int nameY = NameTagLayoutEngine.computeNameY(rect, anchorY, distOffset); - int sectionLift = Math.round(NAMEPLATE_EXTRA_LIFT) + NameTagLayoutEngine.computeEspNameSectionLift(player); - nameY -= sectionLift; - - boolean renderSlots = wantSlots; - if (renderSlots && distanceToggles.get("slots") && dist > slotsDistance.get()) renderSlots = false; - - boolean renderNameplate = wantNameplate; - if (renderNameplate && distanceToggles.get("nameplate") && dist > plateDistance.get()) - renderNameplate = false; - - boolean renderEffects = wantEffects; - if (renderEffects && distanceToggles.get("effects") && dist > effectsDistance.get()) renderEffects = false; - - TotemPopSnapshot totemPopSnapshot = TotemPopCounter.snapshot(player.getUUID()); - float totemPopAnimation = NameTagCache.updateTotemPopAnimation(this, player, totemPopSnapshot, dt); - int totemPopCount = NameTagCache.resolveTotemPopCount(this, player, totemPopSnapshot, totemPopAnimation); - - NameTagModel.LabelInfo labelInfo = renderNameplate ? NameTagLabelBuilder.buildLabelInfo(this, player) : null; - int infoColor = labelInfo != null ? labelInfo.infoColor() : INFO_COLOR_DEFAULT; - NameTagModel.NameplateLayout nameplateLayout = null; - if (renderNameplate && labelInfo != null) { - nameplateLayout = NameTagLayoutEngine.buildLayout( - this, - player, - cachedNameTr != null ? cachedNameTr : textRenderer, - cachedHpTr != null ? cachedHpTr : textRenderer, - labelInfo, - dist, - centerX, - nameY, - totemPopCount, - totemPopAnimation - ); - } - - ItemStack[] slots = renderSlots ? NameTagCache.getCachedEquipmentSlots(this, player) : EMPTY_SLOTS; - List> enchantLines = List.of(); - int maxEnchantLines = 0; - if (renderSlots) { - NameTagModel.EnchantCache cache = NameTagCache.resolveEnchantCache(this, player, slots); - enchantLines = cache.lines; - maxEnchantLines = cache.maxLines; - } - List effects = renderEffects ? NameTagCache.resolveEffects(this, player) : List.of(); - - int itemsY = NameTagLayoutEngine.computeItemsY(rect, nameY, distOffset); - itemsY -= sectionLift; - renderQueue.add(new NameTagModel.RenderEntry(player, dist, distOffset, centerX, nameY, itemsY, rect, - renderSlots, renderNameplate, renderEffects, labelInfo, infoColor, nameplateLayout, - slots, enchantLines, maxEnchantLines, effects, totemPopCount, totemPopAnimation)); - } - - NameTagCache.pruneCaches(this); + NameTagsCollector.collect(this, textRenderer, tickDelta); } @Override @@ -403,7 +201,7 @@ public class NameTags extends Module { } } - private boolean shouldRender(Player player) { + boolean shouldRender(Player player) { Minecraft mc = Minecraft.getInstance(); if (player == null || mc.player == null) return false; if (player == mc.player) { @@ -422,39 +220,4 @@ public class NameTags extends Module { cachedTimeTr = null; NameTagCache.clearCaches(this); } - - private static Vec3 obtainEntityLerpedPos(Entity e, float tickDelta) { - try { - return e.getPosition(tickDelta); - } catch (NoSuchMethodError ex) { - if (e instanceof IEntity access) { - return access.get$InstantRenderPos().lerp(e.position(), tickDelta); - } - return e.position(); - } - } - - private static NameTagModel.ScreenRect projectBoxScreen(AABB box, float tickDelta) { - double minX = Double.POSITIVE_INFINITY, minY = Double.POSITIVE_INFINITY; - double maxX = Double.NEGATIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY; - boolean any = false; - double[] xs = {box.minX, box.maxX}; - double[] ys = {box.minY, box.maxY}; - double[] zs = {box.minZ, box.maxZ}; - for (double x : xs) { - for (double y : ys) { - for (double z : zs) { - Vec3 screen = ScreenProjection.worldToScreen(new Vec3(x, y, z), tickDelta); - if (screen == null) continue; - any = true; - minX = Math.min(minX, screen.x); - minY = Math.min(minY, screen.y); - maxX = Math.max(maxX, screen.x); - maxY = Math.max(maxY, screen.y); - } - } - } - if (!any) return null; - return new NameTagModel.ScreenRect(minX, minY, maxX, maxY); - } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagKeys.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagKeys.java new file mode 100644 index 0000000..6dfe4db --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagKeys.java @@ -0,0 +1,41 @@ +package dev.loki.lovisual.features.module.modules.visuals.nametags; + +/** + * Setting-schema keys for {@link NameTags}. These are the second argument to the + * value factories (the persisted schema id stays the first argument), kept in one + * place so the module class is not peppered with string-literal constants. + * Lives in the {@code impl/} subfolder but the {@code nametags} package. + */ +final class NameTagKeys { + static final String TOGGLES = "toggles"; + static final String DURABILITY_START = "durability_start_percent"; + static final String DISTANCE_TOGGLES = "distance_toggles"; + static final String NAMEPLATE_CUTOFF = "nameplate_cutoff"; + static final String SLOTS_CUTOFF = "slots_cutoff"; + static final String EFFECTS_CUTOFF = "effects_cutoff"; + static final String ARMOR_ENCHANTS = "armor_enchants"; + static final String MELEE_ENCHANTS = "melee_enchants"; + static final String TOOLS_ENCHANTS = "tools_enchants"; + static final String BOW_ENCHANTS = "bow_enchants"; + static final String FISHING_ENCHANTS = "fishing_enchants"; + static final String TRIDENT_ENCHANTS = "trident_enchants"; + static final String CROSSBOW_ENCHANTS = "crossbow_enchants"; + static final String MACE_ENCHANTS = "mace_enchants"; + static final String OTHER_ENCHANTS = "other_enchants"; + static final String ILLEGAL_ENCHANT_COLOR = "illegal_enchant_color"; + static final String ILLEGAL_ENCHANTS_ALWAYS = "illegal_enchants_always"; + static final String TOP_ENCHANT_COLOR = "top_enchant_color"; + static final String TOP_ENCHANT_IGNORE_LIST = "top_enchant_ignore_list"; + static final String HP_DECIMALS = "hp_decimals"; + static final String HP_COLOR_MODE = "hp_color_mode"; + static final String NAMEPLATE_ALPHA = "nameplate_alpha"; + static final String NAMEPLATE_STYLE = "nameplate_style"; + static final String PLAYER_HEAD = "player_head"; + static final String NAMEPLATE_EXTRAS = "nameplate_extras"; + static final String OPPONENT_APPLE_COOLDOWNS = "opponent_apple_cooldowns"; + static final String PVP_PREFIX = "pvp_prefix"; + static final String PVP_PREFIX_COLOR = "pvp_prefix_color"; + + private NameTagKeys() { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagProjection.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagProjection.java new file mode 100644 index 0000000..ded1145 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagProjection.java @@ -0,0 +1,72 @@ +package dev.loki.lovisual.features.module.modules.visuals.nametags; + +import dev.loki.lovisual.mixininterface.entity.IEntity; +import dev.loki.lovisual.render.helpers.clip.ScreenProjection; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +/** + * Pure world-to-screen projection math shared by nametag collection: entity position + * interpolation and bounding-box projection. Lives in the {@code impl/} subfolder but + * the {@code nametags} package so it can reuse {@link NameTagModel.ScreenRect}. + */ +final class NameTagProjection { + + private NameTagProjection() { + } + + /** Screen-space placement anchor for a nametag, or {@code null} if off-screen. */ + record Anchor(int centerX, int anchorY, NameTagModel.ScreenRect rect) { + } + + static Vec3 lerpedPos(Entity e, float tickDelta) { + try { + return e.getPosition(tickDelta); + } catch (NoSuchMethodError ex) { + if (e instanceof IEntity access) { + return access.get$InstantRenderPos().lerp(e.position(), tickDelta); + } + return e.position(); + } + } + + static Anchor resolveAnchor(Player player, Vec3 pos, float tickDelta) { + AABB box = player.getBoundingBox().move(pos.subtract(player.position())); + NameTagModel.ScreenRect rect = projectBoxScreen(box, tickDelta); + if (rect != null) { + int centerX = (int) Math.round((rect.minX() + rect.maxX()) * 0.5); + int anchorY = (int) Math.round(rect.minY()); + return new Anchor(centerX, anchorY, rect); + } + Vec3 topCenter = new Vec3(box.getCenter().x, box.maxY + 0.5, box.getCenter().z); + Vec3 screen = ScreenProjection.worldToScreen(topCenter, tickDelta); + if (screen == null) return null; + return new Anchor((int) screen.x, (int) screen.y, null); + } + + static NameTagModel.ScreenRect projectBoxScreen(AABB box, float tickDelta) { + double minX = Double.POSITIVE_INFINITY, minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY; + boolean any = false; + double[] xs = {box.minX, box.maxX}; + double[] ys = {box.minY, box.maxY}; + double[] zs = {box.minZ, box.maxZ}; + for (double x : xs) { + for (double y : ys) { + for (double z : zs) { + Vec3 screen = ScreenProjection.worldToScreen(new Vec3(x, y, z), tickDelta); + if (screen == null) continue; + any = true; + minX = Math.min(minX, screen.x); + minY = Math.min(minY, screen.y); + maxX = Math.max(maxX, screen.x); + maxY = Math.max(maxY, screen.y); + } + } + } + if (!any) return null; + return new NameTagModel.ScreenRect(minX, minY, maxX, maxY); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagsCollector.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagsCollector.java new file mode 100644 index 0000000..0903ce7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/impl/NameTagsCollector.java @@ -0,0 +1,176 @@ +package dev.loki.lovisual.features.module.modules.visuals.nametags; + +import dev.loki.lovisual.features.module.modules.visuals.nametags.parts.NameTagOpponentRenderer; +import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagLabelBuilder; +import dev.loki.lovisual.features.module.modules.visuals.nametags.render.NameTagRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.player.effect.tracker.StatusEffectView; +import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; +import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot; +import net.minecraft.client.Minecraft; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +import java.util.Comparator; +import java.util.List; + +/** + * Per-frame nametag collection for {@link NameTags}: walks nearby players, applies the + * distance cutoffs and builds the {@code renderQueue} consumed by the foreground pass. + * Lives in the {@code impl/} subfolder but the {@code nametags} package so it can read + * the module's package-private caches and settings. + */ +final class NameTagsCollector { + + private static final int INFO_COLOR_DEFAULT = 0xFFFF8A8A; + private static final float NAMEPLATE_EXTRA_LIFT = 8f; + private static final ItemStack[] EMPTY_SLOTS = new ItemStack[0]; + + private NameTagsCollector() { + } + + static void collect(NameTags m, TextRenderer textRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!m.isEnabled() || mc.level == null || mc.player == null) return; + + NameTagRenderer.ensureFontCache(m, textRenderer); + StatusEffectView.sync(mc); + TotemPopCounter.tick(); + float dt = AnimationUtility.deltaTime(); + + m.renderQueue.clear(); + m.playerBuffer.clear(); + m.seenIds.clear(); + + boolean wantNameplate = m.toggles.get("Show nameplate"); + boolean wantSlots = m.toggles.get("Show armor row"); + boolean wantEffects = m.toggles.get("Show effects"); + boolean wantOpponent = m.opponentAppleCooldowns.get() && NameTagOpponentRenderer.isOpponentAppleTrackingEnabled(); + if (!wantNameplate && !wantSlots && !wantEffects && !wantOpponent) return; + + Cutoff cutoff = computeCutoff(m, wantNameplate, wantSlots, wantEffects, wantOpponent); + Vec3 cameraPos = cameraPos(mc); + gatherPlayers(m, mc, cameraPos); + + Flags want = new Flags(wantNameplate, wantSlots, wantEffects); + for (Player player : m.playerBuffer) { + NameTagModel.RenderEntry entry = buildEntry(m, player, cameraPos, textRenderer, dt, tickDelta, want, cutoff); + if (entry != null) m.renderQueue.add(entry); + } + NameTagCache.pruneCaches(m); + } + + private record Cutoff(double maxCutoff, boolean hasUnlimited) { + } + + private record Flags(boolean nameplate, boolean slots, boolean effects) { + } + + private record Equipment(ItemStack[] slots, List> lines, int maxLines) { + } + + private static Cutoff computeCutoff(NameTags m, boolean wantNameplate, boolean wantSlots, boolean wantEffects, boolean wantOpponent) { + boolean hasUnlimited = false; + double maxCutoff = 0.0; + if (wantNameplate) { + if (m.distanceToggles.get("nameplate")) maxCutoff = Math.max(maxCutoff, m.plateDistance.get()); + else hasUnlimited = true; + } + if (wantSlots) { + if (m.distanceToggles.get("slots")) maxCutoff = Math.max(maxCutoff, m.slotsDistance.get()); + else hasUnlimited = true; + } + if (wantEffects) { + if (m.distanceToggles.get("effects")) maxCutoff = Math.max(maxCutoff, m.effectsDistance.get()); + else hasUnlimited = true; + } + if (wantOpponent) { + double oppCutoff = NameTags.OPPONENT_DISTANCE; + if (oppCutoff > 0) maxCutoff = Math.max(maxCutoff, oppCutoff); + else hasUnlimited = true; + } + return new Cutoff(maxCutoff, hasUnlimited); + } + + private static Vec3 cameraPos(Minecraft mc) { + if (mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { + return mc.gameRenderer.mainCamera().position(); + } + return mc.player != null ? mc.player.position() : null; + } + + private static void gatherPlayers(NameTags m, Minecraft mc, Vec3 cameraPos) { + for (Player player : mc.level.players()) { + if (!m.shouldRender(player)) continue; + m.playerBuffer.add(player); + m.seenIds.add(player.getUUID()); + } + if (cameraPos != null && m.playerBuffer.size() > 1) { + double camX = cameraPos.x; + double camY = cameraPos.y; + double camZ = cameraPos.z; + m.playerBuffer.sort(Comparator.comparingDouble((Player p) -> p.distanceToSqr(camX, camY, camZ)).reversed()); + } + } + + private static NameTagModel.RenderEntry buildEntry(NameTags m, Player player, Vec3 cameraPos, TextRenderer textRenderer, + float dt, float tickDelta, Flags want, Cutoff cutoff) { + Vec3 pos = NameTagProjection.lerpedPos(player, tickDelta); + double dist = (cameraPos == null) ? 0.0 : cameraPos.distanceTo(pos); + if (!cutoff.hasUnlimited() && cutoff.maxCutoff() > 0.0 && dist > cutoff.maxCutoff()) return null; + + NameTagProjection.Anchor anchor = NameTagProjection.resolveAnchor(player, pos, tickDelta); + if (anchor == null) return null; + + float distOffset = NameTagLayoutEngine.computeDistanceOffset(dist); + int sectionLift = Math.round(NAMEPLATE_EXTRA_LIFT) + NameTagLayoutEngine.computeEspNameSectionLift(player); + int nameY = NameTagLayoutEngine.computeNameY(anchor.rect(), anchor.anchorY(), distOffset) - sectionLift; + Flags flags = applyDistanceCutoffs(m, want, dist); + + TotemPopSnapshot snapshot = TotemPopCounter.snapshot(player.getUUID()); + float totemPopAnimation = NameTagCache.updateTotemPopAnimation(m, player, snapshot, dt); + int totemPopCount = NameTagCache.resolveTotemPopCount(m, player, snapshot, totemPopAnimation); + + NameTagModel.LabelInfo labelInfo = flags.nameplate() ? NameTagLabelBuilder.buildLabelInfo(m, player) : null; + int infoColor = labelInfo != null ? labelInfo.infoColor() : INFO_COLOR_DEFAULT; + NameTagModel.NameplateLayout nameplateLayout = + buildLayout(m, player, textRenderer, labelInfo, flags, dist, anchor.centerX(), nameY, totemPopCount, totemPopAnimation); + + Equipment equipment = resolveEquipment(m, player, flags); + List effects = flags.effects() ? NameTagCache.resolveEffects(m, player) : List.of(); + int itemsY = NameTagLayoutEngine.computeItemsY(anchor.rect(), nameY, distOffset) - sectionLift; + + return new NameTagModel.RenderEntry(player, dist, distOffset, anchor.centerX(), nameY, itemsY, anchor.rect(), + flags.slots(), flags.nameplate(), flags.effects(), labelInfo, infoColor, nameplateLayout, + equipment.slots(), equipment.lines(), equipment.maxLines(), effects, totemPopCount, totemPopAnimation); + } + + private static Flags applyDistanceCutoffs(NameTags m, Flags want, double dist) { + boolean renderSlots = want.slots(); + if (renderSlots && m.distanceToggles.get("slots") && dist > m.slotsDistance.get()) renderSlots = false; + boolean renderNameplate = want.nameplate(); + if (renderNameplate && m.distanceToggles.get("nameplate") && dist > m.plateDistance.get()) renderNameplate = false; + boolean renderEffects = want.effects(); + if (renderEffects && m.distanceToggles.get("effects") && dist > m.effectsDistance.get()) renderEffects = false; + return new Flags(renderNameplate, renderSlots, renderEffects); + } + + private static NameTagModel.NameplateLayout buildLayout(NameTags m, Player player, TextRenderer textRenderer, + NameTagModel.LabelInfo labelInfo, Flags flags, double dist, int centerX, int nameY, int popCount, float popAnim) { + if (!flags.nameplate() || labelInfo == null) return null; + return NameTagLayoutEngine.buildLayout(m, player, + m.cachedNameTr != null ? m.cachedNameTr : textRenderer, + m.cachedHpTr != null ? m.cachedHpTr : textRenderer, + labelInfo, dist, centerX, nameY, popCount, popAnim); + } + + private static Equipment resolveEquipment(NameTags m, Player player, Flags flags) { + if (!flags.slots()) return new Equipment(EMPTY_SLOTS, List.of(), 0); + ItemStack[] slots = NameTagCache.getCachedEquipmentSlots(m, player); + NameTagModel.EnchantCache cache = NameTagCache.resolveEnchantCache(m, player, slots); + return new Equipment(slots, cache.lines, cache.maxLines); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java index 2ad0d97..ad0a403 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java @@ -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.module.modules.visuals.nametags.parts; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java index df6eff3..b89a7e1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java @@ -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.module.modules.visuals.nametags.parts; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java index 95a4761..ca000e6 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java @@ -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.module.modules.visuals.nametags.parts; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java index 33f9c90..c01c994 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java @@ -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.module.modules.visuals.nametags.parts; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java index c5a0ae6..ecba52e 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java @@ -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.module.modules.visuals.nametags.render; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java index acaaa90..9ca5e6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java @@ -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.module.modules.visuals.nametags.render; import dev.loki.lovisual.util.item.enchant.EnchantMeta; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java index d71719d..dc89039 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java @@ -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.module.modules.visuals.nametags.render; import dev.loki.lovisual.features.relations.PlayerRelations; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java index 3a956e0..e5e511f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java @@ -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.module.modules.visuals.nametags.render; import dev.loki.lovisual.features.relations.category.CategoryService; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java index 262fea0..0f017ac 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java @@ -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.module.modules.visuals.optimization; import net.minecraft.core.particles.ParticleTypes; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java index 242452f..bd996d1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java @@ -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.module.modules.visuals.particles; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java index 1c517d5..321ae17 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java index 23fdb91..b29e93a 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.config.common.CommonSettingSchemas; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java index 914f854..7d57997 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java @@ -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.module.modules.visuals.player; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java index 323f132..a5fd3bf 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java @@ -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.module.modules.visuals.player; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java index 63a0538..80c0269 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java index 5ebafa2..b127cca 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.features.gui.hud.draggable.impl.combat.Itemizer; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java index bf57001..0a98542 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java index b88277b..1b01364 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java @@ -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.module.modules.visuals.player; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java index 9015f16..10a1be7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java @@ -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.module.modules.visuals.scene; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java index d316f99..cc3cf7c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java @@ -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.module.modules.visuals.scene; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java index a821fdf..d7f50ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java @@ -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.module.modules.visuals.scene; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java index e076ec6..e023e2f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java @@ -4,6 +4,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WaterHighlightUniforms; import dev.loki.lovisual.util.time.GameClock; @@ -89,7 +90,7 @@ final class BlockHighlightOutline { colors.blue(secondary) / 255f, colors.alpha(secondary) / 255f, time ); - Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() + Renderer3DBatchBindings bindings = Renderer3DBatchBindings.none() .withUniform("WaterData", WaterHighlightUniforms.get()); MeshBuilder mesh = renderer.batch(pipeline, Renderer3D.DepthMode.NONE, bindings); if (mesh == null) return; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java index c94b16a..06426b5 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java @@ -1,8 +1,4 @@ -/* - * 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.module.modules.visuals.scene.visual; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedDepthBindings.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedDepthBindings.java new file mode 100644 index 0000000..54c596b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedDepthBindings.java @@ -0,0 +1,37 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import com.mojang.blaze3d.textures.GpuTextureView; + +final class ReimaginedDepthBindings { + private final GpuTextureView main; + private final GpuTextureView translucent; + private final GpuTextureView itemEntity; + private final GpuTextureView particles; + private final GpuTextureView weather; + private final GpuTextureView clouds; + ReimaginedDepthBindings(GpuTextureView main, GpuTextureView translucent, GpuTextureView itemEntity, GpuTextureView particles, GpuTextureView weather, GpuTextureView clouds) { + this.main = main; this.translucent = translucent; this.itemEntity = itemEntity; this.particles = particles; this.weather = weather; this.clouds = clouds; + } + boolean hasAnyDepth() { return primary() != null; } + boolean hasMain() { return main != null; } + boolean hasTranslucent() { return translucent != null; } + boolean hasItemEntity() { return itemEntity != null; } + boolean hasParticles() { return particles != null; } + boolean hasWeather() { return weather != null; } + boolean hasClouds() { return clouds != null; } + GpuTextureView primary() { + if (main != null) return main; + if (translucent != null) return translucent; + if (itemEntity != null) return itemEntity; + if (particles != null) return particles; + if (weather != null) return weather; + return clouds; + } + GpuTextureView mainOr(GpuTextureView p) { return main != null ? main : firstOr(p); } + GpuTextureView translucentOr(GpuTextureView p) { return translucent != null ? translucent : firstOr(p); } + GpuTextureView itemEntityOr(GpuTextureView p) { return itemEntity != null ? itemEntity : firstOr(p); } + GpuTextureView particlesOr(GpuTextureView p) { return particles != null ? particles : firstOr(p); } + GpuTextureView weatherOr(GpuTextureView p) { return weather != null ? weather : firstOr(p); } + GpuTextureView cloudsOr(GpuTextureView p) { return clouds != null ? clouds : firstOr(p); } + private GpuTextureView firstOr(GpuTextureView placeholder) { GpuTextureView p = primary(); return p != null ? p : placeholder; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java index 3b28a61..7749579 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java @@ -1,14 +1,5 @@ -/* - * 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.module.modules.visuals.scene.visual; -import dev.loki.lovisual.features.theme.Theme; -import com.mojang.blaze3d.textures.GpuTextureView; import dev.loki.lovisual.config.values.collection.ConfigValue; import dev.loki.lovisual.config.values.color.RGBColorValue; import dev.loki.lovisual.config.values.mode.EnumValue; @@ -18,657 +9,110 @@ import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.render.engine.postprocess.blur.DepthOfFieldDepthSource; import dev.loki.lovisual.render.engine.postprocess.blur.DepthOfFieldQuality; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext; import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.language.I18n; -import org.joml.Matrix4f; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; -import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; -import dev.loki.lovisual.render.engine.depth.WorldSceneDepth; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; -import dev.loki.lovisual.render.engine.uniform.impl.post.DepthOfFieldUniforms; -import dev.loki.lovisual.render.helpers.material.SodiumMaterialFlags; -import dev.loki.lovisual.render.iris.scene.IrisSceneDepth; import dev.loki.lovisual.render.iris.runtime.IrisRuntime; -import dev.loki.lovisual.util.logging.DebugLog; - +import net.minecraft.client.resources.language.I18n; import java.util.LinkedHashMap; import java.util.Map; -//todo Description @ModuleInfo(id = "reimaginedvisual", displayName = "ReimaginedVisual", category = ModuleCategory.VISUALS) -public class ReimaginedVisual extends Module implements PostProcessPass { - - private static final String SETTING_EFFECTS = "effects"; - private static final String EFFECT_SHADER_SKY = "shader_sky"; - private static final String EFFECT_WORLD_SUN = "world_sun"; - private static final String EFFECT_WAVY_VEGETATION = "wavy_vegetation"; - private static final String EFFECT_DEPTH_OF_FIELD = "depth_of_field"; - private static final String SETTING_SKYBOX_SHADER_SYNC_THEME = "skybox_shader_sync_theme"; - private static final String SETTING_SKYBOX_SHADER_COLOR = "skybox_shader_color"; - private static final String SETTING_SKYBOX_SHADER_MOTION_SPEED = "skybox_shader_motion_speed"; - private static final String SETTING_SKYBOX_SHADER_AURORA_ENABLED = "skybox_shader_aurora_enabled"; - private static final String SETTING_SKYBOX_SHADER_AURORA_INTENSITY = "skybox_shader_aurora_intensity"; - private static final String SETTING_SKYBOX_SHADER_AURORA_SPEED = "skybox_shader_aurora_speed"; - private static final String SETTING_SKYBOX_SHADER_SMALL_STARS = "skybox_shader_small_stars"; - private static final String SETTING_SKYBOX_SHADER_DUST_STARS = "skybox_shader_dust_stars"; - private static final String SETTING_SKYBOX_SHADER_MEDIUM_STARS = "skybox_shader_medium_stars"; - private static final String SETTING_SKYBOX_SHADER_LARGE_STARS = "skybox_shader_large_stars"; - private static final String SETTING_SKYBOX_SHADER_STAR_BRIGHTNESS = "skybox_shader_star_brightness"; - private static final String SETTING_SKYBOX_SHADER_TWINKLE_STRENGTH = "skybox_shader_twinkle_strength"; - private static final String SETTING_SKYBOX_SHADER_LAYERS = "skybox_shader_layers"; - private static final String SETTING_SKYBOX_SKY_FOG_BLEND = "skybox_sky_fog_blend"; - private static final String SETTING_WORLD_SUN_GLARE_ENABLED = "world_sun_glare_enabled"; - private static final String SETTING_WORLD_SUN_SIZE = "world_sun_size"; - private static final String SETTING_WORLD_SUN_GLOW = "world_sun_glow"; - private static final String SETTING_WORLD_SUN_INTENSITY = "world_sun_intensity"; - private static final String SETTING_WAVY_VEGETATION_ROOTED_HORIZONTAL_AMPLITUDE = "wavy_vegetation_rooted_horizontal_amplitude"; - private static final String SETTING_WAVY_VEGETATION_ROOTED_VERTICAL_AMPLITUDE = "wavy_vegetation_rooted_vertical_amplitude"; - private static final String SETTING_WAVY_VEGETATION_FREE_HORIZONTAL_AMPLITUDE = "wavy_vegetation_free_horizontal_amplitude"; - private static final String SETTING_WAVY_VEGETATION_FREE_VERTICAL_AMPLITUDE = "wavy_vegetation_free_vertical_amplitude"; - private static final String SETTING_WAVY_VEGETATION_SPEED = "wavy_vegetation_speed"; - private static final String SETTING_DOF_DEPTH_SOURCE = "dof_depth_source"; - private static final String SETTING_DOF_FAR_START = "dof_far_start"; - private static final String SETTING_DOF_FAR_TRANSITION = "dof_far_transition"; - private static final String SETTING_DOF_STRENGTH = "dof_strength"; - private static final String SETTING_DOF_MAX_RADIUS = "dof_max_radius"; - private static final String SETTING_DOF_QUALITY = "dof_quality"; - private static final String SETTING_DOF_DEBUG_COC = "dof_debug_coc"; - private static final String IRIS_SHADER_SKYBOX_REASON_KEY = "setting.reimaginedvisual.skybox_shader.iris_blocked"; - private static final String IRIS_SHADER_SKYBOX_REASON_FALLBACK = "Iris is loaded; ReimaginedVisual shader skybox is not used."; - private static final String IRIS_DOF_REASON_KEY = "setting.reimaginedvisual.depth_of_field.iris_blocked"; - private static final String IRIS_DOF_REASON_FALLBACK = "Iris shaderpack pipeline is active."; - private static final Map DEFAULT_EFFECTS = createDefaultEffects(); - private static final Map DEFAULT_SKYBOX_SHADER_LAYERS = createDefaultSkyboxShaderLayers(); - private final Matrix4f dofProjection = new Matrix4f(); - private final BooleanMapValue effects = group( - "reimaginedVisualEffects", - SETTING_EFFECTS, - DEFAULT_EFFECTS - ); - private final BooleanValue skyboxShaderSyncTheme = - shaderSkyboxNotAppliedWithIris(visibleWhen(bool("reimaginedVisualSkyboxShaderSyncTheme", SETTING_SKYBOX_SHADER_SYNC_THEME, true), - this::isShaderSkyboxSettingsVisible)); - private final RGBColorValue skyboxShaderColor = - shaderSkyboxNotAppliedWithIris(visibleWhen(colorNoAlpha("reimaginedVisualSkyboxShaderColor", SETTING_SKYBOX_SHADER_COLOR, "#78A7FF"), - () -> isShaderSkyboxSettingsVisible() && !skyboxShaderSyncTheme.get())); - private final NumberValue skyboxShaderMotionSpeed = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderMotionSpeed", SETTING_SKYBOX_SHADER_MOTION_SPEED, 0.45f, 0.0f, 2.0f), - this::isShaderSkyboxSettingsVisible)); - private final BooleanValue skyboxShaderAuroraEnabled = - shaderSkyboxNotAppliedWithIris(visibleWhen(bool("reimaginedVisualSkyboxShaderAuroraEnabled", SETTING_SKYBOX_SHADER_AURORA_ENABLED, false), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderAuroraIntensity = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderAuroraIntensity", SETTING_SKYBOX_SHADER_AURORA_INTENSITY, 0.70f, 0.0f, 1.5f), - () -> isShaderSkyboxSettingsVisible() && skyboxShaderAuroraEnabled.get())); - private final NumberValue skyboxShaderAuroraSpeed = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderAuroraSpeed", SETTING_SKYBOX_SHADER_AURORA_SPEED, 0.38f, 0.0f, 2.0f), - () -> isShaderSkyboxSettingsVisible() && skyboxShaderAuroraEnabled.get())); - private final NumberValue skyboxShaderSmallStars = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderSmallStars", SETTING_SKYBOX_SHADER_SMALL_STARS, 1.60f, 0.0f, 10.0f), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderDustStars = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderDustStars", SETTING_SKYBOX_SHADER_DUST_STARS, 1.40f, 0.0f, 10.0f), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderMediumStars = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderMediumStars", SETTING_SKYBOX_SHADER_MEDIUM_STARS, 1.30f, 0.0f, 10.0f), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderLargeStars = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderLargeStars", SETTING_SKYBOX_SHADER_LARGE_STARS, 1.15f, 0.0f, 10.0f), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderStarBrightness = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderStarBrightness", SETTING_SKYBOX_SHADER_STAR_BRIGHTNESS, 1.35f, 0.0f, 3.0f), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxShaderTwinkleStrength = - shaderSkyboxNotAppliedWithIris(visibleWhen(num("reimaginedVisualSkyboxShaderTwinkleStrength", SETTING_SKYBOX_SHADER_TWINKLE_STRENGTH, 1.35f, 0.0f, 3.0f), - this::isShaderSkyboxSettingsVisible)); - private final BooleanMapValue skyboxShaderLayers = - shaderSkyboxNotAppliedWithIris(visibleWhen(group("reimaginedVisualSkyboxShaderLayers", SETTING_SKYBOX_SHADER_LAYERS, DEFAULT_SKYBOX_SHADER_LAYERS), - this::isShaderSkyboxSettingsVisible)); - private final NumberValue skyboxSkyFogBlend = - shaderSkyboxNotAppliedWithIris(visibleWhen( - num("reimaginedVisualSkyboxSkyFogBlend", SETTING_SKYBOX_SKY_FOG_BLEND, 0.35f, 0.0f, 1.0f), - this::isShaderSkyboxSettingsVisible - )); - private final BooleanValue worldSunGlareEnabled = - visibleWhen(bool("reimaginedVisualWorldSunGlareEnabled", SETTING_WORLD_SUN_GLARE_ENABLED, true), - this::isWorldSunSettingsVisible); - private final NumberValue worldSunSize = - visibleWhen(num("reimaginedVisualWorldSunSize", SETTING_WORLD_SUN_SIZE, 0.06f, 0.01f, 0.20f), - this::isWorldSunSettingsVisible); - private final NumberValue worldSunGlow = - visibleWhen(num("reimaginedVisualWorldSunGlow", SETTING_WORLD_SUN_GLOW, 0.18f, 0.02f, 0.40f), - this::isWorldSunSettingsVisible); - private final NumberValue worldSunIntensity = - visibleWhen(num("reimaginedVisualWorldSunIntensity", SETTING_WORLD_SUN_INTENSITY, 1.0f, 0.0f, 3.0f), - this::isWorldSunSettingsVisible); - private final NumberValue wavyVegetationRootedHorizontalAmplitude = - visibleWhen(num("reimaginedVisualWavyVegetationRootedHorizontalAmplitude", SETTING_WAVY_VEGETATION_ROOTED_HORIZONTAL_AMPLITUDE, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private final NumberValue wavyVegetationRootedVerticalAmplitude = - visibleWhen(num("reimaginedVisualWavyVegetationRootedVerticalAmplitude", SETTING_WAVY_VEGETATION_ROOTED_VERTICAL_AMPLITUDE, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private final NumberValue wavyVegetationFreeHorizontalAmplitude = - visibleWhen(num("reimaginedVisualWavyVegetationFreeHorizontalAmplitude", SETTING_WAVY_VEGETATION_FREE_HORIZONTAL_AMPLITUDE, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private final NumberValue wavyVegetationFreeVerticalAmplitude = - visibleWhen(num("reimaginedVisualWavyVegetationFreeVerticalAmplitude", SETTING_WAVY_VEGETATION_FREE_VERTICAL_AMPLITUDE, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private final NumberValue wavyVegetationSpeed = - visibleWhen(num("reimaginedVisualWavyVegetationSpeed", SETTING_WAVY_VEGETATION_SPEED, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private boolean wavyVegetationStateInitialized; - private boolean lastWavyVegetationActive; - private int lastWavyVegetationSettingsPacked; - private final EnumValue dofDepthSource = - depthOfFieldNotAppliedWithIris(visibleWhen(enumSetting("reimaginedVisualDofDepthSource", SETTING_DOF_DEPTH_SOURCE, DepthOfFieldDepthSource.WORLD_SCENE, DepthOfFieldDepthSource.values()), - this::isDepthOfFieldSettingsVisible)); - private final NumberValue dofFarStart = - depthOfFieldNotAppliedWithIris(visibleWhen(num("reimaginedVisualDofFarStart", SETTING_DOF_FAR_START, 4.0f, 0.0f, 512.0f), - this::isDepthOfFieldSettingsVisible)); - private final NumberValue dofFarTransition = - depthOfFieldNotAppliedWithIris(visibleWhen(num("reimaginedVisualDofFarTransition", SETTING_DOF_FAR_TRANSITION, 24.0f, 1.0f, 1024.0f), - this::isDepthOfFieldSettingsVisible)); - private final NumberValue dofStrength = - depthOfFieldNotAppliedWithIris(visibleWhen(num("reimaginedVisualDofStrength", SETTING_DOF_STRENGTH, 0.65f, 0.0f, 1.5f), - this::isDepthOfFieldSettingsVisible)); - private final NumberValue dofMaxRadius = - depthOfFieldNotAppliedWithIris(visibleWhen(num("reimaginedVisualDofMaxRadius", SETTING_DOF_MAX_RADIUS, 8.0f, 0.0f, 32.0f), - this::isDepthOfFieldSettingsVisible)); - private final EnumValue dofQuality = - depthOfFieldNotAppliedWithIris(visibleWhen(enumSetting("reimaginedVisualDofQuality", SETTING_DOF_QUALITY, DepthOfFieldQuality.MEDIUM, DepthOfFieldQuality.values()), - this::isDepthOfFieldSettingsVisible)); - private final BooleanValue dofDebugCoc = - depthOfFieldNotAppliedWithIris(visibleWhen(bool("reimaginedVisualDofDebugCoc", SETTING_DOF_DEBUG_COC, false), - this::isDepthOfFieldSettingsVisible)); - private boolean depthSamplerSupported = true; - - { - PostProcessManager.register(this); +public class ReimaginedVisual extends Module { + private final BooleanMapValue effects; + private final ReimaginedSkyboxHelper skybox; + private final ReimaginedSunHelper sun; + private final ReimaginedVegetationHelper vegetation; + private final ReimaginedDepthOfFieldHelper depthOfField; + public ReimaginedVisual() { + Map defEffects = createDefaultEffects(); + Map defLayers = createDefaultSkyboxLayers(); + BooleanMapValue eff = group("reimaginedVisualEffects", "effects", defEffects); + this.effects = eff; + BooleanValue syncTheme = decorateSkybox(visibleWhen(bool("reimaginedVisualSkyboxShaderSyncTheme", "skybox_shader_sync_theme", true), () -> eff.get("shader_sky")), eff); + RGBColorValue color = decorateSkybox(visibleWhen(colorNoAlpha("reimaginedVisualSkyboxShaderColor", "skybox_shader_color", "#78A7FF"), () -> eff.get("shader_sky") && !syncTheme.get()), eff); + NumberValue motionSpeed = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderMotionSpeed", "skybox_shader_motion_speed", 0.45f, 0.0f, 2.0f), () -> eff.get("shader_sky")), eff); + BooleanValue auroraEnabled = decorateSkybox(visibleWhen(bool("reimaginedVisualSkyboxShaderAuroraEnabled", "skybox_shader_aurora_enabled", false), () -> eff.get("shader_sky")), eff); + NumberValue auroraIntensity = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderAuroraIntensity", "skybox_shader_aurora_intensity", 0.70f, 0.0f, 1.5f), () -> eff.get("shader_sky") && auroraEnabled.get()), eff); + NumberValue auroraSpeed = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderAuroraSpeed", "skybox_shader_aurora_speed", 0.38f, 0.0f, 2.0f), () -> eff.get("shader_sky") && auroraEnabled.get()), eff); + NumberValue smallStars = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderSmallStars", "skybox_shader_small_stars", 1.60f, 0.0f, 10.0f), () -> eff.get("shader_sky")), eff); + NumberValue dustStars = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderDustStars", "skybox_shader_dust_stars", 1.40f, 0.0f, 10.0f), () -> eff.get("shader_sky")), eff); + NumberValue mediumStars = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderMediumStars", "skybox_shader_medium_stars", 1.30f, 0.0f, 10.0f), () -> eff.get("shader_sky")), eff); + NumberValue largeStars = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderLargeStars", "skybox_shader_large_stars", 1.15f, 0.0f, 10.0f), () -> eff.get("shader_sky")), eff); + NumberValue starBrightness = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderStarBrightness", "skybox_shader_star_brightness", 1.35f, 0.0f, 3.0f), () -> eff.get("shader_sky")), eff); + NumberValue twinkle = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxShaderTwinkleStrength", "skybox_shader_twinkle_strength", 1.35f, 0.0f, 3.0f), () -> eff.get("shader_sky")), eff); + BooleanMapValue layers = decorateSkybox(visibleWhen(group("reimaginedVisualSkyboxShaderLayers", "skybox_shader_layers", defLayers), () -> eff.get("shader_sky")), eff); + NumberValue fogBlend = decorateSkybox(visibleWhen(num("reimaginedVisualSkyboxSkyFogBlend", "skybox_sky_fog_blend", 0.35f, 0.0f, 1.0f), () -> eff.get("shader_sky")), eff); + this.skybox = new ReimaginedSkyboxHelper(syncTheme, color, motionSpeed, auroraEnabled, auroraIntensity, auroraSpeed, smallStars, dustStars, mediumStars, largeStars, starBrightness, twinkle, layers, fogBlend, defLayers); + BooleanValue glare = visibleWhen(bool("reimaginedVisualWorldSunGlareEnabled", "world_sun_glare_enabled", true), () -> eff.get("world_sun")); + NumberValue size = visibleWhen(num("reimaginedVisualWorldSunSize", "world_sun_size", 0.06f, 0.01f, 0.20f), () -> eff.get("world_sun")); + NumberValue glow = visibleWhen(num("reimaginedVisualWorldSunGlow", "world_sun_glow", 0.18f, 0.02f, 0.40f), () -> eff.get("world_sun")); + NumberValue intensity = visibleWhen(num("reimaginedVisualWorldSunIntensity", "world_sun_intensity", 1.0f, 0.0f, 3.0f), () -> eff.get("world_sun")); + this.sun = new ReimaginedSunHelper(glare, size, glow, intensity); + NumberValue rootedH = visibleWhen(num("reimaginedVisualWavyVegetationRootedHorizontalAmplitude", "wavy_vegetation_rooted_horizontal_amplitude", 1.0f, 0.0f, 3.0f), () -> eff.get("wavy_vegetation")); + NumberValue rootedV = visibleWhen(num("reimaginedVisualWavyVegetationRootedVerticalAmplitude", "wavy_vegetation_rooted_vertical_amplitude", 1.0f, 0.0f, 3.0f), () -> eff.get("wavy_vegetation")); + NumberValue freeH = visibleWhen(num("reimaginedVisualWavyVegetationFreeHorizontalAmplitude", "wavy_vegetation_free_horizontal_amplitude", 1.0f, 0.0f, 3.0f), () -> eff.get("wavy_vegetation")); + NumberValue freeV = visibleWhen(num("reimaginedVisualWavyVegetationFreeVerticalAmplitude", "wavy_vegetation_free_vertical_amplitude", 1.0f, 0.0f, 3.0f), () -> eff.get("wavy_vegetation")); + NumberValue speed = visibleWhen(num("reimaginedVisualWavyVegetationSpeed", "wavy_vegetation_speed", 1.0f, 0.0f, 3.0f), () -> eff.get("wavy_vegetation")); + this.vegetation = new ReimaginedVegetationHelper(rootedH, rootedV, freeH, freeV, speed); + EnumValue source = decorateDepth(visibleWhen(enumSetting("reimaginedVisualDofDepthSource", "dof_depth_source", DepthOfFieldDepthSource.WORLD_SCENE, DepthOfFieldDepthSource.values()), () -> eff.get("depth_of_field")), eff); + NumberValue farStart = decorateDepth(visibleWhen(num("reimaginedVisualDofFarStart", "dof_far_start", 4.0f, 0.0f, 512.0f), () -> eff.get("depth_of_field")), eff); + NumberValue farTrans = decorateDepth(visibleWhen(num("reimaginedVisualDofFarTransition", "dof_far_transition", 24.0f, 1.0f, 1024.0f), () -> eff.get("depth_of_field")), eff); + NumberValue strength = decorateDepth(visibleWhen(num("reimaginedVisualDofStrength", "dof_strength", 0.65f, 0.0f, 1.5f), () -> eff.get("depth_of_field")), eff); + NumberValue maxRadius = decorateDepth(visibleWhen(num("reimaginedVisualDofMaxRadius", "dof_max_radius", 8.0f, 0.0f, 32.0f), () -> eff.get("depth_of_field")), eff); + EnumValue quality = decorateDepth(visibleWhen(enumSetting("reimaginedVisualDofQuality", "dof_quality", DepthOfFieldQuality.MEDIUM, DepthOfFieldQuality.values()), () -> eff.get("depth_of_field")), eff); + BooleanValue debugCoc = decorateDepth(visibleWhen(bool("reimaginedVisualDofDebugCoc", "dof_debug_coc", false), () -> eff.get("depth_of_field")), eff); + this.depthOfField = new ReimaginedDepthOfFieldHelper(source, farStart, farTrans, strength, maxRadius, quality, debugCoc); + PostProcessManager.register(depthOfField); } - - private static float clampStarAmount(float value) { - return Math.max(0.0f, Math.min(10.0f, value)); + private > V decorateSkybox(V value, BooleanMapValue eff) { + return notAppliedWhen(visibleWhen(value, () -> eff.get("shader_sky")), () -> eff.get("shader_sky") && IrisRuntime.isModLoaded(), () -> { String t = I18n.get("setting.reimaginedvisual.skybox_shader.iris_blocked"); return "setting.reimaginedvisual.skybox_shader.iris_blocked".equals(t) ? "Iris is loaded; ReimaginedVisual shader skybox is not used." : t; }); } - - private static Map createDefaultEffects() { - LinkedHashMap defaults = new LinkedHashMap<>(); - defaults.put(EFFECT_SHADER_SKY, true); - defaults.put(EFFECT_WORLD_SUN, true); - defaults.put(EFFECT_WAVY_VEGETATION, false); - defaults.put(EFFECT_DEPTH_OF_FIELD, false); - return defaults; + private > V decorateDepth(V value, BooleanMapValue eff) { + return notAppliedWhen(visibleWhen(value, () -> eff.get("depth_of_field")), () -> IrisRuntime.isShaderpackRendererActive(), () -> { String t = I18n.get("setting.reimaginedvisual.depth_of_field.iris_blocked"); return "setting.reimaginedvisual.depth_of_field.iris_blocked".equals(t) ? "Iris shaderpack pipeline is active." : t; }); } - - private static Map createDefaultSkyboxShaderLayers() { - LinkedHashMap defaults = new LinkedHashMap<>(); - defaults.put("waves", true); - defaults.put("ribbons", true); - defaults.put("sweeps", true); - defaults.put("veil", true); - defaults.put("nebula", true); - defaults.put("detail_curtains", true); - defaults.put("polar_arc", true); - defaults.put("bursts", true); - defaults.put("water_veil", true); - defaults.put("caustics", true); - defaults.put("refracted_aurora", true); - defaults.put("northern_aurora", true); - defaults.put("small_stars", true); - defaults.put("dust_stars", true); - defaults.put("medium_stars", true); - defaults.put("large_stars", true); - defaults.put("detail_stars", true); - return defaults; + private Map createDefaultEffects() { + LinkedHashMap m = new LinkedHashMap<>(); + m.put("shader_sky", true); m.put("world_sun", true); m.put("wavy_vegetation", false); m.put("depth_of_field", false); + return m; } - - public static boolean isWorldSunEnabledStatic() { - ReimaginedVisual module = module(); - return module != null && module.isEnabled() && module.isEffectSelected(EFFECT_WORLD_SUN); - } - - public static float getWorldSunSizeStatic() { - ReimaginedVisual module = module(); - return module != null ? module.worldSunSize.get() : 0.06f; - } - - public static float getWorldSunGlowStatic() { - ReimaginedVisual module = module(); - return module != null ? module.worldSunGlow.get() : 0.18f; - } - - public static float getWorldSunIntensityStatic() { - ReimaginedVisual module = module(); - return module != null ? module.worldSunIntensity.get() : 1.0f; - } - - public static boolean isWorldSunGlareEnabledStatic() { - ReimaginedVisual module = module(); - return module != null - && module.isEnabled() - && module.isEffectSelected(EFFECT_WORLD_SUN) - && module.worldSunGlareEnabled.get(); - } - - public static boolean isWavyVegetationEnabledStatic() { - ReimaginedVisual module = module(); - return module != null - && module.isEnabled() - && module.isEffectSelected(EFFECT_WAVY_VEGETATION); - } - - public static int packWavyVegetationSettingsStatic() { - ReimaginedVisual module = module(); - return module != null ? module.packWavyVegetationSettings() : packWavyVegetationSettings(1.0f, 1.0f, 1.0f, 1.0f, 1.0f); - } - - private static ReimaginedVisual module() { - return Modules.get(ReimaginedVisual.class); - } - - public boolean isShaderSkyboxEnabled() { - return isEnabled() && isEffectSelected(EFFECT_SHADER_SKY); - } - - public int getShaderSkyboxColor() { - if (skyboxShaderSyncTheme.get()) { - return Theme.theme().accent() & 0x00FFFFFF; - } - return skyboxShaderColor.getArgb() & 0x00FFFFFF; - } - - public float getSkyboxSkyFogBlend() { - return Math.max(0.0f, Math.min(1.0f, skyboxSkyFogBlend.get())); - } - - public float getSkyboxShaderMotionSpeed() { - return Math.max(0.0f, Math.min(2.0f, skyboxShaderMotionSpeed.get())); - } - - public boolean isSkyboxShaderAuroraEnabled() { - return skyboxShaderAuroraEnabled.get(); - } - - public float getSkyboxShaderAuroraIntensity() { - return skyboxShaderAuroraEnabled.get() ? Math.max(0.0f, Math.min(1.5f, skyboxShaderAuroraIntensity.get())) : 0.0f; - } - - public float getSkyboxShaderAuroraSpeed() { - return Math.max(0.0f, Math.min(2.0f, skyboxShaderAuroraSpeed.get())); - } - - public float getSkyboxShaderSmallStars() { - return clampStarAmount(skyboxShaderSmallStars.get()); - } - - public float getSkyboxShaderDustStars() { - return clampStarAmount(skyboxShaderDustStars.get()); - } - - public float getSkyboxShaderMediumStars() { - return clampStarAmount(skyboxShaderMediumStars.get()); - } - - public float getSkyboxShaderLargeStars() { - return clampStarAmount(skyboxShaderLargeStars.get()); - } - - public float getSkyboxShaderStarBrightness() { - return Math.max(0.0f, Math.min(3.0f, skyboxShaderStarBrightness.get())); - } - - public float getSkyboxShaderTwinkleStrength() { - return Math.max(0.0f, Math.min(3.0f, skyboxShaderTwinkleStrength.get())); - } - - public int getSkyboxShaderLayerMask() { - int mask = 0; - int bit = 0; - for (String key : DEFAULT_SKYBOX_SHADER_LAYERS.keySet()) { - if (skyboxShaderLayers.get(key)) { - mask |= 1 << bit; - } - bit++; - } - return mask; - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public boolean isActive() { - Minecraft mc = Minecraft.getInstance(); - return isEnabled() - && isEffectSelected(EFFECT_DEPTH_OF_FIELD) - && mc != null - && mc.player != null - && mc.level != null - && !isDepthOfFieldBlockedByIris() - && depthSamplerSupported - && dofDepthSource.get() != DepthOfFieldDepthSource.OFF; - } - - public boolean needsWorldSceneDepthCapture() { - return isActive() && dofDepthSource.get() == DepthOfFieldDepthSource.WORLD_SCENE; - } - - public boolean needsPreTranslucentDepthCapture() { - return isActive() && dofDepthSource.get() == DepthOfFieldDepthSource.PRE_TRANSLUCENT; - } - - public boolean needsResolvedMainDepthCapture() { - if (!isActive()) return false; - DepthOfFieldDepthSource source = dofDepthSource.get(); - return source == DepthOfFieldDepthSource.MAIN; - } - - @Override - public int getPriority() { - return 3; - } - - @Override - public Phase getPhase() { - return Phase.PRE_HAND; - } - - @Override - public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { - return false; - } - - @Override - public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) { - if (!isActive() || context == null || src == null || dst == null) { - return false; - } - - DepthBindings depth = resolveDepthBindings(context); - if (!depth.hasAnyDepth()) { - if (!dofDebugCoc.get()) { - return false; - } - depth = DepthBindings.empty(); - } - - buildProjection(); - - DepthOfFieldUniforms.update( - dofProjection, - context.width(), - context.height(), - 0.0f, - 0.0f, - dofFarStart.get(), - dofFarTransition.get(), - dofStrength.get(), - dofMaxRadius.get(), - dofQuality.get().taps(), - 0.85f, - dofDebugCoc.get(), - depth.hasMain(), - depth.hasTranslucent(), - depth.hasItemEntity(), - depth.hasParticles(), - depth.hasWeather(), - depth.hasClouds() - ); - - try { - FullScreenRenderer.ensureInit(); - FullScreenRenderer.begin("LoVisual DepthOfField Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.DEPTH_OF_FIELD) - .uniform("DepthOfField", DepthOfFieldUniforms.get()) - .sampler("u_Texture", src, PostProcessManager.getSampler()) - .sampler("u_MainDepth", depth.mainOr(src), PostProcessManager.getSampler()) - .sampler("u_TranslucentDepth", depth.translucentOr(src), PostProcessManager.getSampler()) - .sampler("u_ItemEntityDepth", depth.itemEntityOr(src), PostProcessManager.getSampler()) - .sampler("u_ParticlesDepth", depth.particlesOr(src), PostProcessManager.getSampler()) - .sampler("u_WeatherDepth", depth.weatherOr(src), PostProcessManager.getSampler()) - .sampler("u_CloudsDepth", depth.cloudsOr(src), PostProcessManager.getSampler()) - .end(); - } catch (Throwable t) { - depthSamplerSupported = false; - DebugLog.warn("[LoVisual] DepthOfField depth sampler path failed; disabling DoF depth sampling for this session: " + t); - return false; - } - - return true; - } - - private void buildProjection() { - Matrix4f projection = LoVisualWorldMatrices.renderProjectionMatrix(); - if (projection == null) { - projection = RenderState.worldProjection; - } - dofProjection.set(projection); - } - - @Override - public void onTick() { - refreshWavyVegetationTerrainState(); - } - - @Override - public void onEnable() { - depthSamplerSupported = true; - refreshWavyVegetationTerrainState(); - } - - @Override - public void onDisable() { - refreshWavyVegetationTerrainState(false); - } - - private void refreshWavyVegetationTerrainState() { - refreshWavyVegetationTerrainState(isEnabled() && isEffectSelected(EFFECT_WAVY_VEGETATION)); - } - - private void refreshWavyVegetationTerrainState(boolean active) { - int settingsPacked = active ? packWavyVegetationSettings() : 0; - if (!wavyVegetationStateInitialized) { - wavyVegetationStateInitialized = true; - lastWavyVegetationActive = false; - lastWavyVegetationSettingsPacked = 0; - } - - if (lastWavyVegetationActive == active && lastWavyVegetationSettingsPacked == settingsPacked) return; - lastWavyVegetationActive = active; - lastWavyVegetationSettingsPacked = settingsPacked; - LoVisualRenderSystem.sodium().reloadWorldRenderer(); - } - - private int packWavyVegetationSettings() { - return packWavyVegetationSettings( - wavyVegetationRootedHorizontalAmplitude.get(), - wavyVegetationRootedVerticalAmplitude.get(), - wavyVegetationFreeHorizontalAmplitude.get(), - wavyVegetationFreeVerticalAmplitude.get(), - wavyVegetationSpeed.get() - ); - } - - private static int packWavyVegetationSettings(float rootedHorizontal, float rootedVertical, float freeHorizontal, float freeVertical, float speed) { - return (encodeWavyVegetationSetting(rootedHorizontal) << SodiumMaterialFlags.WAVE_ROOTED_HORIZONTAL_SHIFT) - | (encodeWavyVegetationSetting(rootedVertical) << SodiumMaterialFlags.WAVE_ROOTED_VERTICAL_SHIFT) - | (encodeWavyVegetationSetting(freeHorizontal) << SodiumMaterialFlags.WAVE_FREE_HORIZONTAL_SHIFT) - | (encodeWavyVegetationSetting(freeVertical) << SodiumMaterialFlags.WAVE_FREE_VERTICAL_SHIFT) - | (encodeWavyVegetationSetting(speed) << SodiumMaterialFlags.WAVE_SPEED_SHIFT); - } - - private static int encodeWavyVegetationSetting(float value) { - float clamped = Math.max(0.0f, Math.min(SodiumMaterialFlags.WAVE_SETTING_MAX, value)); - return Math.round((clamped / SodiumMaterialFlags.WAVE_SETTING_MAX) * SodiumMaterialFlags.WAVE_SETTING_MASK); - } - - private DepthBindings resolveDepthBindings(PostProcessContext context) { - return switch (dofDepthSource.get()) { - case WORLD_SCENE -> resolveWorldSceneDepth(); - case MAIN -> DepthBindings.single(context.mainDepth()); - case PRE_TRANSLUCENT -> DepthBindings.single(context.preTranslucentDepth()); - case OFF -> DepthBindings.empty(); - }; - } - - private DepthBindings resolveWorldSceneDepth() { - if (IrisSceneDepth.isValid()) { - return new DepthBindings( - IrisSceneDepth.mainDepthView(), - IrisSceneDepth.preTranslucentDepthView(), - IrisSceneDepth.preHandDepthView(), - null, - null, - null - ); - } - if (!WorldSceneDepth.isValid() || !WorldSceneDepth.hasMain()) { - return DepthBindings.empty(); - } - return new DepthBindings( - WorldSceneDepth.mainDepthView(), - WorldSceneDepth.translucentDepthView(), - WorldSceneDepth.itemEntityDepthView(), - WorldSceneDepth.particlesDepthView(), - WorldSceneDepth.weatherDepthView(), - null - ); - } - - private boolean isWorldSunSettingsVisible() { - return isEffectSelected(EFFECT_WORLD_SUN); - } - - private boolean isShaderSkyboxSettingsVisible() { - return isEffectSelected(EFFECT_SHADER_SKY); - } - - private boolean isShaderSkyboxBlockedByIris() { - return isEffectSelected(EFFECT_SHADER_SKY) && IrisRuntime.isModLoaded(); - } - - private String shaderSkyboxIrisReason() { - String translated = I18n.get(IRIS_SHADER_SKYBOX_REASON_KEY); - return IRIS_SHADER_SKYBOX_REASON_KEY.equals(translated) ? IRIS_SHADER_SKYBOX_REASON_FALLBACK : translated; - } - - private > V shaderSkyboxNotAppliedWithIris(V value) { - return notAppliedWhen(value, this::isShaderSkyboxBlockedByIris, this::shaderSkyboxIrisReason); - } - - private boolean isDepthOfFieldBlockedByIris() { - return IrisRuntime.isShaderpackRendererActive(); - } - - private String depthOfFieldIrisReason() { - String translated = I18n.get(IRIS_DOF_REASON_KEY); - return IRIS_DOF_REASON_KEY.equals(translated) ? IRIS_DOF_REASON_FALLBACK : translated; - } - - private > V depthOfFieldNotAppliedWithIris(V value) { - return notAppliedWhen(value, this::isDepthOfFieldBlockedByIris, this::depthOfFieldIrisReason); - } - - private boolean isDepthOfFieldSettingsVisible() { - return isEffectSelected(EFFECT_DEPTH_OF_FIELD); - } - - private boolean isWavyVegetationSettingsVisible() { - return isEffectSelected(EFFECT_WAVY_VEGETATION); - } - - private boolean isEffectSelected(String effect) { - return effects.get(effect); - } - - private record DepthBindings( - GpuTextureView main, - GpuTextureView translucent, - GpuTextureView itemEntity, - GpuTextureView particles, - GpuTextureView weather, - GpuTextureView clouds - ) { - static DepthBindings empty() { - return new DepthBindings(null, null, null, null, null, null); - } - - static DepthBindings single(GpuTextureView depth) { - return new DepthBindings(depth, null, null, null, null, null); - } - - boolean hasAnyDepth() { - return primary() != null; - } - - boolean hasMain() { - return main != null; - } - - boolean hasTranslucent() { - return translucent != null; - } - - boolean hasItemEntity() { - return itemEntity != null; - } - - boolean hasParticles() { - return particles != null; - } - - boolean hasWeather() { - return weather != null; - } - - boolean hasClouds() { - return clouds != null; - } - - GpuTextureView primary() { - if (main != null) return main; - if (translucent != null) return translucent; - if (itemEntity != null) return itemEntity; - if (particles != null) return particles; - if (weather != null) return weather; - return clouds; - } - - GpuTextureView mainOr(GpuTextureView placeholder) { - return main != null ? main : firstOr(placeholder); - } - - GpuTextureView translucentOr(GpuTextureView placeholder) { - return translucent != null ? translucent : firstOr(placeholder); - } - - GpuTextureView itemEntityOr(GpuTextureView placeholder) { - return itemEntity != null ? itemEntity : firstOr(placeholder); - } - - GpuTextureView particlesOr(GpuTextureView placeholder) { - return particles != null ? particles : firstOr(placeholder); - } - - GpuTextureView weatherOr(GpuTextureView placeholder) { - return weather != null ? weather : firstOr(placeholder); - } - - GpuTextureView cloudsOr(GpuTextureView placeholder) { - return clouds != null ? clouds : firstOr(placeholder); - } - - private GpuTextureView firstOr(GpuTextureView placeholder) { - GpuTextureView primary = primary(); - return primary != null ? primary : placeholder; - } + private Map createDefaultSkyboxLayers() { + LinkedHashMap m = new LinkedHashMap<>(); + m.put("waves", true); m.put("ribbons", true); m.put("sweeps", true); m.put("veil", true); m.put("nebula", true); + m.put("detail_curtains", true); m.put("polar_arc", true); m.put("bursts", true); m.put("water_veil", true); m.put("caustics", true); + m.put("refracted_aurora", true); m.put("northern_aurora", true); m.put("small_stars", true); m.put("dust_stars", true); + m.put("medium_stars", true); m.put("large_stars", true); m.put("detail_stars", true); + return m; } + public boolean isEffectSelected(String effect) { return effects.get(effect); } + public boolean isShaderSkyboxEnabled() { return isEnabled() && isEffectSelected("shader_sky"); } + public int getShaderSkyboxColor() { return skybox.getShaderSkyboxColor(); } + public float getSkyboxSkyFogBlend() { return skybox.getSkyboxSkyFogBlend(); } + public float getSkyboxShaderMotionSpeed() { return skybox.getSkyboxShaderMotionSpeed(); } + public boolean isSkyboxShaderAuroraEnabled() { return skybox.isSkyboxShaderAuroraEnabled(); } + public float getSkyboxShaderAuroraIntensity() { return skybox.getSkyboxShaderAuroraIntensity(); } + public float getSkyboxShaderAuroraSpeed() { return skybox.getSkyboxShaderAuroraSpeed(); } + public float getSkyboxShaderSmallStars() { return skybox.getSmallStars(); } + public float getSkyboxShaderDustStars() { return skybox.getDustStars(); } + public float getSkyboxShaderMediumStars() { return skybox.getMediumStars(); } + public float getSkyboxShaderLargeStars() { return skybox.getLargeStars(); } + public float getSkyboxShaderStarBrightness() { return skybox.getStarBrightness(); } + public float getSkyboxShaderTwinkleStrength() { return skybox.getTwinkleStrength(); } + public int getSkyboxShaderLayerMask() { return skybox.getLayerMask(); } + public boolean isWorldSunEnabled() { return isEnabled() && isEffectSelected("world_sun"); } + public float getWorldSunSize() { return sun.getSize(); } + public float getWorldSunGlow() { return sun.getGlow(); } + public float getWorldSunIntensity() { return sun.getIntensity(); } + public boolean isWorldSunGlareEnabled() { return isEnabled() && isEffectSelected("world_sun") && sun.isGlareEnabled(); } + public boolean isWavyVegetationEnabled() { return isEnabled() && isEffectSelected("wavy_vegetation"); } + public int packWavyVegetationSettings() { return vegetation.pack(); } + public boolean isActive() { return depthOfField.isActive(); } + @Override public WorldPhase getWorldPhase() { return WorldPhase.END_MAIN; } + public boolean needsWorldSceneDepthCapture() { return depthOfField.needsWorldSceneDepthCapture(); } + public boolean needsPreTranslucentDepthCapture() { return depthOfField.needsPreTranslucentDepthCapture(); } + public boolean needsResolvedMainDepthCapture() { return depthOfField.needsResolvedMainDepthCapture(); } + @Override public void onTick() { vegetation.onTick(isEnabled(), isEffectSelected("wavy_vegetation")); } + @Override public void onEnable() { depthOfField.resetSamplerSupport(); vegetation.onEnable(isEffectSelected("wavy_vegetation")); } + @Override public void onDisable() { vegetation.onDisable(); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedDepthOfFieldHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedDepthOfFieldHelper.java new file mode 100644 index 0000000..18cd834 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedDepthOfFieldHelper.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; +import dev.loki.lovisual.render.engine.depth.WorldSceneDepth; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.postprocess.blur.DepthOfFieldDepthSource; +import dev.loki.lovisual.render.engine.postprocess.blur.DepthOfFieldQuality; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; +import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; +import dev.loki.lovisual.render.engine.uniform.impl.post.DepthOfFieldUniforms; +import dev.loki.lovisual.render.iris.runtime.IrisRuntime; +import dev.loki.lovisual.render.iris.scene.IrisSceneDepth; +import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.client.Minecraft; +import org.joml.Matrix4f; + +final class ReimaginedDepthOfFieldHelper implements PostProcessPass { + private final EnumValue depthSource; + private final NumberValue farStart; + private final NumberValue farTransition; + private final NumberValue strength; + private final NumberValue maxRadius; + private final EnumValue quality; + private final BooleanValue debugCoc; + private final Matrix4f projection = new Matrix4f(); + private boolean depthSamplerSupported = true; + ReimaginedDepthOfFieldHelper(EnumValue depthSource, NumberValue farStart, NumberValue farTransition, NumberValue strength, NumberValue maxRadius, EnumValue quality, BooleanValue debugCoc) { + this.depthSource = depthSource; this.farStart = farStart; this.farTransition = farTransition; this.strength = strength; this.maxRadius = maxRadius; this.quality = quality; this.debugCoc = debugCoc; + } + @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); + ReimaginedVisual module = Modules.get(ReimaginedVisual.class); + boolean moduleActive = module != null && module.isEnabled() && module.isEffectSelected("depth_of_field"); + return moduleActive && mc != null && mc.player != null && mc.level != null && !IrisRuntime.isShaderpackRendererActive() && depthSamplerSupported && depthSource.get() != DepthOfFieldDepthSource.OFF; + } + boolean needsWorldSceneDepthCapture() { return isActive() && depthSource.get() == DepthOfFieldDepthSource.WORLD_SCENE; } + boolean needsPreTranslucentDepthCapture() { return isActive() && depthSource.get() == DepthOfFieldDepthSource.PRE_TRANSLUCENT; } + boolean needsResolvedMainDepthCapture() { return isActive() && depthSource.get() == DepthOfFieldDepthSource.MAIN; } + @Override public int getPriority() { return 3; } + @Override public Phase getPhase() { return Phase.PRE_HAND; } + @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { return false; } + @Override public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) { + if (!isActive() || context == null || src == null || dst == null) return false; + ReimaginedDepthBindings depth = resolveDepthBindings(context); + if (!depth.hasAnyDepth()) { + if (!debugCoc.get()) return false; + depth = new ReimaginedDepthBindings(null, null, null, null, null, null); + } + buildProjection(); + DepthOfFieldUniforms.update(projection, context.width(), context.height(), 0.0f, 0.0f, farStart.get(), farTransition.get(), strength.get(), maxRadius.get(), quality.get().taps(), 0.85f, debugCoc.get(), depth.hasMain(), depth.hasTranslucent(), depth.hasItemEntity(), depth.hasParticles(), depth.hasWeather(), depth.hasClouds()); + try { + FullScreenRenderer.ensureInit(); + FullScreenRenderer.begin("LoVisual DepthOfField Pass").attachment(dst).pipeline(LoVisualRenderPipelines.DEPTH_OF_FIELD).uniform("DepthOfField", DepthOfFieldUniforms.get()).sampler("u_Texture", src, PostProcessManager.getSampler()).sampler("u_MainDepth", depth.mainOr(src), PostProcessManager.getSampler()).sampler("u_TranslucentDepth", depth.translucentOr(src), PostProcessManager.getSampler()).sampler("u_ItemEntityDepth", depth.itemEntityOr(src), PostProcessManager.getSampler()).sampler("u_ParticlesDepth", depth.particlesOr(src), PostProcessManager.getSampler()).sampler("u_WeatherDepth", depth.weatherOr(src), PostProcessManager.getSampler()).sampler("u_CloudsDepth", depth.cloudsOr(src), PostProcessManager.getSampler()).end(); + } catch (Throwable t) { + depthSamplerSupported = false; + DebugLog.warn("[LoVisual] DepthOfField depth sampler path failed; disabling DoF depth sampling for this session: " + t); + return false; + } + return true; + } + private void buildProjection() { + Matrix4f proj = LoVisualWorldMatrices.renderProjectionMatrix(); + if (proj == null) proj = RenderState.worldProjection; + projection.set(proj); + } + void resetSamplerSupport() { depthSamplerSupported = true; } + private ReimaginedDepthBindings resolveDepthBindings(PostProcessContext context) { + DepthOfFieldDepthSource s = depthSource.get(); + if (s == DepthOfFieldDepthSource.WORLD_SCENE) return resolveWorldSceneDepth(); + if (s == DepthOfFieldDepthSource.MAIN) return new ReimaginedDepthBindings(context.mainDepth(), null, null, null, null, null); + if (s == DepthOfFieldDepthSource.PRE_TRANSLUCENT) return new ReimaginedDepthBindings(context.preTranslucentDepth(), null, null, null, null, null); + return new ReimaginedDepthBindings(null, null, null, null, null, null); + } + private ReimaginedDepthBindings resolveWorldSceneDepth() { + if (IrisSceneDepth.isValid()) return new ReimaginedDepthBindings(IrisSceneDepth.mainDepthView(), IrisSceneDepth.preTranslucentDepthView(), IrisSceneDepth.preHandDepthView(), null, null, null); + if (!WorldSceneDepth.isValid() || !WorldSceneDepth.hasMain()) return new ReimaginedDepthBindings(null, null, null, null, null, null); + return new ReimaginedDepthBindings(WorldSceneDepth.mainDepthView(), WorldSceneDepth.translucentDepthView(), WorldSceneDepth.itemEntityDepthView(), WorldSceneDepth.particlesDepthView(), WorldSceneDepth.weatherDepthView(), null); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSkyboxHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSkyboxHelper.java new file mode 100644 index 0000000..77eac34 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSkyboxHelper.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.theme.Theme; +import java.util.Map; + +final class ReimaginedSkyboxHelper { + private final BooleanValue syncTheme; + private final RGBColorValue color; + private final NumberValue motionSpeed; + private final BooleanValue auroraEnabled; + private final NumberValue auroraIntensity; + private final NumberValue auroraSpeed; + private final NumberValue smallStars; + private final NumberValue dustStars; + private final NumberValue mediumStars; + private final NumberValue largeStars; + private final NumberValue starBrightness; + private final NumberValue twinkleStrength; + private final BooleanMapValue layers; + private final NumberValue skyFogBlend; + private final Map defaultLayers; + + ReimaginedSkyboxHelper( + BooleanValue syncTheme, + RGBColorValue color, + NumberValue motionSpeed, + BooleanValue auroraEnabled, + NumberValue auroraIntensity, + NumberValue auroraSpeed, + NumberValue smallStars, + NumberValue dustStars, + NumberValue mediumStars, + NumberValue largeStars, + NumberValue starBrightness, + NumberValue twinkleStrength, + BooleanMapValue layers, + NumberValue skyFogBlend, + Map defaultLayers) { + this.syncTheme = syncTheme; + this.color = color; + this.motionSpeed = motionSpeed; + this.auroraEnabled = auroraEnabled; + this.auroraIntensity = auroraIntensity; + this.auroraSpeed = auroraSpeed; + this.smallStars = smallStars; + this.dustStars = dustStars; + this.mediumStars = mediumStars; + this.largeStars = largeStars; + this.starBrightness = starBrightness; + this.twinkleStrength = twinkleStrength; + this.layers = layers; + this.skyFogBlend = skyFogBlend; + this.defaultLayers = defaultLayers; + } + + int getShaderSkyboxColor() { + if (syncTheme.get()) { + return Theme.theme().accent() & 0x00FFFFFF; + } + return color.getArgb() & 0x00FFFFFF; + } + + float getSkyboxSkyFogBlend() { + return clamp01(skyFogBlend.get()); + } + + float getSkyboxShaderMotionSpeed() { + return clamp(motionSpeed.get(), 0.0f, 2.0f); + } + + boolean isSkyboxShaderAuroraEnabled() { + return auroraEnabled.get(); + } + + float getSkyboxShaderAuroraIntensity() { + if (!auroraEnabled.get()) { + return 0.0f; + } + return clamp(auroraIntensity.get(), 0.0f, 1.5f); + } + + float getSkyboxShaderAuroraSpeed() { + return clamp(auroraSpeed.get(), 0.0f, 2.0f); + } + + float getSmallStars() { + return clampStar(smallStars.get()); + } + + float getDustStars() { + return clampStar(dustStars.get()); + } + + float getMediumStars() { + return clampStar(mediumStars.get()); + } + + float getLargeStars() { + return clampStar(largeStars.get()); + } + + float getStarBrightness() { + return clamp(starBrightness.get(), 0.0f, 3.0f); + } + + float getTwinkleStrength() { + return clamp(twinkleStrength.get(), 0.0f, 3.0f); + } + + int getLayerMask() { + int mask = 0; + int bit = 0; + for (String key : defaultLayers.keySet()) { + if (layers.get(key)) { + mask |= 1 << bit; + } + bit++; + } + return mask; + } + + private float clampStar(float value) { + return Math.max(0.0f, Math.min(10.0f, value)); + } + + private float clamp(float value, float min, float max) { + return Math.max(min, Math.min(max, value)); + } + + private float clamp01(float value) { + return Math.max(0.0f, Math.min(1.0f, value)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSunHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSunHelper.java new file mode 100644 index 0000000..317881b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedSunHelper.java @@ -0,0 +1,38 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; + +final class ReimaginedSunHelper { + private final BooleanValue glareEnabled; + private final NumberValue size; + private final NumberValue glow; + private final NumberValue intensity; + + ReimaginedSunHelper( + BooleanValue glareEnabled, + NumberValue size, + NumberValue glow, + NumberValue intensity) { + this.glareEnabled = glareEnabled; + this.size = size; + this.glow = glow; + this.intensity = intensity; + } + + boolean isGlareEnabled() { + return glareEnabled.get(); + } + + float getSize() { + return size.get(); + } + + float getGlow() { + return glow.get(); + } + + float getIntensity() { + return intensity.get(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedVegetationHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedVegetationHelper.java new file mode 100644 index 0000000..c5635fb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/reimagined/ReimaginedVegetationHelper.java @@ -0,0 +1,73 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.helpers.material.SodiumMaterialFlags; + +final class ReimaginedVegetationHelper { + private final NumberValue rootedHorizontal; + private final NumberValue rootedVertical; + private final NumberValue freeHorizontal; + private final NumberValue freeVertical; + private final NumberValue speed; + private boolean initialized; + private boolean lastActive; + private int lastPacked; + + ReimaginedVegetationHelper( + NumberValue rootedHorizontal, + NumberValue rootedVertical, + NumberValue freeHorizontal, + NumberValue freeVertical, + NumberValue speed) { + this.rootedHorizontal = rootedHorizontal; + this.rootedVertical = rootedVertical; + this.freeHorizontal = freeHorizontal; + this.freeVertical = freeVertical; + this.speed = speed; + } + + void onTick(boolean moduleEnabled, boolean effectSelected) { + refresh(moduleEnabled && effectSelected); + } + + void onEnable(boolean effectSelected) { + refresh(effectSelected); + } + + void onDisable() { + refresh(false); + } + + void refresh(boolean active) { + int packed = active ? pack() : 0; + if (!initialized) { + initialized = true; + lastActive = false; + lastPacked = 0; + } + if (lastActive == active && lastPacked == packed) { + return; + } + lastActive = active; + lastPacked = packed; + LoVisualRenderSystem.sodium().reloadWorldRenderer(); + } + + int pack() { + return pack(rootedHorizontal.get(), rootedVertical.get(), freeHorizontal.get(), freeVertical.get(), speed.get()); + } + + int pack(float rootedH, float rootedV, float freeH, float freeV, float sp) { + return (encode(rootedH) << SodiumMaterialFlags.WAVE_ROOTED_HORIZONTAL_SHIFT) + | (encode(rootedV) << SodiumMaterialFlags.WAVE_ROOTED_VERTICAL_SHIFT) + | (encode(freeH) << SodiumMaterialFlags.WAVE_FREE_HORIZONTAL_SHIFT) + | (encode(freeV) << SodiumMaterialFlags.WAVE_FREE_VERTICAL_SHIFT) + | (encode(sp) << SodiumMaterialFlags.WAVE_SPEED_SHIFT); + } + + private int encode(float value) { + float clamped = Math.max(0.0f, Math.min(SodiumMaterialFlags.WAVE_SETTING_MAX, value)); + return Math.round((clamped / SodiumMaterialFlags.WAVE_SETTING_MAX) * SodiumMaterialFlags.WAVE_SETTING_MASK); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java index 79a9d4c..7408964 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java @@ -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.module.modules.visuals.trollface; import dev.loki.lovisual.config.values.mode.ModeValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java index df4bbb9..30795fa 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java @@ -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.module.modules.visuals.visibility; import dev.loki.lovisual.config.values.primitive.BooleanValue; @@ -20,6 +13,7 @@ import dev.loki.lovisual.features.relations.category.CategoryType; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DCulling; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.features.module.modules.visuals.esp.shader.EspMeshBuilder; import net.minecraft.client.Minecraft; @@ -108,7 +102,7 @@ public class Hitboxes extends Module { for (Entity entity : mc.level.entitiesForRendering()) { if (entity == camera) continue; if (playersOnly.get() && !(entity instanceof Player)) continue; - if (!Renderer3D.Culling.isInFrustum(interpolatedBox(entity, tickDelta))) continue; + if (!Renderer3DCulling.isInFrustum(interpolatedBox(entity, tickDelta))) continue; out.add(entity); } return out; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java index 1f58543..2327981 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java @@ -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.module.modules.visuals.visibility; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java index 87fc71d..c727b54 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java @@ -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.module.modules.visuals.visibility; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java index 64632e4..69601ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java @@ -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.module.modules.visuals.weather; import dev.loki.lovisual.util.time.GameClock; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java index dbd553e..ff9dfa7 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java @@ -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.module.modules.visuals.weather.precip; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java index 4331a34..3ef6b40 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java @@ -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.module.modules.visuals.weather.sky; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java index 7f940f6..23e847d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java @@ -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.module.modules.visuals.wings; /** diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java index be71182..b327960 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java @@ -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.module.modules.visuals.wings; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java index a37bd3b..5b15e9b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java @@ -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.module.modules.visuals.wings; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java index 05241c9..63bd194 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java @@ -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.module.modules.visuals.world; import dev.loki.lovisual.config.values.color.RGBAColorValue; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java index 49325ed..0561fea 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java @@ -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.module.phase; public enum HudPhase { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java index 4ce16a6..784f43b 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java @@ -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.module.phase; public interface ModuleStateListener { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java index ec1be9c..dd2b6b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java @@ -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.module.phase; import dev.loki.lovisual.features.gui.hud.draggable.impl.hudnotifier.HudNotifier; diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java index 934fe75..543662d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java @@ -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.module.phase; public enum WorldPhase { diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java b/mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java index af97e48..454dc31 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java @@ -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.relations; import dev.loki.lovisual.config.definition.ConfigNameProvider; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java b/mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java index dad6220..249747c 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java @@ -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.relations; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java index e375c9d..d1d0c37 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java @@ -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.relations; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java index dab231d..7194917 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java @@ -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.relations; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java index 75ceb54..d33251f 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java @@ -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.relations.category; import dev.loki.lovisual.features.relations.PlayerRelations; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java index d069f7b..f976768 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java @@ -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.relations.category; import dev.loki.lovisual.features.relations.PlayerRelations; diff --git a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java index 98410fd..577d5c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java +++ b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java @@ -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.relations.category; public enum CategoryType { diff --git a/mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java b/mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java index 538ab6f..6ac2cd3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java +++ b/mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java @@ -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.security; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java b/mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java index 28d61c2..58ba5fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java @@ -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.security; import net.minecraft.network.chat.contents.TranslatableContents; diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java b/mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java index ac3c543..c685a93 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java @@ -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.theme; import dev.loki.lovisual.config.definition.SettingDef; @@ -21,6 +14,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +/** + * Mutable, form-backed view over a single custom {@link Themes.ThemeEntry}. Wraps every + * palette slot and gradient stop in its own {@code ConfigValue} so the standard clickgui + * settings renderer can edit them without any bespoke UI. + */ public final class EditableClickGuiTheme implements SettingOwner { private final String id; private final StringValue displayName; @@ -68,16 +66,14 @@ public final class EditableClickGuiTheme implements SettingOwner { } public static EditableClickGuiTheme existing(String id) { - Themes.ThemeEntry entry = Themes.presetEntry(id) == null ? Theme.themes().stream() - .filter(theme -> theme.getId().equals(normalizeId(id))) - .findFirst() - .orElse(null) : null; + if (Themes.presetEntry(id) != null) return null; + String key = normalizeId(id); + Themes.ThemeEntry entry = Theme.themes().stream().filter(t -> t.getId().equals(key)).findFirst().orElse(null); return entry == null || entry.builtin() ? null : new EditableClickGuiTheme(entry.getId(), entry); } public static EditableClickGuiTheme createFromCurrent() { - String name = "Custom Theme"; - Themes.ThemeEntry created = Theme.createCustomTheme(name, Theme.currentEntry(), true); + Themes.ThemeEntry created = Theme.createCustomTheme("Custom Theme", Theme.currentEntry(), true); return created == null ? null : new EditableClickGuiTheme(created.getId(), created); } @@ -123,31 +119,13 @@ public final class EditableClickGuiTheme implements SettingOwner { } public Themes.ThemeEntry toEntry() { - Themes.Theme theme = new Themes.Theme( - windowBg.getArgb(), - windowHeader.getArgb(), - windowStroke.getArgb(), - surface.getArgb(), - surfaceHover.getArgb(), - cardEnabled.getArgb(), - cardDisabled.getArgb(), - textPrimary.getArgb(), - textMuted.getArgb(), - accent.getArgb(), - accentSoft.getArgb(), - strokeSoft.getArgb() - ); - return new Themes.ThemeEntry( - id, - title(), - false, - theme, - windowGradient.toSpec(theme.windowBg()), - headerGradient.toSpec(theme.windowHeader()), - surfaceGradient.toSpec(theme.surface()), - cardGradient.toSpec(theme.cardEnabled()), - strokeGradient.toSpec(theme.windowStroke()) - ); + Themes.Theme theme = new Themes.Theme(windowBg.getArgb(), windowHeader.getArgb(), windowStroke.getArgb(), + surface.getArgb(), surfaceHover.getArgb(), cardEnabled.getArgb(), cardDisabled.getArgb(), + textPrimary.getArgb(), textMuted.getArgb(), accent.getArgb(), accentSoft.getArgb(), strokeSoft.getArgb()); + return new Themes.ThemeEntry(id, title(), false, theme, + windowGradient.toSpec(theme.windowBg()), headerGradient.toSpec(theme.windowHeader()), + surfaceGradient.toSpec(theme.surface()), cardGradient.toSpec(theme.cardEnabled()), + strokeGradient.toSpec(theme.windowStroke())); } @Override @@ -201,12 +179,7 @@ public final class EditableClickGuiTheme implements SettingOwner { } private Themes.GradientSpec toSpec(int baseColor) { - return new Themes.GradientSpec( - enabled.get(), - start.getArgb(), - end.getArgb(), - angle.get().floatValue() - ); + return new Themes.GradientSpec(enabled.get(), start.getArgb(), end.getArgb(), angle.get().floatValue()); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java b/mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java index 4bf81f8..c89fdb3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java @@ -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.theme; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java b/mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java index 4213edf..ab9e808 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java @@ -1,147 +1,44 @@ -/* - * 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.theme; -import com.google.gson.*; -import dev.loki.lovisual.config.io.ConfigPaths; import dev.loki.lovisual.config.profile.service.ConfigProfileStorage; -import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +/** + * Persistent registry of custom themes. Owns the merged (presets + custom) lookup + * index used by {@link Themes} and exposes the CRUD API consumed by the clickgui + * theme editor. All disk access is delegated to {@link ThemeStoreIO}; the JSON + * shape lives in {@link ThemeStoreJson}. + */ public final class ThemeStore { - private static final Path LEGACY_FILE = ConfigPaths.root().resolve("clickgui_themes.json"); - private static final Path THEME_ROOT = ConfigPaths.root().resolve("themes"); - private static final Path CUSTOM_DIR = THEME_ROOT.resolve("custom"); - private static final Path STATE_FILE = THEME_ROOT.resolve("state.json"); - private static final Gson GSON = new GsonBuilder() - .setPrettyPrinting() - .disableHtmlEscaping() - .create(); - private static final ThemeStore INSTANCE = new ThemeStore(); - private final List themes = new ArrayList<>(); - private final List customThemes = new ArrayList<>(); - private final Map themesById = new LinkedHashMap<>(); - private String currentId = Themes.THEME_CLASSIC; + // Package-private so ThemeStoreIO can populate / rebuild them during bootstrap. + final List themes = new ArrayList<>(); + final List customThemes = new ArrayList<>(); + final Map themesById = new LinkedHashMap<>(); + String currentId = Themes.THEME_CLASSIC; private ThemeStore() { - load(); + ThemeStoreIO.load(this); } public static ThemeStore get() { return INSTANCE; } - private static String normalizeId(String id) { - return id == null ? "" : id.toLowerCase(Locale.ROOT).trim(); - } - - private static JsonObject gradientToJson(Themes.GradientSpec spec) { - Themes.GradientSpec out = spec != null ? spec : new Themes.GradientSpec(false, 0xFFFFFFFF, 0xFFFFFFFF, 90f); - JsonObject obj = new JsonObject(); - obj.addProperty("enabled", out.enabled()); - obj.addProperty("start", colorToString(out.start())); - obj.addProperty("end", colorToString(out.end())); - obj.addProperty("angle", out.angleDeg()); - return obj; - } - - private static int parseColor(JsonObject obj, String key, int fallback) { - if (obj == null || !obj.has(key)) return fallback; - return parseColor(obj.get(key), fallback); - } - - private static int parseColor(JsonElement raw, int fallback) { - if (raw == null) return fallback; - if (raw.isJsonPrimitive()) { - var prim = raw.getAsJsonPrimitive(); - if (prim.isNumber()) return prim.getAsInt(); - if (prim.isString()) return parseColorString(prim.getAsString(), fallback); - } - return fallback; - } - - static int parseColorString(String s, int fallback) { - if (s == null || s.isBlank()) return fallback; - String str = s.trim(); - if (str.startsWith("0x") || str.startsWith("0X")) str = str.substring(2); - if (str.startsWith("#")) str = str.substring(1); - try { - long v = Long.parseUnsignedLong(str, 16); - if (str.length() <= 6) { - return (int) (0xFF000000L | v); - } - return (int) v; - } catch (NumberFormatException e) { - return fallback; - } - } - - static String colorToString(int argb) { - return String.format("#%08X", argb); - } - - private static JsonObject getObject(JsonObject obj, String key) { - if (obj == null || key == null || !obj.has(key)) return null; - JsonElement el = obj.get(key); - return el != null && el.isJsonObject() ? el.getAsJsonObject() : null; - } - - private static JsonArray getArray(JsonObject obj, String key) { - if (obj == null || key == null || !obj.has(key)) return null; - JsonElement el = obj.get(key); - return el != null && el.isJsonArray() ? el.getAsJsonArray() : null; - } - - private static boolean getBoolean(JsonObject obj, String key, boolean fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - JsonElement el = obj.get(key); - if (el == null || !el.isJsonPrimitive()) return fallback; - var prim = el.getAsJsonPrimitive(); - if (prim.isBoolean()) return prim.getAsBoolean(); - if (prim.isString()) return Boolean.parseBoolean(prim.getAsString()); - return fallback; - } - - private static String getString(JsonObject obj, String key, String fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - JsonElement el = obj.get(key); - if (el == null || !el.isJsonPrimitive()) return fallback; - var prim = el.getAsJsonPrimitive(); - return prim.isString() ? prim.getAsString() : fallback; - } - - private static float getFloat(JsonObject obj, String key, float fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - JsonElement el = obj.get(key); - if (el == null || !el.isJsonPrimitive()) return fallback; - var prim = el.getAsJsonPrimitive(); - if (prim.isNumber()) return prim.getAsFloat(); - if (prim.isString()) { - try { - return Float.parseFloat(prim.getAsString()); - } catch (NumberFormatException ignored) { - } - } - return fallback; - } - - public List getThemes() { + public List getThemes() { return Collections.unmodifiableList(themes); } - public List getCustomThemes() { + public List getCustomThemes() { return Collections.unmodifiableList(customThemes); } @@ -149,58 +46,50 @@ public final class ThemeStore { return currentId; } + /** Update the active theme id and persist to {@code state.json}. Blank / unchanged ids are ignored. */ public void setCurrentId(String id) { if (id == null || id.isBlank()) return; - String key = normalizeId(id); + String key = ThemeStoreJson.normalizeId(id); if (key.equals(currentId)) return; currentId = key; - saveState(); + ThemeStoreIO.saveState(this); } - public Themes.ThemeEntry getById(String id) { - if (id == null) return null; - return themesById.get(normalizeId(id)); + public ThemeEntry getById(String id) { + return id == null ? null : themesById.get(ThemeStoreJson.normalizeId(id)); } public boolean isCustomId(String id) { - String key = normalizeId(id); + String key = ThemeStoreJson.normalizeId(id); if (key.isEmpty()) return false; - for (Themes.ThemeEntry entry : customThemes) { + for (ThemeEntry entry : customThemes) { if (entry.getId().equals(key)) return true; } return false; } + /** Sanitise {@code requestedName} and append {@code _2}, {@code _3}, ... until the id is unused. */ public String uniqueCustomId(String requestedName) { String base = ConfigProfileStorage.sanitizeFileName(requestedName == null || requestedName.isBlank() ? "custom_theme" : requestedName); if (base.isBlank()) base = "custom_theme"; - String candidate = normalizeId(base); + String candidate = ThemeStoreJson.normalizeId(base); int i = 2; while (Themes.presetEntry(candidate) != null || themesById.containsKey(candidate)) { - candidate = normalizeId(base + "_" + i++); + candidate = ThemeStoreJson.normalizeId(base + "_" + i++); } return candidate; } - public Themes.ThemeEntry saveCustom(Themes.ThemeEntry entry) { + /** Insert or replace a custom entry, keeping the alphabetical order stable, and write it to disk. */ + public ThemeEntry saveCustom(ThemeEntry entry) { if (entry == null) return null; - String id = normalizeId(entry.getId()); + String id = ThemeStoreJson.normalizeId(entry.getId()); if (id.isBlank()) return null; - if (Themes.presetEntry(id) != null) { - id = uniqueCustomId(entry.name()); - } - Themes.ThemeEntry normalized = new Themes.ThemeEntry( - id, + if (Themes.presetEntry(id) != null) id = uniqueCustomId(entry.name()); + ThemeEntry normalized = new ThemeEntry(id, entry.name() == null || entry.name().isBlank() ? id : entry.name().trim(), - false, - entry.theme(), - entry.windowGradient(), - entry.headerGradient(), - entry.surfaceGradient(), - entry.cardGradient(), - entry.strokeGradient() - ); - + false, entry.theme(), entry.windowGradient(), entry.headerGradient(), + entry.surfaceGradient(), entry.cardGradient(), entry.strokeGradient()); boolean replaced = false; for (int i = 0; i < customThemes.size(); i++) { if (customThemes.get(i).getId().equals(id)) { @@ -210,289 +99,30 @@ public final class ThemeStore { } } if (!replaced) customThemes.add(normalized); - customThemes.sort(Comparator.comparing(Themes.ThemeEntry::name, String.CASE_INSENSITIVE_ORDER)); - rebuildMergedIndex(); - saveCustomFile(normalized); + customThemes.sort(Comparator.comparing(ThemeEntry::name, String.CASE_INSENSITIVE_ORDER)); + ThemeStoreIO.rebuildMergedIndex(this); + ThemeStoreIO.saveCustomFile(normalized); return normalized; } + /** Drop a custom entry, delete its file and fall back to classic if it was the active one. */ public boolean deleteCustom(String id) { - String key = normalizeId(id); + String key = ThemeStoreJson.normalizeId(id); if (key.isBlank()) return false; boolean removed = customThemes.removeIf(entry -> entry.getId().equals(key)); if (!removed) return false; - try { - Files.deleteIfExists(customFile(key)); - } catch (IOException e) { - DebugLog.error("ClickGuiThemeStore: delete custom theme failed", e); - } + ThemeStoreIO.deleteCustomFile(key); if (key.equals(currentId)) currentId = Themes.THEME_CLASSIC; - rebuildMergedIndex(); - saveState(); + ThemeStoreIO.rebuildMergedIndex(this); + ThemeStoreIO.saveState(this); return true; } - private void load() { - currentId = loadCurrentId(); - loadCustomThemes(); - migrateLegacyCustomThemes(); - retireLegacyFile(); - rebuildMergedIndex(); - if (getById(currentId) == null) { - currentId = Themes.THEME_CLASSIC; - saveState(); - } + public static int parseColorString(String s, int fallback) { + return ThemeStoreJson.parseColorString(s, fallback); } - private String loadCurrentId() { - if (Files.exists(STATE_FILE)) { - try { - JsonElement parsed = JsonParser.parseString(Files.readString(STATE_FILE)); - if (parsed != null && parsed.isJsonObject()) { - String current = getString(parsed.getAsJsonObject(), "current", null); - if (current != null && !current.isBlank()) return normalizeId(current); - } - } catch (Exception e) { - DebugLog.error("ClickGuiThemeStore: state load failed", e); - } - } - if (Files.exists(LEGACY_FILE)) { - try { - JsonElement parsed = JsonParser.parseString(Files.readString(LEGACY_FILE)); - if (parsed != null && parsed.isJsonObject()) { - String current = getString(parsed.getAsJsonObject(), "current", null); - if (current != null && !current.isBlank()) return normalizeId(current); - } - } catch (Exception ignored) { - } - } - return Themes.THEME_CLASSIC; - } - - private void loadCustomThemes() { - customThemes.clear(); - if (!Files.isDirectory(CUSTOM_DIR)) return; - try (var stream = Files.list(CUSTOM_DIR)) { - stream.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".json")) - .sorted() - .forEach(path -> { - try { - JsonElement parsed = JsonParser.parseString(Files.readString(path)); - if (parsed == null || !parsed.isJsonObject()) return; - Themes.ThemeEntry entry = parseEntry(parsed.getAsJsonObject(), false); - if (entry == null) return; - if (Themes.presetEntry(entry.getId()) != null) return; - customThemes.removeIf(existing -> existing.getId().equals(entry.getId())); - customThemes.add(entry); - } catch (Exception e) { - DebugLog.error("ClickGuiThemeStore: custom theme load failed %s", e, path.toAbsolutePath()); - } - }); - } catch (IOException e) { - DebugLog.error("ClickGuiThemeStore: custom theme list failed", e); - } - customThemes.sort(Comparator.comparing(Themes.ThemeEntry::name, String.CASE_INSENSITIVE_ORDER)); - } - - private void migrateLegacyCustomThemes() { - if (!Files.exists(LEGACY_FILE)) return; - boolean changed = false; - try { - JsonElement parsed = JsonParser.parseString(Files.readString(LEGACY_FILE)); - if (parsed == null || !parsed.isJsonObject()) return; - JsonArray arr = getArray(parsed.getAsJsonObject(), "themes"); - if (arr == null) return; - rebuildMergedIndex(); - for (JsonElement item : arr) { - if (item == null || !item.isJsonObject()) continue; - Themes.ThemeEntry entry = parseEntry(item.getAsJsonObject(), false); - if (entry == null) continue; - if (Themes.presetEntry(entry.getId()) != null) continue; - if (isCustomId(entry.getId())) continue; - saveCustom(entry); - changed = true; - } - } catch (Exception e) { - DebugLog.error("ClickGuiThemeStore: legacy custom migration failed", e); - } - if (changed) saveState(); - } - - private void retireLegacyFile() { - if (!Files.exists(LEGACY_FILE)) return; - try { - Files.deleteIfExists(LEGACY_FILE); - saveState(); - } catch (IOException e) { - DebugLog.error("ClickGuiThemeStore: legacy file cleanup failed", e); - } - } - - private void rebuildMergedIndex() { - themes.clear(); - themesById.clear(); - for (Themes.ThemeEntry preset : Themes.presetEntries()) { - themes.add(preset); - themesById.put(preset.getId(), preset); - } - for (Themes.ThemeEntry custom : customThemes) { - if (custom == null) continue; - themes.add(custom); - themesById.put(custom.getId(), custom); - } - } - - private Themes.ThemeEntry parseEntry(JsonObject obj, boolean allowBuiltin) { - String id = getString(obj, "id", null); - if (id == null || id.isBlank()) return null; - String key = normalizeId(id); - Themes.ThemeEntry preset = Themes.presetEntry(key); - if (preset != null && !allowBuiltin) return null; - - String name = getString(obj, "name", preset != null ? preset.name() : key); - boolean builtin = allowBuiltin && getBoolean(obj, "builtin", preset != null && preset.builtin()); - - JsonObject colors = getObject(obj, "colors"); - Themes.Theme base = preset != null ? preset.theme() : Theme.classic(); - int windowBg = parseColor(colors, "windowBg", base.windowBg()); - int windowHeader = parseColor(colors, "windowHeader", base.windowHeader()); - int windowStroke = parseColor(colors, "windowStroke", base.windowStroke()); - int surface = parseColor(colors, "surface", base.surface()); - int surfaceHover = parseColor(colors, "surfaceHover", base.surfaceHover()); - int cardEnabled = parseColor(colors, "cardEnabled", base.cardEnabled()); - int cardDisabled = parseColor(colors, "cardDisabled", base.cardDisabled()); - int textPrimary = parseColor(colors, "textPrimary", base.textPrimary()); - int textMuted = parseColor(colors, "textMuted", base.textMuted()); - int accent = parseColor(colors, "accent", base.accent()); - int accentSoft = parseColor(colors, "accentSoft", base.accentSoft()); - int strokeSoft = parseColor(colors, "strokeSoft", base.strokeSoft()); - - Themes.Theme theme = new Themes.Theme( - windowBg, - windowHeader, - windowStroke, - surface, - surfaceHover, - cardEnabled, - cardDisabled, - textPrimary, - textMuted, - accent, - accentSoft, - strokeSoft - ); - - JsonObject gradients = getObject(obj, "gradients"); - Themes.GradientSpec windowGradient = parseGradient( - getObject(gradients, "window"), - preset != null ? preset.windowGradient() : new Themes.GradientSpec(false, windowBg, windowBg, 90f), - windowBg - ); - Themes.GradientSpec headerGradient = parseGradient( - getObject(gradients, "header"), - preset != null ? preset.headerGradient() : new Themes.GradientSpec(false, windowHeader, windowHeader, 90f), - windowHeader - ); - Themes.GradientSpec surfaceGradient = parseGradient( - getObject(gradients, "surface"), - preset != null ? preset.surfaceGradient() : new Themes.GradientSpec(false, surface, surface, 90f), - surface - ); - Themes.GradientSpec cardGradient = parseGradient( - getObject(gradients, "card"), - preset != null ? preset.cardGradient() : new Themes.GradientSpec(false, cardEnabled, cardEnabled, 90f), - cardEnabled - ); - Themes.GradientSpec strokeGradient = parseGradient( - getObject(gradients, "stroke"), - preset != null ? preset.strokeGradient() : new Themes.GradientSpec(false, windowStroke, windowStroke, 90f), - windowStroke - ); - - return new Themes.ThemeEntry( - key, - name != null && !name.isBlank() ? name.trim() : key, - builtin, - theme, - windowGradient, - headerGradient, - surfaceGradient, - cardGradient, - strokeGradient - ); - } - - private Themes.GradientSpec parseGradient(JsonObject map, - Themes.GradientSpec fallback, - int baseColor) { - if (map == null || map.size() == 0) return fallback; - boolean enabled = getBoolean(map, "enabled", fallback.enabled()); - int start = parseColor(map, "start", fallback.start() != 0 ? fallback.start() : baseColor); - int end = parseColor(map, "end", fallback.end() != 0 ? fallback.end() : baseColor); - float angle = normalizeAngle(getFloat(map, "angle", fallback.angleDeg())); - return new Themes.GradientSpec(enabled, start, end, angle); - } - - private static float normalizeAngle(float angle) { - if (!Float.isFinite(angle)) return 90f; - angle %= 360f; - if (angle < 0f) angle += 360f; - return angle; - } - - private void saveState() { - try { - Files.createDirectories(THEME_ROOT); - JsonObject root = new JsonObject(); - root.addProperty("current", currentId); - Files.writeString(STATE_FILE, GSON.toJson(root) + "\n"); - } catch (IOException e) { - DebugLog.error("ClickGuiThemeStore: state save failed", e); - } - } - - private void saveCustomFile(Themes.ThemeEntry entry) { - try { - Files.createDirectories(CUSTOM_DIR); - Files.writeString(customFile(entry.getId()), GSON.toJson(entryToJson(entry)) + "\n"); - } catch (IOException e) { - DebugLog.error("ClickGuiThemeStore: custom theme save failed", e); - } - } - - private Path customFile(String id) { - return CUSTOM_DIR.resolve(ConfigProfileStorage.sanitizeFileName(id) + ".json"); - } - - static JsonObject entryToJson(Themes.ThemeEntry entry) { - JsonObject obj = new JsonObject(); - obj.addProperty("id", entry.getId()); - obj.addProperty("name", entry.name()); - obj.addProperty("builtin", false); - - JsonObject colors = new JsonObject(); - Themes.Theme theme = entry.theme(); - colors.addProperty("windowBg", colorToString(theme.windowBg())); - colors.addProperty("windowHeader", colorToString(theme.windowHeader())); - colors.addProperty("windowStroke", colorToString(theme.windowStroke())); - colors.addProperty("surface", colorToString(theme.surface())); - colors.addProperty("surfaceHover", colorToString(theme.surfaceHover())); - colors.addProperty("cardEnabled", colorToString(theme.cardEnabled())); - colors.addProperty("cardDisabled", colorToString(theme.cardDisabled())); - colors.addProperty("textPrimary", colorToString(theme.textPrimary())); - colors.addProperty("textMuted", colorToString(theme.textMuted())); - colors.addProperty("accent", colorToString(theme.accent())); - colors.addProperty("accentSoft", colorToString(theme.accentSoft())); - colors.addProperty("strokeSoft", colorToString(theme.strokeSoft())); - obj.add("colors", colors); - - JsonObject gradients = new JsonObject(); - gradients.add("window", gradientToJson(entry.windowGradient())); - gradients.add("header", gradientToJson(entry.headerGradient())); - gradients.add("surface", gradientToJson(entry.surfaceGradient())); - gradients.add("card", gradientToJson(entry.cardGradient())); - gradients.add("stroke", gradientToJson(entry.strokeGradient())); - obj.add("gradients", gradients); - return obj; + static String colorToString(int argb) { + return ThemeStoreJson.colorToString(argb); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java b/mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java index 15b82a4..e3c66b3 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java @@ -1,21 +1,19 @@ -/* - * 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.theme; -import net.minecraft.util.Util; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; - -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +/** + * Static entry point for the theme system: current theme accessors, the fixed + * preset catalogue, custom-theme CRUD and profile import / export. + *

    + * Mutable state and helpers are split across three sibling files that share the + * same Java package: {@link ThemesTransition} (current pointer + cross-fade + * animation), {@link ThemesBlending} (stateless color / gradient math) and + * {@link ThemesProfileCodec} (flat-map profile serialisation). + */ public enum Themes { ; @@ -37,124 +35,67 @@ public enum Themes { public static final String THEME_PRISM = "prism"; public static final String THEME_CINDER = "cinder"; public static final String THEME_FOREST = "forest"; - private static ThemeEntry currentEntry = ThemePresets1.CLASSIC_ENTRY; - private static ThemeEntry transitionFromEntry = ThemePresets1.CLASSIC_ENTRY; - private static ThemeEntry animatedEntryCache = ThemePresets1.CLASSIC_ENTRY; + static final long THEME_TRANSITION_MS = 340L; + static final ThemeStore STORE = ThemeStore.get(); - - - - - - - - - - - - - - - - private static final Map PRESET_ENTRIES = Map.ofEntries( - Map.entry(THEME_CLASSIC, ThemePresets1.CLASSIC_ENTRY), - Map.entry(THEME_LEGACY, ThemePresets1.LEGACY_ENTRY), - Map.entry(THEME_AMBER, ThemePresets1.AMBER_ENTRY), - Map.entry(THEME_NOIR, ThemePresets1.NOIR_ENTRY), - Map.entry(THEME_PURPLE, ThemePresets1.PURPLE_ENTRY), - Map.entry(THEME_BLUE, ThemePresets1.BLUE_ENTRY), - Map.entry(THEME_AZURE, ThemePresets2.AZURE_ENTRY), - Map.entry(THEME_NITRO, ThemePresets2.NITRO_ENTRY), - Map.entry(THEME_SUNSET, ThemePresets2.SUNSET_ENTRY), - Map.entry(THEME_AURORA, ThemePresets2.AURORA_ENTRY), - Map.entry(THEME_MINT, ThemePresets2.MINT_ENTRY), - Map.entry(THEME_PEACH, ThemePresets2.PEACH_ENTRY), - Map.entry(THEME_LILAC, ThemePresets3.LILAC_ENTRY), - Map.entry(THEME_SAND, ThemePresets3.SAND_ENTRY), - Map.entry(THEME_DUSK, ThemePresets3.DUSK_ENTRY), - Map.entry(THEME_PRISM, ThemePresets3.PRISM_ENTRY), - Map.entry(THEME_CINDER, ThemePresets3.CINDER_ENTRY), - Map.entry(THEME_FOREST, ThemePresets3.FOREST_ENTRY) + /** Display order for the preset picker; also drives {@link #PRESET_ENTRIES}. */ + static final List PRESET_LIST = List.of( + ThemePresets1.CLASSIC_ENTRY, ThemePresets1.LEGACY_ENTRY, ThemePresets1.AMBER_ENTRY, + ThemePresets1.NOIR_ENTRY, ThemePresets1.PURPLE_ENTRY, ThemePresets1.BLUE_ENTRY, + ThemePresets2.AZURE_ENTRY, ThemePresets2.NITRO_ENTRY, ThemePresets2.SUNSET_ENTRY, + ThemePresets2.AURORA_ENTRY, ThemePresets2.MINT_ENTRY, ThemePresets2.PEACH_ENTRY, + ThemePresets3.LILAC_ENTRY, ThemePresets3.SAND_ENTRY, ThemePresets3.DUSK_ENTRY, + ThemePresets3.PRISM_ENTRY, ThemePresets3.CINDER_ENTRY, ThemePresets3.FOREST_ENTRY ); + static final Map PRESET_ENTRIES = buildPresetMap(PRESET_LIST); - private static final ThemeStore STORE = ThemeStore.get(); + private static Map buildPresetMap(List list) { + Map m = new LinkedHashMap<>(); + for (ThemeEntry e : list) m.put(e.getId(), e); + return Collections.unmodifiableMap(m); + } - private static final long THEME_TRANSITION_MS = 340L; - - private static Theme current = ThemePresets1.CLASSIC; - private static String currentId = THEME_CLASSIC; - private static Theme transitionFromTheme = ThemePresets1.CLASSIC; - private static long transitionStartMs = 0L; - private static boolean transitionActive = false; - private static Theme animatedThemeCache = ThemePresets1.CLASSIC; - private static int animatedCacheKey = -1; + // Mutable transition state; read / written by ThemesTransition (same package). + static ThemeEntry currentEntry = ThemePresets1.CLASSIC_ENTRY; + static ThemeEntry transitionFromEntry = ThemePresets1.CLASSIC_ENTRY; + static ThemeEntry animatedEntryCache = ThemePresets1.CLASSIC_ENTRY; + static Theme current = ThemePresets1.CLASSIC; + static String currentId = THEME_CLASSIC; + static Theme transitionFromTheme = ThemePresets1.CLASSIC; + static long transitionStartMs = 0L; + static boolean transitionActive = false; + static Theme animatedThemeCache = ThemePresets1.CLASSIC; + static int animatedCacheKey = -1; static { - applyThemeFromStore(); + ThemesTransition.applyFromStore(); } - static Theme theme() { - return animatedTheme(); - } + static Theme theme() { return ThemesTransition.theme(); } + static ThemeEntry currentEntry() { return ThemesTransition.currentEntry(); } + static String currentId() { return currentId; } + static List themes() { return STORE.getThemes(); } + static List customThemes() { return STORE.getCustomThemes(); } + static boolean isCustomTheme(String id) { return STORE.isCustomId(id); } + static String uniqueCustomId(String name) { return STORE.uniqueCustomId(name); } + static Theme classic() { return ThemePresets1.CLASSIC; } + static void setTheme(String id) { ThemesTransition.setTheme(id); } + static void setTheme(Theme t) { ThemesTransition.setTheme(t); } + static GradientSpec flatGradient(int color) { return ThemesBlending.flatGradient(color); } + static GradientSpec subtleHeaderGradient(int color) { return ThemesBlending.subtleHeaderGradient(color); } - static ThemeEntry currentEntry() { - return animatedEntry(); - } - - static String currentId() { - return currentId; - } - - static List themes() { - return STORE.getThemes(); - } - - public static List presetEntries() { - List out = new ArrayList<>(); - out.add(ThemePresets1.CLASSIC_ENTRY); - out.add(ThemePresets1.LEGACY_ENTRY); - out.add(ThemePresets1.AMBER_ENTRY); - out.add(ThemePresets1.NOIR_ENTRY); - out.add(ThemePresets1.PURPLE_ENTRY); - out.add(ThemePresets1.BLUE_ENTRY); - out.add(ThemePresets2.AZURE_ENTRY); - out.add(ThemePresets2.NITRO_ENTRY); - out.add(ThemePresets2.SUNSET_ENTRY); - out.add(ThemePresets2.AURORA_ENTRY); - out.add(ThemePresets2.MINT_ENTRY); - out.add(ThemePresets2.PEACH_ENTRY); - out.add(ThemePresets3.LILAC_ENTRY); - out.add(ThemePresets3.SAND_ENTRY); - out.add(ThemePresets3.DUSK_ENTRY); - out.add(ThemePresets3.PRISM_ENTRY); - out.add(ThemePresets3.CINDER_ENTRY); - out.add(ThemePresets3.FOREST_ENTRY); - return Collections.unmodifiableList(out); - } + /** Insertion-ordered view of the 18 built-in presets; used by the picker. */ + public static List presetEntries() { return PRESET_LIST; } public static ThemeEntry presetEntry(String id) { - if (id == null) return null; - return PRESET_ENTRIES.get(id.toLowerCase()); - } - - static List customThemes() { - return STORE.getCustomThemes(); - } - - static boolean isCustomTheme(String id) { - return STORE.isCustomId(id); - } - - static String uniqueCustomId(String name) { - return STORE.uniqueCustomId(name); + return id == null ? null : PRESET_ENTRIES.get(id.toLowerCase()); } + /** Persist (or update) a custom entry; optionally switch to it immediately. */ static ThemeEntry saveCustomTheme(ThemeEntry entry, boolean select) { ThemeEntry saved = STORE.saveCustom(entry); - if (saved != null && select) { - setTheme(saved.getId()); - } + if (saved != null && select) setTheme(saved.getId()); return saved; } @@ -162,112 +103,34 @@ public enum Themes { return saveCustomTheme(entry, currentId != null && entry != null && currentId.equals(entry.getId())); } + /** Remove a custom theme; if it was the active one, snap back to classic. */ static boolean deleteCustomTheme(String id) { boolean removed = STORE.deleteCustom(id); - if (removed && (id == null || id.equals(currentId))) { - setTheme(THEME_CLASSIC); - } + if (removed && (id == null || id.equals(currentId))) setTheme(THEME_CLASSIC); return removed; } + /** Clone the base (or current) entry under a new "custom" id and, optionally, activate it. */ static ThemeEntry createCustomTheme(String name, ThemeEntry base, boolean select) { ThemeEntry src = base != null ? base : currentEntry(); if (src == null) src = ThemePresets1.CLASSIC_ENTRY; String displayName = name == null || name.isBlank() ? "Custom Theme" : name.trim(); String id = STORE.uniqueCustomId(displayName); - ThemeEntry entry = new ThemeEntry( - id, - displayName, - false, - src.theme(), - src.windowGradient(), - src.headerGradient(), - src.surfaceGradient(), - src.cardGradient(), - src.strokeGradient() - ); + ThemeEntry entry = new ThemeEntry(id, displayName, false, src.theme(), + src.windowGradient(), src.headerGradient(), src.surfaceGradient(), src.cardGradient(), src.strokeGradient()); return saveCustomTheme(entry, select); } public static Map toProfileValues(ThemeEntry entry) { - Map out = new LinkedHashMap<>(); - if (entry == null) return out; - Theme t = entry.theme(); - out.put("name", entry.name()); - out.put("windowBg", colorString(t.windowBg())); - out.put("windowHeader", colorString(t.windowHeader())); - out.put("windowStroke", colorString(t.windowStroke())); - out.put("surface", colorString(t.surface())); - out.put("surfaceHover", colorString(t.surfaceHover())); - out.put("cardEnabled", colorString(t.cardEnabled())); - out.put("cardDisabled", colorString(t.cardDisabled())); - out.put("textPrimary", colorString(t.textPrimary())); - out.put("textMuted", colorString(t.textMuted())); - out.put("accent", colorString(t.accent())); - out.put("accentSoft", colorString(t.accentSoft())); - out.put("strokeSoft", colorString(t.strokeSoft())); - writeGradient(out, "window", entry.windowGradient()); - writeGradient(out, "header", entry.headerGradient()); - writeGradient(out, "surface", entry.surfaceGradient()); - writeGradient(out, "card", entry.cardGradient()); - writeGradient(out, "stroke", entry.strokeGradient()); - return out; + return ThemesProfileCodec.toProfileValues(entry); } public static ThemeEntry fromProfileValues(String id, String fallbackName, Map values) { - String key = id == null || id.isBlank() ? uniqueCustomId(fallbackName) : id.toLowerCase(); - ThemeEntry existing = STORE.getById(key); - ThemeEntry baseEntry = existing != null ? existing : ThemePresets1.CLASSIC_ENTRY; - Theme base = baseEntry.theme(); - Map map = values == null ? Map.of() : values; - String name = stringValue(map.get("name"), fallbackName == null || fallbackName.isBlank() ? key : fallbackName); - Theme theme = new Theme( - colorValue(map.get("windowBg"), base.windowBg()), - colorValue(map.get("windowHeader"), base.windowHeader()), - colorValue(map.get("windowStroke"), base.windowStroke()), - colorValue(map.get("surface"), base.surface()), - colorValue(map.get("surfaceHover"), base.surfaceHover()), - colorValue(map.get("cardEnabled"), base.cardEnabled()), - colorValue(map.get("cardDisabled"), base.cardDisabled()), - colorValue(map.get("textPrimary"), base.textPrimary()), - colorValue(map.get("textMuted"), base.textMuted()), - colorValue(map.get("accent"), base.accent()), - colorValue(map.get("accentSoft"), base.accentSoft()), - colorValue(map.get("strokeSoft"), base.strokeSoft()) - ); - return new ThemeEntry( - key, - name, - false, - theme, - readGradient(map, "window", baseEntry.windowGradient(), theme.windowBg()), - readGradient(map, "header", baseEntry.headerGradient(), theme.windowHeader()), - readGradient(map, "surface", baseEntry.surfaceGradient(), theme.surface()), - readGradient(map, "card", baseEntry.cardGradient(), theme.cardEnabled()), - readGradient(map, "stroke", baseEntry.strokeGradient(), theme.windowStroke()) - ); + return ThemesProfileCodec.fromProfileValues(id, fallbackName, values); } public static ThemeEntry importProfileValues(String id, String fallbackName, Map values) { - ThemeEntry entry = fromProfileValues(id, fallbackName, values); - return saveCustomTheme(entry, false); - } - - private static void writeGradient(Map out, String prefix, GradientSpec spec) { - GradientSpec g = spec != null ? spec : flatGradient(ThemePresets1.CLASSIC.accent()); - out.put(prefix + "GradientEnabled", g.enabled()); - out.put(prefix + "GradientStart", colorString(g.start())); - out.put(prefix + "GradientEnd", colorString(g.end())); - out.put(prefix + "GradientAngle", g.angleDeg()); - } - - private static GradientSpec readGradient(Map map, String prefix, GradientSpec fallback, int baseColor) { - GradientSpec fb = fallback != null ? fallback : flatGradient(baseColor); - boolean enabled = booleanValue(map.get(prefix + "GradientEnabled"), fb.enabled()); - int start = colorValue(map.get(prefix + "GradientStart"), fb.start()); - int end = colorValue(map.get(prefix + "GradientEnd"), fb.end()); - float angle = floatValue(map.get(prefix + "GradientAngle"), fb.angleDeg()); - return new GradientSpec(enabled, start, end, angle); + return saveCustomTheme(fromProfileValues(id, fallbackName, values), false); } public static String colorString(int argb) { @@ -280,280 +143,23 @@ public enum Themes { return fallback; } - private static String stringValue(Object raw, String fallback) { - if (raw instanceof CharSequence s && !s.toString().isBlank()) return s.toString(); - return fallback; - } - - private static boolean booleanValue(Object raw, boolean fallback) { - if (raw instanceof Boolean b) return b; - if (raw instanceof CharSequence s) return Boolean.parseBoolean(s.toString()); - return fallback; - } - - private static float floatValue(Object raw, float fallback) { - if (raw instanceof Number n) return n.floatValue(); - if (raw instanceof CharSequence s) { - try { - return Float.parseFloat(s.toString()); - } catch (NumberFormatException ignored) { - } - } - return fallback; - } - - static void setTheme(String id) { - if (id == null || id.isBlank()) { - boolean animate = !THEME_CLASSIC.equals(currentId) || transitionActive; - applyThemeFromEntry(ThemePresets1.CLASSIC_ENTRY, animate); - STORE.setCurrentId(THEME_CLASSIC); - return; - } - String key = id.toLowerCase(); - ThemeEntry entry = STORE.getById(key); - if (entry == null) entry = PRESET_ENTRIES.get(key); - if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; - boolean animate = !entry.getId().equals(currentId) || transitionActive; - applyThemeFromEntry(entry, animate); - STORE.setCurrentId(entry.getId()); - } - - static void setTheme(Theme t) { - if (t == null) { - setTheme(THEME_CLASSIC); - return; - } - for (ThemeEntry entry : PRESET_ENTRIES.values()) { - if (entry.theme() == t) { - setTheme(entry.getId()); - return; - } - } - Theme from = displayedThemeRaw(); - ThemeEntry fromEntry = displayedEntryRaw(); - current = t; - currentId = THEME_CLASSIC; - currentEntry = new ThemeEntry( - THEME_CLASSIC, - "Custom", - false, - t, - flatGradient(t.windowBg()), - subtleHeaderGradient(t.windowHeader()), - flatGradient(t.surface()), - flatGradient(t.cardEnabled()), - flatGradient(t.windowStroke()) - ); - beginThemeTransition(from, fromEntry); - } - - static Theme classic() { - return ThemePresets1.CLASSIC; - } - - private static void applyThemeFromStore() { - String id = STORE.getCurrentId(); - ThemeEntry entry = STORE.getById(id); - if (entry == null) entry = PRESET_ENTRIES.get(THEME_CLASSIC); - if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; - applyThemeFromEntry(entry, false); - } - - private static void applyThemeFromEntry(ThemeEntry entry, boolean animate) { - if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; - if (animate) { - beginThemeTransition(displayedThemeRaw(), displayedEntryRaw()); - } else { - transitionActive = false; - animatedCacheKey = -1; - } - current = entry.theme(); - currentId = entry.getId(); - currentEntry = entry; - if (!animate) { - animatedThemeCache = current; - animatedEntryCache = currentEntry; - } - } - - private static void beginThemeTransition(Theme fromTheme, ThemeEntry fromEntry) { - transitionFromTheme = fromTheme != null ? fromTheme : current; - transitionFromEntry = fromEntry != null ? fromEntry : currentEntry; - transitionStartMs = Util.getMillis(); - transitionActive = true; - animatedCacheKey = -1; - } - - private static Theme displayedThemeRaw() { - if (!transitionActive) return current; - return animatedTheme(); - } - - private static ThemeEntry displayedEntryRaw() { - if (!transitionActive) return currentEntry; - return animatedEntry(); - } - - private static Theme animatedTheme() { - if (!transitionActive) return current; - float progress = transitionProgress(); - if (!transitionActive) return current; - int key = Math.round(progress * 1024.0f); - if (key == animatedCacheKey && animatedThemeCache != null) return animatedThemeCache; - animatedThemeCache = blendTheme(transitionFromTheme, current, progress); - animatedEntryCache = null; - animatedCacheKey = key; - return animatedThemeCache; - } - - private static ThemeEntry animatedEntry() { - if (!transitionActive) return currentEntry; - float progress = transitionProgress(); - if (!transitionActive) return currentEntry; - int key = Math.round(progress * 1024.0f); - if (key != animatedCacheKey || animatedThemeCache == null) { - animatedThemeCache = blendTheme(transitionFromTheme, current, progress); - animatedEntryCache = null; - animatedCacheKey = key; - } - if (animatedEntryCache != null) return animatedEntryCache; - animatedEntryCache = new ThemeEntry( - currentEntry.getId(), - currentEntry.name(), - currentEntry.builtin(), - animatedThemeCache, - blendGradient(transitionFromEntry.windowGradient(), currentEntry.windowGradient(), progress), - blendGradient(transitionFromEntry.headerGradient(), currentEntry.headerGradient(), progress), - blendGradient(transitionFromEntry.surfaceGradient(), currentEntry.surfaceGradient(), progress), - blendGradient(transitionFromEntry.cardGradient(), currentEntry.cardGradient(), progress), - blendGradient(transitionFromEntry.strokeGradient(), currentEntry.strokeGradient(), progress) - ); - return animatedEntryCache; - } - - private static float transitionProgress() { - long elapsed = Math.max(0L, Util.getMillis() - transitionStartMs); - if (elapsed >= THEME_TRANSITION_MS) { - transitionActive = false; - animatedThemeCache = current; - animatedEntryCache = currentEntry; - animatedCacheKey = -1; - return 1.0f; - } - float linear = THEME_TRANSITION_MS <= 0L ? 1.0f : (float) elapsed / (float) THEME_TRANSITION_MS; - return AnimationUtility.easeOutCubic(AnimationUtility.clamp01(linear)); - } - - private static Theme blendTheme(Theme from, Theme to, float t) { - if (from == null) from = ThemePresets1.CLASSIC; - if (to == null) to = ThemePresets1.CLASSIC; - return new Theme( - mixArgb(from.windowBg(), to.windowBg(), t), - mixArgb(from.windowHeader(), to.windowHeader(), t), - mixArgb(from.windowStroke(), to.windowStroke(), t), - mixArgb(from.surface(), to.surface(), t), - mixArgb(from.surfaceHover(), to.surfaceHover(), t), - mixArgb(from.cardEnabled(), to.cardEnabled(), t), - mixArgb(from.cardDisabled(), to.cardDisabled(), t), - mixArgb(from.textPrimary(), to.textPrimary(), t), - mixArgb(from.textMuted(), to.textMuted(), t), - mixArgb(from.accent(), to.accent(), t), - mixArgb(from.accentSoft(), to.accentSoft(), t), - mixArgb(from.strokeSoft(), to.strokeSoft(), t) - ); - } - - private static GradientSpec blendGradient(GradientSpec from, GradientSpec to, float t) { - if (from == null) from = flatGradient(ThemePresets1.CLASSIC.accent()); - if (to == null) to = flatGradient(ThemePresets1.CLASSIC.accent()); - boolean enabled = from.enabled() || to.enabled(); - int start = mixArgb(from.start(), to.start(), t); - int end = mixArgb(from.end(), to.end(), t); - float angle = mixAngle(from.angleDeg(), to.angleDeg(), t); - return new GradientSpec(enabled, start, end, angle); - } - - private static int mixArgb(int from, int to, float t) { - t = AnimationUtility.clamp01(t); - int fa = (from >>> 24) & 0xFF; - int fr = (from >>> 16) & 0xFF; - int fg = (from >>> 8) & 0xFF; - int fb = from & 0xFF; - int ta = (to >>> 24) & 0xFF; - int tr = (to >>> 16) & 0xFF; - int tg = (to >>> 8) & 0xFF; - int tb = to & 0xFF; - int a = Math.round(fa + (ta - fa) * t); - int r = Math.round(fr + (tr - fr) * t); - int g = Math.round(fg + (tg - fg) * t); - int b = Math.round(fb + (tb - fb) * t); - return (clampChannel(a) << 24) | (clampChannel(r) << 16) | (clampChannel(g) << 8) | clampChannel(b); - } - - private static float mixAngle(float from, float to, float t) { - float delta = ((to - from + 540.0f) % 360.0f) - 180.0f; - float out = from + delta * AnimationUtility.clamp01(t); - out %= 360.0f; - return out < 0.0f ? out + 360.0f : out; - } - - static GradientSpec flatGradient(int color) { - return new GradientSpec(false, color, color, 90f); - } - - static GradientSpec subtleHeaderGradient(int color) { - int start = adjustBrightness(color, 0.08f); - int end = adjustBrightness(color, -0.06f); - return new GradientSpec(true, start, end, 90f); - } - - private static int adjustBrightness(int argb, float delta) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - r = clampChannel(r + Math.round((delta >= 0 ? (255 - r) : r) * delta)); - g = clampChannel(g + Math.round((delta >= 0 ? (255 - g) : g) * delta)); - b = clampChannel(b + Math.round((delta >= 0 ? (255 - b) : b) * delta)); - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int clampChannel(int v) { - if (v < 0) return 0; - if (v > 255) return 255; - return v; - } - + /** The 12 semantic color slots every theme must define. */ public record Theme( - int windowBg, - int windowHeader, - int windowStroke, - int surface, - int surfaceHover, - int cardEnabled, - int cardDisabled, - int textPrimary, - int textMuted, - int accent, - int accentSoft, - int strokeSoft - ) { - } + int windowBg, int windowHeader, int windowStroke, + int surface, int surfaceHover, + int cardEnabled, int cardDisabled, + int textPrimary, int textMuted, + int accent, int accentSoft, int strokeSoft + ) { } - public record GradientSpec(boolean enabled, int start, int end, float angleDeg) { - } + /** A single gradient stop: enabled flag, two endpoint colors and a direction in degrees. */ + public record GradientSpec(boolean enabled, int start, int end, float angleDeg) { } + /** Full theme definition: color palette plus the five named gradient slots used by the clickgui. */ public record ThemeEntry( - String id, - String name, - boolean builtin, - Theme theme, - GradientSpec windowGradient, - GradientSpec headerGradient, - GradientSpec surfaceGradient, - GradientSpec cardGradient, - GradientSpec strokeGradient + String id, String name, boolean builtin, Theme theme, + GradientSpec windowGradient, GradientSpec headerGradient, + GradientSpec surfaceGradient, GradientSpec cardGradient, GradientSpec strokeGradient ) { public String getId() { return id; diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesBlending.java b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesBlending.java new file mode 100644 index 0000000..644c458 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesBlending.java @@ -0,0 +1,106 @@ +package dev.loki.lovisual.features.theme; + +import dev.loki.lovisual.features.theme.Themes.GradientSpec; +import dev.loki.lovisual.features.theme.Themes.Theme; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +/** + * Stateless color / gradient math shared by {@link ThemesTransition} (frame blending) + * and {@link ThemesProfileCodec} (defaults for missing keys). Kept in a sibling + * folder so the main theme directory stays within the four-file-per-directory limit; + * the package declaration is unchanged so package-private callers keep working. + */ +final class ThemesBlending { + + private ThemesBlending() { } + + /** Non-animated gradient used as the "no gradient" fallback everywhere in the theme system. */ + static GradientSpec flatGradient(int color) { + return new GradientSpec(false, color, color, 90f); + } + + /** Soft top-lit header gradient derived from a single base color. */ + static GradientSpec subtleHeaderGradient(int color) { + int start = adjustBrightness(color, 0.08f); + int end = adjustBrightness(color, -0.06f); + return new GradientSpec(true, start, end, 90f); + } + + static int adjustBrightness(int argb, float delta) { + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + r = clampChannel(r + Math.round((delta >= 0 ? (255 - r) : r) * delta)); + g = clampChannel(g + Math.round((delta >= 0 ? (255 - g) : g) * delta)); + b = clampChannel(b + Math.round((delta >= 0 ? (255 - b) : b) * delta)); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + static int clampChannel(int v) { + if (v < 0) return 0; + if (v > 255) return 255; + return v; + } + + /** Per-channel linear interpolation between two packed ARGB colors. */ + static int mixArgb(int from, int to, float t) { + t = AnimationUtility.clamp01(t); + int fa = (from >>> 24) & 0xFF, fr = (from >>> 16) & 0xFF, fg = (from >>> 8) & 0xFF, fb = from & 0xFF; + int ta = (to >>> 24) & 0xFF, tr = (to >>> 16) & 0xFF, tg = (to >>> 8) & 0xFF, tb = to & 0xFF; + return (clampChannel(Math.round(fa + (ta - fa) * t)) << 24) + | (clampChannel(Math.round(fr + (tr - fr) * t)) << 16) + | (clampChannel(Math.round(fg + (tg - fg) * t)) << 8) + | clampChannel(Math.round(fb + (tb - fb) * t)); + } + + /** Shortest-arc angle interpolation so 350°→10° does not spin the long way around. */ + static float mixAngle(float from, float to, float t) { + float delta = ((to - from + 540.0f) % 360.0f) - 180.0f; + float out = from + delta * AnimationUtility.clamp01(t); + out %= 360.0f; + return out < 0.0f ? out + 360.0f : out; + } + + /** Linear blend of the 12 semantic color slots of a {@link Theme}. */ + static Theme blendTheme(Theme from, Theme to, float t) { + if (from == null) from = ThemePresets1.CLASSIC; + if (to == null) to = ThemePresets1.CLASSIC; + return new Theme( + mixArgb(from.windowBg(), to.windowBg(), t), + mixArgb(from.windowHeader(), to.windowHeader(), t), + mixArgb(from.windowStroke(), to.windowStroke(), t), + mixArgb(from.surface(), to.surface(), t), + mixArgb(from.surfaceHover(), to.surfaceHover(), t), + mixArgb(from.cardEnabled(), to.cardEnabled(), t), + mixArgb(from.cardDisabled(), to.cardDisabled(), t), + mixArgb(from.textPrimary(), to.textPrimary(), t), + mixArgb(from.textMuted(), to.textMuted(), t), + mixArgb(from.accent(), to.accent(), t), + mixArgb(from.accentSoft(), to.accentSoft(), t), + mixArgb(from.strokeSoft(), to.strokeSoft(), t) + ); + } + + /** Blend a pair of gradient specs; the result stays enabled if either endpoint is enabled. */ + static GradientSpec blendGradient(GradientSpec from, GradientSpec to, float t) { + if (from == null) from = flatGradient(ThemePresets1.CLASSIC.accent()); + if (to == null) to = flatGradient(ThemePresets1.CLASSIC.accent()); + boolean enabled = from.enabled() || to.enabled(); + return new GradientSpec(enabled, mixArgb(from.start(), to.start(), t), mixArgb(from.end(), to.end(), t), mixAngle(from.angleDeg(), to.angleDeg(), t)); + } + + /** Blend two full {@link ThemeEntry} objects including every gradient slot; used during transitions. */ + static ThemeEntry blendEntry(ThemeEntry from, ThemeEntry to, float t) { + return new ThemeEntry( + to.getId(), to.name(), to.builtin(), + blendTheme(from.theme(), to.theme(), t), + blendGradient(from.windowGradient(), to.windowGradient(), t), + blendGradient(from.headerGradient(), to.headerGradient(), t), + blendGradient(from.surfaceGradient(), to.surfaceGradient(), t), + blendGradient(from.cardGradient(), to.cardGradient(), t), + blendGradient(from.strokeGradient(), to.strokeGradient(), t) + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesProfileCodec.java b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesProfileCodec.java new file mode 100644 index 0000000..f1057f4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesProfileCodec.java @@ -0,0 +1,120 @@ +package dev.loki.lovisual.features.theme; + +import dev.loki.lovisual.features.theme.Themes.GradientSpec; +import dev.loki.lovisual.features.theme.Themes.Theme; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Serialises a {@link ThemeEntry} into the flat string-keyed map used by exportable + * config profiles, and rebuilds an entry from that map on import. Every field is + * written as a plain scalar so profiles stay human-editable. + *

    + * Lives in a sibling folder to keep the main theme directory within the + * four-file-per-directory limit; the package is shared with {@link Themes} so + * STORE access keeps working through the package-private reference. + */ +final class ThemesProfileCodec { + + private ThemesProfileCodec() { } + + /** Pack a theme entry (12 semantic colors + 5 gradient blocks) into a profile map. */ + static Map toProfileValues(ThemeEntry entry) { + Map out = new LinkedHashMap<>(); + if (entry == null) return out; + Theme t = entry.theme(); + out.put("name", entry.name()); + out.put("windowBg", Themes.colorString(t.windowBg())); + out.put("windowHeader", Themes.colorString(t.windowHeader())); + out.put("windowStroke", Themes.colorString(t.windowStroke())); + out.put("surface", Themes.colorString(t.surface())); + out.put("surfaceHover", Themes.colorString(t.surfaceHover())); + out.put("cardEnabled", Themes.colorString(t.cardEnabled())); + out.put("cardDisabled", Themes.colorString(t.cardDisabled())); + out.put("textPrimary", Themes.colorString(t.textPrimary())); + out.put("textMuted", Themes.colorString(t.textMuted())); + out.put("accent", Themes.colorString(t.accent())); + out.put("accentSoft", Themes.colorString(t.accentSoft())); + out.put("strokeSoft", Themes.colorString(t.strokeSoft())); + writeGradient(out, "window", entry.windowGradient()); + writeGradient(out, "header", entry.headerGradient()); + writeGradient(out, "surface", entry.surfaceGradient()); + writeGradient(out, "card", entry.cardGradient()); + writeGradient(out, "stroke", entry.strokeGradient()); + return out; + } + + /** Rebuild a theme entry from a profile map, using the currently-stored entry (or classic) as fallback. */ + static ThemeEntry fromProfileValues(String id, String fallbackName, Map values) { + String key = id == null || id.isBlank() ? Themes.uniqueCustomId(fallbackName) : id.toLowerCase(); + ThemeEntry existing = Themes.STORE.getById(key); + ThemeEntry baseEntry = existing != null ? existing : ThemePresets1.CLASSIC_ENTRY; + Theme base = baseEntry.theme(); + Map map = values == null ? Map.of() : values; + String name = stringValue(map.get("name"), fallbackName == null || fallbackName.isBlank() ? key : fallbackName); + Theme theme = new Theme( + Themes.colorValue(map.get("windowBg"), base.windowBg()), + Themes.colorValue(map.get("windowHeader"), base.windowHeader()), + Themes.colorValue(map.get("windowStroke"), base.windowStroke()), + Themes.colorValue(map.get("surface"), base.surface()), + Themes.colorValue(map.get("surfaceHover"), base.surfaceHover()), + Themes.colorValue(map.get("cardEnabled"), base.cardEnabled()), + Themes.colorValue(map.get("cardDisabled"), base.cardDisabled()), + Themes.colorValue(map.get("textPrimary"), base.textPrimary()), + Themes.colorValue(map.get("textMuted"), base.textMuted()), + Themes.colorValue(map.get("accent"), base.accent()), + Themes.colorValue(map.get("accentSoft"), base.accentSoft()), + Themes.colorValue(map.get("strokeSoft"), base.strokeSoft()) + ); + return new ThemeEntry( + key, name, false, theme, + readGradient(map, "window", baseEntry.windowGradient(), theme.windowBg()), + readGradient(map, "header", baseEntry.headerGradient(), theme.windowHeader()), + readGradient(map, "surface", baseEntry.surfaceGradient(), theme.surface()), + readGradient(map, "card", baseEntry.cardGradient(), theme.cardEnabled()), + readGradient(map, "stroke", baseEntry.strokeGradient(), theme.windowStroke()) + ); + } + + private static void writeGradient(Map out, String prefix, GradientSpec spec) { + GradientSpec g = spec != null ? spec : ThemesBlending.flatGradient(ThemePresets1.CLASSIC.accent()); + out.put(prefix + "GradientEnabled", g.enabled()); + out.put(prefix + "GradientStart", Themes.colorString(g.start())); + out.put(prefix + "GradientEnd", Themes.colorString(g.end())); + out.put(prefix + "GradientAngle", g.angleDeg()); + } + + private static GradientSpec readGradient(Map map, String prefix, GradientSpec fallback, int baseColor) { + GradientSpec fb = fallback != null ? fallback : ThemesBlending.flatGradient(baseColor); + boolean enabled = booleanValue(map.get(prefix + "GradientEnabled"), fb.enabled()); + int start = Themes.colorValue(map.get(prefix + "GradientStart"), fb.start()); + int end = Themes.colorValue(map.get(prefix + "GradientEnd"), fb.end()); + float angle = floatValue(map.get(prefix + "GradientAngle"), fb.angleDeg()); + return new GradientSpec(enabled, start, end, angle); + } + + private static String stringValue(Object raw, String fallback) { + if (raw instanceof CharSequence s && !s.toString().isBlank()) return s.toString(); + return fallback; + } + + private static boolean booleanValue(Object raw, boolean fallback) { + if (raw instanceof Boolean b) return b; + if (raw instanceof CharSequence s) return Boolean.parseBoolean(s.toString()); + return fallback; + } + + private static float floatValue(Object raw, float fallback) { + if (raw instanceof Number n) return n.floatValue(); + if (raw instanceof CharSequence s) { + try { + return Float.parseFloat(s.toString()); + } catch (NumberFormatException ignored) { + return fallback; + } + } + return fallback; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesTransition.java b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesTransition.java new file mode 100644 index 0000000..d74b14c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/impl/ThemesTransition.java @@ -0,0 +1,162 @@ +package dev.loki.lovisual.features.theme; + +import dev.loki.lovisual.features.theme.Themes.Theme; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import net.minecraft.util.Util; + +/** + * Owns the "current theme" pointer plus the cross-fade animation state used when + * the user switches themes. Every accessor on {@link Themes} that returns a + * {@code Theme} or {@code ThemeEntry} goes through {@link #animatedTheme()} / + * {@link #animatedEntry()} so the visible UI blends smoothly between selections. + *

    + * Lives in a sibling folder to keep the main theme directory within the + * four-file-per-directory limit; the package is shared with {@link Themes} so + * all mutable state stays package-private there. + */ +final class ThemesTransition { + + /** Total cross-fade duration in milliseconds; matches {@code Themes.THEME_TRANSITION_MS}. */ + ThemesTransition() { } + + /** Current theme with any in-flight transition applied; safe to call every frame. */ + static Theme theme() { + return animatedTheme(); + } + + /** Current entry (theme + five gradient slots) with the transition blend applied. */ + static ThemeEntry currentEntry() { + return animatedEntry(); + } + + /** Switch to the theme stored under the given preset / custom id, or fall back to classic. */ + static void setTheme(String id) { + if (id == null || id.isBlank()) { + boolean animate = !Themes.THEME_CLASSIC.equals(Themes.currentId) || Themes.transitionActive; + applyThemeFromEntry(ThemePresets1.CLASSIC_ENTRY, animate); + Themes.STORE.setCurrentId(Themes.THEME_CLASSIC); + return; + } + String key = id.toLowerCase(); + ThemeEntry entry = Themes.STORE.getById(key); + if (entry == null) entry = Themes.PRESET_ENTRIES.get(key); + if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; + boolean animate = !entry.getId().equals(Themes.currentId) || Themes.transitionActive; + applyThemeFromEntry(entry, animate); + Themes.STORE.setCurrentId(entry.getId()); + } + + /** Push a raw {@link Theme} instance (not registered as a preset) and start the fade. */ + static void setTheme(Theme t) { + if (t == null) { + setTheme(Themes.THEME_CLASSIC); + return; + } + for (ThemeEntry entry : Themes.PRESET_ENTRIES.values()) { + if (entry.theme() == t) { + setTheme(entry.getId()); + return; + } + } + Theme from = displayedThemeRaw(); + ThemeEntry fromEntry = displayedEntryRaw(); + Themes.current = t; + Themes.currentId = Themes.THEME_CLASSIC; + Themes.currentEntry = new ThemeEntry( + Themes.THEME_CLASSIC, "Custom", false, t, + ThemesBlending.flatGradient(t.windowBg()), + ThemesBlending.subtleHeaderGradient(t.windowHeader()), + ThemesBlending.flatGradient(t.surface()), + ThemesBlending.flatGradient(t.cardEnabled()), + ThemesBlending.flatGradient(t.windowStroke()) + ); + beginThemeTransition(from, fromEntry); + } + + /** Bootstrap hook: read the persisted id from the store and snap to it without animating. */ + static void applyFromStore() { + String id = Themes.STORE.getCurrentId(); + ThemeEntry entry = Themes.STORE.getById(id); + if (entry == null) entry = Themes.PRESET_ENTRIES.get(Themes.THEME_CLASSIC); + if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; + applyThemeFromEntry(entry, false); + } + + private static void applyThemeFromEntry(ThemeEntry entry, boolean animate) { + if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY; + if (animate) { + beginThemeTransition(displayedThemeRaw(), displayedEntryRaw()); + } else { + Themes.transitionActive = false; + Themes.animatedCacheKey = -1; + } + Themes.current = entry.theme(); + Themes.currentId = entry.getId(); + Themes.currentEntry = entry; + if (!animate) { + Themes.animatedThemeCache = Themes.current; + Themes.animatedEntryCache = Themes.currentEntry; + } + } + + private static void beginThemeTransition(Theme fromTheme, ThemeEntry fromEntry) { + Themes.transitionFromTheme = fromTheme != null ? fromTheme : Themes.current; + Themes.transitionFromEntry = fromEntry != null ? fromEntry : Themes.currentEntry; + Themes.transitionStartMs = Util.getMillis(); + Themes.transitionActive = true; + Themes.animatedCacheKey = -1; + } + + private static Theme displayedThemeRaw() { + if (!Themes.transitionActive) return Themes.current; + return animatedTheme(); + } + + private static ThemeEntry displayedEntryRaw() { + if (!Themes.transitionActive) return Themes.currentEntry; + return animatedEntry(); + } + + /** Blend of the previous and new {@link Theme} at the current transition progress. */ + private static Theme animatedTheme() { + if (!Themes.transitionActive) return Themes.current; + float progress = transitionProgress(); + if (!Themes.transitionActive) return Themes.current; + int key = Math.round(progress * 1024.0f); + if (key == Themes.animatedCacheKey && Themes.animatedThemeCache != null) return Themes.animatedThemeCache; + Themes.animatedThemeCache = ThemesBlending.blendTheme(Themes.transitionFromTheme, Themes.current, progress); + Themes.animatedEntryCache = null; + Themes.animatedCacheKey = key; + return Themes.animatedThemeCache; + } + + /** Same blend, extended to the five gradient slots so headers / cards animate together. */ + private static ThemeEntry animatedEntry() { + if (!Themes.transitionActive) return Themes.currentEntry; + float progress = transitionProgress(); + if (!Themes.transitionActive) return Themes.currentEntry; + int key = Math.round(progress * 1024.0f); + if (key != Themes.animatedCacheKey || Themes.animatedThemeCache == null) { + Themes.animatedThemeCache = ThemesBlending.blendTheme(Themes.transitionFromTheme, Themes.current, progress); + Themes.animatedEntryCache = null; + Themes.animatedCacheKey = key; + } + if (Themes.animatedEntryCache != null) return Themes.animatedEntryCache; + Themes.animatedEntryCache = ThemesBlending.blendEntry(Themes.transitionFromEntry, Themes.currentEntry, progress); + return Themes.animatedEntryCache; + } + + private static float transitionProgress() { + long elapsed = Math.max(0L, Util.getMillis() - Themes.transitionStartMs); + if (elapsed >= Themes.THEME_TRANSITION_MS) { + Themes.transitionActive = false; + Themes.animatedThemeCache = Themes.current; + Themes.animatedEntryCache = Themes.currentEntry; + Themes.animatedCacheKey = -1; + return 1.0f; + } + float linear = Themes.THEME_TRANSITION_MS <= 0L ? 1.0f : (float) elapsed / (float) Themes.THEME_TRANSITION_MS; + return AnimationUtility.easeOutCubic(AnimationUtility.clamp01(linear)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreIO.java b/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreIO.java new file mode 100644 index 0000000..84ccc92 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreIO.java @@ -0,0 +1,169 @@ +package dev.loki.lovisual.features.theme; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import dev.loki.lovisual.config.io.ConfigPaths; +import dev.loki.lovisual.config.profile.service.ConfigProfileStorage; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; + +/** + * Filesystem access for {@link ThemeStore}: layout of the {@code themes/} folder, + * bootstrap / legacy-migration sequence and the {@code state.json} + per-custom-file + * writers. Kept out of the store class so the main theme directory stays within the + * four-file-per-directory limit; the package declaration is unchanged. + */ +final class ThemeStoreIO { + + static final Path LEGACY_FILE = ConfigPaths.root().resolve("clickgui_themes.json"); + static final Path THEME_ROOT = ConfigPaths.root().resolve("themes"); + static final Path CUSTOM_DIR = THEME_ROOT.resolve("custom"); + static final Path STATE_FILE = THEME_ROOT.resolve("state.json"); + static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); + + private ThemeStoreIO() { } + + /** Read persisted state + custom files, migrate the legacy single-file format and rebuild the merged index. */ + static void load(ThemeStore store) { + store.currentId = loadCurrentId(); + loadCustomThemes(store); + migrateLegacyCustomThemes(store); + retireLegacyFile(store); + rebuildMergedIndex(store); + if (store.getById(store.currentId) == null) { + store.currentId = Themes.THEME_CLASSIC; + saveState(store); + } + } + + private static String loadCurrentId() { + String fromState = readCurrentId(STATE_FILE); + if (fromState != null) return fromState; + String fromLegacy = readCurrentId(LEGACY_FILE); + return fromLegacy != null ? fromLegacy : Themes.THEME_CLASSIC; + } + + private static String readCurrentId(Path file) { + if (!Files.exists(file)) return null; + try { + JsonElement parsed = JsonParser.parseString(Files.readString(file)); + if (parsed != null && parsed.isJsonObject()) { + String current = ThemeStoreJson.getString(parsed.getAsJsonObject(), "current", null); + if (current != null && !current.isBlank()) return ThemeStoreJson.normalizeId(current); + } + } catch (Exception e) { + DebugLog.error("ClickGuiThemeStore: state load failed", e); + } + return null; + } + + private static void loadCustomThemes(ThemeStore store) { + store.customThemes.clear(); + if (!Files.isDirectory(CUSTOM_DIR)) return; + try (var stream = Files.list(CUSTOM_DIR)) { + stream.filter(path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".json")) + .sorted() + .forEach(path -> { + try { + JsonElement parsed = JsonParser.parseString(Files.readString(path)); + if (parsed == null || !parsed.isJsonObject()) return; + ThemeEntry entry = ThemeStoreJson.parseEntry(parsed.getAsJsonObject(), false); + if (entry == null || Themes.presetEntry(entry.getId()) != null) return; + store.customThemes.removeIf(existing -> existing.getId().equals(entry.getId())); + store.customThemes.add(entry); + } catch (Exception e) { + DebugLog.error("ClickGuiThemeStore: custom theme load failed %s", e, path.toAbsolutePath()); + } + }); + } catch (IOException e) { + DebugLog.error("ClickGuiThemeStore: custom theme list failed", e); + } + store.customThemes.sort(Comparator.comparing(ThemeEntry::name, String.CASE_INSENSITIVE_ORDER)); + } + + private static void migrateLegacyCustomThemes(ThemeStore store) { + if (!Files.exists(LEGACY_FILE)) return; + boolean changed = false; + try { + JsonElement parsed = JsonParser.parseString(Files.readString(LEGACY_FILE)); + if (parsed == null || !parsed.isJsonObject()) return; + var arr = ThemeStoreJson.getArray(parsed.getAsJsonObject(), "themes"); + if (arr == null) return; + rebuildMergedIndex(store); + for (JsonElement item : arr) { + if (item == null || !item.isJsonObject()) continue; + ThemeEntry entry = ThemeStoreJson.parseEntry(item.getAsJsonObject(), false); + if (entry == null || Themes.presetEntry(entry.getId()) != null || store.isCustomId(entry.getId())) continue; + store.saveCustom(entry); + changed = true; + } + } catch (Exception e) { + DebugLog.error("ClickGuiThemeStore: legacy custom migration failed", e); + } + if (changed) saveState(store); + } + + private static void retireLegacyFile(ThemeStore store) { + if (!Files.exists(LEGACY_FILE)) return; + try { + Files.deleteIfExists(LEGACY_FILE); + saveState(store); + } catch (IOException e) { + DebugLog.error("ClickGuiThemeStore: legacy file cleanup failed", e); + } + } + + /** Merge presets + custom themes into the flat lookup index used by {@code getById}. */ + static void rebuildMergedIndex(ThemeStore store) { + store.themes.clear(); + store.themesById.clear(); + for (ThemeEntry preset : Themes.presetEntries()) { + store.themes.add(preset); + store.themesById.put(preset.getId(), preset); + } + for (ThemeEntry custom : store.customThemes) { + if (custom == null) continue; + store.themes.add(custom); + store.themesById.put(custom.getId(), custom); + } + } + + static void saveState(ThemeStore store) { + try { + Files.createDirectories(THEME_ROOT); + var root = new com.google.gson.JsonObject(); + root.addProperty("current", store.currentId); + Files.writeString(STATE_FILE, GSON.toJson(root) + "\n"); + } catch (IOException e) { + DebugLog.error("ClickGuiThemeStore: state save failed", e); + } + } + + static void saveCustomFile(ThemeEntry entry) { + try { + Files.createDirectories(CUSTOM_DIR); + Files.writeString(customFile(entry.getId()), GSON.toJson(ThemeStoreJson.entryToJson(entry)) + "\n"); + } catch (IOException e) { + DebugLog.error("ClickGuiThemeStore: custom theme save failed", e); + } + } + + static void deleteCustomFile(String id) { + try { + Files.deleteIfExists(customFile(id)); + } catch (IOException e) { + DebugLog.error("ClickGuiThemeStore: delete custom theme failed", e); + } + } + + static Path customFile(String id) { + return CUSTOM_DIR.resolve(ConfigProfileStorage.sanitizeFileName(id) + ".json"); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreJson.java b/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreJson.java new file mode 100644 index 0000000..83c8699 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/theme/store/ThemeStoreJson.java @@ -0,0 +1,193 @@ +package dev.loki.lovisual.features.theme; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import dev.loki.lovisual.features.theme.Themes.GradientSpec; +import dev.loki.lovisual.features.theme.Themes.ThemeEntry; + +import java.util.Locale; + +/** + * Static JSON (de)serialisation helpers for {@link ThemeStore}. Owns the shape of the + * on-disk format: an {@code id} / {@code name} header plus a nested {@code colors} object + * (12 ARGB slots) and a {@code gradients} object (five named gradient specs). + *

    + * Lives in a sibling folder so the main theme directory stays within the + * four-file-per-directory limit; the package declaration is unchanged. + */ +final class ThemeStoreJson { + + private ThemeStoreJson() { } + + static String normalizeId(String id) { + return id == null ? "" : id.toLowerCase(Locale.ROOT).trim(); + } + + static String colorToString(int argb) { + return String.format("#%08X", argb); + } + + /** Accepts {@code #RRGGBB[AAMM]} and bare hex; short forms are treated as RGB with opaque alpha. */ + static int parseColorString(String s, int fallback) { + if (s == null || s.isBlank()) return fallback; + String str = s.trim(); + if (str.startsWith("0x") || str.startsWith("0X")) str = str.substring(2); + if (str.startsWith("#")) str = str.substring(1); + try { + long v = Long.parseUnsignedLong(str, 16); + return str.length() <= 6 ? (int) (0xFF000000L | v) : (int) v; + } catch (NumberFormatException e) { + return fallback; + } + } + + static JsonObject gradientToJson(GradientSpec spec) { + GradientSpec out = spec != null ? spec : new GradientSpec(false, 0xFFFFFFFF, 0xFFFFFFFF, 90f); + JsonObject obj = new JsonObject(); + obj.addProperty("enabled", out.enabled()); + obj.addProperty("start", colorToString(out.start())); + obj.addProperty("end", colorToString(out.end())); + obj.addProperty("angle", out.angleDeg()); + return obj; + } + + static int parseColor(JsonObject obj, String key, int fallback) { + if (obj == null || !obj.has(key)) return fallback; + return parseColor(obj.get(key), fallback); + } + + static int parseColor(JsonElement raw, int fallback) { + if (raw == null || !raw.isJsonPrimitive()) return fallback; + var prim = raw.getAsJsonPrimitive(); + if (prim.isNumber()) return prim.getAsInt(); + if (prim.isString()) return parseColorString(prim.getAsString(), fallback); + return fallback; + } + + static JsonObject getObject(JsonObject obj, String key) { + if (obj == null || key == null || !obj.has(key)) return null; + JsonElement el = obj.get(key); + return el != null && el.isJsonObject() ? el.getAsJsonObject() : null; + } + + static JsonArray getArray(JsonObject obj, String key) { + if (obj == null || key == null || !obj.has(key)) return null; + JsonElement el = obj.get(key); + return el != null && el.isJsonArray() ? el.getAsJsonArray() : null; + } + + static boolean getBoolean(JsonObject obj, String key, boolean fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + JsonElement el = obj.get(key); + if (el == null || !el.isJsonPrimitive()) return fallback; + var prim = el.getAsJsonPrimitive(); + if (prim.isBoolean()) return prim.getAsBoolean(); + if (prim.isString()) return Boolean.parseBoolean(prim.getAsString()); + return fallback; + } + + static String getString(JsonObject obj, String key, String fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + JsonElement el = obj.get(key); + if (el == null || !el.isJsonPrimitive()) return fallback; + var prim = el.getAsJsonPrimitive(); + return prim.isString() ? prim.getAsString() : fallback; + } + + static float getFloat(JsonObject obj, String key, float fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + JsonElement el = obj.get(key); + if (el == null || !el.isJsonPrimitive()) return fallback; + var prim = el.getAsJsonPrimitive(); + if (prim.isNumber()) return prim.getAsFloat(); + if (prim.isString()) { + try { + return Float.parseFloat(prim.getAsString()); + } catch (NumberFormatException ignored) { + return fallback; + } + } + return fallback; + } + + static float normalizeAngle(float angle) { + if (!Float.isFinite(angle)) return 90f; + angle %= 360f; + return angle < 0f ? angle + 360f : angle; + } + + /** Full on-disk shape: id / name / builtin header plus nested colors and gradients objects. */ + static JsonObject entryToJson(ThemeEntry entry) { + JsonObject obj = new JsonObject(); + obj.addProperty("id", entry.getId()); + obj.addProperty("name", entry.name()); + obj.addProperty("builtin", false); + Themes.Theme theme = entry.theme(); + JsonObject colors = new JsonObject(); + colors.addProperty("windowBg", colorToString(theme.windowBg())); + colors.addProperty("windowHeader", colorToString(theme.windowHeader())); + colors.addProperty("windowStroke", colorToString(theme.windowStroke())); + colors.addProperty("surface", colorToString(theme.surface())); + colors.addProperty("surfaceHover", colorToString(theme.surfaceHover())); + colors.addProperty("cardEnabled", colorToString(theme.cardEnabled())); + colors.addProperty("cardDisabled", colorToString(theme.cardDisabled())); + colors.addProperty("textPrimary", colorToString(theme.textPrimary())); + colors.addProperty("textMuted", colorToString(theme.textMuted())); + colors.addProperty("accent", colorToString(theme.accent())); + colors.addProperty("accentSoft", colorToString(theme.accentSoft())); + colors.addProperty("strokeSoft", colorToString(theme.strokeSoft())); + obj.add("colors", colors); + JsonObject gradients = new JsonObject(); + gradients.add("window", gradientToJson(entry.windowGradient())); + gradients.add("header", gradientToJson(entry.headerGradient())); + gradients.add("surface", gradientToJson(entry.surfaceGradient())); + gradients.add("card", gradientToJson(entry.cardGradient())); + gradients.add("stroke", gradientToJson(entry.strokeGradient())); + obj.add("gradients", gradients); + return obj; + } + + /** Parse one theme entry from its JSON object; missing slots fall back to the same-named preset (or classic). */ + static ThemeEntry parseEntry(JsonObject obj, boolean allowBuiltin) { + String id = getString(obj, "id", null); + if (id == null || id.isBlank()) return null; + String key = normalizeId(id); + ThemeEntry preset = Themes.presetEntry(key); + if (preset != null && !allowBuiltin) return null; + String name = getString(obj, "name", preset != null ? preset.name() : key); + boolean builtin = allowBuiltin && getBoolean(obj, "builtin", preset != null && preset.builtin()); + JsonObject colors = getObject(obj, "colors"); + Themes.Theme base = preset != null ? preset.theme() : Theme.classic(); + int windowBg = parseColor(colors, "windowBg", base.windowBg()); + int windowHeader = parseColor(colors, "windowHeader", base.windowHeader()); + int windowStroke = parseColor(colors, "windowStroke", base.windowStroke()); + int surface = parseColor(colors, "surface", base.surface()); + int surfaceHover = parseColor(colors, "surfaceHover", base.surfaceHover()); + int cardEnabled = parseColor(colors, "cardEnabled", base.cardEnabled()); + int cardDisabled = parseColor(colors, "cardDisabled", base.cardDisabled()); + int textPrimary = parseColor(colors, "textPrimary", base.textPrimary()); + int textMuted = parseColor(colors, "textMuted", base.textMuted()); + int accent = parseColor(colors, "accent", base.accent()); + int accentSoft = parseColor(colors, "accentSoft", base.accentSoft()); + int strokeSoft = parseColor(colors, "strokeSoft", base.strokeSoft()); + Themes.Theme theme = new Themes.Theme(windowBg, windowHeader, windowStroke, surface, surfaceHover, cardEnabled, + cardDisabled, textPrimary, textMuted, accent, accentSoft, strokeSoft); + JsonObject gradients = getObject(obj, "gradients"); + return new ThemeEntry(key, name != null && !name.isBlank() ? name.trim() : key, builtin, theme, + parseGradient(getObject(gradients, "window"), preset != null ? preset.windowGradient() : new GradientSpec(false, windowBg, windowBg, 90f), windowBg), + parseGradient(getObject(gradients, "header"), preset != null ? preset.headerGradient() : new GradientSpec(false, windowHeader, windowHeader, 90f), windowHeader), + parseGradient(getObject(gradients, "surface"), preset != null ? preset.surfaceGradient() : new GradientSpec(false, surface, surface, 90f), surface), + parseGradient(getObject(gradients, "card"), preset != null ? preset.cardGradient() : new GradientSpec(false, cardEnabled, cardEnabled, 90f), cardEnabled), + parseGradient(getObject(gradients, "stroke"), preset != null ? preset.strokeGradient() : new GradientSpec(false, windowStroke, windowStroke, 90f), windowStroke)); + } + + static GradientSpec parseGradient(JsonObject map, GradientSpec fallback, int baseColor) { + if (map == null || map.size() == 0) return fallback; + boolean enabled = getBoolean(map, "enabled", fallback.enabled()); + int start = parseColor(map, "start", fallback.start() != 0 ? fallback.start() : baseColor); + int end = parseColor(map, "end", fallback.end() != 0 ? fallback.end() : baseColor); + float angle = normalizeAngle(getFloat(map, "angle", fallback.angleDeg())); + return new GradientSpec(enabled, start, end, angle); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java index ba0357c..0291de9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java @@ -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.mixininterface.device; import com.mojang.blaze3d.opengl.DirectStateAccess; diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java index f8c809e..3570dff 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java @@ -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.mixininterface.device; public interface IGpuDevice { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java index 9b6636a..04ca34f 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java @@ -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.mixininterface.device; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java index 26e1718..d383606 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java @@ -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.mixininterface.device; public interface IMsaaTexture { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java index 365e20f..7502946 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java @@ -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.mixininterface.entity; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java index 00d596c..29bb226 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java @@ -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.mixininterface.entity; public interface ILocalPlayer { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java index 4748e7d..d69cc9d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java @@ -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.mixininterface.entity; public interface IPlayerAttackCooldown { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java index 1d0f346..7e0367a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java @@ -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.mixininterface.entity; public interface IRoundedHitbox { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java index 3c31068..2644408 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java @@ -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.mixininterface.render; public interface IGuiGraphics { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java index 19f7615..c1b3f87 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java @@ -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.mixininterface.render; public interface IItemEntityRenderState { diff --git a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java index f0dfa11..e3f185a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java +++ b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java @@ -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.mixininterface.render; import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java b/mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java index 15b7434..89838e5 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java @@ -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.mixins; import net.fabricmc.loader.api.FabricLoader; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java index 9eebc48..dcf51ba 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java @@ -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.mixins.accessors.blaze3d; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java index 5887932..17de5fd 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java @@ -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.mixins.accessors.blaze3d; import com.mojang.blaze3d.systems.GpuDevice; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java index d7269bb..78ab969 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java @@ -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.mixins.accessors.blaze3d; import net.minecraft.client.renderer.PostChainConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java index 9bc01ec..5b94ee4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java @@ -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.mixins.accessors.blaze3d; import com.mojang.blaze3d.shaders.ShaderType; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java index 09dd01f..e24e157 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java @@ -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.mixins.accessors.chat; import com.mojang.brigadier.suggestion.Suggestions; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java index d441bd8..180f2c6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java @@ -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.mixins.accessors.chat; import net.minecraft.client.gui.components.EditBox; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java index e3dd7a8..9b978ce 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java @@ -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.mixins.accessors.chat; import net.minecraft.client.gui.components.CommandSuggestions; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java index 65efcbd..33271ef 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java @@ -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.mixins.accessors.chat; import net.minecraft.client.gui.components.CommandSuggestions; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java index ee45f14..8c63644 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java @@ -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.mixins.accessors.entity; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java index f69c6c2..ba90978 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java @@ -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.mixins.accessors.entity; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java index 259d311..b4be66a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java @@ -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.mixins.accessors.entity; import net.minecraft.world.entity.LivingEntity; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java index 88f5765..2493d68 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java @@ -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.mixins.accessors.entity; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java index 7dfdc8c..cd1f19b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java @@ -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.mixins.accessors.hud; import net.minecraft.client.gui.components.BossHealthOverlay; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java index 2fb5ee4..2333c33 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java @@ -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.mixins.accessors.hud; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java index 24258d8..aa3974b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java @@ -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.mixins.accessors.hud; import net.minecraft.client.gui.components.PlayerTabOverlay; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java index 56f8188..335b4dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java @@ -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.mixins.accessors.hud; import net.minecraft.client.gui.components.SpriteIconButton; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java index 5d5b407..5615e24 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java @@ -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.mixins.accessors.inventory; import org.spongepowered.asm.mixin.Mixin; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java index f7de969..8d072d2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java @@ -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.mixins.accessors.inventory; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java index f3c2223..d63a658 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java @@ -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.mixins.accessors.inventory; import net.minecraft.world.entity.player.Inventory; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java index 192f016..1af87c2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java @@ -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.mixins.accessors.inventory; import net.minecraft.world.inventory.Slot; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java index f386b87..8e1e066 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java @@ -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.mixins.accessors.misc; import net.minecraft.client.player.ClientInput; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java index 90fa62c..ac0a9fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java @@ -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.mixins.accessors.misc; import com.mojang.blaze3d.platform.InputConstants; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java index 8ef87b8..6fad554 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java @@ -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.mixins.accessors.misc; import com.mojang.authlib.minecraft.UserApiService; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java index c1a3f73..ec6d403 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java @@ -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.mixins.accessors.misc; import net.minecraft.client.multiplayer.MultiPlayerGameMode; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java index 80eefa5..059dcb4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java @@ -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.mixins.accessors.packet; import net.minecraft.network.protocol.game.ClientboundExplodePacket; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java index 1b8502b..6c82875 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java @@ -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.mixins.accessors.packet; import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java index 4cc6dd6..835daa5 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java @@ -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.mixins.accessors.packet; import net.minecraft.network.protocol.game.ServerboundInteractPacket; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java index b220a09..b31e5a6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java @@ -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.mixins.accessors.packet; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java index 5cfc193..238dbd2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java @@ -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.mixins.accessors.projectile; import net.minecraft.world.entity.projectile.arrow.AbstractArrow; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java index 445d8e1..6f95955 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java @@ -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.mixins.accessors.renderer; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java index 0ac82c2..052d352 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java @@ -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.mixins.accessors.renderer; import net.minecraft.client.renderer.entity.EntityRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java index ba41960..7f439bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java @@ -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.mixins.accessors.renderer; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java index 730bb11..533d662 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java @@ -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.mixins.accessors.renderer; import net.minecraft.client.renderer.rendertype.RenderSetup; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java index 9867bc8..78207d7 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java @@ -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.mixins.accessors.state; import net.minecraft.core.particles.SpellParticleOption; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java index 18c11de..15b334e 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java @@ -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.mixins.accessors.state; import net.minecraft.world.item.component.Fireworks; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java index adac897..2b9d493 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java @@ -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.mixins.accessors.state; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java index f1c6b55..a9dbf72 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java @@ -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.mixins.accessors.widget; import net.minecraft.client.gui.components.AbstractWidget; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java index 58f1902..6c942d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java @@ -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.mixins.accessors.widget; import net.minecraft.client.gui.components.CycleButton; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java index 9a77399..870c7d7 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java @@ -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.mixins.accessors.widget; import net.minecraft.client.gui.components.AbstractSliderButton; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java index 6d2de6a..6cda2bd 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java @@ -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.mixins.accessors.widget; import net.minecraft.client.gui.components.EditBox; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java index 30f53ca..da68f00 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java @@ -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.mixins.accessors.world; import net.minecraft.client.multiplayer.ClientLevel; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java index 773f43b..026ffc9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java @@ -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.mixins.accessors.world; import net.minecraft.client.renderer.entity.state.EntityRenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java index 74a3abf..d915fe9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java @@ -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.mixins.accessors.world; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java index a8f4cbd..e58d6db 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java @@ -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.mixins.accessors.world; import net.minecraft.world.level.Level; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java index 15547e5..6e786c7 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java @@ -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.mixins.blaze3d.device; import com.mojang.blaze3d.opengl.DirectStateAccess; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java index 8bae716..36a0aae 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java @@ -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.mixins.blaze3d.device; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java index 3177645..474ab68 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java @@ -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.mixins.blaze3d.device; import com.mojang.blaze3d.systems.GpuSurface; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java index 46947cd..02e6079 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java @@ -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.mixins.blaze3d.device; import com.mojang.blaze3d.GpuFormat; @@ -32,6 +25,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge; +import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanPipelineVariantKey; import dev.loki.lovisual.util.logging.DebugLog; import java.util.HashMap; @@ -46,7 +40,7 @@ public abstract class VulkanDeviceMixin { private ShaderSource defaultShaderSource; @Unique - private final Map lovisual$pipelineVariants = new HashMap<>(); + private final Map lovisual$pipelineVariants = new HashMap<>(); @Invoker("compilePipeline") protected abstract VulkanRenderPipeline lovisual$compilePipelineVariant(RenderPipeline pipeline, ShaderSource source); @@ -136,7 +130,7 @@ public abstract class VulkanDeviceMixin { CallbackInfoReturnable cir) { if (!VulkanRenderStateBridge.needsPipelineVariant(pipeline)) return; - VulkanRenderStateBridge.PipelineVariantKey key = VulkanRenderStateBridge.pipelineVariantKey(pipeline); + VulkanPipelineVariantKey key = VulkanRenderStateBridge.pipelineVariantKey(pipeline); VulkanRenderPipeline variant = lovisual$pipelineVariants.get(key); if (variant != null) { cir.setReturnValue(variant); diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java index 9f3ba8c..bec87ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java @@ -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.mixins.blaze3d.encoder; import net.minecraft.client.renderer.SubmitNodeCollection; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java index ccf6323..39e1521 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java @@ -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.mixins.blaze3d.encoder; import com.mojang.blaze3d.systems.CommandEncoder; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java index 12d6c84..fbb2a1b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java @@ -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.mixins.blaze3d.encoder; import com.llamalad7.mixinextras.sugar.Local; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java index 8d48c1c..f505a46 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java @@ -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.mixins.blaze3d.pipeline; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java index 9bdd32f..2b9b2d2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java @@ -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.mixins.blaze3d.pipeline; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java index b3b4d0d..dd58063 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java @@ -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.mixins.blaze3d.pipeline; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java index 364b7a1..4f908a5 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java @@ -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.mixins.blaze3d.pipeline; import com.llamalad7.mixinextras.sugar.Local; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java index b1ae657..f535b5d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java @@ -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.mixins.blaze3d.shader; import net.minecraft.client.renderer.DynamicUniformStorage; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java index 05cafac..e548c8c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java @@ -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.mixins.blaze3d.shader; import com.mojang.blaze3d.opengl.GlStateManager; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java index 2747774..383fe23 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java @@ -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.mixins.blaze3d.shader; import com.google.common.collect.ImmutableMap; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java index dfc6f47..a9f7763 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java @@ -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.mixins.blaze3d.texture; import org.spongepowered.asm.mixin.Mixin; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java index 0d2a474..86e2822 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java @@ -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.mixins.blaze3d.texture; import com.mojang.blaze3d.opengl.GlTexture; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java index a75f377..62577cd 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java @@ -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.mixins.blaze3d.texture; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java index c546e15..03e029e 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java @@ -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.mixins.blaze3d.texture; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java index be5f835..fa0e160 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java @@ -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.mixins.entity.inventory; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java index 6db4667..47a06b2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java @@ -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.mixins.entity.inventory; import net.minecraft.world.entity.player.Player; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java index 919e9a4..714eaf5 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java @@ -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.mixins.entity.inventory; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java index c3c2977..fbf08aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java @@ -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.mixins.entity.inventory; import net.minecraft.world.entity.player.Inventory; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java index 55a2377..ef36203 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java @@ -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.mixins.entity.living; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java index 551486e..0c92602 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java @@ -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.mixins.entity.living; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java index f9995a9..fefa165 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java @@ -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.mixins.entity.player; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java index c2aca38..1f71c8c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java @@ -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.mixins.entity.player; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java index aa46411..65bd673 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java @@ -1,35 +1,11 @@ -/* - * 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.mixins.entity.player; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import dev.loki.lovisual.events.Events; import dev.loki.lovisual.events.impl.input.SprintControlEvent; -import dev.loki.lovisual.events.impl.movement.MovementInputEvent; -import dev.loki.lovisual.events.impl.network.EventPostSync; -import dev.loki.lovisual.events.impl.network.EventSync; -import dev.loki.lovisual.events.impl.player.PostPlayerUpdateEvent; -import dev.loki.lovisual.events.impl.world.EventPushOutOfBlocks; -import dev.loki.lovisual.util.combat.rotation.SprintController; -import dev.loki.lovisual.util.screen.ClientScreen; +import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; import net.minecraft.client.player.ClientInput; import net.minecraft.client.player.LocalPlayer; -import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.PortalProcessor; import net.minecraft.world.entity.player.Input; -import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl; -import net.minecraft.world.inventory.InventoryMenu; -import net.minecraft.world.level.entity.EntityTypeTest; -import net.minecraft.world.phys.Vec2; -import net.minecraft.world.phys.Vec3; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -37,655 +13,92 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; -import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; -import dev.loki.lovisual.mixins.accessors.entity.EntityAccessor; -import dev.loki.lovisual.mixins.accessors.misc.InputAccessor; -import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; -import dev.loki.lovisual.util.aiming.rotation.RotationManager; -import dev.loki.lovisual.util.aiming.data.Rotation; - -import java.util.ArrayList; -import java.util.List; @Mixin(LocalPlayer.class) public abstract class LocalPlayerMixin implements ILocalPlayer { - @Shadow - public ClientInput input; - @Unique - private float lovisual$lastYaw; - @Unique - private float lovisual$lastPitch; - @Unique - private double lovisual$rotationPrevX; - @Unique - private double lovisual$rotationPrevZ; - @Unique - private float lovisual$rotationPrevBodyYaw; - @Unique - private boolean lovisual$rotationBodyYawInitialized; - @Unique - private float freecam$lastHealth = Float.NaN; - @Unique - private boolean frozen = false; - @Unique - private Vec3 savedVelocity = Vec3.ZERO; - @Unique - private double savedX, savedY, savedZ; - @Unique - private float savedBodyYaw; - @Unique - private float savedHeadYaw; - @Unique - private Runnable lovisual$eventPostAction; - @Unique - private EventSync lovisual$activeSyncEvent; - @Unique - private boolean lovisual$postUpdateLock = false; - @Unique - private Input lastInput; - @Unique - private boolean pendingSpoof = false; - @Unique - private int boostTicks = 0; - @Unique - private double boostX = 0, boostZ = 0; - @Unique - private boolean wasGround = true; + @Shadow public ClientInput input; + @Shadow protected abstract void sendPosition(); - @Unique - private static boolean lovisual$sameInput(Input a, Input b) { - if (a == b) return true; - if (a == null || b == null) return false; - - return a.forward() == b.forward() - && a.backward() == b.backward() - && a.left() == b.left() - && a.right() == b.right() - && a.jump() == b.jump() - && a.shift() == b.shift() - && a.sprint() == b.sprint(); - } - - @Unique - private static float lovisual$impulse(boolean positive, boolean negative) { - if (positive == negative) { - return 0.0f; - } - return positive ? 1.0f : -1.0f; - } - - @Unique - private static float lovisual$simulateBodyYaw( - float yaw, - float prevBodyYaw, - double prevX, - double prevZ, - double currentX, - double currentZ, - float handSwingProgress - ) { - double motionX = currentX - prevX; - double motionZ = currentZ - prevZ; - float motionSquared = (float) (motionX * motionX + motionZ * motionZ); - float bodyYaw = prevBodyYaw; - - if (motionSquared > 0.0025000002F) { - float movementYaw = (float) Mth.atan2(motionZ, motionX) * Mth.RAD_TO_DEG - 90.0F; - float yawDiff = Math.abs(Mth.wrapDegrees(yaw) - movementYaw); - bodyYaw = (yawDiff > 95.0F && yawDiff < 265.0F) ? movementYaw - 180.0F : movementYaw; - } - - if (handSwingProgress - 0.2F > 0.0F) { - bodyYaw = yaw; - } - - float deltaYaw = Mth.wrapDegrees(bodyYaw - prevBodyYaw); - bodyYaw = prevBodyYaw + deltaYaw * 0.3F; - - float yawOffsetDiff = Mth.wrapDegrees(yaw - bodyYaw); - float maxHeadRotation = 52.0F; - if (Math.abs(yawOffsetDiff) > maxHeadRotation) { - bodyYaw += yawOffsetDiff - Math.signum(yawOffsetDiff) * maxHeadRotation; - } - - return bodyYaw; - } + @Unique private float lovisual$lastYaw; + @Unique private float lovisual$lastPitch; + @Unique private final LocalPlayerInputHandler lovisual$inputHandler = new LocalPlayerInputHandler(); + @Unique private final LocalPlayerRotationHandler lovisual$rotationHandler = new LocalPlayerRotationHandler(); + @Unique private final LocalPlayerNetworkHandler lovisual$networkHandler = new LocalPlayerNetworkHandler(); + @Unique private final LocalPlayerMovementHandler lovisual$movementHandler = new LocalPlayerMovementHandler(); @Inject(method = "tick", at = @At("HEAD")) private void lovisual$cacheLastRotations(CallbackInfo ci) { - LocalPlayer self = (LocalPlayer) (Object) this; - lovisual$lastYaw = self.getYRot(); - lovisual$lastPitch = self.getXRot(); - RotationManager.INSTANCE.runTickLifecycle(); - if (!lovisual$rotationBodyYawInitialized) { - lovisual$rotationPrevX = self.getX(); - lovisual$rotationPrevZ = self.getZ(); - lovisual$rotationPrevBodyYaw = self.getVisualRotationYInDegrees(); - lovisual$rotationBodyYawInitialized = true; - } + lovisual$rotationHandler.onTickCache((LocalPlayer) (Object) this); } @Inject(method = "handlePortalTransitionEffect", at = @At("HEAD"), cancellable = true) private void lovisual$disablePortalNausea(boolean inPortal, CallbackInfo ci) { - - LocalPlayer self = (LocalPlayer) (Object) this; - PortalProcessor portalManager = null; - if (self instanceof EntityAccessor accessor) { - portalManager = accessor.lovisual$getPortalManager(); - } - if (portalManager != null && portalManager.isInsidePortalThisTick()) { - if (self instanceof LocalPlayerAccessor accessor) { - accessor.lovisual$setNauseaIntensity(0.0f); - accessor.lovisual$setLastNauseaIntensity(0.0f); - } - ci.cancel(); - } + lovisual$movementHandler.onPortalNausea((LocalPlayer) (Object) this, ci); } - @Override - public float lovisual$getLastYaw() { - return lovisual$lastYaw; - } + @Override public float lovisual$getLastYaw() { return lovisual$lastYaw; } + @Override public float lovisual$getLastPitch() { return lovisual$lastPitch; } + @Override public void lovisual$setLastYaw(float yaw) { lovisual$lastYaw = yaw; } + @Override public void lovisual$setLastPitch(float pitch) { lovisual$lastPitch = pitch; } - @Override - public float lovisual$getLastPitch() { - return lovisual$lastPitch; - } - - @Override - public void lovisual$setLastYaw(float yaw) { - this.lovisual$lastYaw = yaw; - } - - @Override - public void lovisual$setLastPitch(float pitch) { - this.lovisual$lastPitch = pitch; - } - - @ModifyExpressionValue( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;canStartSprinting()Z" - ) - ) + @ModifyExpressionValue(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;canStartSprinting()Z")) private boolean lovisual$sprintMovementStart(boolean original) { - return lovisual$postSprintControl(original, SprintControlEvent.Source.MOVEMENT_TICK); + return lovisual$inputHandler.postSprintControl((LocalPlayer) (Object) this, original, SprintControlEvent.Source.MOVEMENT_TICK); } - @ModifyExpressionValue( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/Input;sprint()Z" - ) - ) + @ModifyExpressionValue(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Input;sprint()Z")) private boolean lovisual$sprintMovementInput(boolean original) { - return lovisual$postSprintControl(original, SprintControlEvent.Source.MOVEMENT_TICK); + return lovisual$inputHandler.postSprintControl((LocalPlayer) (Object) this, original, SprintControlEvent.Source.MOVEMENT_TICK); } - @ModifyExpressionValue( - method = "sendIsSprintingIfNeeded", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;isSprinting()Z" - ) - ) + @ModifyExpressionValue(method = "sendIsSprintingIfNeeded", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;isSprinting()Z")) private boolean lovisual$sprintNetwork(boolean original) { - if (SprintController.INSTANCE.isSprintBlocked()) { - return false; - } - - return original; + return lovisual$networkHandler.shouldBlockSprintNetwork(original); } - @Unique - private boolean lovisual$postSprintControl(boolean original, SprintControlEvent.Source source) { - LocalPlayer self = (LocalPlayer) (Object) this; - - if (self.input == null) { - return original; - } - - if (!Events.BUS.hasListeners(SprintControlEvent.class)) { - return original; - } - - Vec2 movement = self.input.getMoveVector(); - SprintControlEvent event = new SprintControlEvent( - movement.y, - movement.x, - original, - source - ); - Events.BUS.post(event); - return event.shouldSprint(); - } - - @Inject(method = "modifyInput", at = @At("RETURN"), cancellable = true) - private void boostWhenUsing(Vec2 input, CallbackInfoReturnable cir) { - // NoStun module removed; no item-use speed boost remains. - } - - @Inject(method = "aiStep", at = @At("HEAD")) - private void lovisual$elytraFlyVanilla(CallbackInfo ci) { - // ElytraFly module removed; no vanilla elytra override remains. - } - - @Shadow - protected abstract void sendPosition(); - @Inject(method = "applyInput", at = @At("HEAD"), cancellable = true) - private void freecam$blockWASD(CallbackInfo ci) { - Freecam fc = Modules.get(Freecam.class); - if (fc != null && fc.isEnabled() && fc.isCameraInput()) { - ci.cancel(); - } - } + private void freecam$blockWASD(CallbackInfo ci) { if (lovisual$inputHandler.shouldBlockWASD()) ci.cancel(); } - @Inject( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/ClientInput;tick()V", - shift = At.Shift.AFTER - ) - ) - private void freecam$afterInputTick(CallbackInfo ci) { - Freecam fc = Modules.get(Freecam.class); - if (fc == null || !fc.isEnabled() || !fc.isCameraInput()) return; + @Inject(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/ClientInput;tick()V", shift = At.Shift.AFTER)) + private void freecam$afterInputTick(CallbackInfo ci) { lovisual$inputHandler.handleFreecamAfterInputTick((LocalPlayer) (Object) this); } - ((InputAccessor) this.input).setMovementVector(Vec2.ZERO); + @Inject(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/ClientInput;tick()V", shift = At.Shift.AFTER)) + private void lovisual$afterInputTickSprint(CallbackInfo ci) { lovisual$inputHandler.handleAfterInputTickSprint((LocalPlayer) (Object) this); } - boolean sneak = false; - boolean sprint = this.input.keyPresses.sprint(); + @Redirect(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Input;shift()Z", ordinal = 0)) + private boolean freecam$noFlyDown0(Input in) { return lovisual$inputHandler.redirectShift(in, (LocalPlayer) (Object) this); } - this.input.keyPresses = new Input( - false, false, false, false, - false, - sneak, - sprint - ); - } - - @Inject( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/ClientInput;tick()V", - shift = At.Shift.AFTER - ) - ) - private void lovisual$afterInputTickSprint(CallbackInfo ci) { - if (this.input == null) return; - - Input original = this.input.keyPresses; - if (original == null) return; - - Input input = original; - - if (Events.BUS.hasListeners(MovementInputEvent.class)) { - MovementInputEvent event = new MovementInputEvent(input); - Events.BUS.post(event); - - Input eventInput = event.toPlayerInput(); - if (!lovisual$sameInput(input, eventInput)) { - input = eventInput; - } - } - - Input transformedInput = lovisual$transformDirection(input); - if (!lovisual$sameInput(input, transformedInput)) { - input = transformedInput; - } - - if (Events.BUS.hasListeners(SprintControlEvent.class)) { - float forward = lovisual$impulse(input.forward(), input.backward()); - float sideways = lovisual$impulse(input.left(), input.right()); - - SprintControlEvent event = new SprintControlEvent( - forward, - sideways, - input.sprint(), - SprintControlEvent.Source.INPUT - ); - Events.BUS.post(event); - - if (event.shouldSprint() != input.sprint()) { - input = new Input( - input.forward(), - input.backward(), - input.left(), - input.right(), - input.jump(), - input.shift(), - event.shouldSprint() - ); - } - } - - if (!lovisual$sameInput(original, input)) { - lovisual$applyPlayerInput(input); - } - } - - @Unique - private void lovisual$applyPlayerInput(Input input) { - if (this.input == null || input == null) { - return; - } - - InputAccessor accessor = (InputAccessor) this.input; - accessor.setPlayerInput(input); - - float forward = lovisual$impulse(input.forward(), input.backward()); - float sideways = lovisual$impulse(input.left(), input.right()); - - accessor.setMovementVector(new Vec2(sideways, forward).normalized()); - } - - @Unique - private Input lovisual$transformDirection(Input input) { - // Free correction now runs at KeyboardInput.tick(), matching the input pipeline. - // Keep this post-input hook inert to avoid double-transforming movement input. - return input; - } - - @Redirect( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/Input;shift()Z", - ordinal = 0 - ) - ) - private boolean freecam$noFlyDown0(Input in) { - Freecam fc = Modules.get(Freecam.class); - if (fc != null && fc.isEnabled() && fc.isCameraInput()) { - LocalPlayer self = (LocalPlayer) (Object) this; - if (self.getAbilities().flying) return false; - } - return in.shift(); - } - - @Redirect( - method = "aiStep", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/Input;shift()Z", - ordinal = 1 - ), - require = 0 - ) - private boolean freecam$noFlyDown1(Input in) { - Freecam fc = Modules.get(Freecam.class); - if (fc != null && fc.isEnabled() && fc.isCameraInput()) { - LocalPlayer self = (LocalPlayer) (Object) this; - if (self.getAbilities().flying) return false; - } - return in.shift(); - } + @Redirect(method = "aiStep", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Input;shift()Z", ordinal = 1), require = 0) + private boolean freecam$noFlyDown1(Input in) { return lovisual$inputHandler.redirectShift(in, (LocalPlayer) (Object) this); } @Inject(method = "hurtTo", at = @At("HEAD")) - private void freecam$onUpdateHealth(float newHealth, CallbackInfo ci) { - Freecam fc = Modules.get(Freecam.class); - - if (fc == null || !fc.isEnabled() || !fc.disableOnDamage()) { - freecam$lastHealth = newHealth; - return; - } - - if (!Float.isNaN(freecam$lastHealth) && newHealth < freecam$lastHealth) { - fc.toggle(); - } - - freecam$lastHealth = newHealth; - } - - @Unique - private boolean shouldFreezeNow() { - Freecam freecam = Modules.get(Freecam.class); - return freecam != null && freecam.freezePlayer() && freecam.isCameraInput(); - } + private void freecam$onUpdateHealth(float newHealth, CallbackInfo ci) { lovisual$movementHandler.onHurt(newHealth); } @Inject(method = "sendPosition", at = @At("HEAD"), cancellable = true) - private void lovisual$eventSync(CallbackInfo ci) { - if (!Events.BUS.hasListeners(EventSync.class)) return; - lovisual$eventPostAction = null; - LocalPlayer self = (LocalPlayer) (Object) this; - EventSync event = new EventSync(self.getYRot(), self.getXRot()); - Events.BUS.post(event); - lovisual$activeSyncEvent = event; - lovisual$eventPostAction = event.getPostAction(); - if (event.isCancelled()) { - lovisual$activeSyncEvent = null; - lovisual$eventPostAction = null; - ci.cancel(); - } - } + private void lovisual$eventSync(CallbackInfo ci) { lovisual$networkHandler.onSendPositionHead((LocalPlayer) (Object) this, ci); } @Inject(method = "sendPosition", at = @At("RETURN")) - private void lovisual$eventPostSync(CallbackInfo ci) { - if (Events.BUS.hasListeners(EventPostSync.class)) { - Events.BUS.post(new EventPostSync()); - } - if (lovisual$eventPostAction != null) { - lovisual$eventPostAction.run(); - lovisual$eventPostAction = null; - } - lovisual$activeSyncEvent = null; - } + private void lovisual$eventPostSync(CallbackInfo ci) { lovisual$networkHandler.onSendPositionReturn(); } - @ModifyExpressionValue( - method = "sendPosition", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F" - ) - ) - private float lovisual$eventSyncYaw(float original) { - lovisual$updateVisualBodyYaw(); + @ModifyExpressionValue(method = "sendPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F")) + private float lovisual$eventSyncYaw(float original) { return lovisual$rotationHandler.resolveSendYaw((LocalPlayer) (Object) this, original, lovisual$networkHandler.getActiveSyncEvent()); } - EventSync event = lovisual$activeSyncEvent; - if (event != null && event.hasRotationOverride()) return event.getYaw(); + @ModifyExpressionValue(method = "sendPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F")) + private float lovisual$eventSyncPitch(float original) { return lovisual$rotationHandler.resolveSendPitch(original, lovisual$networkHandler.getActiveSyncEvent()); } - Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); - return rotation != null ? rotation.yaw() : original; - } + @ModifyExpressionValue(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F")) + private float lovisual$visualTickYaw(float original) { return lovisual$rotationHandler.resolveTickYaw((LocalPlayer) (Object) this, original); } - @ModifyExpressionValue( - method = "sendPosition", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F" - ) - ) - private float lovisual$eventSyncPitch(float original) { - EventSync event = lovisual$activeSyncEvent; - if (event != null && event.hasRotationOverride()) return Mth.clamp(event.getPitch(), -90.0f, 90.0f); + @ModifyExpressionValue(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F")) + private float lovisual$visualTickPitch(float original) { return lovisual$rotationHandler.resolveTickPitch(original); } - Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); - return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original; - } - - @ModifyExpressionValue( - method = "tick", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;getYRot()F" - ) - ) - private float lovisual$visualTickYaw(float original) { - lovisual$updateVisualBodyYaw(); - - Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); - return rotation != null ? rotation.yaw() : original; - } - - @ModifyExpressionValue( - method = "tick", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;getXRot()F" - ) - ) - private float lovisual$visualTickPitch(float original) { - Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); - return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original; - } - - @Inject( - method = "tick", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/player/LocalPlayer;sendPosition()V", - ordinal = 0, - shift = At.Shift.AFTER - ), - cancellable = true - ) - private void lovisual$postPlayerUpdate(CallbackInfo ci) { - if (!Events.BUS.hasListeners(PostPlayerUpdateEvent.class)) return; - if (lovisual$postUpdateLock) return; - - PostPlayerUpdateEvent event = new PostPlayerUpdateEvent(); - Events.BUS.post(event); - - if (!event.isCancelled()) return; - - ci.cancel(); - - int iterations = event.getIterations(); - if (iterations <= 0) return; - - LocalPlayer self = (LocalPlayer) (Object) this; - for (int i = 0; i < iterations; i++) { - lovisual$postUpdateLock = true; - self.tick(); - lovisual$postUpdateLock = false; - sendPosition(); - } - } - - @Inject(method = "sendPosition", at = @At("HEAD"), cancellable = true) - private void cancelPackets(CallbackInfo ci) { - // Freeze module removed; only Freecam may still freeze movement. - } + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;sendPosition()V", ordinal = 0, shift = At.Shift.AFTER), cancellable = true) + private void lovisual$postPlayerUpdate(CallbackInfo ci) { lovisual$networkHandler.onPostPlayerUpdate((LocalPlayer) (Object) this, ci, this::sendPosition); } @Inject(method = "aiStep", at = @At("HEAD"), cancellable = true) - private void lovisual$freezeTickMovement(CallbackInfo ci) { - LocalPlayer p = (LocalPlayer) (Object) this; - - if (!shouldFreezeNow()) { - if (frozen) { - p.setDeltaMovement(savedVelocity); - frozen = false; - } - return; - } - - if (!frozen) { - frozen = true; - savedX = p.getX(); - savedY = p.getY(); - savedZ = p.getZ(); - savedVelocity = p.getDeltaMovement(); - - if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) { - savedBodyYaw = p.yBodyRot; - savedHeadYaw = p.yHeadRot; - } - } - - p.setDeltaMovement(Vec3.ZERO); - p.absSnapTo(savedX, savedY, savedZ); - - if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) { - p.yBodyRot = savedBodyYaw; - p.yHeadRot = savedHeadYaw; - } - - ci.cancel(); - } - - @Inject(method = "aiStep", at = @At("HEAD")) - private void lovisual$noStunTickMovement(CallbackInfo ci) { - // NoStun module removed; no hurt-time input override remains. - } - - @Unique - private void lovisual$updateVisualBodyYaw() { - LocalPlayer self = (LocalPlayer) (Object) this; - Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); - if (rotation == null) { - lovisual$rotationPrevX = self.getX(); - lovisual$rotationPrevZ = self.getZ(); - lovisual$rotationPrevBodyYaw = self.getVisualRotationYInDegrees(); - return; - } - - float newBodyYaw = lovisual$simulateBodyYaw( - rotation.yaw(), - lovisual$rotationPrevBodyYaw, - lovisual$rotationPrevX, - lovisual$rotationPrevZ, - self.getX(), - self.getZ(), - self.attackAnim - ); - - self.setYBodyRot(newBodyYaw); - lovisual$rotationPrevBodyYaw = newBodyYaw; - lovisual$rotationPrevX = self.getX(); - lovisual$rotationPrevZ = self.getZ(); - } - - @Inject(method = "tick", at = @At("HEAD")) - private void detectPearlImpact(CallbackInfo ci) { - // NoPush module removed; no pearl-phase spoofing remains. - } - - @Inject(method = "sendPosition", at = @At("HEAD")) - private void sendSpoofAfterPearl(CallbackInfo ci) { - // NoPush module removed; no pearl-phase spoofing remains. - } - - @Inject(method = "aiStep", at = @At("TAIL")) - private void onCollideBoost(CallbackInfo ci) { - // Speed module removed; no entity-collision boost remains. - } - - @Inject(method = "aiStep", at = @At("TAIL")) - private void omniLongJump(CallbackInfo ci) { - // Speed module removed; no jump boost remains. - } - - @Inject(method = "modifyInput", at = @At("RETURN"), cancellable = true) - private void applyLegitSpeed(Vec2 input, CallbackInfoReturnable cir) { - // Speed module removed; no input speed multiplier remains. - } - - @Inject(method = "closeContainer", at = @At("HEAD"), cancellable = true) - private void lovisual$inventoryMove$deferCloseHandledScreen(CallbackInfo ci) { - // InventoryMove module removed; no container close deferral remains. - } - - @Inject(method = "closeContainer", at = @At("HEAD"), cancellable = true) - private void lovisual$keepInventoryOpen(CallbackInfo ci) { - // XCarry module removed; no inventory close suppression remains. - } + private void lovisual$freezeTickMovement(CallbackInfo ci) { lovisual$movementHandler.onFreezeTick((LocalPlayer) (Object) this, ci); } @Inject(method = "moveTowardsClosestSpace", at = @At("HEAD"), cancellable = true) - private void lovisual$noPushOutOfBlocks(double x, double z, CallbackInfo ci) { - if (!Events.BUS.hasListeners(EventPushOutOfBlocks.class)) return; - EventPushOutOfBlocks event = new EventPushOutOfBlocks(x, z); - Events.BUS.post(event); - if (event.isCancelled()) { - ci.cancel(); - } - } + private void lovisual$noPushOutOfBlocks(double x, double z, CallbackInfo ci) { lovisual$movementHandler.onPushOutOfBlocks(x, z, ci); } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java index 902f7ee..8df58e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java @@ -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.mixins.entity.player; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerInputHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerInputHandler.java new file mode 100644 index 0000000..c5b7a79 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerInputHandler.java @@ -0,0 +1,90 @@ +package dev.loki.lovisual.mixins.entity.player; + +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.events.impl.input.SprintControlEvent; +import dev.loki.lovisual.events.impl.movement.MovementInputEvent; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.mixins.accessors.misc.InputAccessor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec2; + +final class LocalPlayerInputHandler { + + boolean shouldBlockWASD() { + Freecam fc = Modules.get(Freecam.class); + return fc != null && fc.isEnabled() && fc.isCameraInput(); + } + + void handleFreecamAfterInputTick(LocalPlayer self) { + Freecam fc = Modules.get(Freecam.class); + if (fc == null || !fc.isEnabled() || !fc.isCameraInput()) return; + if (self.input == null) return; + ((InputAccessor) self.input).setMovementVector(Vec2.ZERO); + boolean sprint = self.input.keyPresses.sprint(); + self.input.keyPresses = new Input(false, false, false, false, false, false, sprint); + } + + void handleAfterInputTickSprint(LocalPlayer self) { + if (self.input == null) return; + Input original = self.input.keyPresses; + if (original == null) return; + Input input = original; + if (Events.BUS.hasListeners(MovementInputEvent.class)) { + MovementInputEvent event = new MovementInputEvent(input); + Events.BUS.post(event); + Input converted = event.toPlayerInput(); + if (!sameInput(input, converted)) input = converted; + } + Input transformed = transformDirection(input); + if (!sameInput(input, transformed)) input = transformed; + if (Events.BUS.hasListeners(SprintControlEvent.class)) { + float forward = impulse(input.forward(), input.backward()); + float sideways = impulse(input.left(), input.right()); + SprintControlEvent event = new SprintControlEvent(forward, sideways, input.sprint(), SprintControlEvent.Source.INPUT); + Events.BUS.post(event); + if (event.shouldSprint() != input.sprint()) { + input = new Input(input.forward(), input.backward(), input.left(), input.right(), input.jump(), input.shift(), event.shouldSprint()); + } + } + if (!sameInput(original, input)) applyPlayerInput(self, input); + } + + void applyPlayerInput(LocalPlayer self, Input input) { + if (self.input == null || input == null) return; + InputAccessor accessor = (InputAccessor) self.input; + accessor.setPlayerInput(input); + float forward = impulse(input.forward(), input.backward()); + float sideways = impulse(input.left(), input.right()); + accessor.setMovementVector(new Vec2(sideways, forward).normalized()); + } + + Input transformDirection(Input input) { return input; } + + boolean postSprintControl(LocalPlayer self, boolean original, SprintControlEvent.Source source) { + if (self.input == null) return original; + if (!Events.BUS.hasListeners(SprintControlEvent.class)) return original; + Vec2 movement = self.input.getMoveVector(); + SprintControlEvent event = new SprintControlEvent(movement.y, movement.x, original, source); + Events.BUS.post(event); + return event.shouldSprint(); + } + + boolean redirectShift(Input in, LocalPlayer self) { + Freecam fc = Modules.get(Freecam.class); + if (fc != null && fc.isEnabled() && fc.isCameraInput() && self.getAbilities().flying) return false; + return in.shift(); + } + + boolean sameInput(Input a, Input b) { + if (a == b) return true; + if (a == null || b == null) return false; + return a.forward() == b.forward() && a.backward() == b.backward() && a.left() == b.left() && a.right() == b.right() && a.jump() == b.jump() && a.shift() == b.shift() && a.sprint() == b.sprint(); + } + + float impulse(boolean positive, boolean negative) { + if (positive == negative) return 0.0f; + return positive ? 1.0f : -1.0f; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerMovementHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerMovementHandler.java new file mode 100644 index 0000000..57fa590 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerMovementHandler.java @@ -0,0 +1,73 @@ +package dev.loki.lovisual.mixins.entity.player; + +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.events.impl.world.EventPushOutOfBlocks; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.mixins.accessors.entity.EntityAccessor; +import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.PortalProcessor; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +final class LocalPlayerMovementHandler { + + private float lastHealth = Float.NaN; + private boolean frozen; + private Vec3 savedVelocity = Vec3.ZERO; + private double savedX, savedY, savedZ; + private float savedBodyYaw, savedHeadYaw; + + void onPortalNausea(LocalPlayer self, CallbackInfo ci) { + PortalProcessor portal = null; + if (self instanceof EntityAccessor accessor) portal = accessor.lovisual$getPortalManager(); + if (portal != null && portal.isInsidePortalThisTick()) { + if (self instanceof LocalPlayerAccessor acc) { + acc.lovisual$setNauseaIntensity(0.0f); + acc.lovisual$setLastNauseaIntensity(0.0f); + } + ci.cancel(); + } + } + + void onHurt(float newHealth) { + Freecam fc = Modules.get(Freecam.class); + if (fc == null || !fc.isEnabled() || !fc.disableOnDamage()) { lastHealth = newHealth; return; } + if (!Float.isNaN(lastHealth) && newHealth < lastHealth) fc.toggle(); + lastHealth = newHealth; + } + + void onPushOutOfBlocks(double x, double z, CallbackInfo ci) { + if (!Events.BUS.hasListeners(EventPushOutOfBlocks.class)) return; + EventPushOutOfBlocks event = new EventPushOutOfBlocks(x, z); + Events.BUS.post(event); + if (event.isCancelled()) ci.cancel(); + } + + boolean shouldFreezeNow() { + Freecam freecam = Modules.get(Freecam.class); + return freecam != null && freecam.freezePlayer() && freecam.isCameraInput(); + } + + void onFreezeTick(LocalPlayer self, CallbackInfo ci) { + if (!shouldFreezeNow()) { + if (frozen) { self.setDeltaMovement(savedVelocity); frozen = false; } + return; + } + if (!frozen) { + frozen = true; + savedX = self.getX(); savedY = self.getY(); savedZ = self.getZ(); + savedVelocity = self.getDeltaMovement(); + if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) { + savedBodyYaw = self.yBodyRot; savedHeadYaw = self.yHeadRot; + } + } + self.setDeltaMovement(Vec3.ZERO); + self.absSnapTo(savedX, savedY, savedZ); + if (Modules.get(Freecam.class) == null || !Modules.get(Freecam.class).isEnabled()) { + self.yBodyRot = savedBodyYaw; self.yHeadRot = savedHeadYaw; + } + ci.cancel(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerNetworkHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerNetworkHandler.java new file mode 100644 index 0000000..9a7bd0a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerNetworkHandler.java @@ -0,0 +1,63 @@ +package dev.loki.lovisual.mixins.entity.player; + +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.events.impl.network.EventPostSync; +import dev.loki.lovisual.events.impl.network.EventSync; +import dev.loki.lovisual.events.impl.player.PostPlayerUpdateEvent; +import dev.loki.lovisual.util.combat.rotation.SprintController; +import net.minecraft.client.player.LocalPlayer; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +final class LocalPlayerNetworkHandler { + + private Runnable eventPostAction; + private EventSync activeSyncEvent; + private boolean postUpdateLock; + + boolean onSendPositionHead(LocalPlayer self, CallbackInfo ci) { + if (!Events.BUS.hasListeners(EventSync.class)) return false; + eventPostAction = null; + EventSync event = new EventSync(self.getYRot(), self.getXRot()); + Events.BUS.post(event); + activeSyncEvent = event; + eventPostAction = event.getPostAction(); + if (event.isCancelled()) { + activeSyncEvent = null; + eventPostAction = null; + ci.cancel(); + return true; + } + return false; + } + + void onSendPositionReturn() { + if (Events.BUS.hasListeners(EventPostSync.class)) Events.BUS.post(new EventPostSync()); + if (eventPostAction != null) { eventPostAction.run(); eventPostAction = null; } + activeSyncEvent = null; + } + + EventSync getActiveSyncEvent() { return activeSyncEvent; } + + boolean onPostPlayerUpdate(LocalPlayer self, CallbackInfo ci, Runnable sendPosition) { + if (!Events.BUS.hasListeners(PostPlayerUpdateEvent.class)) return false; + if (postUpdateLock) return false; + PostPlayerUpdateEvent event = new PostPlayerUpdateEvent(); + Events.BUS.post(event); + if (!event.isCancelled()) return false; + ci.cancel(); + int iterations = event.getIterations(); + if (iterations <= 0) return true; + for (int i = 0; i < iterations; i++) { + postUpdateLock = true; + self.tick(); + postUpdateLock = false; + sendPosition.run(); + } + return true; + } + + boolean shouldBlockSprintNetwork(boolean original) { + if (SprintController.INSTANCE.isSprintBlocked()) return false; + return original; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerRotationHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerRotationHandler.java new file mode 100644 index 0000000..48590c8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/handlers/LocalPlayerRotationHandler.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.mixins.entity.player; + +import dev.loki.lovisual.events.impl.network.EventSync; +import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; +import dev.loki.lovisual.util.aiming.data.Rotation; +import dev.loki.lovisual.util.aiming.rotation.RotationManager; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; + +final class LocalPlayerRotationHandler { + + private double rotationPrevX; + private double rotationPrevZ; + private float rotationPrevBodyYaw; + private boolean rotationBodyYawInitialized; + + void onTickCache(LocalPlayer self) { + if (self instanceof ILocalPlayer access) { + access.lovisual$setLastYaw(self.getYRot()); + access.lovisual$setLastPitch(self.getXRot()); + } + RotationManager.INSTANCE.runTickLifecycle(); + if (!rotationBodyYawInitialized) { + rotationPrevX = self.getX(); + rotationPrevZ = self.getZ(); + rotationPrevBodyYaw = self.getVisualRotationYInDegrees(); + rotationBodyYawInitialized = true; + } + } + + float resolveSendYaw(LocalPlayer self, float original, EventSync active) { + updateVisualBodyYaw(self); + if (active != null && active.hasRotationOverride()) return active.getYaw(); + Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); + return rotation != null ? rotation.yaw() : original; + } + + float resolveSendPitch(float original, EventSync active) { + if (active != null && active.hasRotationOverride()) return Mth.clamp(active.getPitch(), -90.0f, 90.0f); + Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); + return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original; + } + + float resolveTickYaw(LocalPlayer self, float original) { + updateVisualBodyYaw(self); + Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); + return rotation != null ? rotation.yaw() : original; + } + + float resolveTickPitch(float original) { + Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); + return rotation != null ? Mth.clamp(rotation.pitch(), -90.0f, 90.0f) : original; + } + + void updateVisualBodyYaw(LocalPlayer self) { + Rotation rotation = RotationManager.INSTANCE.getCurrentRotation(); + if (rotation == null) { + rotationPrevX = self.getX(); + rotationPrevZ = self.getZ(); + rotationPrevBodyYaw = self.getVisualRotationYInDegrees(); + return; + } + float newBodyYaw = simulateBodyYaw(rotation.yaw(), rotationPrevBodyYaw, rotationPrevX, rotationPrevZ, self.getX(), self.getZ(), self.attackAnim); + self.setYBodyRot(newBodyYaw); + rotationPrevBodyYaw = newBodyYaw; + rotationPrevX = self.getX(); + rotationPrevZ = self.getZ(); + } + + float simulateBodyYaw(float yaw, float prevBodyYaw, double prevX, double prevZ, double currentX, double currentZ, float handSwing) { + double motionX = currentX - prevX; + double motionZ = currentZ - prevZ; + float motionSquared = (float) (motionX * motionX + motionZ * motionZ); + float bodyYaw = prevBodyYaw; + if (motionSquared > 0.0025000002F) { + float movementYaw = (float) Mth.atan2(motionZ, motionX) * Mth.RAD_TO_DEG - 90.0F; + float yawDiff = Math.abs(Mth.wrapDegrees(yaw) - movementYaw); + bodyYaw = (yawDiff > 95.0F && yawDiff < 265.0F) ? movementYaw - 180.0F : movementYaw; + } + if (handSwing - 0.2F > 0.0F) bodyYaw = yaw; + float deltaYaw = Mth.wrapDegrees(bodyYaw - prevBodyYaw); + bodyYaw = prevBodyYaw + deltaYaw * 0.3F; + float yawOffsetDiff = Mth.wrapDegrees(yaw - bodyYaw); + if (Math.abs(yawOffsetDiff) > 52.0F) bodyYaw += yawOffsetDiff - Math.signum(yawOffsetDiff) * 52.0F; + return bodyYaw; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java index 5243ae1..5835daa 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java @@ -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.mixins.entity.world; import com.llamalad7.mixinextras.injector.ModifyReturnValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java index 068cdef..677b40a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java @@ -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.mixins.entity.world; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java index d25d8fa..caae5d1 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java @@ -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.mixins.entity.world; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java index 79a0466..dc75f69 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java @@ -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.mixins.gui.chat; import net.minecraft.client.gui.Font; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java index 1846bb7..0ca1c89 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java @@ -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.mixins.gui.chat; import net.minecraft.client.gui.ActiveTextCollector; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java index 1ad6b0e..62019bd 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java @@ -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.mixins.gui.chat; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java index e833a39..cdce310 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java @@ -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.mixins.gui.core; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java index 738e442..2977a88 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java @@ -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.mixins.gui.core; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java index 6408d94..1a0d651 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java @@ -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.mixins.gui.core; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java index 51484fb..6f1f67a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java @@ -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.mixins.gui.core; import net.minecraft.client.gui.components.AbstractWidget; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java index 461c867..123475e 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java @@ -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.mixins.gui.multiplayer; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java index 1f75deb..625fac4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java @@ -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.mixins.gui.multiplayer; import net.minecraft.client.gui.screens.inventory.AbstractRecipeBookScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java index 9863c31..a032e3a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java @@ -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.mixins.gui.screens; import com.llamalad7.mixinextras.sugar.Local; @@ -25,6 +18,8 @@ import dev.loki.lovisual.features.gui.clickgui.picker.screen.ClickGuiPickerScree import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; import dev.loki.lovisual.features.gui.clickgui.screen.ClickGuiScreen; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.jump.CustomJumpBarRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.locator.panel.CustomLocatorRenderer; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.dynamicisland.DynamicIsland; import dev.loki.lovisual.features.module.modules.misc.ClickGui; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; @@ -43,12 +38,12 @@ public abstract class GuiScreenMixin { CustomBar bar = CustomBar.get(); if (bar == null || !bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return; - if (CustomBar.shouldUseJumpBar(mc) || !CustomBar.shouldUseLocator(mc, bar)) return; + if (CustomJumpBarRenderer.shouldUseJumpBar(mc) || !CustomBar.shouldUseLocator(mc, bar)) return; LoVisualRenderSystem.ensureFrameContext(); try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.SCREEN_TOP, "2d:hud:custom_bar_heads")) { Renderer2D.withDeferredLayer(Renderer2D.Deferred2DLayer.AFTER_VANILLA_GUI, () -> { - CustomBar.renderLocatorHeads(drawContext, tickCounter, mc); + CustomLocatorRenderer.renderLocatorHeads(drawContext, tickCounter, mc); Renderer2D.COLOR.render(); }); } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java index c71e1c1..4ed352b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java @@ -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.mixins.gui.screens; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java index 80ad627..462d607 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java @@ -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.mixins.gui.screens; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java index 0fd0eb8..8418196 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java @@ -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.mixins.gui.screens; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java index 53adcf1..99b6b4f 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java @@ -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.mixins.gui.widgets; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java index e24a4d4..b8a8e7c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java @@ -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.mixins.gui.widgets; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java index 6741612..527f1ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java @@ -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.mixins.gui.widgets; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java index fe0adaa..b1280e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java @@ -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.mixins.gui.widgets; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java index 1188e21..d804865 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java @@ -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.mixins.hud; import net.minecraft.client.gui.components.DebugScreenOverlay; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java index 615a22c..d717826 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java @@ -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.mixins.hud; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java index 6e38ea4..a45daae 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java @@ -1,67 +1,37 @@ -/* - * 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.mixins.hud; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.Hud; -import net.minecraft.client.player.LocalPlayer; import net.minecraft.core.Holder; import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.attributes.Attribute; -import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.scores.Objective; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import dev.loki.lovisual.events.Events; -import dev.loki.lovisual.events.impl.pvp.PvpOverlayEvent; -import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.Scoreboard; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.draggable.impl.status.Potions; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.SwapTooltip; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomHealthBar; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar.CustomHotbar; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.misc.protect.MessageFilter; -import dev.loki.lovisual.features.module.modules.visuals.player.Crosshair; -import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; -import dev.loki.lovisual.mixininterface.render.IGuiGraphics; -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.debug.RenderThread2DDebugRenderer; // disabled: debug probes are manual-only -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.util.item.food.IllegalItemUtil; -import dev.loki.lovisual.util.item.rarity.RarityColorUtil; -import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; -import dev.loki.lovisual.util.logging.DebugLog; -import dev.loki.lovisual.util.text.format.TextJsonUtil; +/** + * Hooks into vanilla {@link Hud} state extraction. All bodies live in the {@code handlers} package: + * overlay toggles ({@link GuiOverlayHandler}), custom bar/hotbar/health rendering + * ({@link GuiHudBarHandler}), heart-layout redirects ({@link GuiHealthLayoutHandler}) and held-item + * tooltip recoloring ({@link GuiTooltipHandler}). + */ @Mixin(Hud.class) public abstract class GuiMixin { @Shadow private int toolHighlightTimer; @Shadow - private net.minecraft.world.item.ItemStack lastToolHighlight; + private ItemStack lastToolHighlight; @Shadow @Final private Minecraft minecraft; @@ -71,359 +41,74 @@ public abstract class GuiMixin { // RenderThread2DDebugRenderer.extractGuiStateProbe(ctx); } - @Unique - private static void lovisual$renderCustomBarOnly(GuiGraphicsExtractor ctx, - DeltaTracker tickCounter, - Minecraft mc, - CustomBar bar) { - boolean useJump = CustomBar.shouldUseJumpBar(mc); - boolean useLocator = !useJump && CustomBar.shouldUseLocator(mc, bar); - boolean drawXp = !useJump && !useLocator; - - LoVisualRenderSystem.ensureFrameContext(); - try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_bar")) { - Renderer2D.withDeferredLayer(LOVISUAL$VANILLA_HUD_REPLACEMENT_LAYER, () -> { - ViewportContext.beginUnscaled(ctx); - Renderer2D.COLOR.begin(); - if (drawXp) { - CustomBar.renderXpBarBackground(Renderer2D.COLOR, mc); - } - if (useJump) { - CustomBar.renderJumpBarBackground(Renderer2D.COLOR, mc); - } - if (useLocator) { - CustomBar.renderLocatorBackground(Renderer2D.COLOR, mc); - } - Renderer2D.COLOR.render(); - ViewportContext.end(ctx); - - if (drawXp) { - CustomBar.renderXpBarText(ctx, mc); - } - }); - lovisual$appendVanillaHudReplacementMarker(ctx); - } - } - - @Unique - private static final Renderer2D.Deferred2DLayer LOVISUAL$VANILLA_HUD_REPLACEMENT_LAYER = - Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS; - - @Unique - private static void lovisual$appendVanillaHudReplacementMarker(GuiGraphicsExtractor ctx) { - if (!(ctx instanceof IGuiGraphics graphics)) return; - ctx.nextStratum(); - graphics.lovisual$addDeferredHudMarker(LOVISUAL$VANILLA_HUD_REPLACEMENT_LAYER); - ctx.nextStratum(); - } - - @Unique - private static int argbFrom(float[] rgba) { - if (rgba == null || rgba.length < 3) return 0xFFFFFFFF; - float r = rgba[0], g = rgba[1], b = rgba[2]; - float a = rgba.length > 3 ? rgba[3] : 1f; - int ai = (int) (a * 255) & 0xFF; - int ri = (int) (r * 255) & 0xFF; - int gi = (int) (g * 255) & 0xFF; - int bi = (int) (b * 255) & 0xFF; - return (ai << 24) | (ri << 16) | (gi << 8) | bi; - } - - @Inject( - method = "displayScoreboardSidebar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/scores/Objective;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "displayScoreboardSidebar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/scores/Objective;)V", at = @At("HEAD"), cancellable = true) private void lovisual$hideScoreboardSidebar(GuiGraphicsExtractor context, Objective objective, CallbackInfo ci) { - - NoRender noRender = Modules.get(NoRender.class); - if ((noRender != null && noRender.hideScoreboard()) || Scoreboard.shouldReplaceVanilla()) { - ci.cancel(); - } + GuiOverlayHandler.hideScoreboardSidebar(ci); } - @Inject( - method = "setOverlayMessage(Lnet/minecraft/network/chat/Component;Z)V", - at = @At("HEAD"), - cancellable = true - ) - private void lovisual$filterOverlayMessage(net.minecraft.network.chat.Component message, boolean tinted, CallbackInfo ci) { - if (DebugLog.serverOnly()) { - if (message == null) { - // DebugLog.server("HUD overlay message: tinted=%s raw=null", tinted); - } else { - String raw = message.getString(); - String json = TextJsonUtil.toJson(message); - // DebugLog.server("HUD overlay message: tinted=%s raw=\"%s\" json=%s", tinted, raw, json); - } - } - if (Events.BUS.hasListeners(PvpOverlayEvent.class)) { - Events.BUS.post(new PvpOverlayEvent(message, tinted)); - } - - MessageFilter messageFilter = Modules.get(MessageFilter.class); - if (messageFilter != null && messageFilter.shouldHideHud(message)) { - ci.cancel(); - } + @Inject(method = "setOverlayMessage(Lnet/minecraft/network/chat/Component;Z)V", at = @At("HEAD"), cancellable = true) + private void lovisual$filterOverlayMessage(Component message, boolean tinted, CallbackInfo ci) { + GuiOverlayHandler.filterOverlayMessage(message, tinted, ci); } - @Inject( - method = "extractEffects(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractEffects(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("HEAD"), cancellable = true) private void lovisual$hideVanillaPotions(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) { - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null) return; - if (!DraggableHudElementRegistry.isEnabled(Potions.class)) return; - if (mc.player.getActiveEffects().isEmpty()) return; - ci.cancel(); + GuiOverlayHandler.hideVanillaPotions(ci); } - @Inject( - method = "extractCrosshair(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractCrosshair(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("HEAD"), cancellable = true) private void lovisual$replaceCrosshair(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) { - - Crosshair crosshair = Modules.get(Crosshair.class); - if (crosshair != null && crosshair.shouldHideVanilla()) { - ci.cancel(); - } + GuiOverlayHandler.replaceCrosshair(ci); } - @Inject( - method = "extractVignette(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/Entity;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractVignette(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD"), cancellable = true) private void lovisual$killVignette(GuiGraphicsExtractor context, Entity entity, CallbackInfo ci) { - - // Completely remove vanilla vignette post-processing - ci.cancel(); + GuiOverlayHandler.killVignette(ci); } - @Inject( - method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("HEAD"), cancellable = true) private void lovisual$customHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) { - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null) return; - - CustomHotbar hotbar = CustomHotbar.get(); - CustomBar bar = CustomBar.get(); - if (hotbar == null && bar == null) return; - - boolean hotbarEnabled = hotbar != null && hotbar.isHudHotbarEnabled(); - boolean barEnabled = bar != null && bar.isHudBarEnabled() && bar.isHotbarXpBarEnabled(); - if (!hotbarEnabled && !barEnabled) return; - if (!hotbarEnabled) return; - - boolean useJump = barEnabled && CustomBar.shouldUseJumpBar(mc); - boolean useLocator = barEnabled && !useJump && CustomBar.shouldUseLocator(mc, bar); - boolean drawXp = barEnabled && !useJump && !useLocator; - - LoVisualRenderSystem.ensureFrameContext(); - try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_hotbar")) { - Renderer2D.withDeferredLayer(LOVISUAL$VANILLA_HUD_REPLACEMENT_LAYER, () -> { - ViewportContext.beginUnscaled(ctx); - Renderer2D.COLOR.begin(); - if (hotbarEnabled) { - CustomHotbar.renderBackground(Renderer2D.COLOR, mc); - } - if (drawXp) { - CustomBar.renderXpBarBackground(Renderer2D.COLOR, mc); - } - if (barEnabled && useJump) { - CustomBar.renderJumpBarBackground(Renderer2D.COLOR, mc); - } - if (barEnabled && useLocator) { - CustomBar.renderLocatorBackground(Renderer2D.COLOR, mc); - } - Renderer2D.COLOR.render(); - ViewportContext.end(ctx); - - if (hotbarEnabled) { - CustomHotbar.renderItems(ctx, tickCounter, mc); - } - }); - lovisual$appendVanillaHudReplacementMarker(ctx); - - if (drawXp) { - CustomBar.renderXpBarText(ctx, mc); - } - if (hotbarEnabled) { - ci.cancel(); - } - } + GuiHudBarHandler.renderHotbar(ctx, tickCounter, ci); } - @Inject( - method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", - at = @At("TAIL") - ) + @Inject(method = "extractItemHotbar(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("TAIL")) private void lovisual$customBarAfterVanillaHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) { - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null) return; - - CustomHotbar hotbar = CustomHotbar.get(); - if (hotbar != null && hotbar.isHudHotbarEnabled()) return; - - CustomBar bar = CustomBar.get(); - if (bar == null || !bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return; - - lovisual$renderCustomBarOnly(ctx, tickCounter, mc, bar); + GuiHudBarHandler.afterVanillaHotbar(ctx, tickCounter); } - @Inject( - method = "extractPortalOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;F)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractPortalOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;F)V", at = @At("HEAD"), cancellable = true) private void lovisual$disableVanillaPortalOverlay(GuiGraphicsExtractor context, float nauseaStrength, CallbackInfo ci) { - - ci.cancel(); + GuiOverlayHandler.disablePortalOverlay(ci); } - @Inject( - method = "extractSleepOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractSleepOverlay(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/client/DeltaTracker;)V", at = @At("HEAD"), cancellable = true) private void lovisual$disableVanillaSleepOverlay(GuiGraphicsExtractor context, DeltaTracker tickCounter, CallbackInfo ci) { - - ci.cancel(); + GuiOverlayHandler.disableSleepOverlay(ci); } - @Inject( - method = "extractHearts(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/player/Player;IIIIFIIIZ)V", - at = @At("HEAD"), - cancellable = true - ) + @Inject(method = "extractHearts(Lnet/minecraft/client/gui/GuiGraphicsExtractor;Lnet/minecraft/world/entity/player/Player;IIIIFIIIZ)V", at = @At("HEAD"), cancellable = true) private void lovisual$renderHealthBar(GuiGraphicsExtractor context, - Player player, - int x, int y, int lines, - int regeneratingHeartIndex, float maxHealth, - int lastHealth, int health, - int absorption, boolean blinking, - CallbackInfo ci) { - if (!Lifecycle.canRunHud()) return; - if (!(player instanceof LocalPlayer clientPlayer)) return; - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return; - - if (!CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, maxHealth)) return; - - LoVisualRenderSystem.ensureFrameContext(); - try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_health")) { - Renderer2D.withDeferredLayer(LOVISUAL$VANILLA_HUD_REPLACEMENT_LAYER, () -> { - ViewportContext.beginUnscaled(context); - Renderer2D.COLOR.begin(); - CustomHealthBar.HealthTextInfo textInfo = - CustomHealthBar.renderHealthBarBackground( - Renderer2D.COLOR, - mc, - clientPlayer, - x, - y, - maxHealth - ); - CustomHealthBar.renderHealthBarText(textInfo); - Renderer2D.COLOR.render(); - ViewportContext.end(context); - }); - lovisual$appendVanillaHudReplacementMarker(context); - ci.cancel(); - } + Player player, + int x, int y, int lines, + int regeneratingHeartIndex, float maxHealth, + int lastHealth, int health, + int absorption, boolean blinking, + CallbackInfo ci) { + GuiHudBarHandler.renderHealth(context, player, x, y, maxHealth, ci); } @Inject(method = "extractSelectedItemName", at = @At("HEAD"), cancellable = true) private void lovisual$colorizeHeldItemTooltip(GuiGraphicsExtractor context, CallbackInfo ci) { - if (!Lifecycle.canRunHud()) return; - SwapTooltip swapTooltip = SwapTooltip.get(); - if (swapTooltip == null || !swapTooltip.isEnabled()) { - return; - } - if (this.toolHighlightTimer <= 0 || this.lastToolHighlight == null || this.lastToolHighlight.isEmpty()) { - SwapTooltip.clear(); - return; - } - - MutableComponent name = Component.empty().append(this.lastToolHighlight.getHoverName()); - if (this.lastToolHighlight.has(net.minecraft.core.component.DataComponents.CUSTOM_NAME)) { - name.withStyle(net.minecraft.ChatFormatting.ITALIC); - } - - int illegalColor = IllegalItemUtil.illegalColor(); - int topColor = TopEnchantUtil.topColor(); - int rarityColor = argbFrom(RarityColorUtil.INSTANCE.getRarityColor(this.lastToolHighlight)); - boolean hasIllegal = IllegalItemUtil.isIllegal(this.lastToolHighlight); - boolean hasTop = !hasIllegal && TopEnchantUtil.hasTopEnchant(this.lastToolHighlight); - int nameColor = hasIllegal ? illegalColor : (hasTop ? topColor : rarityColor); - int alpha = (int) ((float) this.toolHighlightTimer * 256.0F / 10.0F); - if (alpha > 255) alpha = 255; - if (alpha <= 0) { - SwapTooltip.clear(); - return; - } - float fade = alpha / 255f; - int argb = 0xFF000000 | (nameColor & 0x00FFFFFF); - SwapTooltip.capture( - name.getString(), - argb, - fade, - this.minecraft.gameMode.canHurtPlayer() - ); - ci.cancel(); + GuiTooltipHandler.colorizeHeldItem(this.toolHighlightTimer, this.lastToolHighlight, this.minecraft, ci); } - @Redirect( - method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/Player;getAbsorptionAmount()F" - ) - ) + @Redirect(method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;getAbsorptionAmount()F")) private float lovisual$ignoreAbsorptionForLayout(Player player) { - if (!Lifecycle.canRunHud()) { - return player.getAbsorptionAmount(); - } - Minecraft mc = Minecraft.getInstance(); - if (mc != null && player instanceof LocalPlayer clientPlayer) { - float actualMaxHealth = (float) player.getAttributeValue(Attributes.MAX_HEALTH); - if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) { - return 0.0f; - } - } - return player.getAbsorptionAmount(); + return GuiHealthLayoutHandler.ignoreAbsorptionForLayout(player); } - @Redirect( - method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/world/entity/player/Player;getAttributeValue(Lnet/minecraft/core/Holder;)D" - ) - ) + @Redirect(method = "extractPlayerHealth(Lnet/minecraft/client/gui/GuiGraphicsExtractor;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;getAttributeValue(Lnet/minecraft/core/Holder;)D")) private double lovisual$clampMaxHealthForLayout(Player player, Holder attribute) { - if (!Lifecycle.canRunHud()) { - return player.getAttributes().getValue(attribute); - } - if (attribute == Attributes.MAX_HEALTH) { - Minecraft mc = Minecraft.getInstance(); - if (mc != null && player instanceof LocalPlayer clientPlayer) { - float actualMaxHealth = (float) player.getAttributes().getValue(attribute); - if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) { - return 20.0; - } - } - } - return player.getAttributes().getValue(attribute); + return GuiHealthLayoutHandler.clampMaxHealthForLayout(player, attribute); } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java index 5689c4b..dd02682 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java @@ -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.mixins.hud; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java index 5b742cc..3d37c24 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java @@ -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.mixins.hud.accessibility; import net.minecraft.client.GameNarrator; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java index 4a2f9b7..5533d3d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java @@ -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.mixins.hud.accessibility; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java index 36d2c45..9b23bf9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java @@ -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.mixins.hud.bars; import dev.loki.lovisual.events.Events; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java index 58950f8..6ddbadb 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java @@ -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.mixins.hud.bars; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java index 6a6ae91..8c437a6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java @@ -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.mixins.hud.bars; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java index 6166eb9..ee64eab 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java @@ -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.mixins.hud.bars; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHealthLayoutHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHealthLayoutHandler.java new file mode 100644 index 0000000..f94dc35 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHealthLayoutHandler.java @@ -0,0 +1,50 @@ +package dev.loki.lovisual.mixins.hud; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomHealthBar; + +/** + * Vanilla heart-layout redirects for {@link GuiMixin}. When the custom health bar takes over, the + * vanilla {@code extractPlayerHealth} pass is fed neutral values (absorption 0, max health 20) so it + * reserves the correct space without drawing hearts we replace ourselves. + */ +final class GuiHealthLayoutHandler { + private GuiHealthLayoutHandler() { + } + + static float ignoreAbsorptionForLayout(Player player) { + if (!Lifecycle.canRunHud()) { + return player.getAbsorptionAmount(); + } + Minecraft mc = Minecraft.getInstance(); + if (mc != null && player instanceof LocalPlayer clientPlayer) { + float actualMaxHealth = (float) player.getAttributeValue(Attributes.MAX_HEALTH); + if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) { + return 0.0f; + } + } + return player.getAbsorptionAmount(); + } + + static double clampMaxHealthForLayout(Player player, Holder attribute) { + if (!Lifecycle.canRunHud()) { + return player.getAttributes().getValue(attribute); + } + if (attribute == Attributes.MAX_HEALTH) { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && player instanceof LocalPlayer clientPlayer) { + float actualMaxHealth = (float) player.getAttributes().getValue(attribute); + if (CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, actualMaxHealth)) { + return 20.0; + } + } + } + return player.getAttributes().getValue(attribute); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHudBarHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHudBarHandler.java new file mode 100644 index 0000000..ec40efd --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiHudBarHandler.java @@ -0,0 +1,170 @@ +package dev.loki.lovisual.mixins.hud; + +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomBar; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomHealthBar; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.CustomXpBarRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.bars.jump.CustomJumpBarRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.locator.panel.CustomLocatorRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.hotbar.CustomHotbar; +import dev.loki.lovisual.mixininterface.render.IGuiGraphics; +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.renderer.Renderer2D; + +/** + * Custom HUD replacement rendering for {@link GuiMixin}: hotbar, XP/jump/locator bars and the + * health bar. All passes are emitted through a deferred {@link Renderer2D} layer so they land after + * vanilla misc overlays, then drop a stratum marker via {@link IGuiGraphics}. + */ +final class GuiHudBarHandler { + private GuiHudBarHandler() { + } + + static final Renderer2D.Deferred2DLayer VANILLA_HUD_REPLACEMENT_LAYER = + Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS; + + static void renderHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter, CallbackInfo ci) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) return; + + CustomHotbar hotbar = CustomHotbar.get(); + CustomBar bar = CustomBar.get(); + if (hotbar == null && bar == null) return; + + boolean hotbarEnabled = hotbar != null && hotbar.isHudHotbarEnabled(); + boolean barEnabled = bar != null && bar.isHudBarEnabled() && bar.isHotbarXpBarEnabled(); + if (!hotbarEnabled && !barEnabled) return; + if (!hotbarEnabled) return; + + boolean useJump = barEnabled && CustomJumpBarRenderer.shouldUseJumpBar(mc); + boolean useLocator = barEnabled && !useJump && CustomBar.shouldUseLocator(mc, bar); + boolean drawXp = barEnabled && !useJump && !useLocator; + + LoVisualRenderSystem.ensureFrameContext(); + try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_hotbar")) { + Renderer2D.withDeferredLayer(VANILLA_HUD_REPLACEMENT_LAYER, () -> { + ViewportContext.beginUnscaled(ctx); + Renderer2D.COLOR.begin(); + if (hotbarEnabled) { + CustomHotbar.renderBackground(Renderer2D.COLOR, mc); + } + if (drawXp) { + CustomXpBarRenderer.renderXpBarBackground(Renderer2D.COLOR, mc); + } + if (barEnabled && useJump) { + CustomJumpBarRenderer.renderJumpBarBackground(Renderer2D.COLOR, mc); + } + if (barEnabled && useLocator) { + CustomLocatorRenderer.renderLocatorBackground(Renderer2D.COLOR, mc); + } + Renderer2D.COLOR.render(); + ViewportContext.end(ctx); + + if (hotbarEnabled) { + CustomHotbar.renderItems(ctx, tickCounter, mc); + } + }); + appendVanillaHudReplacementMarker(ctx); + + if (drawXp) { + CustomXpBarRenderer.renderXpBarText(ctx, mc); + } + if (hotbarEnabled) { + ci.cancel(); + } + } + } + + static void afterVanillaHotbar(GuiGraphicsExtractor ctx, DeltaTracker tickCounter) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) return; + + CustomHotbar hotbar = CustomHotbar.get(); + if (hotbar != null && hotbar.isHudHotbarEnabled()) return; + + CustomBar bar = CustomBar.get(); + if (bar == null || !bar.isHudBarEnabled() || !bar.isHotbarXpBarEnabled()) return; + + renderCustomBarOnly(ctx, tickCounter, mc, bar); + } + + static void renderHealth(GuiGraphicsExtractor context, Player player, int x, int y, float maxHealth, CallbackInfo ci) { + if (!Lifecycle.canRunHud()) return; + if (!(player instanceof LocalPlayer clientPlayer)) return; + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return; + + if (!CustomHealthBar.shouldRenderHealthBar(mc, clientPlayer, maxHealth)) return; + + LoVisualRenderSystem.ensureFrameContext(); + try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_health")) { + Renderer2D.withDeferredLayer(VANILLA_HUD_REPLACEMENT_LAYER, () -> { + ViewportContext.beginUnscaled(context); + Renderer2D.COLOR.begin(); + CustomHealthBar.HealthTextInfo textInfo = + CustomHealthBar.renderHealthBarBackground( + Renderer2D.COLOR, + mc, + clientPlayer, + x, + y, + maxHealth + ); + CustomHealthBar.renderHealthBarText(textInfo); + Renderer2D.COLOR.render(); + ViewportContext.end(context); + }); + appendVanillaHudReplacementMarker(context); + ci.cancel(); + } + } + + private static void renderCustomBarOnly(GuiGraphicsExtractor ctx, + DeltaTracker tickCounter, + Minecraft mc, + CustomBar bar) { + boolean useJump = CustomJumpBarRenderer.shouldUseJumpBar(mc); + boolean useLocator = !useJump && CustomBar.shouldUseLocator(mc, bar); + boolean drawXp = !useJump && !useLocator; + + LoVisualRenderSystem.ensureFrameContext(); + try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.HUD_MAIN, "2d:hud:custom_bar")) { + Renderer2D.withDeferredLayer(VANILLA_HUD_REPLACEMENT_LAYER, () -> { + ViewportContext.beginUnscaled(ctx); + Renderer2D.COLOR.begin(); + if (drawXp) { + CustomXpBarRenderer.renderXpBarBackground(Renderer2D.COLOR, mc); + } + if (useJump) { + CustomJumpBarRenderer.renderJumpBarBackground(Renderer2D.COLOR, mc); + } + if (useLocator) { + CustomLocatorRenderer.renderLocatorBackground(Renderer2D.COLOR, mc); + } + Renderer2D.COLOR.render(); + ViewportContext.end(ctx); + + if (drawXp) { + CustomXpBarRenderer.renderXpBarText(ctx, mc); + } + }); + appendVanillaHudReplacementMarker(ctx); + } + } + + private static void appendVanillaHudReplacementMarker(GuiGraphicsExtractor ctx) { + if (!(ctx instanceof IGuiGraphics graphics)) return; + ctx.nextStratum(); + graphics.lovisual$addDeferredHudMarker(VANILLA_HUD_REPLACEMENT_LAYER); + ctx.nextStratum(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiOverlayHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiOverlayHandler.java new file mode 100644 index 0000000..854f935 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiOverlayHandler.java @@ -0,0 +1,81 @@ +package dev.loki.lovisual.mixins.hud; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.events.impl.pvp.PvpOverlayEvent; +import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.Scoreboard; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.status.Potions; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.misc.protect.MessageFilter; +import dev.loki.lovisual.features.module.modules.visuals.player.Crosshair; +import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.text.format.TextJsonUtil; + +/** + * Vanilla HUD element hide/replace toggles for {@link GuiMixin}: scoreboard, overlay messages, + * potion effects, crosshair, vignette and the portal/sleep overlays. Each entry point cancels the + * vanilla {@code extract*}/{@code display*} call when a module or custom HUD takes over. + */ +final class GuiOverlayHandler { + private GuiOverlayHandler() { + } + + static void hideScoreboardSidebar(CallbackInfo ci) { + NoRender noRender = Modules.get(NoRender.class); + if ((noRender != null && noRender.hideScoreboard()) || Scoreboard.shouldReplaceVanilla()) { + ci.cancel(); + } + } + + static void filterOverlayMessage(Component message, boolean tinted, CallbackInfo ci) { + if (DebugLog.serverOnly()) { + if (message == null) { + // DebugLog.server("HUD overlay message: tinted=%s raw=null", tinted); + } else { + String raw = message.getString(); + String json = TextJsonUtil.toJson(message); + // DebugLog.server("HUD overlay message: tinted=%s raw=\"%s\" json=%s", tinted, raw, json); + } + } + if (Events.BUS.hasListeners(PvpOverlayEvent.class)) { + Events.BUS.post(new PvpOverlayEvent(message, tinted)); + } + + MessageFilter messageFilter = Modules.get(MessageFilter.class); + if (messageFilter != null && messageFilter.shouldHideHud(message)) { + ci.cancel(); + } + } + + static void hideVanillaPotions(CallbackInfo ci) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) return; + if (!DraggableHudElementRegistry.isEnabled(Potions.class)) return; + if (mc.player.getActiveEffects().isEmpty()) return; + ci.cancel(); + } + + static void replaceCrosshair(CallbackInfo ci) { + Crosshair crosshair = Modules.get(Crosshair.class); + if (crosshair != null && crosshair.shouldHideVanilla()) { + ci.cancel(); + } + } + + static void killVignette(CallbackInfo ci) { + // Completely remove vanilla vignette post-processing + ci.cancel(); + } + + static void disablePortalOverlay(CallbackInfo ci) { + ci.cancel(); + } + + static void disableSleepOverlay(CallbackInfo ci) { + ci.cancel(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiTooltipHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiTooltipHandler.java new file mode 100644 index 0000000..d4aa784 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/handlers/GuiTooltipHandler.java @@ -0,0 +1,74 @@ +package dev.loki.lovisual.mixins.hud; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.SwapTooltip; +import dev.loki.lovisual.util.item.food.IllegalItemUtil; +import dev.loki.lovisual.util.item.rarity.RarityColorUtil; +import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; + +/** + * Held-item tooltip recoloring for {@link GuiMixin}. Captures the vanilla swap-item tooltip with a + * rarity / illegal / top-enchant derived name color and a fade driven by the highlight timer, + * cancelling the vanilla render when the custom SwapTooltip is active. + */ +final class GuiTooltipHandler { + private GuiTooltipHandler() { + } + + static void colorizeHeldItem(int toolHighlightTimer, ItemStack lastToolHighlight, Minecraft minecraft, CallbackInfo ci) { + if (!Lifecycle.canRunHud()) return; + SwapTooltip swapTooltip = SwapTooltip.get(); + if (swapTooltip == null || !swapTooltip.isEnabled()) { + return; + } + if (toolHighlightTimer <= 0 || lastToolHighlight == null || lastToolHighlight.isEmpty()) { + SwapTooltip.clear(); + return; + } + + MutableComponent name = Component.empty().append(lastToolHighlight.getHoverName()); + if (lastToolHighlight.has(DataComponents.CUSTOM_NAME)) { + name.withStyle(ChatFormatting.ITALIC); + } + + int illegalColor = IllegalItemUtil.illegalColor(); + int topColor = TopEnchantUtil.topColor(); + int rarityColor = argbFrom(RarityColorUtil.INSTANCE.getRarityColor(lastToolHighlight)); + boolean hasIllegal = IllegalItemUtil.isIllegal(lastToolHighlight); + boolean hasTop = !hasIllegal && TopEnchantUtil.hasTopEnchant(lastToolHighlight); + int nameColor = hasIllegal ? illegalColor : (hasTop ? topColor : rarityColor); + int alpha = (int) ((float) toolHighlightTimer * 256.0F / 10.0F); + if (alpha > 255) alpha = 255; + if (alpha <= 0) { + SwapTooltip.clear(); + return; + } + float fade = alpha / 255f; + int argb = 0xFF000000 | (nameColor & 0x00FFFFFF); + SwapTooltip.capture( + name.getString(), + argb, + fade, + minecraft.gameMode.canHurtPlayer() + ); + ci.cancel(); + } + + private static int argbFrom(float[] rgba) { + if (rgba == null || rgba.length < 3) return 0xFFFFFFFF; + float r = rgba[0], g = rgba[1], b = rgba[2]; + float a = rgba.length > 3 ? rgba[3] : 1f; + int ai = (int) (a * 255) & 0xFF; + int ri = (int) (r * 255) & 0xFF; + int gi = (int) (g * 255) & 0xFF; + int bi = (int) (b * 255) & 0xFF; + return (ai << 24) | (ri << 16) | (gi << 8) | bi; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java index 383e24b..1e374e8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java @@ -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.mixins.hud.tablist; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.CustomTabList; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java index c16b361..a25f7f6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java @@ -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.mixins.hud.tooltips; import net.minecraft.client.gui.components.toasts.Toast; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java index 2d8584b..b3d34b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java @@ -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.mixins.hud.tooltips; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java index b70f4ce..b3a4aa8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java @@ -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.mixins.input.keyboard; import net.minecraft.client.KeyMapping; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java index 4fb212d..9d596c4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java @@ -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.mixins.input.keyboard; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java index 1f7dd26..3b5b51c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java @@ -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.mixins.input.keyboard; import com.mojang.blaze3d.platform.InputConstants; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java index dcba269..d21dc84 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java @@ -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.mixins.input.mouse; import net.minecraft.client.player.ClientInput; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java index 886ffde..32ada4c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java @@ -1,14 +1,7 @@ -/* - * 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.mixins.input.mouse; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import dev.loki.lovisual.features.gui.mainmenu.LoVisualProxyManagerScreen; +import dev.loki.lovisual.features.gui.mainmenu.proxy.LoVisualProxyManagerScreen; import dev.loki.lovisual.util.screen.ClientScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java index 3a0e05f..a98d9e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java @@ -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.mixins.iris.hand; import net.irisshaders.iris.pathways.HandRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java index 77d768e..408e4a2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java @@ -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.mixins.iris.hand; import net.irisshaders.iris.pathways.HandRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java index e390e35..041c300 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java @@ -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.mixins.iris.hand; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java index 6e0416b..800b074 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java @@ -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.mixins.iris.processor; import com.google.common.collect.ImmutableList; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java index fdad2d6..4cfb6d6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java @@ -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.mixins.iris.render; import net.irisshaders.iris.gl.uniform.UniformHolder; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java index 5b5ed98..83310e5 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java @@ -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.mixins.iris.render; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java index 478166a..cae6c6c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java @@ -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.mixins.iris.render; import net.irisshaders.iris.pipeline.IrisRenderingPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java index 812f6fd..3a2d593 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java @@ -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.mixins.iris.render; import net.irisshaders.iris.Iris; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java index acc3466..11fbca8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java @@ -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.mixins.moreculling; import dev.loki.lovisual.render.ShaderEspRenderContext; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java index e01b94b..35c81ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java @@ -1,63 +1,23 @@ -/* - * 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.mixins.network.game; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientPacketListener; -import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.*; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.Items; -import net.minecraft.world.phys.Vec3; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; -import dev.loki.lovisual.events.Events; -import dev.loki.lovisual.events.impl.pvp.PvpChatEvent; -import dev.loki.lovisual.events.impl.pvp.PvpTabEvent; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns; -import dev.loki.lovisual.features.module.modules.misc.sound.NoSound; -import dev.loki.lovisual.features.module.modules.visuals.esp.audio.SoundESP; -import dev.loki.lovisual.features.module.modules.visuals.fx.TotemFX; import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; -import dev.loki.lovisual.features.relations.StaffTracker; -import dev.loki.lovisual.util.logging.DebugLog; -import dev.loki.lovisual.util.logging.ServerDumpUtil; -import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; -import dev.loki.lovisual.util.player.effect.heuristics.StatusEffectHeuristics; -import dev.loki.lovisual.util.pvp.client.CooldownsState; -import dev.loki.lovisual.util.pvp.opponents.OpponentCooldownManager; -import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; - -import java.util.UUID; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +/** + * Hook-only mixin: packet bodies live in handlers/ (ClientPacketListenerPvpHandler, + * ClientPacketListenerStaffHandler, ClientPacketListenerSoundHandler). Disabled + * legacy sound/inventory cooldown hooks are documented in ClientPacketListenerPvpHandler. + */ @Mixin(ClientPacketListener.class) public abstract class ClientPacketListenerMixin { - // "TPS: 20.0" или "TPS: *20.0" - @Unique - private static final Pattern TPS_PATTERN = - Pattern.compile("TPS:\\s*\\*?(\\d+(?:\\.\\d+)?)"); - // "Пинг: 57" - @Unique - private static final Pattern PING_PATTERN = - Pattern.compile("Пинг:\\s*(\\d+)"); - @Inject(method = "handlePlayerAbilities", at = @At("HEAD"), cancellable = true) private void lovisual$flightAbilities(ClientboundPlayerAbilitiesPacket packet, CallbackInfo ci) { // Flight module removed; no abilities override remains. @@ -65,91 +25,32 @@ public abstract class ClientPacketListenerMixin { @Inject(method = "handleTabListCustomisation", at = @At("TAIL")) private void lovisual$captureTab(ClientboundTabListPacket packet, CallbackInfo ci) { - String header = packet.header() == null ? "" : packet.header().getString(); - String footer = packet.footer() == null ? "" : packet.footer().getString(); - ServerDumpUtil.dumpTabText("HEADER", packet.header()); - ServerDumpUtil.dumpTabText("FOOTER", packet.footer()); - if (Events.BUS.hasListeners(PvpTabEvent.class)) { - Events.BUS.post(new PvpTabEvent(header, footer)); - } + ClientPacketListenerPvpHandler.captureTab(packet); } @Inject(method = "handleSystemChat", at = @At("HEAD")) private void onChat(ClientboundSystemChatPacket packet, CallbackInfo ci) { - String msg = packet.content().getString(); - - // PvP state listeners via events; legacy direct setters disabled. - if (msg != null && !msg.isBlank() && Events.BUS.hasListeners(PvpChatEvent.class)) { - Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.GAME_MESSAGE)); - } - /* - if (msg.contains("Вы вошли в режим PVP")) { - CooldownsState.MANAGER.enterPvp(); - } else if (msg.contains("PVP окончено")) { - CooldownsState.MANAGER.exitPvp(); - } - */ + ClientPacketListenerPvpHandler.onSystemChat(packet); } @Inject(method = "handleSoundEvent", at = @At("HEAD")) private void lovisual$onChorus(ClientboundSoundPacket packet, CallbackInfo ci) { - // Opponent cooldown tracking disabled (legacy system). - /* - if (!CooldownsState.shouldTrackOpponents()) return; - if (packet.getSound().value() != SoundEvents.ITEM_CHORUS_FRUIT_TELEPORT) return; - - MinecraftClient mc = MinecraftClient.getInstance(); - if (mc.world == null || mc.player == null) return; - - Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ()); - - if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return; - - PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6); - if (p == null) return; - - OpponentCooldownManager.start(p.getUuid(), Items.CHORUS_FRUIT); - */ + // Opponent cooldown tracking disabled (legacy system; see ClientPacketListenerPvpHandler). } @Inject(method = "handlePlayerChat", at = @At("HEAD")) private void lovisual$pvpChatMessage(ClientboundPlayerChatPacket packet, CallbackInfo ci) { - String msg = packet == null ? null : packet.body().content(); - if (msg == null || msg.isBlank()) return; - if (Events.BUS.hasListeners(PvpChatEvent.class)) { - Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.CHAT_MESSAGE)); - } + ClientPacketListenerPvpHandler.onPlayerChat(packet); } @Inject(method = "handleRemoveEntities", at = @At("TAIL")) private void lovisual$onEntitiesDestroy(ClientboundRemoveEntitiesPacket packet, CallbackInfo ci) { - - Minecraft mc = Minecraft.getInstance(); - ClientLevel world = mc.level; - if (world == null) return; - - for (int id : packet.getEntityIds()) { - - Entity e = world.getEntity(id); - if (!(e instanceof Player player)) continue; - - StaffTracker.INSTANCE.onEntityDisappear(player.getUUID()); - } + ClientPacketListenerStaffHandler.onEntitiesDestroy(packet); } @Inject(method = "handleAddEntity", at = @At("TAIL")) private void lovisual$onEntitySpawn(ClientboundAddEntityPacket packet, CallbackInfo ci) { - - Minecraft mc = Minecraft.getInstance(); - ClientLevel world = mc.level; - if (world == null) return; - - if (packet.getType() != net.minecraft.world.entity.EntityTypes.PLAYER) return; - - Entity entity = world.getEntity(packet.getId()); - if (!(entity instanceof Player player)) return; - - StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name()); + ClientPacketListenerStaffHandler.onEntitySpawn(packet); } @Redirect(method = "handlePlayerInfoUpdate", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V")) @@ -162,43 +63,12 @@ public abstract class ClientPacketListenerMixin { @Inject(method = "handlePlayerInfoUpdate", at = @At("TAIL")) private void lovisual$onPlayerList(ClientboundPlayerInfoUpdatePacket packet, CallbackInfo ci) { - - var actions = packet.actions(); - boolean listedAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED) - || actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); - - boolean gmAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE) - || actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); - - for (var entry : packet.entries()) { - - var uuid = entry.profileId(); - String name = entry.profile() != null ? entry.profile().name() : null; - - Boolean listed = listedAffects ? entry.listed() : null; - var gm = gmAffects ? entry.gameMode() : null; - - // ВАЖНО: берём ПОЛНЫЙ Text из TAB - var displayName = entry.displayName(); - if (displayName == null && name != null) { - displayName = net.minecraft.network.chat.Component.literal(name); - } - - StaffTracker.INSTANCE.onTabUpdate(uuid, name, displayName, listed, gm); - } + ClientPacketListenerStaffHandler.onPlayerList(packet); } @Inject(method = "handlePlayerInfoRemove", at = @At("TAIL")) private void lovisual$onPlayerRemove(ClientboundPlayerInfoRemovePacket packet, CallbackInfo ci) { - - for (UUID id : packet.profileIds()) { - - // убрали из TAB - StaffTracker.INSTANCE.onTabUpdate(id, null, null, false, null); - TotemPopCounter.onPlayerLogout(id); - // ВАЖНО — фикс VANISH после кика - StaffTracker.INSTANCE.onEntityDisappear(id); - } + ClientPacketListenerStaffHandler.onPlayerRemove(packet); } @Inject(method = "handleSetTime", at = @At("HEAD")) @@ -208,228 +78,66 @@ public abstract class ClientPacketListenerMixin { @Inject(method = "handleTabListCustomisation", at = @At("TAIL")) private void lovisual$onTab(ClientboundTabListPacket packet, CallbackInfo ci) { - parse(packet.header()); - } - - @Unique - private void parse(Component text) { - if (text == null) return; - - String raw = text.getString(); - if (raw.isEmpty()) return; - - Matcher tps = TPS_PATTERN.matcher(raw); - if (tps.find()) { - try { - NetworkStatsUtil.updateTabTps(Float.parseFloat(tps.group(1))); - } catch (NumberFormatException ignored) { - } - } - - Matcher ping = PING_PATTERN.matcher(raw); - if (ping.find()) { - try { - NetworkStatsUtil.updateTabPing(Integer.parseInt(ping.group(1))); - } catch (NumberFormatException ignored) { - } - } + ClientPacketListenerPvpHandler.parseTabMetrics(packet.header()); } @Inject(method = "handleContainerSetSlot", at = @At("HEAD")) private void lovisual$debugSlotUpdate(ClientboundContainerSetSlotPacket packet, CallbackInfo ci) { - // PvP cooldown confirmation disabled (legacy system). - /* - int syncId = packet.getSyncId(); - int slot = packet.getSlot(); - ItemStack stack = packet.getStack(); - - //System.out.println("[LoVisual][DEBUG] Packet slot update: syncId=" + syncId + " slot=" + slot + " item=" + stack.getItem() + " x" + stack.getCount()); - - var pending = CooldownsState.PENDING.consumeIfConfirmed(slot, stack.getCount()); - if (pending != null) { - //System.out.println("[LoVisual][DEBUG] Confirmed use of " + pending.item() + " (count decreased from " + pending.count() + " to " + stack.getCount() + ")"); - CooldownsState.MANAGER.commitConfirmedUse(pending.item(), pending.startedAtMs()); - } - */ + // PvP cooldown confirmation disabled (legacy system; see ClientPacketListenerPvpHandler). } @Inject(method = "handleContainerContent", at = @At("HEAD")) private void lovisual$debugFullInventory(ClientboundContainerSetContentPacket packet, CallbackInfo ci) { - // PvP cooldown confirmation disabled (legacy system). - /* - int syncId = packet.syncId(); - List contents = packet.contents(); - - //System.out.println("[LoVisual][DEBUG] Full inventory syncId=" + syncId + ", size=" + contents.size()); - - for (int i = 0; i < contents.size(); i++) { - ItemStack stack = contents.get(i); - if (stack.isEmpty()) continue; - - var pending = CooldownsState.PENDING.consumeIfConfirmed(i, stack.getCount()); - if (pending != null) { - //System.out.println("[LoVisual][DEBUG] Confirmed (FULL INV) " + pending.item() + " slot=" + i + " count=" + stack.getCount()); - CooldownsState.MANAGER.commitConfirmedUse(pending.item(), pending.startedAtMs()); - } - } - */ + // PvP cooldown confirmation disabled (legacy system; see ClientPacketListenerPvpHandler). } @Inject(method = "handleSoundEvent", at = @At("HEAD"), cancellable = true) private void lovisual$filterSound(ClientboundSoundPacket packet, CallbackInfo ci) { - NoSound module = Modules.get(NoSound.class); - if (module == null || !module.isEnabled()) return; - if (packet == null || packet.getSound() == null) return; - String id = packet.getSound().value().location().toString().toLowerCase(); - if (module.shouldMute(id)) { - ci.cancel(); - } + ClientPacketListenerSoundHandler.filterSound(packet, ci); } @Inject(method = "handleSoundEntityEvent", at = @At("HEAD"), cancellable = true) private void lovisual$filterSoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) { - NoSound module = Modules.get(NoSound.class); - if (module == null || !module.isEnabled()) return; - if (packet == null || packet.getSound() == null) return; - String id = packet.getSound().value().location().toString().toLowerCase(); - if (module.shouldMute(id)) { - ci.cancel(); - } + ClientPacketListenerSoundHandler.filterSoundFromEntity(packet, ci); } @Inject(method = "handleParticleEvent", at = @At("HEAD")) private void lovisual$heuristicParticle(ClientboundLevelParticlesPacket packet, CallbackInfo ci) { - var p = packet.getParticle(); - // Log raw particles once per hook for diagnostics - DebugLog.info("Particle packet type=%s pos=%.2f %.2f %.2f count=%d", - p.getType().toString(), - packet.getX(), packet.getY(), packet.getZ(), - packet.getCount()); - StatusEffectHeuristics.handle(p, packet.getX(), packet.getY(), packet.getZ()); + ClientPacketListenerSoundHandler.heuristicParticle(packet); } @Inject(method = "handleSoundEvent", at = @At("HEAD")) private void lovisual$onPearl(ClientboundSoundPacket packet, CallbackInfo ci) { - // Opponent cooldown tracking disabled (legacy system). - /* - if (!CooldownsState.shouldTrackOpponents()) return; - if (packet.getSound().value() != SoundEvents.ENTITY_ENDER_PEARL_THROW) return; - - MinecraftClient mc = MinecraftClient.getInstance(); - if (mc.world == null || mc.player == null) return; - - Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ()); - - if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return; - - PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6); - if (p == null) return; - - OpponentCooldownManager.start(p.getUuid(), Items.ENDER_PEARL); - */ + // Opponent cooldown tracking disabled (legacy system; see ClientPacketListenerPvpHandler). } @Inject(method = "handleSoundEvent", at = @At("HEAD")) private void lovisual$onPlaySound(ClientboundSoundPacket packet, CallbackInfo ci) { - SoundESP esp = Modules.get(SoundESP.class); - if (esp == null || !esp.isEnabled()) return; - if (packet == null || packet.getSound() == null) return; - Vec3 pos = new Vec3(packet.getX(), packet.getY(), packet.getZ()); - esp.handleSound(packet.getSound().value().location(), pos); + ClientPacketListenerSoundHandler.onPlaySound(packet); } @Inject(method = "handleSoundEntityEvent", at = @At("HEAD")) private void lovisual$onPlaySoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) { - SoundESP esp = Modules.get(SoundESP.class); - if (esp == null || !esp.isEnabled()) return; - if (packet == null || packet.getSound() == null) return; - - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return; - Entity entity = mc.level.getEntity(packet.getId()); - if (entity == null) return; - - esp.handleSound(packet.getSound().value().location(), entity.position()); + ClientPacketListenerSoundHandler.onPlaySoundFromEntity(packet); } @Inject(method = "handleUpdateMobEffect", at = @At("HEAD")) private void lovisual$logStatusEffect(ClientboundUpdateMobEffectPacket packet, CallbackInfo ci) { - DebugLog.server("Packet: add/refresh effect eid=%d effect=%s amp=%d dur=%d particles=%s icon=%s ambient=%s keepFading=%s", - packet.getEntityId(), - packet.getEffect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown"), - packet.getEffectAmplifier(), - packet.getEffectDurationTicks(), - packet.isEffectVisible(), - packet.effectShowsIcon(), - packet.isEffectAmbient(), - packet.shouldBlend() - ); + ClientPacketListenerSoundHandler.logStatusEffect(packet); } @Inject(method = "handleRemoveMobEffect", at = @At("HEAD")) private void lovisual$logRemove(ClientboundRemoveMobEffectPacket packet, CallbackInfo ci) { - DebugLog.server("Packet: remove effect eid=%d (%s) effect=%s", - packet.entityId(), - resolveName(packet.entityId()), - packet.effect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown")); - } - - @Unique - private String resolveName(int entityId) { - var mc = Minecraft.getInstance(); - if (mc == null || mc.level == null) return "unknown"; - Entity e = mc.level.getEntity(entityId); - if (e == null) return "unknown"; - if (e instanceof Player p) return p.getName().getString(); - return e.getName().getString(); + ClientPacketListenerSoundHandler.logRemove(packet); } @Inject(method = "handleSoundEvent", at = @At("HEAD")) private void lovisual$onTotemSound(ClientboundSoundPacket packet, CallbackInfo ci) { - // Opponent cooldown tracking disabled (legacy system). - /* - if (!CooldownsState.shouldTrackOpponents()) return; - if (packet.getSound().value() != SoundEvents.ITEM_TOTEM_USE) return; - - MinecraftClient mc = MinecraftClient.getInstance(); - if (mc.world == null || mc.player == null) return; - - Vec3d pos = new Vec3d(packet.getX(), packet.getY(), packet.getZ()); - - if (!SoundEntityResolver.isUnambiguousOpponent(pos, 1.6)) return; - - PlayerEntity p = SoundEntityResolver.findClosestOpponent(pos, 1.6); - if (p == null) return; - - OpponentCooldownManager.start(p.getUuid(), Items.TOTEM_OF_UNDYING); - */ + // Opponent cooldown tracking disabled (legacy system; see ClientPacketListenerPvpHandler). } @Inject(method = "handleEntityEvent", at = @At("HEAD")) private void lovisual$onTotemStatus(ClientboundEntityEventPacket packet, CallbackInfo ci) { - if (packet.getEventId() == 35) { // 35 = сработал тотем бессмертия - ClientPacketListener handler = (ClientPacketListener) (Object) this; - Minecraft client = Minecraft.getInstance(); - - Entity entity = packet.getEntity(handler.getLevel()); - if (entity == null) return; - if (entity.equals(client.player)) { - PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class); - if (cooldowns != null) { - cooldowns.tryStartLocalCooldown(Items.TOTEM_OF_UNDYING, ItemCooldownRulesValue.Trigger.TOTEM_POP); - } - TotemFX fx = Modules.get(TotemFX.class); - if (fx != null) { - fx.onTotemPop(); - } - return; - } - if (entity instanceof Player player) { - TotemPopCounter.recordPop(player); - if (CooldownsState.shouldTrackOpponents()) { - OpponentCooldownManager.recordUse(player.getUUID(), Items.TOTEM_OF_UNDYING); - } - } - } + ClientPacketListenerPvpHandler.onTotemStatus(packet, (ClientPacketListener) (Object) this); } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java index b937953..814e5ed 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java @@ -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.mixins.network.game; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java index 2b1083b..fb71515 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java @@ -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.mixins.network.game; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java index eec97bf..cbb2bd4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java @@ -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.mixins.network.game; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerPvpHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerPvpHandler.java new file mode 100644 index 0000000..75fe487 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerPvpHandler.java @@ -0,0 +1,140 @@ +package dev.loki.lovisual.mixins.network.game; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundEntityEventPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; +import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; +import net.minecraft.network.protocol.game.ClientboundTabListPacket; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; +import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; +import dev.loki.lovisual.events.Events; +import dev.loki.lovisual.events.impl.pvp.PvpChatEvent; +import dev.loki.lovisual.events.impl.pvp.PvpTabEvent; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns; +import dev.loki.lovisual.features.module.modules.visuals.fx.TotemFX; +import dev.loki.lovisual.util.logging.ServerDumpUtil; +import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; +import dev.loki.lovisual.util.pvp.client.CooldownsState; +import dev.loki.lovisual.util.pvp.opponents.OpponentCooldownManager; +import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Chat, tab-metric and totem-pop packet bodies for {@link ClientPacketListenerMixin}. + * Physically under {@code handlers/} but declares the parent package for direct access. + */ +final class ClientPacketListenerPvpHandler { + + // "TPS: 20.0" или "TPS: *20.0" + private static final Pattern TPS_PATTERN = + Pattern.compile("TPS:\\s*\\*?(\\d+(?:\\.\\d+)?)"); + // "Пинг: 57" + private static final Pattern PING_PATTERN = + Pattern.compile("Пинг:\\s*(\\d+)"); + + private ClientPacketListenerPvpHandler() { + } + + static void captureTab(ClientboundTabListPacket packet) { + String header = packet.header() == null ? "" : packet.header().getString(); + String footer = packet.footer() == null ? "" : packet.footer().getString(); + ServerDumpUtil.dumpTabText("HEADER", packet.header()); + ServerDumpUtil.dumpTabText("FOOTER", packet.footer()); + if (Events.BUS.hasListeners(PvpTabEvent.class)) { + Events.BUS.post(new PvpTabEvent(header, footer)); + } + } + + static void onSystemChat(ClientboundSystemChatPacket packet) { + String msg = packet.content().getString(); + + // PvP state listeners via events; legacy direct setters disabled. + if (msg != null && !msg.isBlank() && Events.BUS.hasListeners(PvpChatEvent.class)) { + Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.GAME_MESSAGE)); + } + } + + static void onPlayerChat(ClientboundPlayerChatPacket packet) { + String msg = packet == null ? null : packet.body().content(); + if (msg == null || msg.isBlank()) return; + if (Events.BUS.hasListeners(PvpChatEvent.class)) { + Events.BUS.post(new PvpChatEvent(msg, PvpChatEvent.Source.CHAT_MESSAGE)); + } + } + + static void parseTabMetrics(Component text) { + if (text == null) return; + + String raw = text.getString(); + if (raw.isEmpty()) return; + + Matcher tps = TPS_PATTERN.matcher(raw); + if (tps.find()) { + try { + NetworkStatsUtil.updateTabTps(Float.parseFloat(tps.group(1))); + } catch (NumberFormatException ignored) { + } + } + + Matcher ping = PING_PATTERN.matcher(raw); + if (ping.find()) { + try { + NetworkStatsUtil.updateTabPing(Integer.parseInt(ping.group(1))); + } catch (NumberFormatException ignored) { + } + } + } + + static void onTotemStatus(ClientboundEntityEventPacket packet, ClientPacketListener listener) { + if (packet.getEventId() == 35) { // 35 = сработал тотем бессмертия + Minecraft client = Minecraft.getInstance(); + + Entity entity = packet.getEntity(listener.getLevel()); + if (entity == null) return; + if (entity.equals(client.player)) { + PvpCooldowns cooldowns = Modules.get(PvpCooldowns.class); + if (cooldowns != null) { + cooldowns.tryStartLocalCooldown(Items.TOTEM_OF_UNDYING, ItemCooldownRulesValue.Trigger.TOTEM_POP); + } + TotemFX fx = Modules.get(TotemFX.class); + if (fx != null) { + fx.onTotemPop(); + } + return; + } + if (entity instanceof Player player) { + TotemPopCounter.recordPop(player); + if (CooldownsState.shouldTrackOpponents()) { + OpponentCooldownManager.recordUse(player.getUUID(), Items.TOTEM_OF_UNDYING); + } + } + } + } + + /* + * Disabled legacy hooks kept for reference (the old sound/inventory-driven + * opponent-cooldown and PvP-state systems). They are wired out of the mixin and + * intentionally inert; re-enable by restoring bodies into the matching + * ClientPacketListenerMixin injection. + * + * handleSystemChat -> PvP enter/exit: + * if (msg.contains("Вы вошли в режим PVP")) CooldownsState.MANAGER.enterPvp(); + * else if (msg.contains("PVP окончено")) CooldownsState.MANAGER.exitPvp(); + * + * handleSoundEvent (chorus / ender pearl / totem use) -> OpponentCooldownManager.start( + * uuid, Items.CHORUS_FRUIT | ENDER_PEARL | TOTEM_OF_UNDYING) guarded by + * CooldownsState.shouldTrackOpponents(), SoundEvents match and + * SoundEntityResolver.isUnambiguousOpponent(pos, 1.6). + * + * handleContainerSetSlot / handleContainerContent -> + * CooldownsState.PENDING.consumeIfConfirmed(slot, count) then + * CooldownsState.MANAGER.commitConfirmedUse(item, startedAtMs). + */ +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerSoundHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerSoundHandler.java new file mode 100644 index 0000000..e12f6ea --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerSoundHandler.java @@ -0,0 +1,109 @@ +package dev.loki.lovisual.mixins.network.game; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; +import net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket; +import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.misc.sound.NoSound; +import dev.loki.lovisual.features.module.modules.visuals.esp.audio.SoundESP; +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.player.effect.heuristics.StatusEffectHeuristics; + +/** + * Sound, particle and status-effect packet bodies for {@link ClientPacketListenerMixin}. + * Hook-only mixin delegates here so the injection class stays focused on the + * {@code @Inject}/{@code @Redirect} wiring. Physically under {@code handlers/} but + * declares the parent package for direct access. + */ +final class ClientPacketListenerSoundHandler { + + private ClientPacketListenerSoundHandler() { + } + + static void filterSound(ClientboundSoundPacket packet, CallbackInfo ci) { + NoSound module = Modules.get(NoSound.class); + if (module == null || !module.isEnabled()) return; + if (packet == null || packet.getSound() == null) return; + String id = packet.getSound().value().location().toString().toLowerCase(); + if (module.shouldMute(id)) { + ci.cancel(); + } + } + + static void filterSoundFromEntity(ClientboundSoundEntityPacket packet, CallbackInfo ci) { + NoSound module = Modules.get(NoSound.class); + if (module == null || !module.isEnabled()) return; + if (packet == null || packet.getSound() == null) return; + String id = packet.getSound().value().location().toString().toLowerCase(); + if (module.shouldMute(id)) { + ci.cancel(); + } + } + + static void onPlaySound(ClientboundSoundPacket packet) { + SoundESP esp = Modules.get(SoundESP.class); + if (esp == null || !esp.isEnabled()) return; + if (packet == null || packet.getSound() == null) return; + Vec3 pos = new Vec3(packet.getX(), packet.getY(), packet.getZ()); + esp.handleSound(packet.getSound().value().location(), pos); + } + + static void onPlaySoundFromEntity(ClientboundSoundEntityPacket packet) { + SoundESP esp = Modules.get(SoundESP.class); + if (esp == null || !esp.isEnabled()) return; + if (packet == null || packet.getSound() == null) return; + + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return; + Entity entity = mc.level.getEntity(packet.getId()); + if (entity == null) return; + + esp.handleSound(packet.getSound().value().location(), entity.position()); + } + + static void heuristicParticle(ClientboundLevelParticlesPacket packet) { + var p = packet.getParticle(); + // Log raw particles once per hook for diagnostics + DebugLog.info("Particle packet type=%s pos=%.2f %.2f %.2f count=%d", + p.getType().toString(), + packet.getX(), packet.getY(), packet.getZ(), + packet.getCount()); + StatusEffectHeuristics.handle(p, packet.getX(), packet.getY(), packet.getZ()); + } + + static void logStatusEffect(ClientboundUpdateMobEffectPacket packet) { + DebugLog.server("Packet: add/refresh effect eid=%d effect=%s amp=%d dur=%d particles=%s icon=%s ambient=%s keepFading=%s", + packet.getEntityId(), + packet.getEffect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown"), + packet.getEffectAmplifier(), + packet.getEffectDurationTicks(), + packet.isEffectVisible(), + packet.effectShowsIcon(), + packet.isEffectAmbient(), + packet.shouldBlend() + ); + } + + static void logRemove(ClientboundRemoveMobEffectPacket packet) { + DebugLog.server("Packet: remove effect eid=%d (%s) effect=%s", + packet.entityId(), + resolveName(packet.entityId()), + packet.effect().unwrapKey().map(k -> k.identifier().toString()).orElse("unknown")); + } + + private static String resolveName(int entityId) { + var mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) return "unknown"; + Entity e = mc.level.getEntity(entityId); + if (e == null) return "unknown"; + if (e instanceof Player p) return p.getName().getString(); + return e.getName().getString(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerStaffHandler.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerStaffHandler.java new file mode 100644 index 0000000..7420708 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/handlers/ClientPacketListenerStaffHandler.java @@ -0,0 +1,90 @@ +package dev.loki.lovisual.mixins.network.game; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import dev.loki.lovisual.features.relations.StaffTracker; +import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; + +import java.util.UUID; + +/** + * Entity and tab-list packet bodies for {@link ClientPacketListenerMixin}: feeds the + * staff tracker and totem-pop counter from spawn/despawn and player-info updates. + * Physically under {@code handlers/} but declares the parent package for direct access. + */ +final class ClientPacketListenerStaffHandler { + + private ClientPacketListenerStaffHandler() { + } + + static void onEntitiesDestroy(ClientboundRemoveEntitiesPacket packet) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) return; + + for (int id : packet.getEntityIds()) { + + Entity e = world.getEntity(id); + if (!(e instanceof Player player)) continue; + + StaffTracker.INSTANCE.onEntityDisappear(player.getUUID()); + } + } + + static void onEntitySpawn(ClientboundAddEntityPacket packet) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) return; + + if (packet.getType() != net.minecraft.world.entity.EntityTypes.PLAYER) return; + + Entity entity = world.getEntity(packet.getId()); + if (!(entity instanceof Player player)) return; + + StaffTracker.INSTANCE.onEntityAppear(player.getUUID(), player.getGameProfile().name()); + } + + static void onPlayerList(ClientboundPlayerInfoUpdatePacket packet) { + var actions = packet.actions(); + boolean listedAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED) + || actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + + boolean gmAffects = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE) + || actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER); + + for (var entry : packet.entries()) { + + var uuid = entry.profileId(); + String name = entry.profile() != null ? entry.profile().name() : null; + + Boolean listed = listedAffects ? entry.listed() : null; + var gm = gmAffects ? entry.gameMode() : null; + + // ВАЖНО: берём ПОЛНЫЙ Text из TAB + Component displayName = entry.displayName(); + if (displayName == null && name != null) { + displayName = Component.literal(name); + } + + StaffTracker.INSTANCE.onTabUpdate(uuid, name, displayName, listed, gm); + } + } + + static void onPlayerRemove(ClientboundPlayerInfoRemovePacket packet) { + for (UUID id : packet.profileIds()) { + + // убрали из TAB + StaffTracker.INSTANCE.onTabUpdate(id, null, null, false, null); + TotemPopCounter.onPlayerLogout(id); + // ВАЖНО — фикс VANISH после кика + StaffTracker.INSTANCE.onEntityDisappear(id); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java index 91e354c..7e64866 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java @@ -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.mixins.network.packet; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java index dd10fbd..e855612 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java @@ -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.mixins.network.packet; import io.netty.channel.ChannelPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java index 37d0803..c7564a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java @@ -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.mixins.network.packet; import net.minecraft.network.protocol.game.ServerboundUseItemPacket; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java index 4e09e6c..428a856 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java @@ -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.mixins.render.entity; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java index 93c0abe..e350c1c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java @@ -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.mixins.render.entity; import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java index 3ebf53c..23612c6 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java @@ -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.mixins.render.entity.crystal; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java index 153b212..97ad937 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java @@ -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.mixins.render.entity.crystal; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java index a43fe3c..b3f593b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java @@ -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.mixins.render.entity.item; import net.minecraft.client.renderer.entity.state.ItemEntityRenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java index f39a0e3..7aeb826 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java @@ -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.mixins.render.entity.item; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java index 9e4b4c2..ad91fec 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java @@ -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.mixins.render.fog; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java index a402a77..e5cfa71 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java @@ -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.mixins.render.fog; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java index bcf2ba5..4294b7d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java @@ -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.mixins.render.fog; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java index 2bda06d..a071366 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java @@ -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.mixins.render.fog; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java index 6be1538..5222da4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java @@ -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.mixins.render.fogmod; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java index 7a1bbfb..82b9614 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java @@ -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.mixins.render.item; import net.minecraft.client.renderer.entity.layers.EquipmentLayerRenderer; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java index 0a4b48b..a9461e4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java @@ -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.mixins.render.item; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java index 79e4f45..6ebfe58 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java @@ -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.mixins.render.item; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java index 137711e..9d1299c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java @@ -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.mixins.render.item; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java index 0c78424..5ead288 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java @@ -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.mixins.render.level; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java index 941a852..fe681dd 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java @@ -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.mixins.render.level; import com.llamalad7.mixinextras.sugar.Local; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java index e972b39..1a76b04 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java @@ -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.mixins.render.level; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java index b3cb19c..00649cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java @@ -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.mixins.render.level.particles; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java index 1a6ef68..83a8407 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java @@ -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.mixins.render.level.particles; import net.minecraft.client.particle.Particle; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java index 7e7bb55..09ae935 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java @@ -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.mixins.render.light; import net.minecraft.client.renderer.Lightmap; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java index 0d4048e..a1baca8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java @@ -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.mixins.render.light; import net.minecraft.client.renderer.LightmapRenderStateExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java index a052d8a..c537a0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java @@ -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.mixins.render.main; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java index 18ec95a..556a6f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java @@ -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.mixins.render.main; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java index 2adbf2a..bf9d195 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java @@ -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.mixins.render.main; import com.mojang.blaze3d.systems.GpuSurface; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java index a000389..771ebf8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java @@ -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.mixins.render.main; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java index 6017fa6..e3793bf 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java @@ -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.mixins.render.main; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java index 01d1317..4b6f198 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java @@ -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.mixins.render.sound; import com.mojang.blaze3d.audio.DeviceList; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java index f2efe63..c6cf11a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java @@ -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.mixins.render.sound; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java index 51f5c79..cea304d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java @@ -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.mixins.render.text; import com.google.gson.JsonParseException; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java index 12fbb73..8e413f9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java @@ -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.mixins.security.net; import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java index 89d336d..1899982 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java @@ -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.mixins.security.net; import net.minecraft.client.KeyMapping; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java index 27cc71a..6259e37 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java @@ -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.mixins.security.net; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java index 340f2cb..8a7e882 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java @@ -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.mixins.security.text; import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java index f6fc4eb..7af38e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java @@ -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.mixins.security.text; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java index 6c318f6..b1fa6b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java @@ -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.mixins.security.text; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java index 6014ba5..5c36acb 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java @@ -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.mixins.security.text; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java index e43fc96..4aa5041 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java @@ -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.mixins.sodium.terrain; import com.llamalad7.mixinextras.sugar.Local; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java index 13b4ff4..0ac9c5c 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java @@ -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.mixins.sodium.terrain; import dev.loki.lovisual.render.sodium.terrain.LoVisualChunkMeshFormats; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java index e5f601d..83bd2a0 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java @@ -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.mixins.sodium.terrain; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java index fddd321..08c1263 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java @@ -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.mixins.sodium.vertex; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkVertexEncoder; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java index a40319a..89c457d 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java @@ -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.mixins.sodium.vertex; import org.slf4j.Logger; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java index dff3b28..8e888c8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java @@ -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.mixins.sodium.vertex; import org.spongepowered.asm.mixin.Mixin; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java index 86593ac..9f29649 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java @@ -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.mixins.world.block; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java index 4c68ba6..ddeca21 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java @@ -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.mixins.world.block; import net.minecraft.world.level.block.Block; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java index 1f2105d..b80e061 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java @@ -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.mixins.world.block; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java index 4a44703..4a1a9db 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java @@ -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.mixins.world.block; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java index 1f5dfa6..103540f 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java @@ -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.mixins.world.fluid; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java index 537e506..d607792 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java @@ -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.mixins.world.level; import net.minecraft.client.ClientClockManager; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java index 28b1402..4fd978b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java @@ -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.mixins.world.level; import net.minecraft.client.multiplayer.ClientLevel; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java index fc01902..009c38a 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java @@ -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.mixins.world.level; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java index fafc1c4..0b987f0 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java @@ -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.mixins.world.level; import net.minecraft.world.level.Level; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java index 2c30ed5..28853ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java @@ -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.mixins.xaero.minimap; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java index 94f9349..947fd89 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java @@ -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.mixins.xaero.worldmap; import org.spongepowered.asm.mixin.*; diff --git a/mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java b/mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java index d10c203..80075f2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java +++ b/mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java @@ -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.render; import com.mojang.blaze3d.PrimitiveTopology; diff --git a/mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java index 9d01c15..8eca540 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java @@ -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.render; public enum ShaderEspRenderContext { diff --git a/mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java b/mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java index e69e9c0..bd6605a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java +++ b/mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java @@ -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.render.compat; import dev.tr7zw.entityculling.versionless.access.Cullable; diff --git a/mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java b/mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java index c0ac39d..ddddfc3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java @@ -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.render.effects; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java b/mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java index ff98367..0f17838 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java @@ -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.render.effects; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java b/mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java index 077e7a7..ea2e1b8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java @@ -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.render.engine; import dev.loki.lovisual.render.engine.guard.boundary.RenderBoundaryAudit; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java index 8404b7b..d2b1070 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java @@ -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.render.engine; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java index f17c8d1..5d90de1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java @@ -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.render.engine; import net.minecraft.client.renderer.culling.Frustum; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java index 8ee9f74..970da03 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java @@ -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.render.engine.animation; import dev.loki.lovisual.render.engine.animation.text.AnimatedTextLine; import dev.loki.lovisual.render.engine.animation.text.AnimatedTextStyle; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java index 91f5c73..1e372eb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java @@ -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.render.engine.animation; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java index edca84a..1a8e84b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java @@ -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.render.engine.animation; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java index 8ddacf6..e86048c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java @@ -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.render.engine.animation; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java index 7e52084..7e1994f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java @@ -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.render.engine.animation.text; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java index 401749c..bace48f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java @@ -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.render.engine.animation.text; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java index 40e00b9..0c5b41a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java @@ -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.render.engine.animation.text; public final class AnimatedTextStyle { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java index de36301..dd41af9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java @@ -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.render.engine.animation.text; public enum AnimatedTextTransition { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java index f3c99a9..74de345 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java @@ -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.render.engine.color; public enum ColorBlendMode { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java index dedc28a..360f7b5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java @@ -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.render.engine.color; public enum ColorEffects { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java index 5923025..ff04b1e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java @@ -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.render.engine.color; import java.awt.*; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java index 2e77bef..998b04b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java @@ -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.render.engine.color; public class RenderColor { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java index 34f316d..85bbcbf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java @@ -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.render.engine.command.core; import dev.loki.lovisual.render.engine.command.core.UiCommandKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java index 3b2aa08..76e18cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java @@ -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.render.engine.command.core; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java index 5bbd8a7..631ffc5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java @@ -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.render.engine.command.core; public enum UiCommandKind { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java index 1ff102f..510c39f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java @@ -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.render.engine.command.core; import dev.loki.lovisual.render.engine.command.core.UiCommand; import dev.loki.lovisual.render.engine.command.world.UiStatsSnapshot; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java index 2800c60..338fbe7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java @@ -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.render.engine.command.effect; import dev.loki.lovisual.render.engine.command.core.UiCommand; import dev.loki.lovisual.render.engine.command.core.UiCommandKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java index 9812414..65caa3c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java @@ -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.render.engine.command.effect; import dev.loki.lovisual.render.engine.command.core.UiCommand; import dev.loki.lovisual.render.engine.command.core.UiCommandKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java index cc382e7..f2c5922 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java @@ -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.render.engine.command.effect; import dev.loki.lovisual.render.engine.renderer.ui.draw.effect.UiEffectSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java index 6a4be0e..a3c2d54 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java @@ -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.render.engine.command.shape; import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaint; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java index e3b8998..e49b0dd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java @@ -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.render.engine.command.shape; import dev.loki.lovisual.render.engine.command.core.UiCommand; import dev.loki.lovisual.render.engine.command.core.UiCommandKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java index 6c081f6..6f25135 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java @@ -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.render.engine.command.shape; import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaint; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java index ef1b07f..54d4bb3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java @@ -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.render.engine.command.texture; import net.minecraft.world.item.ItemStack; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java index 705e8a0..866003f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java @@ -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.render.engine.command.texture; import dev.loki.lovisual.render.engine.command.core.UiCommand; import dev.loki.lovisual.render.engine.command.core.UiCommandKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java index 6637186..5904161 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java @@ -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.render.engine.command.texture; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java index 0730189..95bbebc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java @@ -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.render.engine.command.texture; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java index 81d0a6d..9076856 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java @@ -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.render.engine.command.world; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java index acf6ab8..616c4db 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java @@ -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.render.engine.command.world; public record UiStatsSnapshot(long frameId, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java index ab155db..081f3bc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java @@ -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.render.engine.command.world; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java index 9d407c9..549e887 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java @@ -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.render.engine.compat.immediatelyfast; import net.raphimc.immediatelyfast.ImmediatelyFast; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java index 6faea92..cf9f335 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java @@ -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.render.engine.compat.immediatelyfast; import net.fabricmc.loader.api.FabricLoader; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java index 82f8d42..a6eab37 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java @@ -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.render.engine.compat.immediatelyfast; public record ImmediatelyFastRuntimeSnapshot( diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java index ac9dd36..faad2ae 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java @@ -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.render.engine.core.context; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java index 949de56..648be77 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java @@ -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.render.engine.core.context; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java index 1d5cfc2..a433885 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java @@ -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.render.engine.core.context; import dev.loki.lovisual.render.engine.Projection2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java index ba90b8c..4d2449d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java @@ -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.render.engine.core.frame; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java index 830f05c..de02f0d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java @@ -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.render.engine.core.frame; import dev.loki.lovisual.render.engine.RenderState; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java index 77e4e62..f27128b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java @@ -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.render.engine.core.frame; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java index 72fd91b..6b1efca 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java @@ -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.render.engine.core.frame; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; import dev.loki.lovisual.render.engine.core.frame.RenderPhase; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java index e9fe0ee..3d4a807 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java @@ -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.render.engine.core.policy; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java index f86061d..b8219a2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java @@ -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.render.engine.core.policy; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java index ee78ebf..270efec 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java @@ -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.render.engine.core.policy; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java index 9545375..3db2aa4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java @@ -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.render.engine.core.policy; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java index 348bedf..f725561 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java @@ -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.render.engine.core.policy.fog; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java index 7f1ef46..650d785 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java @@ -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.render.engine.core.policy.fog; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java index 887b0ee..cf847c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java @@ -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.render.engine.core.system; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java index 1396282..8fc0cfd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java @@ -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.render.engine.core.system; import org.joml.Matrix4f; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java index ec396ad..640dab9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java @@ -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.render.engine.debug; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java index 49ca718..6da0dc3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java @@ -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.render.engine.depth; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java index b8f1eb5..5122b60 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java @@ -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.render.engine.depth; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java index c71dcd5..50278fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java @@ -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.render.engine.framegraph; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java index 8d49346..0bc9d8f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java @@ -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.render.engine.framegraph; import dev.loki.lovisual.render.engine.core.frame.RenderPhase; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java index 34bd455..d4a3c66 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java @@ -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.render.engine.framegraph; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java index fd20f7b..acb19e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java @@ -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.render.engine.guard; import dev.loki.lovisual.render.engine.rhi.backend.SodiumGlBackend; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java index 27d29f4..eb58d37 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java @@ -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.render.engine.guard; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java index 30957e7..3260ad5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java @@ -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.render.engine.guard.boundary; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java index 1c4d9a7..214e1e5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java @@ -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.render.engine.guard.boundary; import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java index 1bb9e33..287870d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java @@ -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.render.engine.guard.boundary; import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java index 3107230..b71fd53 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java @@ -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.render.engine.light; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java index 63db53c..9b1a7af 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java @@ -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.render.engine.math; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java index e0d0b54..cbca721 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java @@ -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.render.engine.math; public enum HudScale { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java index d58ffb1..a1ea2c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java @@ -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.render.engine.math; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java index 022523c..89595f8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java @@ -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.render.engine.msaa; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java index 28ea2d5..8c76f81 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java @@ -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.render.engine.msaa; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java index a7c5331..8a63974 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java @@ -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.render.engine.msaa; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java index 123216d..5b3784b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java @@ -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.render.engine.msaa; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java index 17b5e95..029a384 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java @@ -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.render.engine.pipeline; import com.mojang.blaze3d.platform.CompareOp; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java index 7016a0a..0bdde7b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java @@ -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.render.engine.pipeline; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java index f7e74be..c882cec 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java @@ -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.render.engine.pipeline; import com.mojang.blaze3d.PrimitiveTopology; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java index 7276174..c6b57fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java @@ -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.render.engine.pipeline.pipelines; import com.mojang.blaze3d.pipeline.BlendFunction; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java index 6acea68..7a51e87 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java @@ -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.render.engine.pipeline.pipelines; import com.mojang.blaze3d.PrimitiveTopology; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java index 40ae811..d1b7a7d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java @@ -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.render.engine.pipeline.pipelines; import com.mojang.blaze3d.PrimitiveTopology; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java index 3c4c8f5..1972015 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java @@ -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.render.engine.pipeline.pipelines; import com.mojang.blaze3d.PrimitiveTopology; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java index 4390695..12923c6 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java @@ -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.render.engine.postprocess.blur; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java index 37b3ada..04af11f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java @@ -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.render.engine.postprocess.blur; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java index c1738de..a785dc9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java @@ -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.render.engine.postprocess.blur; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java index 4e59fa7..8b8934b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java @@ -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.render.engine.postprocess.blur; import com.mojang.blaze3d.pipeline.TextureTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java index bebe5bc..b986b2a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java @@ -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.render.engine.postprocess.graph; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java index 07e4b41..837f3b8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java @@ -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.render.engine.postprocess.graph; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java index 62ab72a..54e88e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java @@ -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.render.engine.postprocess.graph; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java index 042b5d3..4e395f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java @@ -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.render.engine.postprocess.graph.resource; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java index ae825d7..fd8283b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java @@ -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.render.engine.postprocess.graph.resource; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java index 36be75f..059c585 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java @@ -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.render.engine.postprocess.graph.resource; public enum PostProcessResolution { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java index 7f30d51..79ab276 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java @@ -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.render.engine.postprocess.graph.resource; public enum PostProcessResource { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java index db87911..88fe993 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java @@ -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.render.engine.postprocess.post; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java index ffdc705..85646bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java @@ -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.render.engine.postprocess.post; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java index 36f9b11..9022260 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java @@ -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.render.engine.postprocess.post; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java index 0a7ee07..15e2ea7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java @@ -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.render.engine.postprocess.render; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java index 8bec63c..7964c17 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java @@ -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.render.engine.postprocess.render; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java index eaab000..6a854fa 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java @@ -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.render.engine.postprocess.render; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java index 0989113..6da4812 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java @@ -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.render.engine.prewarm; import dev.loki.lovisual.events.meta.EventHandler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java index 7af7c82..e983b9c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java @@ -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.render.engine.prewarm; import dev.loki.lovisual.events.Events; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java index b07997c..b19487f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java @@ -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.render.engine.profiler.bridge; import java.lang.reflect.Method; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java index 6f2f2aa..cabcd7f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java @@ -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.render.engine.profiler.bridge; import java.lang.reflect.Method; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java index eb3393f..efe3b8c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java @@ -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.render.engine.profiler.bridge; import dev.loki.lovisual.render.engine.profiler.bridge.DevProfilerBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java index 58a0eed..d46f423 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java @@ -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.render.engine.profiler.bridge; import dev.loki.lovisual.render.engine.profiler.bridge.DevProfilerBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java index 5fadcd7..11dcc3f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java @@ -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.render.engine.profiler.config; import dev.loki.lovisual.render.engine.profiler.bridge.DevProfilerBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java index 999ea5b..44f6e86 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java @@ -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.render.engine.profiler.config; public enum ProfilerSettings { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java index fd454f7..83a2d3a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java @@ -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.render.engine.profiler.frame; import dev.loki.lovisual.render.engine.rhi.stats.RhiStatsSnapshot; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java index b417d91..0298c30 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java @@ -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.render.engine.profiler.frame; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java index 964a14f..2acc1e3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java @@ -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.render.engine.profiler.frame; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java index 2c13b02..822dcb2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java @@ -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.render.engine.profiler.frame; import dev.loki.lovisual.render.engine.core.frame.RenderPhase; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java index 2028011..041516e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java @@ -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.render.engine.profiler.render; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java index 70c5cdd..0001e29 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java @@ -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.render.engine.profiler.render; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java index 1bcc422..a42abca 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java @@ -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.render.engine.profiler.render; import java.util.List; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java index 8182f6f..d765fc5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java @@ -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.render.engine.profiler.render; import dev.loki.lovisual.render.engine.profiler.bridge.DevProfilerBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java index b305fcd..d0a6641 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java @@ -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.render.engine.renderer; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java index bb1e2ca..fc24f5e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java @@ -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.render.engine.renderer; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java index 78ca371..082a7ca 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java @@ -1,12 +1,6 @@ -/* - * 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.render.engine.renderer; import dev.loki.lovisual.render.engine.renderer.warp.RenderWarp; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpFactory; import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; @@ -93,6 +87,7 @@ public final class Renderer2D { private static final int DEFAULT_ITEM_DURABILITY_THRESHOLD = 100; private static final int DEFAULT_ITEM_DURABILITY_TEXT_COLOR_THRESHOLD = 70; public static OrderedUiBatcher UI_BATCHER = new OrderedUiBatcher(); + public static final ItemBatchRenderer ITEM_BATCH_RENDERER = new ItemBatchRenderer(); public static final BatchStats BATCH_STATS = new BatchStats(); /** @@ -169,7 +164,7 @@ public final class Renderer2D { public static void init() { COLOR = new Renderer2D(false); TEXTURE = new Renderer2D(true); - ItemBatchRenderer.init(); + ITEM_BATCH_RENDERER.init(); } // ---- Public drawing facade ------------------------------------------------- @@ -1935,7 +1930,7 @@ public final class Renderer2D { float depth, float perspective, float scale) { - return RenderWarpStack.push(RenderWarp.perspective( + return RenderWarpStack.push(RenderWarpFactory.perspective( x, y, width, height, yawDeg, pitchDeg, rollDeg, depth, perspective, scale )); } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java index 8efeb3a..6a133ac 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java @@ -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.render.engine.renderer; import com.mojang.blaze3d.buffers.GpuBufferSlice; @@ -15,24 +8,14 @@ import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.texture.AbstractTexture; import net.minecraft.resources.Identifier; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; -import dev.loki.lovisual.render.engine.core.policy.VisibilityProvider; -import dev.loki.lovisual.render.engine.core.policy.VisibilityQuery; -import dev.loki.lovisual.render.engine.math.RenderMath; -import dev.loki.lovisual.render.engine.rhi.command.RhiDrawCommand; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.world.WorldCommandBuffer; import dev.loki.lovisual.render.engine.world.WorldPassCompiler; import dev.loki.lovisual.render.engine.world.stats.WorldRenderStatsSnapshot; -import java.util.ArrayList; -import java.util.List; - /** * * Renderer3D is the public world-primitive API only: @@ -40,6 +23,8 @@ import java.util.List; * - calls record CPU-side world commands into WorldCommandBuffer; * - WorldPassCompiler converts commands to RHI draw commands; * - mesh upload/draw/depth/fog/visibility are delegated to context/RHI/providers. + * Batch bindings ({@link Renderer3DBatchBindings}) and culling ({@link Renderer3DCulling}, + * {@link Renderer3DCullOptions}) live as sibling types in the same package. */ public final class Renderer3D { private static final String DEFAULT_SAMPLER = "u_Texture"; @@ -68,11 +53,11 @@ public final class Renderer3D { } public MeshBuilder lines() { - return commands.batch(linesPipeline, DepthMode.MAIN, RenderState.lineWidth, BatchBindings.none()); + return commands.batch(linesPipeline, DepthMode.MAIN, RenderState.lineWidth, Renderer3DBatchBindings.none()); } public MeshBuilder tris() { - return commands.batch(trisPipeline, DepthMode.MAIN, 1.0f, BatchBindings.none()); + return commands.batch(trisPipeline, DepthMode.MAIN, 1.0f, Renderer3DBatchBindings.none()); } public MeshBuilder batch(RenderPipeline pipeline) { @@ -80,10 +65,10 @@ public final class Renderer3D { } public MeshBuilder batch(RenderPipeline pipeline, DepthMode depthMode) { - return batch(pipeline, depthMode, BatchBindings.none()); + return batch(pipeline, depthMode, Renderer3DBatchBindings.none()); } - public MeshBuilder batch(RenderPipeline pipeline, DepthMode depthMode, BatchBindings bindings) { + public MeshBuilder batch(RenderPipeline pipeline, DepthMode depthMode, Renderer3DBatchBindings bindings) { return commands.batch(pipeline, depthMode, RenderState.lineWidth, bindings); } @@ -93,7 +78,7 @@ public final class Renderer3D { public MeshBuilder batchTextured(RenderPipeline pipeline, Identifier textureId, DepthMode depthMode) { return batch(pipeline, depthMode, - BatchBindings.none().withSampler(DEFAULT_SAMPLER, textureId)); + Renderer3DBatchBindings.none().withSampler(DEFAULT_SAMPLER, textureId)); } public MeshBuilder batchTextured(RenderPipeline pipeline, @@ -107,7 +92,7 @@ public final class Renderer3D { GpuSampler sampler, DepthMode depthMode) { return batch(pipeline, depthMode, - BatchBindings.none().withSampler(DEFAULT_SAMPLER, view, sampler)); + Renderer3DBatchBindings.none().withSampler(DEFAULT_SAMPLER, view, sampler)); } public MeshBuilder batchTextured(RenderPipeline pipeline, @@ -201,233 +186,8 @@ public final class Renderer3D { NONE } - public enum Culling { - ; - - public static boolean shouldRender(Entity target, float tickDelta, CullOptions options) { - if (target == null) return false; - if (options == null || options == CullOptions.NONE) return true; - - AABB visibilityBox = interpolatedBox(target, tickDelta); - if (options.frustum() && !isInFrustum(visibilityBox)) return false; - if (options.sectionVisibility() && !isSectionVisible(visibilityBox)) return false; - if (options.lookAtTarget() && !isInFront(target, tickDelta, options.minLookDot())) return false; - - if (options.visibleOnly()) { - if (!(options.skipVisibilityWhenTranslucent() && RenderState.worldTranslucent)) { - Minecraft mc = Minecraft.getInstance(); - return mc == null || mc.player == null || mc.player.hasLineOfSight(target); - } - } - - return true; - } - - public static boolean isInFrustum(Entity target, float tickDelta) { - return isInFrustum(interpolatedBox(target, tickDelta)); - } - - public static boolean isInFrustum(AABB box) { - if (RenderState.frustum == null || box == null) return true; - return RenderState.frustum.isVisible(box); - } - - public static boolean isSectionVisible(AABB box) { - if (box == null) return true; - RenderFrameContext ctx = LoVisualRenderSystem.currentContext(); - VisibilityProvider provider = ctx != null ? ctx.visibilityProvider() : LoVisualRenderSystem.sodium().visibilityProvider(); - if (provider == null) return true; - return provider.isBoxVisible(box, VisibilityQuery.worldOverlay(box)); - } - - private static AABB interpolatedBox(Entity target, float tickDelta) { - AABB box = target.getBoundingBox(); - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - Vec3 offset = pos.subtract(target.position()); - return box.move(offset); - } - - public static boolean isInFront(Entity target, float tickDelta, double minDot) { - Vec3 center = getTargetCenter(target, tickDelta); - Vec3 camPos = RenderState.cameraPos; - Vec3 to = center.subtract(camPos); - if (to.lengthSqr() <= 1.0e-6) return true; - Vec3 look = RenderState.cameraLook; - if (look.lengthSqr() <= 1.0e-6) { - look = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize(); - } - return look.dot(to.normalize()) > minDot; - } - - private static Vec3 getTargetCenter(Entity target, float tickDelta) { - Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); - return pos.add(0.0, target.getBbHeight() * 0.5, 0.0); - } - } - @FunctionalInterface public interface UniformResolver { GpuBufferSlice resolve(); } - - // ----------------------- - // Culling helpers - // ----------------------- - - public static final class BatchBindings { - private static final BatchBindings EMPTY = new BatchBindings(List.of(), List.of()); - - private final List samplers; - private final List uniforms; - - private BatchBindings(List samplers, List uniforms) { - this.samplers = samplers; - this.uniforms = uniforms; - } - - public static BatchBindings none() { - return EMPTY; - } - - private static String nameOrDefault(String name) { - return name != null ? name : DEFAULT_SAMPLER; - } - - public BatchBindings withSampler(String name, Identifier textureId) { - if (textureId == null) return this; - return appendSampler(new SamplerBinding(nameOrDefault(name), textureId, null, null)); - } - - public BatchBindings withSampler(String name, GpuTextureView view, GpuSampler sampler) { - if (view == null || sampler == null) return this; - return appendSampler(new SamplerBinding(nameOrDefault(name), null, view, sampler)); - } - - public BatchBindings withUniform(String name, GpuBufferSlice slice) { - if (name == null || slice == null) return this; - return appendUniform(UniformBinding.direct(name, slice)); - } - - public BatchBindings withUniform(String name, Object mergeKey, UniformResolver resolver) { - if (name == null || mergeKey == null || resolver == null) return this; - return appendUniform(UniformBinding.deferred(name, mergeKey, resolver)); - } - - private BatchBindings appendSampler(SamplerBinding binding) { - ArrayList next = new ArrayList<>(samplers.size() + 1); - next.addAll(samplers); - next.add(binding); - return new BatchBindings(List.copyOf(next), uniforms); - } - - private BatchBindings appendUniform(UniformBinding binding) { - ArrayList next = new ArrayList<>(uniforms.size() + 1); - next.addAll(uniforms); - next.add(binding); - return new BatchBindings(samplers, List.copyOf(next)); - } - - public boolean compatibleWith(BatchBindings other) { - if (other == null) return false; - if (samplers.size() != other.samplers.size() || uniforms.size() != other.uniforms.size()) { - return false; - } - - for (int i = 0; i < samplers.size(); i++) { - if (!samplers.get(i).sameBinding(other.samplers.get(i))) { - return false; - } - } - - for (int i = 0; i < uniforms.size(); i++) { - if (!uniforms.get(i).sameBinding(other.uniforms.get(i))) { - return false; - } - } - - return true; - } - - public void applyTo(RhiDrawCommand.Builder builder, Minecraft mc) { - for (SamplerBinding sampler : samplers) { - sampler.apply(builder, mc); - } - for (UniformBinding uniform : uniforms) { - uniform.apply(builder); - } - } - } - - public record CullOptions(boolean frustum, - boolean sectionVisibility, - boolean visibleOnly, - boolean lookAtTarget, - boolean skipVisibilityWhenTranslucent, - double minLookDot) { - public static final CullOptions NONE = - new CullOptions(false, false, false, false, false, 0.0); - public static final CullOptions FRUSTUM_LOOK = - new CullOptions(true, true, false, true, false, 0.05); - public static final CullOptions SMART_TARGET = - new CullOptions(true, true, true, true, true, 0.05); - - public CullOptions withoutSectionVisibility() { - return new CullOptions(frustum, false, visibleOnly, lookAtTarget, skipVisibilityWhenTranslucent, minLookDot); - } - } - - private record SamplerBinding(String name, - Identifier textureId, - GpuTextureView view, - GpuSampler sampler) { - private boolean sameBinding(SamplerBinding other) { - if (other == null) return false; - if (!name.equals(other.name)) return false; - if (textureId != null || other.textureId != null) { - return textureId != null && textureId.equals(other.textureId); - } - return view == other.view && sampler == other.sampler; - } - - private void apply(RhiDrawCommand.Builder builder, Minecraft mc) { - if (textureId != null) { - if (mc == null) return; - AbstractTexture texture = mc.getTextureManager().getTexture(textureId); - if (texture == null) return; - GpuTextureView textureView = texture.getTextureView(); - GpuSampler textureSampler = texture.getSampler(); - if (textureView == null || textureSampler == null) return; - builder.sampler(name, textureView, textureSampler); - return; - } - - if (view != null && sampler != null) { - builder.sampler(name, view, sampler); - } - } - } - - private record UniformBinding(String name, - Object mergeKey, - UniformResolver resolver) { - private static UniformBinding direct(String name, GpuBufferSlice slice) { - return new UniformBinding(name, slice, () -> slice); - } - - private static UniformBinding deferred(String name, Object mergeKey, UniformResolver resolver) { - return new UniformBinding(name, mergeKey, resolver); - } - - private boolean sameBinding(UniformBinding other) { - if (other == null) return false; - return name.equals(other.name) && mergeKey.equals(other.mergeKey); - } - - private void apply(RhiDrawCommand.Builder builder) { - GpuBufferSlice slice = resolver.resolve(); - if (slice != null) { - builder.uniform(name, slice); - } - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DBatchBindings.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DBatchBindings.java new file mode 100644 index 0000000..9870b7d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DBatchBindings.java @@ -0,0 +1,160 @@ +package dev.loki.lovisual.render.engine.renderer; + +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.textures.GpuSampler; +import com.mojang.blaze3d.textures.GpuTextureView; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.AbstractTexture; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.render.engine.rhi.command.RhiDrawCommand; + +import java.util.ArrayList; +import java.util.List; + +/** + * Immutable sampler/uniform binding set for a {@link Renderer3D} batch. Built via the + * fluent {@code withSampler}/{@code withUniform} methods; {@link Renderer3D#batch} and + * the {@code batchTextured} overloads thread it through to the world command buffer. + * Extracted from {@code Renderer3DBatchBindings}; Physically under {@code impl/} but + * declares the parent package for direct access to {@link Renderer3D.UniformResolver}. + */ +public final class Renderer3DBatchBindings { + private static final String DEFAULT_SAMPLER = "u_Texture"; + + private static final Renderer3DBatchBindings EMPTY = new Renderer3DBatchBindings(List.of(), List.of()); + + private final List samplers; + private final List uniforms; + + private Renderer3DBatchBindings(List samplers, List uniforms) { + this.samplers = samplers; + this.uniforms = uniforms; + } + + public static Renderer3DBatchBindings none() { + return EMPTY; + } + + private static String nameOrDefault(String name) { + return name != null ? name : DEFAULT_SAMPLER; + } + + public Renderer3DBatchBindings withSampler(String name, Identifier textureId) { + if (textureId == null) return this; + return appendSampler(new SamplerBinding(nameOrDefault(name), textureId, null, null)); + } + + public Renderer3DBatchBindings withSampler(String name, GpuTextureView view, GpuSampler sampler) { + if (view == null || sampler == null) return this; + return appendSampler(new SamplerBinding(nameOrDefault(name), null, view, sampler)); + } + + public Renderer3DBatchBindings withUniform(String name, GpuBufferSlice slice) { + if (name == null || slice == null) return this; + return appendUniform(UniformBinding.direct(name, slice)); + } + + public Renderer3DBatchBindings withUniform(String name, Object mergeKey, Renderer3D.UniformResolver resolver) { + if (name == null || mergeKey == null || resolver == null) return this; + return appendUniform(UniformBinding.deferred(name, mergeKey, resolver)); + } + + private Renderer3DBatchBindings appendSampler(SamplerBinding binding) { + ArrayList next = new ArrayList<>(samplers.size() + 1); + next.addAll(samplers); + next.add(binding); + return new Renderer3DBatchBindings(List.copyOf(next), uniforms); + } + + private Renderer3DBatchBindings appendUniform(UniformBinding binding) { + ArrayList next = new ArrayList<>(uniforms.size() + 1); + next.addAll(uniforms); + next.add(binding); + return new Renderer3DBatchBindings(samplers, List.copyOf(next)); + } + + public boolean compatibleWith(Renderer3DBatchBindings other) { + if (other == null) return false; + if (samplers.size() != other.samplers.size() || uniforms.size() != other.uniforms.size()) { + return false; + } + + for (int i = 0; i < samplers.size(); i++) { + if (!samplers.get(i).sameBinding(other.samplers.get(i))) { + return false; + } + } + + for (int i = 0; i < uniforms.size(); i++) { + if (!uniforms.get(i).sameBinding(other.uniforms.get(i))) { + return false; + } + } + + return true; + } + + public void applyTo(RhiDrawCommand.Builder builder, Minecraft mc) { + for (SamplerBinding sampler : samplers) { + sampler.apply(builder, mc); + } + for (UniformBinding uniform : uniforms) { + uniform.apply(builder); + } + } + + private record SamplerBinding(String name, + Identifier textureId, + GpuTextureView view, + GpuSampler sampler) { + private boolean sameBinding(SamplerBinding other) { + if (other == null) return false; + if (!name.equals(other.name)) return false; + if (textureId != null || other.textureId != null) { + return textureId != null && textureId.equals(other.textureId); + } + return view == other.view && sampler == other.sampler; + } + + private void apply(RhiDrawCommand.Builder builder, Minecraft mc) { + if (textureId != null) { + if (mc == null) return; + AbstractTexture texture = mc.getTextureManager().getTexture(textureId); + if (texture == null) return; + GpuTextureView textureView = texture.getTextureView(); + GpuSampler textureSampler = texture.getSampler(); + if (textureView == null || textureSampler == null) return; + builder.sampler(name, textureView, textureSampler); + return; + } + + if (view != null && sampler != null) { + builder.sampler(name, view, sampler); + } + } + } + + private record UniformBinding(String name, + Object mergeKey, + Renderer3D.UniformResolver resolver) { + private static UniformBinding direct(String name, GpuBufferSlice slice) { + return new UniformBinding(name, slice, () -> slice); + } + + private static UniformBinding deferred(String name, Object mergeKey, Renderer3D.UniformResolver resolver) { + return new UniformBinding(name, mergeKey, resolver); + } + + private boolean sameBinding(UniformBinding other) { + if (other == null) return false; + return name.equals(other.name) && mergeKey.equals(other.mergeKey); + } + + private void apply(RhiDrawCommand.Builder builder) { + GpuBufferSlice slice = resolver.resolve(); + if (slice != null) { + builder.uniform(name, slice); + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCullOptions.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCullOptions.java new file mode 100644 index 0000000..a4b3492 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCullOptions.java @@ -0,0 +1,24 @@ +package dev.loki.lovisual.render.engine.renderer; + +/** + * Culling flag bundle for {@link Renderer3DCulling}. Extracted from + * {@code Renderer3DCullOptions}; Physically under {@code impl/} but declares the + * parent package to preserve the {@code renderer} namespace. + */ +public record Renderer3DCullOptions(boolean frustum, + boolean sectionVisibility, + boolean visibleOnly, + boolean lookAtTarget, + boolean skipVisibilityWhenTranslucent, + double minLookDot) { + public static final Renderer3DCullOptions NONE = + new Renderer3DCullOptions(false, false, false, false, false, 0.0); + public static final Renderer3DCullOptions FRUSTUM_LOOK = + new Renderer3DCullOptions(true, true, false, true, false, 0.05); + public static final Renderer3DCullOptions SMART_TARGET = + new Renderer3DCullOptions(true, true, true, true, true, 0.05); + + public Renderer3DCullOptions withoutSectionVisibility() { + return new Renderer3DCullOptions(frustum, false, visibleOnly, lookAtTarget, skipVisibilityWhenTranslucent, minLookDot); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCulling.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCulling.java new file mode 100644 index 0000000..8569ee3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/impl/Renderer3DCulling.java @@ -0,0 +1,82 @@ +package dev.loki.lovisual.render.engine.renderer; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; +import dev.loki.lovisual.render.engine.core.policy.VisibilityProvider; +import dev.loki.lovisual.render.engine.core.policy.VisibilityQuery; +import dev.loki.lovisual.render.engine.math.RenderMath; + +/** + * World-primitive visibility/culling helpers for {@link Renderer3D}: frustum, section + * and look-at tests plus line-of-sight, driven by {@link Renderer3DCullOptions}. + * Extracted from {@code Renderer3DCulling}; Physically under {@code impl/} but + * declares the parent package for direct access. + */ +public enum Renderer3DCulling { + ; + + public static boolean shouldRender(Entity target, float tickDelta, Renderer3DCullOptions options) { + if (target == null) return false; + if (options == null || options == Renderer3DCullOptions.NONE) return true; + + AABB visibilityBox = interpolatedBox(target, tickDelta); + if (options.frustum() && !isInFrustum(visibilityBox)) return false; + if (options.sectionVisibility() && !isSectionVisible(visibilityBox)) return false; + if (options.lookAtTarget() && !isInFront(target, tickDelta, options.minLookDot())) return false; + + if (options.visibleOnly()) { + if (!(options.skipVisibilityWhenTranslucent() && RenderState.worldTranslucent)) { + Minecraft mc = Minecraft.getInstance(); + return mc == null || mc.player == null || mc.player.hasLineOfSight(target); + } + } + + return true; + } + + public static boolean isInFrustum(Entity target, float tickDelta) { + return isInFrustum(interpolatedBox(target, tickDelta)); + } + + public static boolean isInFrustum(AABB box) { + if (RenderState.frustum == null || box == null) return true; + return RenderState.frustum.isVisible(box); + } + + public static boolean isSectionVisible(AABB box) { + if (box == null) return true; + RenderFrameContext ctx = LoVisualRenderSystem.currentContext(); + VisibilityProvider provider = ctx != null ? ctx.visibilityProvider() : LoVisualRenderSystem.sodium().visibilityProvider(); + if (provider == null) return true; + return provider.isBoxVisible(box, VisibilityQuery.worldOverlay(box)); + } + + private static AABB interpolatedBox(Entity target, float tickDelta) { + AABB box = target.getBoundingBox(); + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + Vec3 offset = pos.subtract(target.position()); + return box.move(offset); + } + + public static boolean isInFront(Entity target, float tickDelta, double minDot) { + Vec3 center = getTargetCenter(target, tickDelta); + Vec3 camPos = RenderState.cameraPos; + Vec3 to = center.subtract(camPos); + if (to.lengthSqr() <= 1.0e-6) return true; + Vec3 look = RenderState.cameraLook; + if (look.lengthSqr() <= 1.0e-6) { + look = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize(); + } + return look.dot(to.normalize()) > minDot; + } + + private static Vec3 getTargetCenter(Entity target, float tickDelta) { + Vec3 pos = RenderMath.getLerpedPos(target, tickDelta); + return pos.add(0.0, target.getBbHeight() * 0.5, 0.0); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java index 66da8d9..1b94903 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java @@ -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.render.engine.renderer.renderer2d; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java index 3d153f3..34d31f1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java @@ -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.render.engine.renderer.renderer2d; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java index 01ad1a4..7c48d98 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java @@ -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.render.engine.renderer.renderer2d; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java index de1a7c7..5f7d59d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java @@ -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.render.engine.renderer.renderer2d; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java index b3d8a53..8e5efa1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java @@ -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.render.engine.renderer.renderer2d.fx; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java index 0d6306e..e449516 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java @@ -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.render.engine.renderer.renderer2d.fx; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java index a1997b4..af9dca5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java @@ -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.render.engine.renderer.renderer2d.fx; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java index 6812c02..fdc2bb3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java @@ -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.render.engine.renderer.renderer2d.fx; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java index bf382bd..f9eb3e1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java @@ -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.render.engine.renderer.renderer2d.media; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java index e2d5724..8fc5b8a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java @@ -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.render.engine.renderer.renderer2d.media; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java index e55147d..6871087 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java @@ -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.render.engine.renderer.renderer2d.path; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java index acc44f6..a005062 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java @@ -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.render.engine.renderer.renderer2d.path; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java index 6069598..88661cb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java @@ -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.render.engine.renderer.renderer2d.path; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java index c55ca4b..24a19d8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java @@ -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.render.engine.renderer.renderer2d.path; import dev.loki.lovisual.render.engine.command.shape.UiShapeCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java index 5d3ced3..9e0d502 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java @@ -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.render.engine.renderer.ui.batches; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java index 6bed0cb..a2ec884 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java @@ -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.render.engine.renderer.ui.batches; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java index c11852c..18a73aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java @@ -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.render.engine.renderer.ui.batches; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java index f49579a..d9ec721 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java @@ -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.render.engine.renderer.ui.batches; import dev.loki.lovisual.render.engine.command.core.UiCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java index f5830a5..22f72a1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java @@ -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.render.engine.renderer.ui.batches.ordered; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java index 875578f..fc907a5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java @@ -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.render.engine.renderer.ui.batches.ordered; public record UiBatchPlan(int commandCount, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java index 952f501..1a26e8d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java @@ -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.render.engine.renderer.ui.batches.ordered; import dev.loki.lovisual.render.engine.profiler.bridge.DevProfilerBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java index 9f6eb20..c60e486 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java @@ -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.render.engine.renderer.ui.batches.ordered; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java index 791216e..ca9e23f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java @@ -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.render.engine.renderer.ui.batches.ordered.flush; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java index a304a07..7e27be3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java @@ -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.render.engine.renderer.ui.batches.ordered.flush; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java index a125068..4311115 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java @@ -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.render.engine.renderer.ui.batches.ordered.flush; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java index ce2cf8a..911d105 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java @@ -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.render.engine.renderer.ui.batches.ordered.flush; import com.mojang.blaze3d.buffers.GpuBufferSlice; @@ -19,7 +12,7 @@ import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType; import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.order.OrderState; import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources; import dev.loki.lovisual.render.engine.renderer.ui.blur.kawase.SharedKawaseBlur; -import dev.loki.lovisual.render.engine.renderer.ui.item.ItemBatchRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.render.TextRenderSystem; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms; @@ -47,7 +40,7 @@ public final class OrderedFlushRenderer { for (Object entry : state.entries()) { if (entry instanceof ItemBatch itemBatch) { if (!itemBatch.isEmpty()) { - ctx.drawCalls += ItemBatchRenderer.flush(itemBatch); + ctx.drawCalls += Renderer2D.ITEM_BATCH_RENDERER.flush(itemBatch); } continue; } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java index 73e0125..8c71298 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java @@ -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.render.engine.renderer.ui.batches.ordered.order; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java index d618408..267f6c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java @@ -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.render.engine.renderer.ui.batches.ordered.order; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java index f389302..2203d7d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java @@ -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.render.engine.renderer.ui.batches.ordered.order; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java index 2e2e214..1d37ebb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java @@ -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.render.engine.renderer.ui.blur; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java index 083ffe6..1c1e780 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java @@ -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.render.engine.renderer.ui.blur; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java index 1849688..439097e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java @@ -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.render.engine.renderer.ui.blur; @Deprecated(forRemoval = true) diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java index b35f979..f3fc593 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java @@ -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.render.engine.renderer.ui.blur; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java index 37783e6..2a6b809 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java @@ -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.render.engine.renderer.ui.blur.kawase; import com.mojang.blaze3d.pipeline.TextureTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java index 81396f0..828e783 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java @@ -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.render.engine.renderer.ui.blur.kawase; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java index ff5ddd3..ce523ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java @@ -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.render.engine.renderer.ui.blur.kawase; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java index 5278e83..474f97e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java @@ -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.render.engine.renderer.ui.deferred; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java index 0733675..7665b08 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java @@ -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.render.engine.renderer.ui.deferred; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java index 72889b8..8b12f2c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java @@ -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.render.engine.renderer.ui.deferred; import dev.loki.lovisual.render.engine.core.context.ViewportContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java index 9c64550..63355b7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java @@ -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.render.engine.renderer.ui.deferred; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java index c2231ad..5a9367b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java @@ -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.render.engine.renderer.ui.deferred.sched; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java index 107e7b8..2b588f9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java @@ -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.render.engine.renderer.ui.deferred.sched; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java index eba4ff8..3f5d218 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java @@ -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.render.engine.renderer.ui.dispatch; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java index 97d1b54..901cf7a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java @@ -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.render.engine.renderer.ui.dispatch; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java index 5995ee5..a8dedd1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java @@ -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.render.engine.renderer.ui.dispatch; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java index aa9e88b..4ea4157 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java @@ -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.render.engine.renderer.ui.draw.box; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiRect; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java index 2fade0b..1c40ec7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java @@ -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.render.engine.renderer.ui.draw.box; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java index a75ac27..573ec6f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java @@ -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.render.engine.renderer.ui.draw.box; public record UiChamferRadii(float topLeftX, float topLeftY, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java index 83be4dd..d6f0a09 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java @@ -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.render.engine.renderer.ui.draw.box; public record UiRect(float x, float y, float width, float height) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java index 1451869..93876c8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java @@ -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.render.engine.renderer.ui.draw.corner; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java index ebb5ec1..092ab24 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java @@ -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.render.engine.renderer.ui.draw.corner; public enum UiCornerMode { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java index d9c85a7..559aa7f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java @@ -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.render.engine.renderer.ui.draw.corner; public record UiCornerRadii(float topLeft, float topRight, float bottomRight, float bottomLeft) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java index 06970cc..4d0457c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java @@ -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.render.engine.renderer.ui.draw.corner; import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java index d1df21a..eaf3e1b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java @@ -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.render.engine.renderer.ui.draw.edge; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java index 4e7f776..e995095 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java @@ -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.render.engine.renderer.ui.draw.edge; import dev.loki.lovisual.render.engine.renderer.ui.draw.edge.UiEdgeKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java index 7a622ea..c122dc8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java @@ -1,9 +1,4 @@ -/* - * 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.render.engine.renderer.ui.draw.edge; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java index fec430d..533d8b2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java @@ -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.render.engine.renderer.ui.draw.edge; import dev.loki.lovisual.render.engine.renderer.ui.draw.effect.UiPathCap; import dev.loki.lovisual.render.engine.renderer.ui.draw.effect.UiPathJoin; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java index c5d7c7a..09cb430 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java @@ -1,9 +1,4 @@ -/* - * 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.render.engine.renderer.ui.draw.edge; public final class UiGradientGeometry { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java index 092657f..7a963be 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java @@ -1,9 +1,4 @@ -/* - * 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.render.engine.renderer.ui.draw.edge; public final class UiRectGeometry { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java index 8150a65..dfedc92 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java @@ -1,9 +1,4 @@ -/* - * 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.render.engine.renderer.ui.draw.edge; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java index 54ec4bf..67b4c53 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java @@ -1,9 +1,4 @@ -/* - * 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.render.engine.renderer.ui.draw.edge; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java index b807835..3cf5001 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java @@ -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.render.engine.renderer.ui.draw.effect; public enum UiEffectKind { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java index b0b5720..0ac0415 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java @@ -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.render.engine.renderer.ui.draw.effect; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; import dev.loki.lovisual.render.engine.renderer.ui.draw.effect.UiEffectKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java index 86f1112..9e33fda 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java @@ -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.render.engine.renderer.ui.draw.effect; public enum UiPathCap { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java index fc8fb92..e536fd9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java @@ -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.render.engine.renderer.ui.draw.effect; public enum UiPathJoin { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java index 344a699..5400057 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java @@ -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.render.engine.renderer.ui.draw.paint; import dev.loki.lovisual.render.engine.renderer.ui.draw.paint.UiPaintKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java index 472bbb4..8c044a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java @@ -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.render.engine.renderer.ui.draw.paint; public enum UiPaintKind { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java index 725ca16..efb4ab2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java @@ -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.render.engine.renderer.ui.draw.paint; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java index f019c19..45f01ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java @@ -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.render.engine.renderer.ui.draw.shape; import java.util.Arrays; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java index 96d8307..3bf2add 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java @@ -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.render.engine.renderer.ui.draw.shape; public enum UiShapeKind { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java index 57d2510..26b0d8d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java @@ -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.render.engine.renderer.ui.draw.shape; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java index 66f350a..01f68b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java @@ -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.render.engine.renderer.ui.draw.shape; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java index ff7fd13..549187c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java @@ -1,677 +1,93 @@ -/* - * 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.render.engine.renderer.ui.item; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; - import com.mojang.blaze3d.ProjectionType; import com.mojang.blaze3d.buffers.GpuBufferSlice; -import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.FilterMode; import com.mojang.blaze3d.textures.GpuSampler; import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; +import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.render.GuiItemAtlas; -import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; import net.minecraft.client.renderer.item.TrackingItemStackRenderState; -import net.minecraft.util.ARGB; -import net.minecraft.util.CommonColors; -import net.minecraft.util.Mth; import net.minecraft.world.item.ItemDisplayContext; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.Nullable; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; -import dev.loki.lovisual.render.engine.renderer.MeshRenderer; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms; - -import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_COOLDOWN; -import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_COUNT; -import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_DURABILITY; -import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_DURABILITY_TEXT; -import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch; public final class ItemBatchRenderer { - private static final int ITEM_DURABILITY_TRACK_COLOR = 0x72000000; - private static final int ITEM_DURABILITY_TRACK_HIGHLIGHT = 0x26FFFFFF; - private static final int ITEM_OVERLAY_PREALLOCATED_ITEMS = Math.max(32, - Integer.getInteger("lovisual.render.itemOverlay.preallocatedItems", 256)); - - private static FeatureRenderDispatcher itemFeatureDispatcher; - private static GuiItemAtlas itemAtlas; - private static int itemAtlasSlotTextureSize; - private static int itemAtlasTextureSize; - private static MeshBuilder itemBlitMesh; - private static MeshBuilder itemDurabilityGlowMesh; - private static MeshBuilder itemDurabilityRoundedMesh; - private static MeshBuilder itemCooldownMesh; - - static float clampRoundedRadius(float radius, double w, double h) { - return (float) Mth.clamp(radius, 0.0f, (float) Math.min(w, h) * 0.5f); + private final ItemBatchMeshes meshes; + private final ItemBatchAtlas atlas; + private final ItemBatchGeometry geometry; + private final ItemBatchOverlays overlays; + public ItemBatchRenderer() { + this.meshes = new ItemBatchMeshes(); + this.atlas = new ItemBatchAtlas(); + this.geometry = new ItemBatchGeometry(); + this.overlays = new ItemBatchOverlays(); } - - public static void init() { - itemBlitMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_TEXTURED, 1); - itemDurabilityGlowMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, 1); - itemDurabilityRoundedMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_ROUNDED_BATCH, 3); - itemCooldownMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_COLORED, 1); + public void init() { + meshes.init(); } - - private static MeshBuilder createItemOverlayMesh(RenderPipeline pipeline, int quadsPerItem) { - int quads = Math.max(1, ITEM_OVERLAY_PREALLOCATED_ITEMS * Math.max(1, quadsPerItem)); - return new MeshBuilder(pipeline.getVertexFormatBinding(0), pipeline.getPrimitiveTopology(), quads * 4, quads * 6); - } - - private static MeshBuilder beginItemOverlayMesh(MeshBuilder mesh, int commandCount, int quadsPerCommand) { - if (mesh.isBuilding()) { - mesh.end(); - } - - int quads = Math.max(1, Math.max(ITEM_OVERLAY_PREALLOCATED_ITEMS, commandCount) * Math.max(1, quadsPerCommand)); - mesh.reserve(quads * 4, quads * 6); - mesh.begin(); - return mesh; - } - - private static MeshBuilder beginItemBlitMesh(int commandCount) { - if (itemBlitMesh == null) { - itemBlitMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_TEXTURED, 1); - } - return beginItemOverlayMesh(itemBlitMesh, commandCount, 1); - } - - private static MeshBuilder beginItemDurabilityGlowMesh(int commandCount) { - if (itemDurabilityGlowMesh == null) { - itemDurabilityGlowMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, 1); - } - return beginItemOverlayMesh(itemDurabilityGlowMesh, commandCount, 1); - } - - private static MeshBuilder beginItemDurabilityRoundedMesh(int commandCount) { - if (itemDurabilityRoundedMesh == null) { - itemDurabilityRoundedMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_ROUNDED_BATCH, 3); - } - return beginItemOverlayMesh(itemDurabilityRoundedMesh, commandCount, 3); - } - - private static MeshBuilder beginItemCooldownMesh(int commandCount) { - if (itemCooldownMesh == null) { - itemCooldownMesh = createItemOverlayMesh(LoVisualRenderPipelines.UI_COLORED, 1); - } - return beginItemOverlayMesh(itemCooldownMesh, commandCount, 1); - } - - public static int flush(ItemBatch batch) { - + public int flush(ItemBatch batch) { Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.gameRenderer == null || batch.isEmpty()) { - return 0; - } - - try (RenderCostProfiler.Scope ignoredItems = RenderCostProfiler.itemRender("item_batch:" + batch.commands.size())) { + if (mc == null || mc.gameRenderer == null || batch.isEmpty()) return 0; + try (RenderCostProfiler.Scope scope = RenderCostProfiler.itemRender("item_batch:" + batch.commands.size())) { int drawCalls = 0; Object2ObjectOpenHashMap resolved = batch.resolvedStates; resolved.clear(); - GpuTextureView previousOutputColor = RenderSystem.outputColorTextureOverride; - GpuTextureView previousOutputDepth = RenderSystem.outputDepthTextureOverride; + GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; + GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; try { - FeatureRenderDispatcher dispatcher = getItemFeatureDispatcher(mc); - ObjectOpenHashSet modelIdentities = new ObjectOpenHashSet<>(); - for (ItemDrawCommand command : batch.commands) { - if (command.stack.isEmpty() || !command.drawItem || command.alpha <= 0.001f) { - continue; - } - - TrackingItemStackRenderState renderState = resolved.computeIfAbsent( - new ItemResolveKey(command.player, command.stack, command.seed), - key -> { - try (RenderCostProfiler.Scope ignoredResolve = RenderCostProfiler.itemRender("resolve_model")) { - TrackingItemStackRenderState state = new TrackingItemStackRenderState(); - mc.getItemModelResolver().updateForTopItem( - state, - command.stack, + FeatureRenderDispatcher dispatcher = atlas.getDispatcher(mc); + ObjectOpenHashSet identities = new ObjectOpenHashSet<>(); + for (ItemDrawCommand c : batch.commands) { + if (c.stack.isEmpty() || !c.drawItem || c.alpha <= 0.001f) continue; + TrackingItemStackRenderState st = resolved.computeIfAbsent( + new ItemResolveKey(c.player, c.stack, c.seed), key -> { + try (RenderCostProfiler.Scope s = RenderCostProfiler.itemRender("resolve_model")) { + TrackingItemStackRenderState ns = new TrackingItemStackRenderState(); + mc.getItemModelResolver().updateForTopItem(ns, c.stack, ItemDisplayContext.GUI, - command.player != null ? command.player.level() : mc.level, - command.player, - command.seed - ); - return state; + c.player != null ? c.player.level() : mc.level, + c.player, c.seed); + return ns; } - } - ); - - if (!renderState.isEmpty()) { - modelIdentities.add(renderState.getModelIdentity()); - } + }); + if (!st.isEmpty()) identities.add(st.getModelIdentity()); } - - GuiItemAtlas atlas = null; + GuiItemAtlas atlasObj = null; MeshBuilder itemMesh = null; - GpuSampler itemSampler = null; - if (!modelIdentities.isEmpty()) { - atlas = ensureItemAtlas(mc, dispatcher, modelIdentities); - itemMesh = beginItemBlitMesh(batch.commands.size()); - itemSampler = RenderSystem.getSamplerCache().getRepeat(FilterMode.NEAREST); + GpuSampler sampler = null; + if (!identities.isEmpty()) { + atlasObj = atlas.ensureAtlas(mc, dispatcher, identities); + itemMesh = meshes.beginBlitMesh(batch.commands.size()); + sampler = RenderSystem.getSamplerCache().getRepeat(FilterMode.NEAREST); } - - GpuTextureView itemAtlasTextureView = null; - GpuBufferSlice previousProjection = RenderSystem.getProjectionMatrixBuffer(); - ProjectionType previousProjectionType = RenderSystem.getProjectionType(); + GpuTextureView atlasView = null; + GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer(); + ProjectionType prevType = RenderSystem.getProjectionType(); try { - for (ItemDrawCommand command : batch.commands) { - if (atlas == null || itemMesh == null || itemSampler == null || command.stack.isEmpty() || !command.drawItem || command.alpha <= 0.001f) { - continue; - } - - TrackingItemStackRenderState renderState = resolved.get(new ItemResolveKey(command.player, command.stack, command.seed)); - if (renderState == null || renderState.isEmpty()) { - continue; - } - - try (RenderCostProfiler.Scope ignoredReplay = RenderCostProfiler.itemRender("atlas_model")) { - GuiItemAtlas.SlotView slot = atlas.getOrUpdate(renderState); - if (slot != null && slot.textureView() != null) { - itemAtlasTextureView = slot.textureView(); - appendItemAtlasBlit(itemMesh, command, slot); - } - } + if (atlasObj != null && itemMesh != null && sampler != null) { + atlasView = atlas.fillBlits(batch, resolved, atlasObj, itemMesh, geometry); } } finally { - if (previousProjection != null && previousProjectionType != null) { - RenderSystem.setProjectionMatrix(previousProjection, previousProjectionType); - } - RenderSystem.outputColorTextureOverride = previousOutputColor; - RenderSystem.outputDepthTextureOverride = previousOutputDepth; - } - - if (submitItemBlitMesh(mc, itemMesh, itemAtlasTextureView, itemSampler)) { - drawCalls++; - } - if (atlas != null) { - atlas.endFrame(); - } - - MeshBuilder durabilityGlowMesh = null; - MeshBuilder durabilityRoundedMesh = null; - MeshBuilder cooldownMesh = null; - for (ItemDrawCommand command : batch.commands) { - if (hasItemDurabilityBar(command)) { - if (durabilityGlowMesh == null) { - durabilityGlowMesh = beginItemDurabilityGlowMesh(batch.commands.size()); - } - if (durabilityRoundedMesh == null) { - durabilityRoundedMesh = beginItemDurabilityRoundedMesh(batch.commands.size()); - } - appendItemDurabilityBar(command, durabilityGlowMesh, durabilityRoundedMesh); - } - - if (hasItemCooldownOverlay(mc, command)) { - if (cooldownMesh == null) { - cooldownMesh = beginItemCooldownMesh(batch.commands.size()); - } - appendItemCooldownOverlayQuads(mc, command, cooldownMesh); - } - } - - if (submitItemOverlayMesh(mc, LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, durabilityGlowMesh, true)) { - drawCalls++; - } - if (submitItemOverlayMesh(mc, LoVisualRenderPipelines.UI_ROUNDED_BATCH, durabilityRoundedMesh, true)) { - drawCalls++; - } - if (submitItemOverlayMesh(mc, LoVisualRenderPipelines.UI_COLORED, cooldownMesh, false)) { - drawCalls++; - } - - TextRenderer overlayTextRenderer = null; - float overlayTextScale = Float.NaN; - try { - for (ItemDrawCommand command : batch.commands) { - if (hasItemDurabilityText(command)) { - String durabilityText = getItemDurabilityText(command); - if (durabilityText != null && !durabilityText.isEmpty()) { - float textScale = Math.max(0.0001f, command.scaleX * 0.44f); - if (overlayTextRenderer == null) { - overlayTextRenderer = TextRenderer.get(); - } - if (!overlayTextRenderer.isBuilding() || Float.compare(overlayTextScale, textScale) != 0) { - if (overlayTextRenderer.isBuilding()) { - overlayTextRenderer.end(); - } - overlayTextRenderer.begin(textScale, false, false); - overlayTextScale = textScale; - } - - float textW = (float) overlayTextRenderer.getWidth(durabilityText, false); - float textX = command.x + (16.0f * command.scaleX - textW) * 0.5f; - float textY = command.y + 11.0f * command.scaleX; - overlayTextRenderer.render(durabilityText, textX, textY, - new RenderColor(multiplyAlpha(getItemDurabilityTextColor(command), command.alpha)), true); - } - } - - String overlayText = getItemOverlayText(command); - if (overlayText == null || overlayText.isEmpty()) { - continue; - } - - float textScale = Math.max(0.0001f, command.scaleX * 0.5f); - if (overlayTextRenderer == null) { - overlayTextRenderer = TextRenderer.get(); - } - if (!overlayTextRenderer.isBuilding() || Float.compare(overlayTextScale, textScale) != 0) { - if (overlayTextRenderer.isBuilding()) { - overlayTextRenderer.end(); - } - overlayTextRenderer.begin(textScale, false, false); - overlayTextScale = textScale; - } - - float textX = command.x + 19.0f * command.scaleX - 2.0f * command.scaleX - - (float) overlayTextRenderer.getWidth(overlayText, false); - float textY = command.y + 9.0f * command.scaleX; - overlayTextRenderer.render(overlayText, textX, textY, new RenderColor(multiplyAlpha(CommonColors.WHITE, command.alpha)), true); - } - } finally { - if (overlayTextRenderer != null && overlayTextRenderer.isBuilding()) { - overlayTextRenderer.end(); - } + if (prevProj != null && prevType != null) RenderSystem.setProjectionMatrix(prevProj, prevType); + RenderSystem.outputColorTextureOverride = prevColor; + RenderSystem.outputDepthTextureOverride = prevDepth; } + if (meshes.submitBlitMesh(mc, itemMesh, atlasView, sampler)) drawCalls++; + if (atlasObj != null) atlasObj.endFrame(); + drawCalls += overlays.handleOverlays(batch, mc, meshes, geometry); + overlays.renderTexts(batch, geometry); } finally { - RenderSystem.outputColorTextureOverride = previousOutputColor; - RenderSystem.outputDepthTextureOverride = previousOutputDepth; + RenderSystem.outputColorTextureOverride = prevColor; + RenderSystem.outputDepthTextureOverride = prevDepth; resolved.clear(); } - return drawCalls; } } - - private static GuiItemAtlas ensureItemAtlas(Minecraft mc, - FeatureRenderDispatcher dispatcher, - ObjectOpenHashSet modelIdentities) { - int guiScale = Math.max(1, (int) Math.round(mc.getWindow().getGuiScale())); - int slotTextureSize = Math.max(16, 16 * guiScale); - int requiredTextureSize = GuiItemAtlas.computeTextureSizeFor(slotTextureSize, Math.max(1, modelIdentities.size())); - - if (itemAtlas != null - && itemAtlasSlotTextureSize == slotTextureSize - && itemAtlasTextureSize >= requiredTextureSize - && !itemAtlas.tryPrepareFor(modelIdentities)) { - closeItemAtlas(); - } - - if (itemAtlas == null || itemAtlasSlotTextureSize != slotTextureSize || itemAtlasTextureSize < requiredTextureSize) { - closeItemAtlas(); - itemAtlas = new GuiItemAtlas(dispatcher, requiredTextureSize, slotTextureSize); - itemAtlasSlotTextureSize = slotTextureSize; - itemAtlasTextureSize = requiredTextureSize; - } - - return itemAtlas; - } - - private static void closeItemAtlas() { - if (itemAtlas != null) { - itemAtlas.close(); - itemAtlas = null; - } - itemAtlasSlotTextureSize = 0; - itemAtlasTextureSize = 0; - } - - private static FeatureRenderDispatcher getItemFeatureDispatcher(Minecraft mc) { - if (itemFeatureDispatcher == null) { - itemFeatureDispatcher = new FeatureRenderDispatcher( - mc.gameRenderer.renderBuffers(), - mc.getModelManager(), - mc.getAtlasManager(), - mc.font, - mc.gameRenderer.gameRenderState() - ); - } - return itemFeatureDispatcher; - } - - private static boolean hasItemDurabilityBar(ItemDrawCommand command) { - int overlayFlags = command.overlayFlags; - return (overlayFlags & ITEM_OVERLAY_DURABILITY) != 0 - && (overlayFlags & ITEM_OVERLAY_DURABILITY_TEXT) == 0 - && isItemDurabilityOverlayVisible(command); - } - - private static boolean hasItemDurabilityText(ItemDrawCommand command) { - return (command.overlayFlags & ITEM_OVERLAY_DURABILITY_TEXT) != 0 - && isItemDurabilityOverlayVisible(command); - } - - private static boolean isItemDurabilityOverlayVisible(ItemDrawCommand command) { - ItemStack stack = command.stack; - if (stack == null || stack.isEmpty() || !stack.isDamageableItem() || !stack.isBarVisible()) { - return false; - } - int threshold = Mth.clamp(command.durabilityThresholdPercent, 0, 100); - return durabilityPercentInt(stack) <= threshold; - } - - private static boolean hasItemCooldownOverlay(Minecraft mc, ItemDrawCommand command) { - if ((command.overlayFlags & ITEM_OVERLAY_COOLDOWN) == 0) { - return false; - } - LocalPlayer player = mc.player; - float progress = player == null - ? 0.0f - : player.getCooldowns().getCooldownPercent(command.stack, mc.getDeltaTracker().getGameTimeDeltaPartialTick(true)); - return progress > 0.0f; - } - - private static void appendItemDurabilityBar(ItemDrawCommand command, MeshBuilder glowMesh, MeshBuilder roundedMesh) { - ItemStack stack = command.stack; - float x = command.x; - float y = command.y; - float scale = command.scaleX; - float barX = x + 2.0f * scale; - float barY = y + 13.15f * scale; - float barW = 13.0f * scale; - float barH = Math.max(1.35f * scale, 1.0f); - float radius = Math.max(0.85f * scale, barH * 0.5f); - float pct = durabilityPercent(stack); - float fillW = Mth.clamp(barW * pct, 0.0f, barW); - if (fillW <= 0.01f) { - return; - } - - int base = ARGB.opaque(stack.getBarColor()); - int bright = adjustRgb(base, 1.18f); - int deep = adjustRgb(base, 0.74f); - int glow = multiplyAlpha(withAlpha(base, Mth.clamp(96 + Math.round((1.0f - pct) * 48.0f), 96, 144)), command.alpha); - int innerGlow = multiplyAlpha(withAlpha(adjustRgb(base, 1.35f), 118), command.alpha); - int trackTop = multiplyAlpha(ITEM_DURABILITY_TRACK_HIGHLIGHT, command.alpha); - int trackBottom = multiplyAlpha(ITEM_DURABILITY_TRACK_COLOR, command.alpha); - int brightAlpha = multiplyAlpha(bright, command.alpha); - int deepAlpha = multiplyAlpha(deep, command.alpha); - - appendRoundedGlowQuad(glowMesh, barX, barY, fillW, barH, radius, Math.max(3.0f * scale, 2.0f), glow); - appendRoundedRectQuad(roundedMesh, barX, barY, barW, barH, - barX, barY, barW, barH, - radius, trackTop, trackTop, trackBottom, trackBottom); - appendRoundedRectQuad(roundedMesh, barX, barY, fillW, barH, - barX, barY, barW, barH, - radius, brightAlpha, brightAlpha, deepAlpha, deepAlpha); - appendRoundedRectQuad(roundedMesh, barX, barY, Math.min(fillW, Math.max(1.2f * scale, fillW * 0.28f)), barH, - barX, barY, barW, barH, - radius, innerGlow, multiplyAlpha(withAlpha(innerGlow, 70), command.alpha), multiplyAlpha(withAlpha(innerGlow, 34), command.alpha), multiplyAlpha(withAlpha(innerGlow, 58), command.alpha)); - } - - private static void appendItemCooldownOverlayQuads(Minecraft mc, ItemDrawCommand command, MeshBuilder mesh) { - if ((command.overlayFlags & ITEM_OVERLAY_COOLDOWN) == 0) { - return; - } - LocalPlayer player = mc.player; - float progress = player == null - ? 0.0f - : player.getCooldowns().getCooldownPercent(command.stack, mc.getDeltaTracker().getGameTimeDeltaPartialTick(true)); - if (progress > 0.0f) { - float scale = command.scaleX; - float top = command.y + Mth.floor(16.0f * (1.0f - progress)) * scale; - float height = Mth.ceil(16.0f * progress) * scale; - appendQuad(mesh, command.x, top, 16.0f * scale, height, multiplyAlpha(Integer.MAX_VALUE, command.alpha)); - } - } - - private static @Nullable String getItemOverlayText(ItemStack stack, int overlayFlags, @Nullable String stackCountText) { - if (stack == null || stack.isEmpty()) return null; - if ((overlayFlags & ITEM_OVERLAY_COUNT) != 0 && (stack.getCount() != 1 || stackCountText != null)) { - return stackCountText == null ? String.valueOf(stack.getCount()) : stackCountText; - } - return null; - } - - private static @Nullable String getItemOverlayText(ItemDrawCommand command) { - return getItemOverlayText(command.stack, command.overlayFlags, command.stackCountText); - } - - private static @Nullable String getItemDurabilityText(ItemDrawCommand command) { - if (!hasItemDurabilityText(command)) { - return null; - } - return durabilityPercentInt(command.stack) + "%"; - } - - private static int getItemDurabilityTextColor(ItemDrawCommand command) { - int pct = durabilityPercentInt(command.stack); - int threshold = Mth.clamp(command.durabilityTextColorThresholdPercent, 0, 100); - if (pct <= threshold) { - return ARGB.opaque(command.stack.getBarColor()); - } - return CommonColors.WHITE; - } - - private static boolean submitItemBlitMesh(Minecraft mc, - @Nullable MeshBuilder mesh, - @Nullable GpuTextureView atlasTextureView, - @Nullable GpuSampler sampler) { - if (mesh == null || sampler == null || atlasTextureView == null) { - return false; - } - if (mesh.isBuilding()) { - mesh.end(); - } - if (mesh.getIndicesCount() <= 0) { - return false; - } - - MeshRenderer.begin() - .attachments(mc.gameRenderer.mainRenderTarget().getColorTextureView(), null) - .pipeline(LoVisualRenderPipelines.UI_TEXTURED) - .mesh(mesh) - .sampler("u_Texture", atlasTextureView, sampler) - .end(); - return true; - } - - private static void appendItemAtlasBlit(MeshBuilder mesh, ItemDrawCommand command, GuiItemAtlas.SlotView slot) { - mesh.ensureQuadCapacity(); - - double x0; - double y0; - double x1; - double y1; - double x2; - double y2; - double x3; - double y3; - - if (command.pivoted) { - x0 = transformItemX(command, 0.0f); - y0 = transformItemY(command, 0.0f); - x1 = transformItemX(command, 0.0f); - y1 = transformItemY(command, 16.0f); - x2 = transformItemX(command, 16.0f); - y2 = transformItemY(command, 16.0f); - x3 = transformItemX(command, 16.0f); - y3 = transformItemY(command, 0.0f); - } else { - x0 = command.x; - y0 = command.y; - x1 = command.x; - y1 = command.y + 16.0f * command.scaleY; - x2 = command.x + 16.0f * command.scaleX; - y2 = command.y + 16.0f * command.scaleY; - x3 = command.x + 16.0f * command.scaleX; - y3 = command.y; - } - - int alpha = Mth.clamp(Math.round(command.alpha * 255.0f), 0, 255); - int i1 = mesh.vec2(x0, y0).raw2(slot.u0(), slot.v0()).color(255, 255, 255, alpha).next(); - int i2 = mesh.vec2(x1, y1).raw2(slot.u0(), slot.v1()).color(255, 255, 255, alpha).next(); - int i3 = mesh.vec2(x2, y2).raw2(slot.u1(), slot.v1()).color(255, 255, 255, alpha).next(); - int i4 = mesh.vec2(x3, y3).raw2(slot.u1(), slot.v0()).color(255, 255, 255, alpha).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static double transformItemX(ItemDrawCommand command, float localX) { - return command.x + command.pivotX + (localX - command.pivotX) * command.scaleX; - } - - private static double transformItemY(ItemDrawCommand command, float localY) { - return command.y + command.pivotY + (localY - command.pivotY) * command.scaleY; - } - - private static boolean submitItemOverlayMesh(Minecraft mc, - RenderPipeline pipeline, - @Nullable MeshBuilder mesh, - boolean uiBatchUniform) { - if (mesh == null) { - return false; - } - if (mesh.isBuilding()) { - mesh.end(); - } - if (mesh.getIndicesCount() <= 0) { - return false; - } - - MeshRenderer renderer = MeshRenderer.begin() - .attachments(mc.gameRenderer.mainRenderTarget().getColorTextureView(), null) - .pipeline(pipeline) - .mesh(mesh); - if (uiBatchUniform) { - UIBatchUniforms.update(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - renderer.uniform("UIBatch", UIBatchUniforms.get()); - } - renderer.end(); - return true; - } - - private static void appendRoundedRectQuad(MeshBuilder mesh, - double x, - double y, - double w, - double h, - double maskX, - double maskY, - double maskW, - double maskH, - float radius, - int cTopLeft, - int cTopRight, - int cBottomRight, - int cBottomLeft) { - if (w <= 0.0 || h <= 0.0 || maskW <= 0.0 || maskH <= 0.0) { - return; - } - mesh.ensureQuadCapacity(); - - int tlA = (cTopLeft >>> 24) & 0xFF; - int tlR = (cTopLeft >>> 16) & 0xFF; - int tlG = (cTopLeft >>> 8) & 0xFF; - int tlB = cTopLeft & 0xFF; - int trA = (cTopRight >>> 24) & 0xFF; - int trR = (cTopRight >>> 16) & 0xFF; - int trG = (cTopRight >>> 8) & 0xFF; - int trB = cTopRight & 0xFF; - int brA = (cBottomRight >>> 24) & 0xFF; - int brR = (cBottomRight >>> 16) & 0xFF; - int brG = (cBottomRight >>> 8) & 0xFF; - int brB = cBottomRight & 0xFF; - int blA = (cBottomLeft >>> 24) & 0xFF; - int blR = (cBottomLeft >>> 16) & 0xFF; - int blG = (cBottomLeft >>> 8) & 0xFF; - int blB = cBottomLeft & 0xFF; - float clampedRadius = clampRoundedRadius(radius, maskW, maskH); - - int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); - int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); - int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); - int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(maskX, maskY, maskW, maskH).vec4(clampedRadius, 0.0f, 0f, 0f).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void appendRoundedGlowQuad(MeshBuilder mesh, - double x, - double y, - double w, - double h, - float radius, - float glow, - int argb) { - if (w <= 0.0 || h <= 0.0 || glow <= 0.0f) { - return; - } - mesh.ensureQuadCapacity(); - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - float clampedRadius = clampRoundedRadius(radius, w, h); - - double gx = x - glow; - double gy = y - glow; - double gw = w + glow * 2.0; - double gh = h + glow * 2.0; - - int i1 = mesh.vec2(gx, gy).local2(gx, gy).color(r, g, b, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); - int i2 = mesh.vec2(gx, gy + gh).local2(gx, gy + gh).color(r, g, b, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); - int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(r, g, b, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); - int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(r, g, b, a).vec4(x, y, w, h).vec4(clampedRadius, 0.0f, glow, 0f).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static float durabilityPercent(ItemStack stack) { - int max = stack.getMaxDamage(); - if (max <= 0) { - return 1.0f; - } - return Mth.clamp((max - stack.getDamageValue()) / (float) max, 0.0f, 1.0f); - } - - private static int durabilityPercentInt(ItemStack stack) { - return Math.round(durabilityPercent(stack) * 100.0f); - } - - private static int withAlpha(int argb, int alpha) { - return (Mth.clamp(alpha, 0, 255) << 24) | (argb & 0x00FFFFFF); - } - - private static int multiplyAlpha(int argb, float alphaFactor) { - int alpha = (argb >>> 24) & 0xFF; - return withAlpha(argb, Math.round(alpha * Mth.clamp(alphaFactor, 0.0f, 1.0f))); - } - - private static int adjustRgb(int argb, float factor) { - int a = (argb >>> 24) & 0xFF; - int r = Mth.clamp(Math.round(((argb >>> 16) & 0xFF) * factor), 0, 255); - int g = Mth.clamp(Math.round(((argb >>> 8) & 0xFF) * factor), 0, 255); - int b = Mth.clamp(Math.round((argb & 0xFF) * factor), 0, 255); - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static void appendQuad(MeshBuilder mesh, double x, double y, double width, double height, int argb) { - mesh.ensureQuadCapacity(); - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - int i1 = mesh.vec2(x, y).color(r, g, b, a).next(); - int i2 = mesh.vec2(x, y + height).color(r, g, b, a).next(); - int i3 = mesh.vec2(x + width, y + height).color(r, g, b, a).next(); - int i4 = mesh.vec2(x + width, y).color(r, g, b, a).next(); - mesh.quad(i1, i2, i3, i4); - } - } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java index 1ab103f..bb6f841 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java @@ -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.render.engine.renderer.ui.item; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java index aef57a0..f337746 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java @@ -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.render.engine.renderer.ui.item; import net.minecraft.client.player.LocalPlayer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java index b43eb01..cc448b3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java @@ -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.render.engine.renderer.ui.item; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchAtlas.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchAtlas.java new file mode 100644 index 0000000..381fc99 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchAtlas.java @@ -0,0 +1,49 @@ +package dev.loki.lovisual.render.engine.renderer.ui.item; + +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.render.GuiItemAtlas; +import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; +import net.minecraft.client.renderer.item.TrackingItemStackRenderState; + +final class ItemBatchAtlas { + private FeatureRenderDispatcher dispatcher; + private GuiItemAtlas atlas; + private int slotTextureSize; + private int textureSize; + FeatureRenderDispatcher getDispatcher(Minecraft mc) { + if (dispatcher == null) dispatcher = new FeatureRenderDispatcher(mc.gameRenderer.renderBuffers(), + mc.getModelManager(), mc.getAtlasManager(), mc.font, mc.gameRenderer.gameRenderState()); + return dispatcher; + } + GuiItemAtlas ensureAtlas(Minecraft mc, FeatureRenderDispatcher d, ObjectOpenHashSet ids) { + int guiScale = Math.max(1, (int) Math.round(mc.getWindow().getGuiScale())); + int slotSize = Math.max(16, 16 * guiScale); + int req = GuiItemAtlas.computeTextureSizeFor(slotSize, Math.max(1, ids.size())); + if (atlas != null && slotTextureSize == slotSize && textureSize >= req && !atlas.tryPrepareFor(ids)) closeAtlas(); + if (atlas == null || slotTextureSize != slotSize || textureSize < req) { + closeAtlas(); atlas = new GuiItemAtlas(d, req, slotSize); slotTextureSize = slotSize; textureSize = req; + } + return atlas; + } + void closeAtlas() { if (atlas != null) { atlas.close(); atlas = null; } slotTextureSize = 0; textureSize = 0; } + GpuTextureView fillBlits(dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch batch, + Object2ObjectOpenHashMap resolved, + GuiItemAtlas atlasObj, MeshBuilder mesh, ItemBatchGeometry g) { + GpuTextureView view = null; + for (ItemDrawCommand c : batch.commands) { + if (c.stack.isEmpty() || !c.drawItem || c.alpha <= 0.001f) continue; + TrackingItemStackRenderState st = resolved.get(new ItemResolveKey(c.player, c.stack, c.seed)); + if (st == null || st.isEmpty()) continue; + try (RenderCostProfiler.Scope ign = RenderCostProfiler.itemRender("atlas_model")) { + GuiItemAtlas.SlotView slot = atlasObj.getOrUpdate(st); + if (slot != null && slot.textureView() != null) { view = slot.textureView(); g.appendItemAtlasBlit(mesh, c, slot); } + } + } + return view; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchGeometry.java new file mode 100644 index 0000000..fe99ffb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchGeometry.java @@ -0,0 +1,91 @@ +package dev.loki.lovisual.render.engine.renderer.ui.item; + +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.gui.render.GuiItemAtlas; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemStack; + +final class ItemBatchGeometry { + float clampRoundedRadius(float radius, double w, double h) { + return (float) Mth.clamp(radius, 0.0f, (float) Math.min(w, h) * 0.5f); + } + void appendItemAtlasBlit(MeshBuilder mesh, ItemDrawCommand command, GuiItemAtlas.SlotView slot) { + mesh.ensureQuadCapacity(); + double x0; double y0; double x1; double y1; double x2; double y2; double x3; double y3; + if (command.pivoted) { + x0 = transformItemX(command, 0.0f); y0 = transformItemY(command, 0.0f); + x1 = transformItemX(command, 0.0f); y1 = transformItemY(command, 16.0f); + x2 = transformItemX(command, 16.0f); y2 = transformItemY(command, 16.0f); + x3 = transformItemX(command, 16.0f); y3 = transformItemY(command, 0.0f); + } else { + x0 = command.x; y0 = command.y; + x1 = command.x; y1 = command.y + 16.0f * command.scaleY; + x2 = command.x + 16.0f * command.scaleX; y2 = command.y + 16.0f * command.scaleY; + x3 = command.x + 16.0f * command.scaleX; y3 = command.y; + } + int alpha = Mth.clamp(Math.round(command.alpha * 255.0f), 0, 255); + int i1 = mesh.vec2(x0, y0).raw2(slot.u0(), slot.v0()).color(255, 255, 255, alpha).next(); + int i2 = mesh.vec2(x1, y1).raw2(slot.u0(), slot.v1()).color(255, 255, 255, alpha).next(); + int i3 = mesh.vec2(x2, y2).raw2(slot.u1(), slot.v1()).color(255, 255, 255, alpha).next(); + int i4 = mesh.vec2(x3, y3).raw2(slot.u1(), slot.v0()).color(255, 255, 255, alpha).next(); + mesh.quad(i1, i2, i3, i4); + } + double transformItemX(ItemDrawCommand c, float lx) { return c.x + c.pivotX + (lx - c.pivotX) * c.scaleX; } + double transformItemY(ItemDrawCommand c, float ly) { return c.y + c.pivotY + (ly - c.pivotY) * c.scaleY; } + void appendRoundedRectQuad(MeshBuilder mesh, double x, double y, double w, double h, + double maskX, double maskY, double maskW, double maskH, + float radius, int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { + if (w <= 0.0 || h <= 0.0 || maskW <= 0.0 || maskH <= 0.0) return; + mesh.ensureQuadCapacity(); + int tlA = (cTopLeft >>> 24) & 0xFF; int tlR = (cTopLeft >>> 16) & 0xFF; + int tlG = (cTopLeft >>> 8) & 0xFF; int tlB = cTopLeft & 0xFF; + int trA = (cTopRight >>> 24) & 0xFF; int trR = (cTopRight >>> 16) & 0xFF; + int trG = (cTopRight >>> 8) & 0xFF; int trB = cTopRight & 0xFF; + int brA = (cBottomRight >>> 24) & 0xFF; int brR = (cBottomRight >>> 16) & 0xFF; + int brG = (cBottomRight >>> 8) & 0xFF; int brB = cBottomRight & 0xFF; + int blA = (cBottomLeft >>> 24) & 0xFF; int blR = (cBottomLeft >>> 16) & 0xFF; + int blG = (cBottomLeft >>> 8) & 0xFF; int blB = cBottomLeft & 0xFF; + float cr = clampRoundedRadius(radius, maskW, maskH); + int i1 = mesh.vec2(x, y).local2(x, y).color(tlR, tlG, tlB, tlA).vec4(maskX, maskY, maskW, maskH).vec4(cr, 0.0f, 0f, 0f).next(); + int i2 = mesh.vec2(x, y + h).local2(x, y + h).color(blR, blG, blB, blA).vec4(maskX, maskY, maskW, maskH).vec4(cr, 0.0f, 0f, 0f).next(); + int i3 = mesh.vec2(x + w, y + h).local2(x + w, y + h).color(brR, brG, brB, brA).vec4(maskX, maskY, maskW, maskH).vec4(cr, 0.0f, 0f, 0f).next(); + int i4 = mesh.vec2(x + w, y).local2(x + w, y).color(trR, trG, trB, trA).vec4(maskX, maskY, maskW, maskH).vec4(cr, 0.0f, 0f, 0f).next(); + mesh.quad(i1, i2, i3, i4); + } + void appendRoundedGlowQuad(MeshBuilder mesh, double x, double y, double w, double h, + float radius, float glow, int argb) { + if (w <= 0.0 || h <= 0.0 || glow <= 0.0f) return; + mesh.ensureQuadCapacity(); + int a = (argb >>> 24) & 0xFF; int r = (argb >>> 16) & 0xFF; int g = (argb >>> 8) & 0xFF; int b = argb & 0xFF; + float cr = clampRoundedRadius(radius, w, h); + double gx = x - glow; double gy = y - glow; double gw = w + glow * 2.0; double gh = h + glow * 2.0; + int i1 = mesh.vec2(gx, gy).local2(gx, gy).color(r, g, b, a).vec4(x, y, w, h).vec4(cr, 0.0f, glow, 0f).next(); + int i2 = mesh.vec2(gx, gy + gh).local2(gx, gy + gh).color(r, g, b, a).vec4(x, y, w, h).vec4(cr, 0.0f, glow, 0f).next(); + int i3 = mesh.vec2(gx + gw, gy + gh).local2(gx + gw, gy + gh).color(r, g, b, a).vec4(x, y, w, h).vec4(cr, 0.0f, glow, 0f).next(); + int i4 = mesh.vec2(gx + gw, gy).local2(gx + gw, gy).color(r, g, b, a).vec4(x, y, w, h).vec4(cr, 0.0f, glow, 0f).next(); + mesh.quad(i1, i2, i3, i4); + } + void appendQuad(MeshBuilder mesh, double x, double y, double width, double height, int argb) { + mesh.ensureQuadCapacity(); + int a = (argb >>> 24) & 0xFF; int r = (argb >>> 16) & 0xFF; int g = (argb >>> 8) & 0xFF; int b = argb & 0xFF; + int i1 = mesh.vec2(x, y).color(r, g, b, a).next(); + int i2 = mesh.vec2(x, y + height).color(r, g, b, a).next(); + int i3 = mesh.vec2(x + width, y + height).color(r, g, b, a).next(); + int i4 = mesh.vec2(x + width, y).color(r, g, b, a).next(); + mesh.quad(i1, i2, i3, i4); + } + float durabilityPercent(ItemStack s) { + int max = s.getMaxDamage(); if (max <= 0) return 1.0f; + return Mth.clamp((max - s.getDamageValue()) / (float) max, 0.0f, 1.0f); + } + int durabilityPercentInt(ItemStack s) { return Math.round(durabilityPercent(s) * 100.0f); } + int withAlpha(int argb, int alpha) { return (Mth.clamp(alpha, 0, 255) << 24) | (argb & 0x00FFFFFF); } + int multiplyAlpha(int argb, float f) { int a = (argb >>> 24) & 0xFF; return withAlpha(argb, Math.round(a * Mth.clamp(f, 0.0f, 1.0f))); } + int adjustRgb(int argb, float t) { + int a = (argb >>> 24) & 0xFF; + int r = Mth.clamp(Math.round(((argb >>> 16) & 0xFF) * t), 0, 255); + int g = Mth.clamp(Math.round(((argb >>> 8) & 0xFF) * t), 0, 255); + int b = Mth.clamp(Math.round((argb & 0xFF) * t), 0, 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchMeshes.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchMeshes.java new file mode 100644 index 0000000..c803acd --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchMeshes.java @@ -0,0 +1,70 @@ +package dev.loki.lovisual.render.engine.renderer.ui.item; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.textures.GpuSampler; +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms; +import net.minecraft.client.Minecraft; +import org.jetbrains.annotations.Nullable; + +final class ItemBatchMeshes { + private final int preallocatedItems; + private MeshBuilder itemBlitMesh; + private MeshBuilder itemDurabilityGlowMesh; + private MeshBuilder itemDurabilityRoundedMesh; + private MeshBuilder itemCooldownMesh; + ItemBatchMeshes() { + int configured = Integer.getInteger("lovisual.render.itemOverlay.preallocatedItems", 256); + this.preallocatedItems = Math.max(32, configured); + } + void init() { + itemBlitMesh = createMesh(LoVisualRenderPipelines.UI_TEXTURED, 1); + itemDurabilityGlowMesh = createMesh(LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, 1); + itemDurabilityRoundedMesh = createMesh(LoVisualRenderPipelines.UI_ROUNDED_BATCH, 3); + itemCooldownMesh = createMesh(LoVisualRenderPipelines.UI_COLORED, 1); + } + MeshBuilder createMesh(RenderPipeline p, int q) { + int quads = Math.max(1, preallocatedItems * Math.max(1, q)); + return new MeshBuilder(p.getVertexFormatBinding(0), p.getPrimitiveTopology(), quads * 4, quads * 6); + } + MeshBuilder beginOverlayMesh(MeshBuilder mesh, int count, int qpc) { + if (mesh.isBuilding()) mesh.end(); + int quads = Math.max(1, Math.max(preallocatedItems, count) * Math.max(1, qpc)); + mesh.reserve(quads * 4, quads * 6); mesh.begin(); return mesh; + } + MeshBuilder beginBlitMesh(int c) { + if (itemBlitMesh == null) itemBlitMesh = createMesh(LoVisualRenderPipelines.UI_TEXTURED, 1); + return beginOverlayMesh(itemBlitMesh, c, 1); + } + MeshBuilder beginDurabilityGlowMesh(int c) { + if (itemDurabilityGlowMesh == null) itemDurabilityGlowMesh = createMesh(LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, 1); + return beginOverlayMesh(itemDurabilityGlowMesh, c, 1); + } + MeshBuilder beginDurabilityRoundedMesh(int c) { + if (itemDurabilityRoundedMesh == null) itemDurabilityRoundedMesh = createMesh(LoVisualRenderPipelines.UI_ROUNDED_BATCH, 3); + return beginOverlayMesh(itemDurabilityRoundedMesh, c, 3); + } + MeshBuilder beginCooldownMesh(int c) { + if (itemCooldownMesh == null) itemCooldownMesh = createMesh(LoVisualRenderPipelines.UI_COLORED, 1); + return beginOverlayMesh(itemCooldownMesh, c, 1); + } + boolean submitBlitMesh(Minecraft mc, @Nullable MeshBuilder mesh, @Nullable GpuTextureView view, @Nullable GpuSampler s) { + if (mesh == null || s == null || view == null) return false; + if (mesh.isBuilding()) mesh.end(); + if (mesh.getIndicesCount() <= 0) return false; + MeshRenderer.begin().attachments(mc.gameRenderer.mainRenderTarget().getColorTextureView(), null) + .pipeline(LoVisualRenderPipelines.UI_TEXTURED).mesh(mesh).sampler("u_Texture", view, s).end(); + return true; + } + boolean submitOverlayMesh(Minecraft mc, RenderPipeline p, @Nullable MeshBuilder mesh, boolean uniform) { + if (mesh == null) return false; + if (mesh.isBuilding()) mesh.end(); + if (mesh.getIndicesCount() <= 0) return false; + MeshRenderer r = MeshRenderer.begin().attachments(mc.gameRenderer.mainRenderTarget().getColorTextureView(), null).pipeline(p).mesh(mesh); + if (uniform) { UIBatchUniforms.update(mc.getWindow().getWidth(), mc.getWindow().getHeight()); r.uniform("UIBatch", UIBatchUniforms.get()); } + r.end(); return true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchOverlays.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchOverlays.java new file mode 100644 index 0000000..94944b0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/batch/ItemBatchOverlays.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.render.engine.renderer.ui.item; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_COOLDOWN; +import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_COUNT; +import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_DURABILITY; +import static dev.loki.lovisual.render.engine.renderer.Renderer2D.ITEM_OVERLAY_DURABILITY_TEXT; + +final class ItemBatchOverlays { + private final int trackColor = 0x72000000; + private final int trackHighlight = 0x26FFFFFF; + boolean hasDurabilityBar(ItemDrawCommand c) { + return (c.overlayFlags & ITEM_OVERLAY_DURABILITY) != 0 + && (c.overlayFlags & ITEM_OVERLAY_DURABILITY_TEXT) == 0 && isDurabilityVisible(c); + } + boolean hasDurabilityText(ItemDrawCommand c) { return (c.overlayFlags & ITEM_OVERLAY_DURABILITY_TEXT) != 0 && isDurabilityVisible(c); } + boolean isDurabilityVisible(ItemDrawCommand c) { + ItemStack s = c.stack; + if (s == null || s.isEmpty() || !s.isDamageableItem() || !s.isBarVisible()) return false; + return durabilityPercentInt(s) <= Mth.clamp(c.durabilityThresholdPercent, 0, 100); + } + boolean hasCooldownOverlay(Minecraft mc, ItemDrawCommand c) { + if ((c.overlayFlags & ITEM_OVERLAY_COOLDOWN) == 0) return false; + LocalPlayer p = mc.player; + float prog = p == null ? 0.0f : p.getCooldowns().getCooldownPercent(c.stack, mc.getDeltaTracker().getGameTimeDeltaPartialTick(true)); + return prog > 0.0f; + } + void appendDurabilityBar(ItemDrawCommand cmd, MeshBuilder glowMesh, MeshBuilder roundedMesh, ItemBatchGeometry g) { + ItemStack stack = cmd.stack; + float scale = cmd.scaleX; + float barX = cmd.x + 2.0f * scale; float barY = cmd.y + 13.15f * scale; + float barW = 13.0f * scale; float barH = Math.max(1.35f * scale, 1.0f); + float radius = Math.max(0.85f * scale, barH * 0.5f); + float pct = g.durabilityPercent(stack); + float fillW = Mth.clamp(barW * pct, 0.0f, barW); + if (fillW <= 0.01f) return; + int base = ARGB.opaque(stack.getBarColor()); + int bright = g.adjustRgb(base, 1.18f); int deep = g.adjustRgb(base, 0.74f); + int glow = g.multiplyAlpha(g.withAlpha(base, Mth.clamp(96 + Math.round((1.0f - pct) * 48.0f), 96, 144)), cmd.alpha); + int innerGlow = g.multiplyAlpha(g.withAlpha(g.adjustRgb(base, 1.35f), 118), cmd.alpha); + int trackTop = g.multiplyAlpha(trackHighlight, cmd.alpha); + int trackBottom = g.multiplyAlpha(trackColor, cmd.alpha); + int brightAlpha = g.multiplyAlpha(bright, cmd.alpha); int deepAlpha = g.multiplyAlpha(deep, cmd.alpha); + g.appendRoundedGlowQuad(glowMesh, barX, barY, fillW, barH, radius, Math.max(3.0f * scale, 2.0f), glow); + g.appendRoundedRectQuad(roundedMesh, barX, barY, barW, barH, barX, barY, barW, barH, radius, trackTop, trackTop, trackBottom, trackBottom); + g.appendRoundedRectQuad(roundedMesh, barX, barY, fillW, barH, barX, barY, barW, barH, radius, brightAlpha, brightAlpha, deepAlpha, deepAlpha); + g.appendRoundedRectQuad(roundedMesh, barX, barY, Math.min(fillW, Math.max(1.2f * scale, fillW * 0.28f)), barH, + barX, barY, barW, barH, radius, innerGlow, + g.multiplyAlpha(g.withAlpha(innerGlow, 70), cmd.alpha), + g.multiplyAlpha(g.withAlpha(innerGlow, 34), cmd.alpha), + g.multiplyAlpha(g.withAlpha(innerGlow, 58), cmd.alpha)); + } + void appendCooldownQuads(Minecraft mc, ItemDrawCommand cmd, MeshBuilder mesh, ItemBatchGeometry g) { + if ((cmd.overlayFlags & ITEM_OVERLAY_COOLDOWN) == 0) return; + LocalPlayer p = mc.player; + float prog = p == null ? 0.0f : p.getCooldowns().getCooldownPercent(cmd.stack, mc.getDeltaTracker().getGameTimeDeltaPartialTick(true)); + if (prog > 0.0f) { + float scale = cmd.scaleX; + float top = cmd.y + Mth.floor(16.0f * (1.0f - prog)) * scale; + float h = Mth.ceil(16.0f * prog) * scale; + g.appendQuad(mesh, cmd.x, top, 16.0f * scale, h, g.multiplyAlpha(Integer.MAX_VALUE, cmd.alpha)); + } + } + @Nullable String getOverlayText(ItemDrawCommand c) { return getOverlayText(c.stack, c.overlayFlags, c.stackCountText); } + @Nullable String getOverlayText(ItemStack s, int flags, @Nullable String txt) { + if (s == null || s.isEmpty()) return null; + if ((flags & ITEM_OVERLAY_COUNT) != 0 && (s.getCount() != 1 || txt != null)) return txt == null ? String.valueOf(s.getCount()) : txt; + return null; + } + @Nullable String getDurabilityText(ItemDrawCommand c) { if (!hasDurabilityText(c)) return null; return durabilityPercentInt(c.stack) + "%"; } + int getDurabilityTextColor(ItemDrawCommand c) { + int pct = durabilityPercentInt(c.stack); + int thr = Mth.clamp(c.durabilityTextColorThresholdPercent, 0, 100); + return pct <= thr ? ARGB.opaque(c.stack.getBarColor()) : 0xFFFFFFFF; + } + int handleOverlays(dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch batch, Minecraft mc, ItemBatchMeshes meshes, ItemBatchGeometry g) { + MeshBuilder glow = null, rounded = null, cooldown = null; + for (ItemDrawCommand c : batch.commands) { + if (hasDurabilityBar(c)) { + if (glow == null) glow = meshes.beginDurabilityGlowMesh(batch.commands.size()); + if (rounded == null) rounded = meshes.beginDurabilityRoundedMesh(batch.commands.size()); + appendDurabilityBar(c, glow, rounded, g); + } + if (hasCooldownOverlay(mc, c)) { + if (cooldown == null) cooldown = meshes.beginCooldownMesh(batch.commands.size()); + appendCooldownQuads(mc, c, cooldown, g); + } + } + int calls = 0; + if (meshes.submitOverlayMesh(mc, LoVisualRenderPipelines.UI_ROUNDED_GLOW_BATCH, glow, true)) calls++; + if (meshes.submitOverlayMesh(mc, LoVisualRenderPipelines.UI_ROUNDED_BATCH, rounded, true)) calls++; + if (meshes.submitOverlayMesh(mc, LoVisualRenderPipelines.UI_COLORED, cooldown, false)) calls++; + return calls; + } + void renderTexts(dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch batch, ItemBatchGeometry g) { + TextRenderer r = null; float scale = Float.NaN; + try { + for (ItemDrawCommand c : batch.commands) { + if (hasDurabilityText(c)) { + String t = getDurabilityText(c); + if (t != null && !t.isEmpty()) { + float ts = Math.max(0.0001f, c.scaleX * 0.44f); + if (r == null) r = TextRenderer.get(); + if (!r.isBuilding() || Float.compare(scale, ts) != 0) { + if (r.isBuilding()) r.end(); r.begin(ts, false, false); scale = ts; + } + float w = (float) r.getWidth(t, false); + float x = c.x + (16.0f * c.scaleX - w) * 0.5f; float y = c.y + 11.0f * c.scaleX; + r.render(t, x, y, new RenderColor(g.multiplyAlpha(getDurabilityTextColor(c), c.alpha)), true); + } + } + String ov = getOverlayText(c); + if (ov == null || ov.isEmpty()) continue; + float ts = Math.max(0.0001f, c.scaleX * 0.5f); + if (r == null) r = TextRenderer.get(); + if (!r.isBuilding() || Float.compare(scale, ts) != 0) { + if (r.isBuilding()) r.end(); r.begin(ts, false, false); scale = ts; + } + float x = c.x + 19.0f * c.scaleX - 2.0f * c.scaleX - (float) r.getWidth(ov, false); + float y = c.y + 9.0f * c.scaleX; + r.render(ov, x, y, new RenderColor(g.multiplyAlpha(0xFFFFFFFF, c.alpha)), true); + } + } finally { if (r != null && r.isBuilding()) r.end(); } + } + private float durabilityPercent(ItemStack s) { + int max = s.getMaxDamage(); if (max <= 0) return 1.0f; + return Mth.clamp((max - s.getDamageValue()) / (float) max, 0.0f, 1.0f); + } + private int durabilityPercentInt(ItemStack s) { return Math.round(durabilityPercent(s) * 100.0f); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java index de68c38..318a34b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java @@ -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.render.engine.renderer.ui.pass; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java index aa8f8cf..78f097b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java @@ -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.render.engine.renderer.ui.pass; import dev.loki.lovisual.render.engine.command.core.UiCommandBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java index 6f09812..7626dcf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java @@ -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.render.engine.renderer.ui.pass; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java index ca54041..46668cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java @@ -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.render.engine.renderer.ui.runtime.action; @FunctionalInterface diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java index 3b2fb06..ddc43bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java @@ -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.render.engine.renderer.ui.runtime.action; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java index 9df6c46..4a45acb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java @@ -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.render.engine.renderer.ui.runtime.action; public record UiActionRef(String namespace, String name, String argument) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java index d3a3f68..00ee5bd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java @@ -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.render.engine.renderer.ui.runtime.action; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java index 84c2f74..5f3a96e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java @@ -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.render.engine.renderer.ui.runtime.animation; public record UiAnimationSpec(String property, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java index 239bc1d..7f439ed 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java @@ -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.render.engine.renderer.ui.runtime.animation; import net.minecraft.util.Util; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java index b2fc3d6..e0d8bea 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java @@ -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.render.engine.renderer.ui.runtime.animation; import dev.loki.lovisual.render.engine.animation.AnimationUtility; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java index a3822b5..41490e7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java @@ -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.render.engine.renderer.ui.runtime.asset; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java index eda10a4..729066c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java @@ -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.render.engine.renderer.ui.runtime.asset; import dev.loki.lovisual.render.engine.renderer.ui.runtime.asset.registry.UiAssetKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java index 8098b12..546465f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java @@ -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.render.engine.renderer.ui.runtime.asset; import dev.loki.lovisual.render.engine.renderer.ui.runtime.asset.registry.UiAssetKind; import dev.loki.lovisual.render.engine.renderer.ui.runtime.asset.registry.UiAssetRegistry; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java index 88e3ca1..e8e5120 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java @@ -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.render.engine.renderer.ui.runtime.asset.registry; public enum UiAssetKind { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java index 1dba87e..285dec2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java @@ -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.render.engine.renderer.ui.runtime.asset.registry; import dev.loki.lovisual.render.engine.renderer.ui.runtime.asset.UiAssetProvider; import dev.loki.lovisual.render.engine.renderer.ui.runtime.asset.UiAssetRef; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java index d947cee..a40cb70 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java @@ -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.render.engine.renderer.ui.runtime.core; public record UiBounds(float x, float y, float width, float height) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java index f803685..c9b7c03 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java @@ -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.render.engine.renderer.ui.runtime.core; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java index 4914c3e..4a331ee 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java @@ -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.render.engine.renderer.ui.runtime.core; public final class UiRuntimeException extends RuntimeException { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java index e78adea..1ba21e8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java @@ -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.render.engine.renderer.ui.runtime.core.component; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java index 06872c6..5ea09aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java @@ -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.render.engine.renderer.ui.runtime.core.component; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.component.UiComponentFactory; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java index 62d5c68..f1412da 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java @@ -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.render.engine.renderer.ui.runtime.core.node; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java index 9369615..b11a23c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java @@ -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.render.engine.renderer.ui.runtime.core.node; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java index a4071d5..c7c0484 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java @@ -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.render.engine.renderer.ui.runtime.core.node; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiState; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java index 3ed3889..480944b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java @@ -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.render.engine.renderer.ui.runtime.core.node; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java index c76c9b1..0654ff7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java @@ -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.render.engine.renderer.ui.runtime.core.runtime; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java index efcb5cc..aabbcbd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java @@ -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.render.engine.renderer.ui.runtime.core.runtime; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiDocument; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java index 11d5999..c464c20 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java @@ -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.render.engine.renderer.ui.runtime.core.runtime; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java index 0ae94d9..3c7a539 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java @@ -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.render.engine.renderer.ui.runtime.debug; public final class UiPerfCounters { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java index 8c32cac..ad51ba8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java @@ -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.render.engine.renderer.ui.runtime.debug; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java index b4ddbcc..3f4ec7a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java @@ -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.render.engine.renderer.ui.runtime.debug; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java index a6012df..a7c9c27 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java @@ -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.render.engine.renderer.ui.runtime.error; public enum UiErrorPolicy { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java index 3f75caa..d6bb8f5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java @@ -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.render.engine.renderer.ui.runtime.input; import dev.loki.lovisual.render.engine.renderer.ui.runtime.input.event.UiHitResult; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java index b60ed89..3f641e5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java @@ -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.render.engine.renderer.ui.runtime.input; import dev.loki.lovisual.render.engine.renderer.ui.runtime.input.focus.UiCursorManager; import dev.loki.lovisual.render.engine.renderer.ui.runtime.input.focus.UiFocusManager; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java index d1a00ee..20237c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java @@ -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.render.engine.renderer.ui.runtime.input; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java index 86c9d97..c42b1f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java @@ -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.render.engine.renderer.ui.runtime.input.event; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java index 59f6853..866bdc5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java @@ -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.render.engine.renderer.ui.runtime.input.event; public record UiKeyboardEvent(int keyCode, int scanCode, int modifiers) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java index 37f4474..bb72f95 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java @@ -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.render.engine.renderer.ui.runtime.input.event; public record UiPointerEvent(float x, float y, int button) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java index c7e618b..a7f16ac 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java @@ -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.render.engine.renderer.ui.runtime.input.event; public record UiScrollEvent(float x, float y, double amountX, double amountY) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java index 97ad7f5..a6cd1fc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java @@ -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.render.engine.renderer.ui.runtime.input.focus; import dev.loki.lovisual.render.helpers.util.SystemCursor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java index a6aa407..2050770 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java @@ -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.render.engine.renderer.ui.runtime.input.focus; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java index bca868a..e282373 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java @@ -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.render.engine.renderer.ui.runtime.layout; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java index 8a00844..347f5ad 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java @@ -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.render.engine.renderer.ui.runtime.reconcile; public final class UiDiffStats { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java index d3ac5a5..0827256 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java @@ -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.render.engine.renderer.ui.runtime.reconcile; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiLifecycle; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java index 7b35f96..bf5076a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java @@ -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.render.engine.renderer.ui.runtime.reconcile; import dev.loki.lovisual.render.engine.renderer.ui.runtime.reconcile.match.UiNodeMatcher; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java index 3c24044..5c369ad 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java @@ -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.render.engine.renderer.ui.runtime.reconcile.match; public record UiKeyPath(String value) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java index 4de025b..bb16ea1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java @@ -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.render.engine.renderer.ui.runtime.reconcile.match; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java index acee9f7..3fcd6ae 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java @@ -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.render.engine.renderer.ui.runtime.render.bridge; import com.mojang.blaze3d.pipeline.BlendFunction; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java index 05850a5..8bfe254 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java @@ -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.render.engine.renderer.ui.runtime.render.bridge; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java index 9e8dcc5..0bd57c3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java @@ -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.render.engine.renderer.ui.runtime.render.bridge; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java index f57e109..eb3b77a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java @@ -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.render.engine.renderer.ui.runtime.render.bridge; import net.minecraft.core.registries.BuiltInRegistries; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java index c250433..587999a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java @@ -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.render.engine.renderer.ui.runtime.render.context; public enum UiProjectionMode { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java index 5bad764..5cf5d0c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java @@ -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.render.engine.renderer.ui.runtime.render.context; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java index 021c2fa..5c5a7a8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java @@ -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.render.engine.renderer.ui.runtime.render.primitive; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java index 4bbd1a9..9215812 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java @@ -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.render.engine.renderer.ui.runtime.render.primitive; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java index d8dcd67..1ab0a25 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java @@ -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.render.engine.renderer.ui.runtime.render.primitive; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java index fcb4591..fad658f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java @@ -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.render.engine.renderer.ui.runtime.render.primitive; import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerKind; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java index 7282861..0089627 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java @@ -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.render.engine.renderer.ui.runtime.render.rect; import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.primitive.UiShapeContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java index 77c6749..a075ae3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java @@ -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.render.engine.renderer.ui.runtime.render.rect; import static dev.loki.lovisual.render.engine.renderer.ui.runtime.render.primitive.UiShapeContext.UiShapeFrame; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java index 625a519..ed1931d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java @@ -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.render.engine.renderer.ui.runtime.render.render; import dev.loki.lovisual.render.engine.renderer.Renderer2D; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java index b7a9cde..c0cd1ae 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java @@ -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.render.engine.renderer.ui.runtime.render.render; import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java index 68182b8..2b01a43 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java @@ -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.render.engine.renderer.ui.runtime.render.render; import dev.loki.lovisual.render.engine.text.font.Font; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java index cce623d..bce1247 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java @@ -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.render.engine.renderer.ui.runtime.render.render; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java index 2378c44..05dba92 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java @@ -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.render.engine.renderer.ui.runtime.script.engine; import com.caoccao.javet.enums.JSRuntimeType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java index d1f6437..30e347a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java @@ -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.render.engine.renderer.ui.runtime.script.engine; import com.caoccao.javet.exceptions.JavetException; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java index c4e5a5a..d7b24d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java @@ -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.render.engine.renderer.ui.runtime.script.engine; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptRenderContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java index 31bb746..50688ce 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java @@ -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.render.engine.renderer.ui.runtime.script.engine; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.JavetUiScriptEngine; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.UiScriptEngine; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java index ddc004c..738c60b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java @@ -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.render.engine.renderer.ui.runtime.script.module; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java index 948f926..2cf3286 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java @@ -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.render.engine.renderer.ui.runtime.script.module; import net.minecraft.server.packs.resources.ResourceManager; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java index cd20562..e1ecb80 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java @@ -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.render.engine.renderer.ui.runtime.script.module; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java index 0b09625..81446a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java @@ -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.render.engine.renderer.ui.runtime.script.module; import net.fabricmc.loader.api.FabricLoader; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java index c989711..82d25b2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java @@ -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.render.engine.renderer.ui.runtime.script.patch; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java index 7b6958d..8b1417d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java @@ -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.render.engine.renderer.ui.runtime.script.patch; /** Utility color helpers shared by Java-side UI script patch builders. */ diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java index 11f70e8..d51c2ed 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java @@ -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.render.engine.renderer.ui.runtime.script.patch; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java index c3bdb0b..c933243 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java @@ -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.render.engine.renderer.ui.runtime.script.patch; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java index 830327d..d076bb4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java @@ -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.render.engine.renderer.ui.runtime.script.registry; import net.minecraft.server.packs.resources.ResourceManager; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java index bcbc4ee..faf3c78 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java @@ -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.render.engine.renderer.ui.runtime.script.registry; public enum UiScriptModuleTransform { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java index 2ca31f2..469534e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java @@ -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.render.engine.renderer.ui.runtime.script.registry; public enum UiTypeScriptStripper { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java index f00094b..0aa3a22 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java @@ -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.render.engine.renderer.ui.runtime.script.render; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java index 5d81af1..13bc321 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java @@ -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.render.engine.renderer.ui.runtime.script.render; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java index e464bcd..26c40a3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java @@ -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.render.engine.renderer.ui.runtime.script.render; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java index 85be6ea..013e436 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java @@ -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.render.engine.renderer.ui.runtime.script.render; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiNodeSpec; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java index f52065a..492d11b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java @@ -1,28 +1,11 @@ -/* - * 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.render.engine.renderer.ui.runtime.script.runtime; -import java.util.LinkedHashMap; -import net.minecraft.util.Util; -import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; +import java.util.Map; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.util.Map; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.UiScriptEngine; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.UiScriptEngineProvider; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptRenderContext; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptRenderResult; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.UiScriptRuntimeError; /** * Caches script output as runtime trees. @@ -31,73 +14,52 @@ import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.UiScri * are skipped while the caller-provided signatures are unchanged. The render pass * still runs every frame because it writes the already baked tree to the current * draw context.

    + * + *

    This is the public API surface; the JS/layout/patch execution lives in + * {@code impl.UiScriptRuntimeCore}, hashing in {@code impl.UiScriptSignature} and telemetry in + * {@code impl.UiScriptFrameStats}.

    */ public final class CachedUiScriptRuntime { - private static final FrameStats FRAME_STATS = new FrameStats(); - private final LinkedHashMap states = new LinkedHashMap<>(); - private final Reporter reporter; - private UiScriptEngine scriptEngine = UiScriptEngineProvider.javet(); - private long frame; - private long lastFrameMs; + private final UiScriptRuntimeCore core; public CachedUiScriptRuntime() { this(Reporter.NOOP); } public CachedUiScriptRuntime(Reporter reporter) { - this.reporter = reporter != null ? reporter : Reporter.NOOP; - } - - private static String runtimeLabel(UiScriptModuleHandle handle, String key) { - String id = handle != null && handle.getId() != null ? handle.getId().toString() : "unknown"; - String k = key != null && !key.isBlank() ? key : "default"; - return id + ":" + k; + this.core = new UiScriptRuntimeCore(reporter != null ? reporter : Reporter.NOOP); } public static void resetFrameStats() { - FRAME_STATS.reset(); + UiScriptFrameStats.GLOBAL.reset(); } public static FrameStatsSnapshot frameStatsSnapshot() { - return FRAME_STATS.snapshot(); + return UiScriptFrameStats.GLOBAL.snapshot(); } public static long signature(Map values) { - if (values == null || values.isEmpty()) return 0xcbf29ce484222325L; - long h = 0xcbf29ce484222325L; - for (Map.Entry entry : values.entrySet()) { - long e = 0xcbf29ce484222325L; - e = mix(e, entry.getKey()); - e = mixObject(e, entry.getValue()); - h ^= e; - h *= 0x100000001b3L; - } - return h; + return UiScriptSignature.signature(values); } public static long mix(long hash, boolean value) { - return mix(hash, value ? 1 : 0); + return UiScriptSignature.mix(hash, value); } public static long mix(long hash, int value) { - hash ^= value; - return hash * 0x100000001b3L; + return UiScriptSignature.mix(hash, value); } public static long mix(long hash, float value) { - return mix(hash, Float.floatToIntBits(value)); + return UiScriptSignature.mix(hash, value); } public static long mix(long hash, String value) { - return mix(hash, value != null ? value.hashCode() : 0); + return UiScriptSignature.mix(hash, value); } public static long mixObject(long hash, Object value) { - if (value instanceof Boolean b) return mix(hash, b); - if (value instanceof Integer i) return mix(hash, i); - if (value instanceof Float f) return mix(hash, f); - if (value instanceof Number n) return mix(hash, Float.floatToIntBits(n.floatValue())); - return mix(hash, value != null ? value.toString() : ""); + return UiScriptSignature.mixObject(hash, value); } public UiRuntime bake(UiScriptModuleHandle handle, @@ -113,7 +75,7 @@ public final class CachedUiScriptRuntime { float layoutW, float layoutH, PropsFactory propsFactory) { - return bake(handle, module, key, treeSignature, 0L, layoutSignature, scriptWidth, scriptHeight, + return core.bake(handle, module, key, treeSignature, 0L, layoutSignature, scriptWidth, scriptHeight, textRenderer, layoutX, layoutY, layoutW, layoutH, propsFactory, null, null); } @@ -132,7 +94,7 @@ public final class CachedUiScriptRuntime { float layoutH, PropsFactory propsFactory, RuntimePatchFactory patchFactory) { - return bake(handle, module, key, treeSignature, dataSignature, layoutSignature, scriptWidth, scriptHeight, + return core.bake(handle, module, key, treeSignature, dataSignature, layoutSignature, scriptWidth, scriptHeight, textRenderer, layoutX, layoutY, layoutW, layoutH, propsFactory, patchFactory, null); } @@ -152,75 +114,8 @@ public final class CachedUiScriptRuntime { PropsFactory propsFactory, RuntimePatchFactory patchFactory, BoundsPatchFactory boundsPatchFactory) { - FRAME_STATS.bakeCalls++; - if (handle == null || module == null || textRenderer == null || propsFactory == null) return null; - if (handle.isRuntimeBlocked()) { - FRAME_STATS.blockedCalls++; - return null; - } - - State state = state(key); - state.runtime.diagnostics().counters().setPatchNanos(0L); - state.runtime.diagnostics().counters().setPropPatchCount(0); - state.runtime.diagnostics().counters().setBoundsPatchCount(0); - if (!state.treeReady || state.treeSignature != treeSignature) { - long now = Util.getMillis(); - double delta = lastFrameMs > 0L ? Math.max(0.0, (now - lastFrameMs) / 1000.0) : 0.0; - lastFrameMs = now; - - UiScriptRenderResult result; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("js:" + runtimeLabel(handle, key))) { - result = scriptEngine.render( - module, - new UiScriptRenderContext( - frame++, - now / 1000.0, - delta, - scriptWidth, - scriptHeight, - new UiProps(propsFactory.create()) - ) - ); - } - if (!result.success()) { - FRAME_STATS.failedRenders++; - reporter.reportRuntimeError(handle, result.error()); - return null; - } - FRAME_STATS.jsRenders++; - state.runtime.setTree(result.root()); - state.treeSignature = treeSignature; - state.dataSignature = dataSignature; - state.layoutReady = false; - state.treeReady = true; - } else if (state.dataSignature != dataSignature) { - if (patchFactory != null) { - FRAME_STATS.propPatchPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchProps:" + runtimeLabel(handle, key))) { - FRAME_STATS.propPatches += state.runtime.patchPropsByKey(patchFactory.create()); - } - } - state.dataSignature = dataSignature; - } - - if (!state.layoutReady || state.layoutSignature != layoutSignature) { - FRAME_STATS.layoutPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("layout:" + runtimeLabel(handle, key))) { - state.runtime.layout(textRenderer, layoutX, layoutY, layoutW, layoutH); - } - state.layoutSignature = layoutSignature; - state.layoutReady = true; - } - - if (boundsPatchFactory != null) { - FRAME_STATS.boundsPatchPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchBounds:" + runtimeLabel(handle, key))) { - FRAME_STATS.boundsPatches += state.runtime.patchBoundsByKey(boundsPatchFactory.create()); - } - } - - reporter.reportRenderState(handle, state.runtime, scriptWidth, scriptHeight); - return state.runtime; + return core.bake(handle, module, key, treeSignature, dataSignature, layoutSignature, scriptWidth, scriptHeight, + textRenderer, layoutX, layoutY, layoutW, layoutH, propsFactory, patchFactory, boundsPatchFactory); } public UiRuntime updatePersistent(UiScriptModuleHandle handle, @@ -256,92 +151,12 @@ public final class CachedUiScriptRuntime { PropsFactory templatePropsFactory, RuntimePatchFactory patchFactory, BoundsPatchFactory boundsPatchFactory) { - if (handle == null || module == null || textRenderer == null || templatePropsFactory == null) return null; - if (handle.isRuntimeBlocked()) { - FRAME_STATS.blockedCalls++; - return null; - } - - State state = state(key); - state.runtime.diagnostics().counters().setPatchNanos(0L); - state.runtime.diagnostics().counters().setPropPatchCount(0); - state.runtime.diagnostics().counters().setBoundsPatchCount(0); - if (!state.treeReady || state.treeSignature != treeSignature) { - FRAME_STATS.bakeCalls++; - long now = Util.getMillis(); - double delta = lastFrameMs > 0L ? Math.max(0.0, (now - lastFrameMs) / 1000.0) : 0.0; - lastFrameMs = now; - - UiScriptRenderResult result; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("js:" + runtimeLabel(handle, key))) { - result = scriptEngine.render( - module, - new UiScriptRenderContext( - frame++, - now / 1000.0, - delta, - scriptWidth, - scriptHeight, - new UiProps(templatePropsFactory.create()) - ) - ); - } - if (!result.success()) { - FRAME_STATS.failedRenders++; - reporter.reportRuntimeError(handle, result.error()); - return null; - } - FRAME_STATS.jsRenders++; - state.runtime.setTree(result.root()); - state.treeSignature = treeSignature; - state.layoutReady = false; - state.treeReady = true; - } - - if (!state.layoutReady || state.layoutSignature != layoutSignature) { - FRAME_STATS.layoutPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("layout:" + runtimeLabel(handle, key))) { - state.runtime.layout(textRenderer, layoutX, layoutY, layoutW, layoutH); - } - state.layoutSignature = layoutSignature; - state.layoutReady = true; - } - - if (patchFactory != null) { - FRAME_STATS.propPatchPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchProps:" + runtimeLabel(handle, key))) { - FRAME_STATS.propPatches += state.runtime.patchPropsByKey(patchFactory.create()); - } - } - - if (boundsPatchFactory != null) { - FRAME_STATS.boundsPatchPasses++; - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchBounds:" + runtimeLabel(handle, key))) { - FRAME_STATS.boundsPatches += state.runtime.patchBoundsByKey(boundsPatchFactory.create()); - } - } - - reporter.reportRenderState(handle, state.runtime, scriptWidth, scriptHeight); - return state.runtime; + return core.updatePersistent(handle, module, key, treeSignature, layoutSignature, scriptWidth, scriptHeight, + textRenderer, layoutX, layoutY, layoutW, layoutH, templatePropsFactory, patchFactory, boundsPatchFactory); } public void reset() { - try { - scriptEngine.close(); - } catch (Exception ignored) { - } - scriptEngine = UiScriptEngineProvider.javet(); - states.clear(); - lastFrameMs = 0L; - } - - private State state(String key) { - String resolved = key != null && !key.isEmpty() ? key : "default"; - State existing = states.get(resolved); - if (existing != null) return existing; - State created = new State(); - states.put(resolved, created); - return created; + core.reset(); } public interface Reporter { @@ -382,51 +197,4 @@ public final class CachedUiScriptRuntime { int blockedCalls, int failedRenders) { } - - private static final class FrameStats { - private int bakeCalls; - private int jsRenders; - private int propPatchPasses; - private int propPatches; - private int boundsPatchPasses; - private int boundsPatches; - private int layoutPasses; - private int blockedCalls; - private int failedRenders; - - private void reset() { - bakeCalls = 0; - jsRenders = 0; - propPatchPasses = 0; - propPatches = 0; - boundsPatchPasses = 0; - boundsPatches = 0; - layoutPasses = 0; - blockedCalls = 0; - failedRenders = 0; - } - - private FrameStatsSnapshot snapshot() { - return new FrameStatsSnapshot( - bakeCalls, - jsRenders, - propPatchPasses, - propPatches, - boundsPatchPasses, - boundsPatches, - layoutPasses, - blockedCalls, - failedRenders - ); - } - } - - private static final class State { - private final UiRuntime runtime = new UiRuntime(); - private long treeSignature = Long.MIN_VALUE; - private long dataSignature = Long.MIN_VALUE; - private long layoutSignature = Long.MIN_VALUE; - private boolean treeReady; - private boolean layoutReady; - } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java index 555fa7f..aa1dc64 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java @@ -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.render.engine.renderer.ui.runtime.script.runtime; import java.io.PrintWriter; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java index 16965dd..58c2d6f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java @@ -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.render.engine.renderer.ui.runtime.script.runtime; public enum UiScriptRuntimeSource { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java index 4806b62..f0849cb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java @@ -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.render.engine.renderer.ui.runtime.script.runtime; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptFrameStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptFrameStats.java new file mode 100644 index 0000000..39693ea --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptFrameStats.java @@ -0,0 +1,49 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime; + +/** + * Mutable per-frame counters for {@link CachedUiScriptRuntime} (debug telemetry only). Fields are + * package-private so the runtime engine can bump them inline; {@link #snapshot()} freezes them into + * the public {@link CachedUiScriptRuntime.FrameStatsSnapshot}. + */ +final class UiScriptFrameStats { + static final UiScriptFrameStats GLOBAL = new UiScriptFrameStats(); + + int bakeCalls; + int jsRenders; + int propPatchPasses; + int propPatches; + int boundsPatchPasses; + int boundsPatches; + int layoutPasses; + int blockedCalls; + int failedRenders; + + private UiScriptFrameStats() { + } + + void reset() { + bakeCalls = 0; + jsRenders = 0; + propPatchPasses = 0; + propPatches = 0; + boundsPatchPasses = 0; + boundsPatches = 0; + layoutPasses = 0; + blockedCalls = 0; + failedRenders = 0; + } + + CachedUiScriptRuntime.FrameStatsSnapshot snapshot() { + return new CachedUiScriptRuntime.FrameStatsSnapshot( + bakeCalls, + jsRenders, + propPatchPasses, + propPatches, + boundsPatchPasses, + boundsPatches, + layoutPasses, + blockedCalls, + failedRenders + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptRuntimeCore.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptRuntimeCore.java new file mode 100644 index 0000000..44562bc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptRuntimeCore.java @@ -0,0 +1,239 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime; + +import java.util.LinkedHashMap; +import net.minecraft.util.Util; +import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.node.UiProps; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.UiScriptEngine; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.engine.UiScriptEngineProvider; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptRenderContext; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.render.UiScriptRenderResult; + +/** + * Execution engine behind {@link CachedUiScriptRuntime}: owns the per-key {@link State} cache, the + * script engine handle and frame clock, and runs the JS-render / layout / prop-patch / bounds-patch + * passes. {@code bake} rebuilds the tree when its signature changes; {@code updatePersistent} keeps a + * persistent tree and patches props in place. + */ +final class UiScriptRuntimeCore { + private static final UiScriptFrameStats FRAME_STATS = UiScriptFrameStats.GLOBAL; + + private final LinkedHashMap states = new LinkedHashMap<>(); + private final CachedUiScriptRuntime.Reporter reporter; + private UiScriptEngine scriptEngine = UiScriptEngineProvider.javet(); + private long frame; + private long lastFrameMs; + + UiScriptRuntimeCore(CachedUiScriptRuntime.Reporter reporter) { + this.reporter = reporter; + } + + UiRuntime bake(UiScriptModuleHandle handle, + UiScriptModule module, + String key, + long treeSignature, + long dataSignature, + long layoutSignature, + float scriptWidth, + float scriptHeight, + TextRenderer textRenderer, + float layoutX, + float layoutY, + float layoutW, + float layoutH, + CachedUiScriptRuntime.PropsFactory propsFactory, + CachedUiScriptRuntime.RuntimePatchFactory patchFactory, + CachedUiScriptRuntime.BoundsPatchFactory boundsPatchFactory) { + FRAME_STATS.bakeCalls++; + if (handle == null || module == null || textRenderer == null || propsFactory == null) return null; + if (handle.isRuntimeBlocked()) { + FRAME_STATS.blockedCalls++; + return null; + } + + State state = state(key); + state.runtime.diagnostics().counters().setPatchNanos(0L); + state.runtime.diagnostics().counters().setPropPatchCount(0); + state.runtime.diagnostics().counters().setBoundsPatchCount(0); + if (!state.treeReady || state.treeSignature != treeSignature) { + long now = Util.getMillis(); + double delta = lastFrameMs > 0L ? Math.max(0.0, (now - lastFrameMs) / 1000.0) : 0.0; + lastFrameMs = now; + + UiScriptRenderResult result; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("js:" + runtimeLabel(handle, key))) { + result = scriptEngine.render( + module, + new UiScriptRenderContext( + frame++, + now / 1000.0, + delta, + scriptWidth, + scriptHeight, + new UiProps(propsFactory.create()) + ) + ); + } + if (!result.success()) { + FRAME_STATS.failedRenders++; + reporter.reportRuntimeError(handle, result.error()); + return null; + } + FRAME_STATS.jsRenders++; + state.runtime.setTree(result.root()); + state.treeSignature = treeSignature; + state.dataSignature = dataSignature; + state.layoutReady = false; + state.treeReady = true; + } else if (state.dataSignature != dataSignature) { + if (patchFactory != null) { + FRAME_STATS.propPatchPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchProps:" + runtimeLabel(handle, key))) { + FRAME_STATS.propPatches += state.runtime.patchPropsByKey(patchFactory.create()); + } + } + state.dataSignature = dataSignature; + } + + if (!state.layoutReady || state.layoutSignature != layoutSignature) { + FRAME_STATS.layoutPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("layout:" + runtimeLabel(handle, key))) { + state.runtime.layout(textRenderer, layoutX, layoutY, layoutW, layoutH); + } + state.layoutSignature = layoutSignature; + state.layoutReady = true; + } + + if (boundsPatchFactory != null) { + FRAME_STATS.boundsPatchPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchBounds:" + runtimeLabel(handle, key))) { + FRAME_STATS.boundsPatches += state.runtime.patchBoundsByKey(boundsPatchFactory.create()); + } + } + + reporter.reportRenderState(handle, state.runtime, scriptWidth, scriptHeight); + return state.runtime; + } + + UiRuntime updatePersistent(UiScriptModuleHandle handle, + UiScriptModule module, + String key, + long treeSignature, + long layoutSignature, + float scriptWidth, + float scriptHeight, + TextRenderer textRenderer, + float layoutX, + float layoutY, + float layoutW, + float layoutH, + CachedUiScriptRuntime.PropsFactory templatePropsFactory, + CachedUiScriptRuntime.RuntimePatchFactory patchFactory, + CachedUiScriptRuntime.BoundsPatchFactory boundsPatchFactory) { + if (handle == null || module == null || textRenderer == null || templatePropsFactory == null) return null; + if (handle.isRuntimeBlocked()) { + FRAME_STATS.blockedCalls++; + return null; + } + + State state = state(key); + state.runtime.diagnostics().counters().setPatchNanos(0L); + state.runtime.diagnostics().counters().setPropPatchCount(0); + state.runtime.diagnostics().counters().setBoundsPatchCount(0); + if (!state.treeReady || state.treeSignature != treeSignature) { + FRAME_STATS.bakeCalls++; + long now = Util.getMillis(); + double delta = lastFrameMs > 0L ? Math.max(0.0, (now - lastFrameMs) / 1000.0) : 0.0; + lastFrameMs = now; + + UiScriptRenderResult result; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("js:" + runtimeLabel(handle, key))) { + result = scriptEngine.render( + module, + new UiScriptRenderContext( + frame++, + now / 1000.0, + delta, + scriptWidth, + scriptHeight, + new UiProps(templatePropsFactory.create()) + ) + ); + } + if (!result.success()) { + FRAME_STATS.failedRenders++; + reporter.reportRuntimeError(handle, result.error()); + return null; + } + FRAME_STATS.jsRenders++; + state.runtime.setTree(result.root()); + state.treeSignature = treeSignature; + state.layoutReady = false; + state.treeReady = true; + } + + if (!state.layoutReady || state.layoutSignature != layoutSignature) { + FRAME_STATS.layoutPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("layout:" + runtimeLabel(handle, key))) { + state.runtime.layout(textRenderer, layoutX, layoutY, layoutW, layoutH); + } + state.layoutSignature = layoutSignature; + state.layoutReady = true; + } + + if (patchFactory != null) { + FRAME_STATS.propPatchPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchProps:" + runtimeLabel(handle, key))) { + FRAME_STATS.propPatches += state.runtime.patchPropsByKey(patchFactory.create()); + } + } + + if (boundsPatchFactory != null) { + FRAME_STATS.boundsPatchPasses++; + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.uiRuntime("patchBounds:" + runtimeLabel(handle, key))) { + FRAME_STATS.boundsPatches += state.runtime.patchBoundsByKey(boundsPatchFactory.create()); + } + } + + reporter.reportRenderState(handle, state.runtime, scriptWidth, scriptHeight); + return state.runtime; + } + + void reset() { + try { + scriptEngine.close(); + } catch (Exception ignored) { + } + scriptEngine = UiScriptEngineProvider.javet(); + states.clear(); + lastFrameMs = 0L; + } + + private State state(String key) { + String resolved = key != null && !key.isEmpty() ? key : "default"; + State existing = states.get(resolved); + if (existing != null) return existing; + State created = new State(); + states.put(resolved, created); + return created; + } + + private static String runtimeLabel(UiScriptModuleHandle handle, String key) { + String id = handle != null && handle.getId() != null ? handle.getId().toString() : "unknown"; + String k = key != null && !key.isBlank() ? key : "default"; + return id + ":" + k; + } + + private static final class State { + private final UiRuntime runtime = new UiRuntime(); + private long treeSignature = Long.MIN_VALUE; + private long dataSignature = Long.MIN_VALUE; + private long layoutSignature = Long.MIN_VALUE; + private boolean treeReady; + private boolean layoutReady; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptSignature.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptSignature.java new file mode 100644 index 0000000..13d13aa --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/impl/UiScriptSignature.java @@ -0,0 +1,52 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime; + +import java.util.Map; + +/** + * FNV-1a style hashing helpers used to fold prop maps into stable {@code long} signatures. + * + *

    Backs the public {@link CachedUiScriptRuntime#signature} / {@code mix} API; callers build + * tree/layout/data signatures from these without allocating.

    + */ +final class UiScriptSignature { + private UiScriptSignature() { + } + + static long signature(Map values) { + if (values == null || values.isEmpty()) return 0xcbf29ce484222325L; + long h = 0xcbf29ce484222325L; + for (Map.Entry entry : values.entrySet()) { + long e = 0xcbf29ce484222325L; + e = mix(e, entry.getKey()); + e = mixObject(e, entry.getValue()); + h ^= e; + h *= 0x100000001b3L; + } + return h; + } + + static long mix(long hash, boolean value) { + return mix(hash, value ? 1 : 0); + } + + static long mix(long hash, int value) { + hash ^= value; + return hash * 0x100000001b3L; + } + + static long mix(long hash, float value) { + return mix(hash, Float.floatToIntBits(value)); + } + + static long mix(long hash, String value) { + return mix(hash, value != null ? value.hashCode() : 0); + } + + static long mixObject(long hash, Object value) { + if (value instanceof Boolean b) return mix(hash, b); + if (value instanceof Integer i) return mix(hash, i); + if (value instanceof Float f) return mix(hash, f); + if (value instanceof Number n) return mix(hash, Float.floatToIntBits(n.floatValue())); + return mix(hash, value != null ? value.toString() : ""); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java index 5f882cc..df2699b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java @@ -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.render.engine.renderer.ui.runtime.style; public record UiResolvedStyle(UiStyle style, int stateFlags, int themeVersion) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java index 6b2012a..0404191 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java @@ -1,644 +1,163 @@ -/* - * 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.render.engine.renderer.ui.runtime.style; + import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiAlign; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiJustify; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiOverflow; - - import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.bridge.UiBlendSpec; import dev.loki.lovisual.render.engine.text.font.info.FontInfo; public final class UiStyle { public static final UiStyle DEFAULT = builder().build(); - private final Float width; - private final Float height; - private final Float minWidth; - private final Float minHeight; - private final Float maxWidth; - private final Float maxHeight; - private final float paddingLeft; - private final float paddingTop; - private final float paddingRight; - private final float paddingBottom; - private final float marginLeft; - private final float marginTop; - private final float marginRight; - private final float marginBottom; - private final float gap; - private final float grow; - private final boolean absolute; - private final Float offsetX; - private final Float offsetY; - private final UiAlign align; - private final UiJustify justify; - private final UiOverflow overflow; - private final float radius; - private final Integer backgroundColor; - private final Integer strokeColor; - private final float strokeWidth; - private final Integer shadowColor; - private final float shadowBlur; - private final float shadowInnerAlpha; - private final boolean blur; - private final boolean liquidGlass; - private final boolean clip; - private final boolean marquee; - private final float blurQuality; - private final float blurBrightness; - private final float blurAlpha; - private final Integer textColor; - private final String fontFamily; - private final FontInfo.Type fontType; - private final float textScale; - private final boolean textShadow; - private final String textEffect; - private final int textEffectSpeed; - private final String textBackend; - private final float maxTextWidth; - private final boolean ellipsis; - private final String textAlign; - private final String cursor; - private final UiBlendSpec blend; + private final UiStyleLayout layout; + private final UiStylePalette palette; + private final UiStyleText text; private UiStyle(Builder builder) { - this.width = builder.width; - this.height = builder.height; - this.minWidth = builder.minWidth; - this.minHeight = builder.minHeight; - this.maxWidth = builder.maxWidth; - this.maxHeight = builder.maxHeight; - this.paddingLeft = builder.paddingLeft; - this.paddingTop = builder.paddingTop; - this.paddingRight = builder.paddingRight; - this.paddingBottom = builder.paddingBottom; - this.marginLeft = builder.marginLeft; - this.marginTop = builder.marginTop; - this.marginRight = builder.marginRight; - this.marginBottom = builder.marginBottom; - this.gap = builder.gap; - this.grow = builder.grow; - this.absolute = builder.absolute; - this.offsetX = builder.offsetX; - this.offsetY = builder.offsetY; - this.align = builder.align; - this.justify = builder.justify; - this.overflow = builder.overflow; - this.radius = builder.radius; - this.backgroundColor = builder.backgroundColor; - this.strokeColor = builder.strokeColor; - this.strokeWidth = builder.strokeWidth; - this.shadowColor = builder.shadowColor; - this.shadowBlur = builder.shadowBlur; - this.shadowInnerAlpha = builder.shadowInnerAlpha; - this.blur = builder.blur; - this.liquidGlass = builder.liquidGlass; - this.clip = builder.clip; - this.marquee = builder.marquee; - this.blurQuality = builder.blurQuality; - this.blurBrightness = builder.blurBrightness; - this.blurAlpha = builder.blurAlpha; - this.textColor = builder.textColor; - this.fontFamily = builder.fontFamily; - this.fontType = builder.fontType; - this.textScale = builder.textScale; - this.textShadow = builder.textShadow; - this.textEffect = builder.textEffect; - this.textEffectSpeed = builder.textEffectSpeed; - this.textBackend = builder.textBackend; - this.maxTextWidth = builder.maxTextWidth; - this.ellipsis = builder.ellipsis; - this.textAlign = builder.textAlign; - this.cursor = builder.cursor; - this.blend = builder.blend; + this.layout = new UiStyleLayout(builder.layout); + this.palette = new UiStylePalette(builder.palette); + this.text = new UiStyleText(builder.text); } - private static float clamp(float value, Float min, Float max) { - float out = value; - if (min != null) out = Math.max(out, min); - if (max != null) out = Math.min(out, max); - return out; - } + public static Builder builder() { return new Builder(); } - public static Builder builder() { - return new Builder(); - } + public Float width() { return layout.width(); } + public Float height() { return layout.height(); } + public Float minWidth() { return layout.minWidth(); } + public Float minHeight() { return layout.minHeight(); } + public Float maxWidth() { return layout.maxWidth(); } + public Float maxHeight() { return layout.maxHeight(); } + public float paddingLeft() { return layout.paddingLeft(); } + public float paddingTop() { return layout.paddingTop(); } + public float paddingRight() { return layout.paddingRight(); } + public float paddingBottom() { return layout.paddingBottom(); } + public float paddingX() { return layout.paddingX(); } + public float paddingY() { return layout.paddingY(); } + public float marginLeft() { return layout.marginLeft(); } + public float marginTop() { return layout.marginTop(); } + public float marginRight() { return layout.marginRight(); } + public float marginBottom() { return layout.marginBottom(); } + public float marginX() { return layout.marginX(); } + public float marginY() { return layout.marginY(); } + public float gap() { return layout.gap(); } + public float grow() { return layout.grow(); } + public boolean absolute() { return layout.absolute(); } + public Float offsetX() { return layout.offsetX(); } + public Float offsetY() { return layout.offsetY(); } + public UiAlign align() { return layout.align(); } + public UiJustify justify() { return layout.justify(); } + public UiOverflow overflow() { return layout.overflow(); } + public float radius() { return palette.radius(); } + public Integer backgroundColor() { return palette.backgroundColor(); } + public Integer strokeColor() { return palette.strokeColor(); } + public float strokeWidth() { return palette.strokeWidth(); } + public Integer shadowColor() { return palette.shadowColor(); } + public float shadowBlur() { return palette.shadowBlur(); } + public float shadowInnerAlpha() { return palette.shadowInnerAlpha(); } + public boolean blur() { return palette.blur(); } + public boolean liquidGlass() { return palette.liquidGlass(); } + public boolean clip() { return palette.clip(); } + public boolean marquee() { return palette.marquee(); } + public float blurQuality() { return palette.blurQuality(); } + public float blurBrightness() { return palette.blurBrightness(); } + public float blurAlpha() { return palette.blurAlpha(); } + public Integer textColor() { return text.textColor(); } + public String fontFamily() { return text.fontFamily(); } + public FontInfo.Type fontType() { return text.fontType(); } + public float textScale() { return text.textScale(); } + public boolean textShadow() { return text.textShadow(); } + public String textEffect() { return text.textEffect(); } + public int textEffectSpeed() { return text.textEffectSpeed(); } + public String textBackend() { return text.textBackend(); } + public float maxTextWidth() { return text.maxTextWidth(); } + public boolean ellipsis() { return text.ellipsis(); } + public String textAlign() { return text.textAlign(); } + public String cursor() { return text.cursor(); } + public UiBlendSpec blend() { return palette.blend(); } - public Float width() { - return width; - } - - public Float height() { - return height; - } - - public Float minWidth() { - return minWidth; - } - - public Float minHeight() { - return minHeight; - } - - public Float maxWidth() { - return maxWidth; - } - - public Float maxHeight() { - return maxHeight; - } - - public float paddingLeft() { - return paddingLeft; - } - - public float paddingTop() { - return paddingTop; - } - - public float paddingRight() { - return paddingRight; - } - - public float paddingBottom() { - return paddingBottom; - } - - public float paddingX() { - return paddingLeft + paddingRight; - } - - public float paddingY() { - return paddingTop + paddingBottom; - } - - public float marginLeft() { - return marginLeft; - } - - public float marginTop() { - return marginTop; - } - - public float marginRight() { - return marginRight; - } - - public float marginBottom() { - return marginBottom; - } - - public float marginX() { - return marginLeft + marginRight; - } - - public float marginY() { - return marginTop + marginBottom; - } - - public float gap() { - return gap; - } - - public float grow() { - return grow; - } - - public boolean absolute() { - return absolute; - } - - public Float offsetX() { - return offsetX; - } - - public Float offsetY() { - return offsetY; - } - - public UiAlign align() { - return align; - } - - public UiJustify justify() { - return justify; - } - - public UiOverflow overflow() { - return overflow; - } - - public float radius() { - return radius; - } - - public Integer backgroundColor() { - return backgroundColor; - } - - public Integer strokeColor() { - return strokeColor; - } - - public float strokeWidth() { - return strokeWidth; - } - - public Integer shadowColor() { - return shadowColor; - } - - public float shadowBlur() { - return shadowBlur; - } - - public float shadowInnerAlpha() { - return shadowInnerAlpha; - } - - public boolean blur() { - return blur; - } - - public boolean liquidGlass() { - return liquidGlass; - } - - public boolean clip() { - return clip; - } - - public boolean marquee() { - return marquee; - } - - public float blurQuality() { - return blurQuality; - } - - public float blurBrightness() { - return blurBrightness; - } - - public float blurAlpha() { - return blurAlpha; - } - - public Integer textColor() { - return textColor; - } - - public String fontFamily() { - return fontFamily; - } - - public FontInfo.Type fontType() { - return fontType; - } - - public float textScale() { - return textScale; - } - - public boolean textShadow() { - return textShadow; - } - - public String textEffect() { - return textEffect; - } - - public int textEffectSpeed() { - return textEffectSpeed; - } - - public String textBackend() { - return textBackend; - } - - public float maxTextWidth() { - return maxTextWidth; - } - - public boolean ellipsis() { - return ellipsis; - } - - public String textAlign() { - return textAlign; - } - - public String cursor() { - return cursor; - } - - public UiBlendSpec blend() { - return blend; - } - - public float resolveWidth(float fallback) { - return clamp(width != null ? width : fallback, minWidth, maxWidth); - } - - public float resolveHeight(float fallback) { - return clamp(height != null ? height : fallback, minHeight, maxHeight); - } + public float resolveWidth(float fallback) { return layout.resolveWidth(fallback); } + public float resolveHeight(float fallback) { return layout.resolveHeight(fallback); } public static final class Builder { - private Float width; - private Float height; - private Float minWidth; - private Float minHeight; - private Float maxWidth; - private Float maxHeight; - private float paddingLeft; - private float paddingTop; - private float paddingRight; - private float paddingBottom; - private float marginLeft; - private float marginTop; - private float marginRight; - private float marginBottom; - private float gap; - private float grow; - private boolean absolute; - private Float offsetX; - private Float offsetY; - private UiAlign align = UiAlign.START; - private UiJustify justify = UiJustify.START; - private UiOverflow overflow = UiOverflow.VISIBLE; - private float radius; - private Integer backgroundColor; - private Integer strokeColor; - private float strokeWidth = 1.0f; - private Integer shadowColor; - private float shadowBlur = 10.0f; - private float shadowInnerAlpha = 0.18f; - private boolean blur; - private boolean liquidGlass; - private boolean clip; - private boolean marquee; - private float blurQuality = 8.0f; - private float blurBrightness = 1.0f; - private float blurAlpha = 0.35f; - private Integer textColor = 0xFFFFFFFF; - private String fontFamily; - private FontInfo.Type fontType = FontInfo.Type.Regular; - private float textScale = 1.0f; - private boolean textShadow; - private String textEffect = ""; - private int textEffectSpeed = 18; - private String textBackend = "auto"; - private float maxTextWidth; - private boolean ellipsis; - private String textAlign = "left"; - private String cursor = ""; - private UiBlendSpec blend = UiBlendSpec.TRANSLUCENT; + final UiStyleLayout.State layout = new UiStyleLayout.State(); + final UiStylePalette.State palette = new UiStylePalette.State(); + final UiStyleText.State text = new UiStyleText.State(); - public Builder width(float width) { - this.width = width; - return this; + public Builder width(float v) { layout.width = v; return this; } + public Builder height(float v) { layout.height = v; return this; } + public Builder minWidth(float v) { layout.minWidth = v; return this; } + public Builder minHeight(float v) { layout.minHeight = v; return this; } + public Builder maxWidth(float v) { layout.maxWidth = v; return this; } + public Builder maxHeight(float v) { layout.maxHeight = v; return this; } + public Builder padding(float p) { return padding(p, p, p, p); } + public Builder padding(float h, float v) { return padding(h, v, h, v); } + public Builder padding(float l, float t, float r, float b) { + layout.paddingLeft = l; layout.paddingTop = t; + layout.paddingRight = r; layout.paddingBottom = b; return this; + } + public Builder margin(float m) { return margin(m, m, m, m); } + public Builder margin(float h, float v) { return margin(h, v, h, v); } + public Builder margin(float l, float t, float r, float b) { + layout.marginLeft = l; layout.marginTop = t; + layout.marginRight = r; layout.marginBottom = b; return this; + } + public Builder marginLeft(float v) { layout.marginLeft = v; return this; } + public Builder marginTop(float v) { layout.marginTop = v; return this; } + public Builder marginRight(float v) { layout.marginRight = v; return this; } + public Builder marginBottom(float v) { layout.marginBottom = v; return this; } + public Builder gap(float v) { layout.gap = v; return this; } + public Builder grow(float v) { layout.grow = Math.max(0.0f, v); return this; } + public Builder absolute(boolean v) { layout.absolute = v; return this; } + public Builder offsetX(float v) { layout.offsetX = v; return this; } + public Builder offsetY(float v) { layout.offsetY = v; return this; } + public Builder align(UiAlign v) { + layout.align = v != null ? v : UiAlign.START; return this; + } + public Builder justify(UiJustify v) { + layout.justify = v != null ? v : UiJustify.START; return this; + } + public Builder overflow(UiOverflow v) { + layout.overflow = v != null ? v : UiOverflow.VISIBLE; + if (layout.overflow.clips()) palette.clip = true; return this; + } + public Builder radius(float v) { palette.radius = v; return this; } + public Builder backgroundColor(int v) { palette.backgroundColor = v; return this; } + public Builder strokeColor(int v) { palette.strokeColor = v; return this; } + public Builder strokeWidth(float v) { palette.strokeWidth = v; return this; } + public Builder shadow(int c, float b, float a) { + palette.shadowColor = c; palette.shadowBlur = Math.max(0.0f, b); + palette.shadowInnerAlpha = Math.max(0.0f, Math.min(1.0f, a)); return this; + } + public Builder blur(boolean v) { palette.blur = v; return this; } + public Builder liquidGlass(boolean v) { palette.liquidGlass = v; return this; } + public Builder clip(boolean v) { palette.clip = v; return this; } + public Builder marquee(boolean v) { + palette.marquee = v; palette.clip = v || palette.clip; return this; + } + public Builder blur(float q, float br, float a) { + palette.blur = true; palette.blurQuality = q; + palette.blurBrightness = br; palette.blurAlpha = a; return this; + } + public Builder textColor(int v) { text.textColor = v; return this; } + public Builder font(String f, FontInfo.Type t) { + text.fontFamily = f; text.fontType = t != null ? t : FontInfo.Type.Regular; return this; + } + public Builder textScale(float v) { text.textScale = v; return this; } + public Builder textShadow(boolean v) { text.textShadow = v; return this; } + public Builder textEffect(String e, int s) { + text.textEffect = e != null ? e : ""; text.textEffectSpeed = Math.max(1, s); return this; + } + public Builder textBackend(String v) { + text.textBackend = v != null && !v.isBlank() ? v : "auto"; return this; + } + public Builder maxTextWidth(float v) { text.maxTextWidth = Math.max(0.0f, v); return this; } + public Builder ellipsis(boolean v) { text.ellipsis = v; return this; } + public Builder textAlign(String v) { text.textAlign = v != null ? v : "left"; return this; } + public Builder cursor(String v) { text.cursor = v != null ? v : ""; return this; } + public Builder blend(UiBlendSpec v) { + palette.blend = v != null ? v : UiBlendSpec.TRANSLUCENT; return this; } - public Builder height(float height) { - this.height = height; - return this; - } - - public Builder minWidth(float minWidth) { - this.minWidth = minWidth; - return this; - } - - public Builder minHeight(float minHeight) { - this.minHeight = minHeight; - return this; - } - - public Builder maxWidth(float maxWidth) { - this.maxWidth = maxWidth; - return this; - } - - public Builder maxHeight(float maxHeight) { - this.maxHeight = maxHeight; - return this; - } - - public Builder padding(float padding) { - return padding(padding, padding, padding, padding); - } - - public Builder padding(float horizontal, float vertical) { - return padding(horizontal, vertical, horizontal, vertical); - } - - public Builder padding(float left, float top, float right, float bottom) { - this.paddingLeft = left; - this.paddingTop = top; - this.paddingRight = right; - this.paddingBottom = bottom; - return this; - } - - public Builder margin(float margin) { - return margin(margin, margin, margin, margin); - } - - public Builder margin(float horizontal, float vertical) { - return margin(horizontal, vertical, horizontal, vertical); - } - - public Builder margin(float left, float top, float right, float bottom) { - this.marginLeft = left; - this.marginTop = top; - this.marginRight = right; - this.marginBottom = bottom; - return this; - } - - public Builder marginLeft(float marginLeft) { - this.marginLeft = marginLeft; - return this; - } - - public Builder marginTop(float marginTop) { - this.marginTop = marginTop; - return this; - } - - public Builder marginRight(float marginRight) { - this.marginRight = marginRight; - return this; - } - - public Builder marginBottom(float marginBottom) { - this.marginBottom = marginBottom; - return this; - } - - public Builder gap(float gap) { - this.gap = gap; - return this; - } - - public Builder grow(float grow) { - this.grow = Math.max(0.0f, grow); - return this; - } - - public Builder absolute(boolean absolute) { - this.absolute = absolute; - return this; - } - - public Builder offsetX(float offsetX) { - this.offsetX = offsetX; - return this; - } - - public Builder offsetY(float offsetY) { - this.offsetY = offsetY; - return this; - } - - public Builder align(UiAlign align) { - this.align = align != null ? align : UiAlign.START; - return this; - } - - public Builder justify(UiJustify justify) { - this.justify = justify != null ? justify : UiJustify.START; - return this; - } - - public Builder overflow(UiOverflow overflow) { - this.overflow = overflow != null ? overflow : UiOverflow.VISIBLE; - if (this.overflow.clips()) this.clip = true; - return this; - } - - public Builder radius(float radius) { - this.radius = radius; - return this; - } - - public Builder backgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - return this; - } - - public Builder strokeColor(int strokeColor) { - this.strokeColor = strokeColor; - return this; - } - - public Builder strokeWidth(float strokeWidth) { - this.strokeWidth = strokeWidth; - return this; - } - - public Builder shadow(int color, float blur, float innerAlpha) { - this.shadowColor = color; - this.shadowBlur = Math.max(0.0f, blur); - this.shadowInnerAlpha = Math.max(0.0f, Math.min(1.0f, innerAlpha)); - return this; - } - - public Builder blur(boolean blur) { - this.blur = blur; - return this; - } - - public Builder liquidGlass(boolean liquidGlass) { - this.liquidGlass = liquidGlass; - return this; - } - - public Builder clip(boolean clip) { - this.clip = clip; - return this; - } - - public Builder marquee(boolean marquee) { - this.marquee = marquee; - this.clip = marquee || this.clip; - return this; - } - - public Builder blur(float quality, float brightness, float alpha) { - this.blur = true; - this.blurQuality = quality; - this.blurBrightness = brightness; - this.blurAlpha = alpha; - return this; - } - - public Builder textColor(int textColor) { - this.textColor = textColor; - return this; - } - - public Builder font(String family, FontInfo.Type type) { - this.fontFamily = family; - this.fontType = type != null ? type : FontInfo.Type.Regular; - return this; - } - - public Builder textScale(float textScale) { - this.textScale = textScale; - return this; - } - - public Builder textShadow(boolean textShadow) { - this.textShadow = textShadow; - return this; - } - - public Builder textEffect(String textEffect, int speed) { - this.textEffect = textEffect != null ? textEffect : ""; - this.textEffectSpeed = Math.max(1, speed); - return this; - } - - public Builder textBackend(String textBackend) { - this.textBackend = textBackend != null && !textBackend.isBlank() ? textBackend : "auto"; - return this; - } - - public Builder maxTextWidth(float maxTextWidth) { - this.maxTextWidth = Math.max(0.0f, maxTextWidth); - return this; - } - - public Builder ellipsis(boolean ellipsis) { - this.ellipsis = ellipsis; - return this; - } - - public Builder textAlign(String textAlign) { - this.textAlign = textAlign != null ? textAlign : "left"; - return this; - } - - public Builder cursor(String cursor) { - this.cursor = cursor != null ? cursor : ""; - return this; - } - - public Builder blend(UiBlendSpec blend) { - this.blend = blend != null ? blend : UiBlendSpec.TRANSLUCENT; - return this; - } - - public UiStyle build() { - return new UiStyle(this); - } + public UiStyle build() { return new UiStyle(this); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java index 6099d63..18ee798 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java @@ -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.render.engine.renderer.ui.runtime.style.parse; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.UiStyle; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.parse.UiStyleParser; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java index a2e5de7..8dac92b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java @@ -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.render.engine.renderer.ui.runtime.style.parse; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiAlign; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.theme.UiColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java index d71964b..8b25bc4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java @@ -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.render.engine.renderer.ui.runtime.style.parse; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.UiStyle; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.parse.UiStyleCache; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleLayout.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleLayout.java new file mode 100644 index 0000000..444a4f0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleLayout.java @@ -0,0 +1,122 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.style; + +import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiAlign; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiJustify; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.token.UiOverflow; + +final class UiStyleLayout { + private final Float width; + private final Float height; + private final Float minWidth; + private final Float minHeight; + private final Float maxWidth; + private final Float maxHeight; + private final float paddingLeft; + private final float paddingTop; + private final float paddingRight; + private final float paddingBottom; + private final float marginLeft; + private final float marginTop; + private final float marginRight; + private final float marginBottom; + private final float gap; + private final float grow; + private final boolean absolute; + private final Float offsetX; + private final Float offsetY; + private final UiAlign align; + private final UiJustify justify; + private final UiOverflow overflow; + + UiStyleLayout(State state) { + this.width = state.width; + this.height = state.height; + this.minWidth = state.minWidth; + this.minHeight = state.minHeight; + this.maxWidth = state.maxWidth; + this.maxHeight = state.maxHeight; + this.paddingLeft = state.paddingLeft; + this.paddingTop = state.paddingTop; + this.paddingRight = state.paddingRight; + this.paddingBottom = state.paddingBottom; + this.marginLeft = state.marginLeft; + this.marginTop = state.marginTop; + this.marginRight = state.marginRight; + this.marginBottom = state.marginBottom; + this.gap = state.gap; + this.grow = state.grow; + this.absolute = state.absolute; + this.offsetX = state.offsetX; + this.offsetY = state.offsetY; + this.align = state.align; + this.justify = state.justify; + this.overflow = state.overflow; + } + + Float width() { return width; } + Float height() { return height; } + Float minWidth() { return minWidth; } + Float minHeight() { return minHeight; } + Float maxWidth() { return maxWidth; } + Float maxHeight() { return maxHeight; } + float paddingLeft() { return paddingLeft; } + float paddingTop() { return paddingTop; } + float paddingRight() { return paddingRight; } + float paddingBottom() { return paddingBottom; } + float paddingX() { return paddingLeft + paddingRight; } + float paddingY() { return paddingTop + paddingBottom; } + float marginLeft() { return marginLeft; } + float marginTop() { return marginTop; } + float marginRight() { return marginRight; } + float marginBottom() { return marginBottom; } + float marginX() { return marginLeft + marginRight; } + float marginY() { return marginTop + marginBottom; } + float gap() { return gap; } + float grow() { return grow; } + boolean absolute() { return absolute; } + Float offsetX() { return offsetX; } + Float offsetY() { return offsetY; } + UiAlign align() { return align; } + UiJustify justify() { return justify; } + UiOverflow overflow() { return overflow; } + + float resolveWidth(float fallback) { + return clamp(width != null ? width : fallback, minWidth, maxWidth); + } + + float resolveHeight(float fallback) { + return clamp(height != null ? height : fallback, minHeight, maxHeight); + } + + private static float clamp(float value, Float min, Float max) { + float out = value; + if (min != null) out = Math.max(out, min); + if (max != null) out = Math.min(out, max); + return out; + } + + static final class State { + Float width; + Float height; + Float minWidth; + Float minHeight; + Float maxWidth; + Float maxHeight; + float paddingLeft; + float paddingTop; + float paddingRight; + float paddingBottom; + float marginLeft; + float marginTop; + float marginRight; + float marginBottom; + float gap; + float grow; + boolean absolute; + Float offsetX; + Float offsetY; + UiAlign align = UiAlign.START; + UiJustify justify = UiJustify.START; + UiOverflow overflow = UiOverflow.VISIBLE; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStylePalette.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStylePalette.java new file mode 100644 index 0000000..07126d5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStylePalette.java @@ -0,0 +1,73 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.style; + +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.bridge.UiBlendSpec; + +final class UiStylePalette { + private final float radius; + private final Integer backgroundColor; + private final Integer strokeColor; + private final float strokeWidth; + private final Integer shadowColor; + private final float shadowBlur; + private final float shadowInnerAlpha; + private final boolean blur; + private final boolean liquidGlass; + private final boolean clip; + private final boolean marquee; + private final float blurQuality; + private final float blurBrightness; + private final float blurAlpha; + private final UiBlendSpec blend; + + UiStylePalette(State state) { + this.radius = state.radius; + this.backgroundColor = state.backgroundColor; + this.strokeColor = state.strokeColor; + this.strokeWidth = state.strokeWidth; + this.shadowColor = state.shadowColor; + this.shadowBlur = state.shadowBlur; + this.shadowInnerAlpha = state.shadowInnerAlpha; + this.blur = state.blur; + this.liquidGlass = state.liquidGlass; + this.clip = state.clip; + this.marquee = state.marquee; + this.blurQuality = state.blurQuality; + this.blurBrightness = state.blurBrightness; + this.blurAlpha = state.blurAlpha; + this.blend = state.blend; + } + + float radius() { return radius; } + Integer backgroundColor() { return backgroundColor; } + Integer strokeColor() { return strokeColor; } + float strokeWidth() { return strokeWidth; } + Integer shadowColor() { return shadowColor; } + float shadowBlur() { return shadowBlur; } + float shadowInnerAlpha() { return shadowInnerAlpha; } + boolean blur() { return blur; } + boolean liquidGlass() { return liquidGlass; } + boolean clip() { return clip; } + boolean marquee() { return marquee; } + float blurQuality() { return blurQuality; } + float blurBrightness() { return blurBrightness; } + float blurAlpha() { return blurAlpha; } + UiBlendSpec blend() { return blend; } + + static final class State { + float radius; + Integer backgroundColor; + Integer strokeColor; + float strokeWidth = 1.0f; + Integer shadowColor; + float shadowBlur = 10.0f; + float shadowInnerAlpha = 0.18f; + boolean blur; + boolean liquidGlass; + boolean clip; + boolean marquee; + float blurQuality = 8.0f; + float blurBrightness = 1.0f; + float blurAlpha = 0.35f; + UiBlendSpec blend = UiBlendSpec.TRANSLUCENT; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleText.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleText.java new file mode 100644 index 0000000..43fd324 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/style/UiStyleText.java @@ -0,0 +1,61 @@ +package dev.loki.lovisual.render.engine.renderer.ui.runtime.style; + +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; + +final class UiStyleText { + private final Integer textColor; + private final String fontFamily; + private final FontInfo.Type fontType; + private final float textScale; + private final boolean textShadow; + private final String textEffect; + private final int textEffectSpeed; + private final String textBackend; + private final float maxTextWidth; + private final boolean ellipsis; + private final String textAlign; + private final String cursor; + + UiStyleText(State state) { + this.textColor = state.textColor; + this.fontFamily = state.fontFamily; + this.fontType = state.fontType; + this.textScale = state.textScale; + this.textShadow = state.textShadow; + this.textEffect = state.textEffect; + this.textEffectSpeed = state.textEffectSpeed; + this.textBackend = state.textBackend; + this.maxTextWidth = state.maxTextWidth; + this.ellipsis = state.ellipsis; + this.textAlign = state.textAlign; + this.cursor = state.cursor; + } + + Integer textColor() { return textColor; } + String fontFamily() { return fontFamily; } + FontInfo.Type fontType() { return fontType; } + float textScale() { return textScale; } + boolean textShadow() { return textShadow; } + String textEffect() { return textEffect; } + int textEffectSpeed() { return textEffectSpeed; } + String textBackend() { return textBackend; } + float maxTextWidth() { return maxTextWidth; } + boolean ellipsis() { return ellipsis; } + String textAlign() { return textAlign; } + String cursor() { return cursor; } + + static final class State { + Integer textColor = 0xFFFFFFFF; + String fontFamily; + FontInfo.Type fontType = FontInfo.Type.Regular; + float textScale = 1.0f; + boolean textShadow; + String textEffect = ""; + int textEffectSpeed = 18; + String textBackend = "auto"; + float maxTextWidth; + boolean ellipsis; + String textAlign = "left"; + String cursor = ""; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java index dc1b982..edd887c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java @@ -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.render.engine.renderer.ui.runtime.style.theme; public enum UiColor { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java index 2b0dcca..aed215c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java @@ -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.render.engine.renderer.ui.runtime.style.theme; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java index 68d43ad..8554846 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java @@ -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.render.engine.renderer.ui.runtime.style.theme; import dev.loki.lovisual.render.engine.renderer.ui.runtime.style.theme.UiTheme; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java index 59ffc71..5211381 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java @@ -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.render.engine.renderer.ui.runtime.style.token; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java index e40e5ad..f45fe26 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java @@ -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.render.engine.renderer.ui.runtime.style.token; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java index 2b9e71a..188ccbf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java @@ -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.render.engine.renderer.ui.runtime.style.token; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java index c9327a9..ac4d883 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java @@ -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.render.engine.renderer.ui.runtime.style.token; public record UiStyleToken(String raw, String variant, String name, String value) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java index 1396c95..d929b32 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java @@ -1,15 +1,11 @@ -/* - * 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.render.engine.renderer.warp; -import dev.loki.lovisual.features.gui.hud.draggable.impl.metrics.Coordinates; /** * Screen-space UI warp. Coordinates stay in the same 2D HUD space; only emitted vertices are remapped. + * + *

    Instances are immutable value objects; build them via {@link RenderWarpFactory} + * ({@code corners}/{@code tilt}/{@code angles}/{@code perspective}) and query them through the + * {@code map*}/{@code *Corner} accessors used by the renderer.

    */ public final class RenderWarp { public static final RenderWarp IDENTITY = new RenderWarp( @@ -53,20 +49,20 @@ public final class RenderWarp { private final double h20; private final double h21; - private RenderWarp(boolean active, - float x, - float y, - float width, - float height, - float tlX, - float tlY, - float trX, - float trY, - float brX, - float brY, - float blX, - float blY, - float strength) { + RenderWarp(boolean active, + float x, + float y, + float width, + float height, + float tlX, + float tlY, + float trX, + float trY, + float brX, + float brY, + float blX, + float blY, + float strength) { this.active = active; this.x = x; this.y = y; @@ -90,7 +86,7 @@ public final class RenderWarp { this.dstBlX = x + blX * strength; this.dstBlY = y + height + blY * strength; - double[] matrix = computeUnitSquareHomography( + double[] matrix = RenderWarpGeometry.computeUnitSquareHomography( dstTlX, dstTlY, dstTrX, dstTrY, dstBrX, dstBrY, @@ -106,213 +102,6 @@ public final class RenderWarp { this.h21 = matrix[7]; } - public static RenderWarp corners(float x, - float y, - float width, - float height, - float tlX, - float tlY, - float trX, - float trY, - float brX, - float brY, - float blX, - float blY) { - return corners(x, y, width, height, tlX, tlY, trX, trY, brX, brY, blX, blY, 1.0f); - } - - public static RenderWarp corners(float x, - float y, - float width, - float height, - float tlX, - float tlY, - float trX, - float trY, - float brX, - float brY, - float blX, - float blY, - float strength) { - if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f || Math.abs(strength) <= 0.0001f) { - return IDENTITY; - } - return new RenderWarp(true, x, y, width, height, - tlX, tlY, trX, trY, brX, brY, blX, blY, clampStrength(strength)); - } - - public static RenderWarp tilt(float x, - float y, - float width, - float height, - float strength, - float hover, - float mouseX, - float mouseY) { - if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f) { - return IDENTITY; - } - - float cx = x + width * 0.5f; - float cy = y + height * 0.5f; - float dx = clamp((mouseX - cx) / width, -1.0f, 1.0f); - float dy = clamp((mouseY - cy) / height, -1.0f, 1.0f); - float amount = Math.max(0.0f, hover) * strength; - if (Math.abs(amount) <= 0.0001f) { - return IDENTITY; - } - - return angles(x, y, width, height, -dx * amount, dy * amount, 0.0f, - 3.5f, 1.0f, 1.0f + Math.max(0.0f, hover) * 0.025f); - } - - public static RenderWarp angles(float x, - float y, - float width, - float height, - float yawDeg, - float pitchDeg, - float rollDeg) { - return angles(x, y, width, height, yawDeg, pitchDeg, rollDeg, 3.5f, 1.0f, 1.0f); - } - - public static RenderWarp perspective(float x, - float y, - float width, - float height, - float yawDeg, - float pitchDeg, - float rollDeg, - float depth, - float perspective, - float scale) { - return angles(x, y, width, height, yawDeg, pitchDeg, rollDeg, depth, perspective, scale); - } - - public static RenderWarp angles(float x, - float y, - float width, - float height, - float yawDeg, - float pitchDeg, - float rollDeg, - float depth, - float perspective, - float scale) { - if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f) { - return IDENTITY; - } - if (Math.abs(yawDeg) <= 0.0001f - && Math.abs(pitchDeg) <= 0.0001f - && Math.abs(rollDeg) <= 0.0001f - && Math.abs(scale - 1.0f) <= 0.0001f) { - return IDENTITY; - } - - float cx = x + width * 0.5f; - float cy = y + height * 0.5f; - float cameraDistance = Math.max(1.0f, Math.max(Math.abs(width), Math.abs(height)) * Math.max(0.75f, depth)); - float persp = Math.max(0.0f, perspective); - float s = Math.max(0.05f, scale); - - float[] tl = projectCorner(-width * 0.5f, -height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); - float[] tr = projectCorner(width * 0.5f, -height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); - float[] br = projectCorner(width * 0.5f, height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); - float[] bl = projectCorner(-width * 0.5f, height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); - - return corners( - x, y, width, height, - tl[0] - x, tl[1] - y, - tr[0] - (x + width), tr[1] - y, - br[0] - (x + width), br[1] - (y + height), - bl[0] - x, bl[1] - (y + height), - 1.0f - ); - } - - private static double[] computeUnitSquareHomography(float x0, float y0, - float x1, float y1, - float x2, float y2, - float x3, float y3) { - double dx1 = x1 - x2; - double dy1 = y1 - y2; - double dx2 = x3 - x2; - double dy2 = y3 - y2; - double dx3 = x0 - x1 + x2 - x3; - double dy3 = y0 - y1 + y2 - y3; - double det = dx1 * dy2 - dx2 * dy1; - - double g = 0.0; - double h = 0.0; - if (Math.abs(det) > 0.000001) { - g = (dx3 * dy2 - dx2 * dy3) / det; - h = (dx1 * dy3 - dx3 * dy1) / det; - } - - double a = x1 - x0 + g * x1; - double b = x3 - x0 + h * x3; - double c = x0; - double d = y1 - y0 + g * y1; - double e = y3 - y0 + h * y3; - double f = y0; - return new double[]{a, b, c, d, e, f, g, h}; - } - - private static float[] projectCorner(float localX, - float localY, - float yawDeg, - float pitchDeg, - float rollDeg, - float cameraDistance, - float perspective, - float scale, - float centerX, - float centerY) { - float yaw = (float) Math.toRadians(clamp(yawDeg, -85.0f, 85.0f)); - float pitch = (float) Math.toRadians(clamp(pitchDeg, -85.0f, 85.0f)); - float roll = (float) Math.toRadians(rollDeg); - - float x0 = localX; - float y0 = localY; - float z0 = 0.0f; - - float cosX = (float) Math.cos(pitch); - float sinX = (float) Math.sin(pitch); - float y1 = y0 * cosX - z0 * sinX; - float z1 = y0 * sinX + z0 * cosX; - float x1 = x0; - - float cosY = (float) Math.cos(yaw); - float sinY = (float) Math.sin(yaw); - float x2 = x1 * cosY + z1 * sinY; - float z2 = -x1 * sinY + z1 * cosY; - float y2 = y1; - - float cosZ = (float) Math.cos(roll); - float sinZ = (float) Math.sin(roll); - float x3 = x2 * cosZ - y2 * sinZ; - float y3 = x2 * sinZ + y2 * cosZ; - float z3 = z2; - - float denom = Math.max(cameraDistance * 0.15f, cameraDistance + z3 * perspective); - float p = cameraDistance / denom; - return new float[]{centerX + x3 * p * scale, centerY + y3 * p * scale}; - } - - private static float lerp(float a, float b, float t) { - return a + (b - a) * t; - } - - private static float clampStrength(float value) { - return clamp(value, -4.0f, 4.0f); - } - - private static float clamp(float value, float min, float max) { - if (value < min) return min; - if (value > max) return max; - return value; - } - public boolean active() { return active; } @@ -335,7 +124,7 @@ public final class RenderWarp { public RenderWarp withStrength(float multiplier) { if (!active) return this; - return corners(x, y, width, height, tlX, tlY, trX, trY, brX, brY, blX, blY, strength * multiplier); + return RenderWarpFactory.corners(x, y, width, height, tlX, tlY, trX, trY, brX, brY, blX, blY, strength * multiplier); } public double mapX(double px, double py) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java index 032f661..0caedcf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java @@ -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.render.engine.renderer.warp; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpFactory.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpFactory.java new file mode 100644 index 0000000..b7dfa63 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpFactory.java @@ -0,0 +1,135 @@ +package dev.loki.lovisual.render.engine.renderer.warp; + +/** + * Static construction entry points for {@link RenderWarp}: corner warps, mouse tilt and + * yaw/pitch/roll perspective. Produces immutable warp values via the package-private + * {@link RenderWarp} constructor, delegating the trigonometry to {@link RenderWarpGeometry}. + */ +public final class RenderWarpFactory { + private RenderWarpFactory() { + } + + public static RenderWarp corners(float x, + float y, + float width, + float height, + float tlX, + float tlY, + float trX, + float trY, + float brX, + float brY, + float blX, + float blY) { + return corners(x, y, width, height, tlX, tlY, trX, trY, brX, brY, blX, blY, 1.0f); + } + + public static RenderWarp corners(float x, + float y, + float width, + float height, + float tlX, + float tlY, + float trX, + float trY, + float brX, + float brY, + float blX, + float blY, + float strength) { + if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f || Math.abs(strength) <= 0.0001f) { + return RenderWarp.IDENTITY; + } + return new RenderWarp(true, x, y, width, height, + tlX, tlY, trX, trY, brX, brY, blX, blY, RenderWarpGeometry.clampStrength(strength)); + } + + public static RenderWarp tilt(float x, + float y, + float width, + float height, + float strength, + float hover, + float mouseX, + float mouseY) { + if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f) { + return RenderWarp.IDENTITY; + } + + float cx = x + width * 0.5f; + float cy = y + height * 0.5f; + float dx = RenderWarpGeometry.clamp((mouseX - cx) / width, -1.0f, 1.0f); + float dy = RenderWarpGeometry.clamp((mouseY - cy) / height, -1.0f, 1.0f); + float amount = Math.max(0.0f, hover) * strength; + if (Math.abs(amount) <= 0.0001f) { + return RenderWarp.IDENTITY; + } + + return angles(x, y, width, height, -dx * amount, dy * amount, 0.0f, + 3.5f, 1.0f, 1.0f + Math.max(0.0f, hover) * 0.025f); + } + + public static RenderWarp angles(float x, + float y, + float width, + float height, + float yawDeg, + float pitchDeg, + float rollDeg) { + return angles(x, y, width, height, yawDeg, pitchDeg, rollDeg, 3.5f, 1.0f, 1.0f); + } + + public static RenderWarp perspective(float x, + float y, + float width, + float height, + float yawDeg, + float pitchDeg, + float rollDeg, + float depth, + float perspective, + float scale) { + return angles(x, y, width, height, yawDeg, pitchDeg, rollDeg, depth, perspective, scale); + } + + public static RenderWarp angles(float x, + float y, + float width, + float height, + float yawDeg, + float pitchDeg, + float rollDeg, + float depth, + float perspective, + float scale) { + if (Math.abs(width) <= 0.0001f || Math.abs(height) <= 0.0001f) { + return RenderWarp.IDENTITY; + } + if (Math.abs(yawDeg) <= 0.0001f + && Math.abs(pitchDeg) <= 0.0001f + && Math.abs(rollDeg) <= 0.0001f + && Math.abs(scale - 1.0f) <= 0.0001f) { + return RenderWarp.IDENTITY; + } + + float cx = x + width * 0.5f; + float cy = y + height * 0.5f; + float cameraDistance = Math.max(1.0f, Math.max(Math.abs(width), Math.abs(height)) * Math.max(0.75f, depth)); + float persp = Math.max(0.0f, perspective); + float s = Math.max(0.05f, scale); + + float[] tl = RenderWarpGeometry.projectCorner(-width * 0.5f, -height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); + float[] tr = RenderWarpGeometry.projectCorner(width * 0.5f, -height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); + float[] br = RenderWarpGeometry.projectCorner(width * 0.5f, height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); + float[] bl = RenderWarpGeometry.projectCorner(-width * 0.5f, height * 0.5f, yawDeg, pitchDeg, rollDeg, cameraDistance, persp, s, cx, cy); + + return corners( + x, y, width, height, + tl[0] - x, tl[1] - y, + tr[0] - (x + width), tr[1] - y, + br[0] - (x + width), br[1] - (y + height), + bl[0] - x, bl[1] - (y + height), + 1.0f + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpGeometry.java new file mode 100644 index 0000000..12024ab --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/impl/RenderWarpGeometry.java @@ -0,0 +1,93 @@ +package dev.loki.lovisual.render.engine.renderer.warp; + +/** + * Pure numeric helpers backing {@link RenderWarp} construction: unit-square homography solve, + * perspective corner projection and scalar clamping. Stateless by design. + */ +final class RenderWarpGeometry { + private RenderWarpGeometry() { + } + + static double[] computeUnitSquareHomography(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3) { + double dx1 = x1 - x2; + double dy1 = y1 - y2; + double dx2 = x3 - x2; + double dy2 = y3 - y2; + double dx3 = x0 - x1 + x2 - x3; + double dy3 = y0 - y1 + y2 - y3; + double det = dx1 * dy2 - dx2 * dy1; + + double g = 0.0; + double h = 0.0; + if (Math.abs(det) > 0.000001) { + g = (dx3 * dy2 - dx2 * dy3) / det; + h = (dx1 * dy3 - dx3 * dy1) / det; + } + + double a = x1 - x0 + g * x1; + double b = x3 - x0 + h * x3; + double c = x0; + double d = y1 - y0 + g * y1; + double e = y3 - y0 + h * y3; + double f = y0; + return new double[]{a, b, c, d, e, f, g, h}; + } + + static float[] projectCorner(float localX, + float localY, + float yawDeg, + float pitchDeg, + float rollDeg, + float cameraDistance, + float perspective, + float scale, + float centerX, + float centerY) { + float yaw = (float) Math.toRadians(clamp(yawDeg, -85.0f, 85.0f)); + float pitch = (float) Math.toRadians(clamp(pitchDeg, -85.0f, 85.0f)); + float roll = (float) Math.toRadians(rollDeg); + + float x0 = localX; + float y0 = localY; + float z0 = 0.0f; + + float cosX = (float) Math.cos(pitch); + float sinX = (float) Math.sin(pitch); + float y1 = y0 * cosX - z0 * sinX; + float z1 = y0 * sinX + z0 * cosX; + float x1 = x0; + + float cosY = (float) Math.cos(yaw); + float sinY = (float) Math.sin(yaw); + float x2 = x1 * cosY + z1 * sinY; + float z2 = -x1 * sinY + z1 * cosY; + float y2 = y1; + + float cosZ = (float) Math.cos(roll); + float sinZ = (float) Math.sin(roll); + float x3 = x2 * cosZ - y2 * sinZ; + float y3 = x2 * sinZ + y2 * cosZ; + float z3 = z2; + + float denom = Math.max(cameraDistance * 0.15f, cameraDistance + z3 * perspective); + float p = cameraDistance / denom; + return new float[]{centerX + x3 * p * scale, centerY + y3 * p * scale}; + } + + static float clampStrength(float value) { + return clamp(value, -4.0f, 4.0f); + } + + static float clamp(float value, float min, float max) { + if (value < min) return min; + if (value > max) return max; + return value; + } + + static float lerp(float a, float b, float t) { + return a + (b - a) * t; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java index 3ec9314..858cbaf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java @@ -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.render.engine.rhi.backend; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java index 13b87e4..2cb410a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java @@ -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.render.engine.rhi.backend.gl; import com.mojang.blaze3d.systems.GpuDevice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java index 5891183..cf8efd3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java @@ -1,188 +1,29 @@ -/* - * 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.render.engine.rhi.backend.gl.clip; -import dev.loki.lovisual.render.engine.texture.Texture; - -import com.mojang.blaze3d.opengl.FrameBufferAttachment; import com.mojang.blaze3d.opengl.GlConst; import com.mojang.blaze3d.opengl.GlStateManager; import com.mojang.blaze3d.opengl.GlTextureView; import com.mojang.blaze3d.pipeline.RenderTarget; import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.mixininterface.device.IGlBackendInfo; +import dev.loki.lovisual.render.engine.rhi.backend.gl.GlBackendAccess; import net.minecraft.client.Minecraft; import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.*; -import dev.loki.lovisual.mixininterface.device.IGlBackendInfo; -import dev.loki.lovisual.render.engine.msaa.MsaaTextureRegistry; -import dev.loki.lovisual.render.engine.rhi.backend.gl.GlBackendAccess; -import dev.loki.lovisual.util.logging.DebugLog; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL30C; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; -import dev.loki.lovisual.render.engine.rhi.backend.gl.clip.GlStencilShapeClipBackend; -/** - * GL-only runtime stencil attachment/probe for Mojang framebuffers. - * - *

    Minecraft 1.21.x framebuffers normally expose a color texture plus a DEPTH32 texture. Some GL - * drivers reject adding a separate GL_STENCIL_INDEX8 renderbuffer to that color/depth combination - * with GL_FRAMEBUFFER_UNSUPPORTED (0x8CDD). When that happens this backend switches to a real packed - * depth-stencil renderbuffer for the active clip scope and restores the original attachments when - * shape clipping is disabled.

    - */ public final class GlStencilFramebufferSupport { - /** - * GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_SAMPLES_EXT is not a core framebuffer attachment pname and - * Mesa/X11 reports it through KHR_debug as GL_INVALID_ENUM. Do not use attachment-level sample - * queries. Texture sample detection is registry-first and then a guarded DSA texture-level query. - */ - private static boolean textureSamplesDsaProbeDisabled; - private final GlStencilShapeClipBackend owner; - private final Map attachments = new HashMap<>(); - private boolean warnedUnsupported; - private String lastFailure = "none"; - GlStencilFramebufferSupport(GlStencilShapeClipBackend owner) { - this.owner = owner; - } + private final GlStencilGl gl; + private final GlStencilSampleProbe probe; + private final GlStencilAttachmentManager manager; - private static void restoreFramebufferBindings(int readFramebuffer, int drawFramebuffer) { - // Mojang tracks read and draw FBOs separately. getFrameBuffer(GL_FRAMEBUFFER) - // returns 0, so any helper that temporarily binds GL_FRAMEBUFFER must restore - // GL_READ_FRAMEBUFFER and GL_DRAW_FRAMEBUFFER explicitly or the active render - // pass silently falls back to the default FBO with no stencil attachment. - if (readFramebuffer == drawFramebuffer) { - GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, readFramebuffer); - return; - } - GlStateManager._glBindFramebuffer(GlConst.GL_READ_FRAMEBUFFER, readFramebuffer); - GlStateManager._glBindFramebuffer(GlConst.GL_DRAW_FRAMEBUFFER, drawFramebuffer); - } - - private static int framebufferFor(IGlBackendInfo backend, GlTextureView colorView, @Nullable GpuTextureView depthView) { - FrameBufferAttachment depthAttachment = null; - if (depthView != null) { - if (!(depthView instanceof GlTextureView glDepthView)) { - throw new IllegalArgumentException("depth view is not GlTextureView: " + depthView.getClass().getName()); - } - depthAttachment = glDepthView; - } - return backend.lovisual$frameBufferCache().getFbo( - backend.lovisual$directStateAccess(), - List.of(colorView), - depthAttachment - ); - } - - private static int detectBoundFramebufferSamples() { - int colorSamples = attachmentSamples(GL30C.GL_COLOR_ATTACHMENT0); - int depthSamples = attachmentSamples(GL30C.GL_DEPTH_ATTACHMENT); - int stencilSamples = attachmentSamples(GL30C.GL_STENCIL_ATTACHMENT); - return Math.max(1, Math.max(colorSamples, Math.max(depthSamples, stencilSamples))); - } - - private static int attachmentSamples(int attachmentPoint) { - int type = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE - ); - if (type == GL11C.GL_NONE) return 1; - - if (type == GL30C.GL_RENDERBUFFER) { - int renderbuffer = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME - ); - return renderbufferSamples(renderbuffer); - } - - if (type == GL11C.GL_TEXTURE) { - int texture = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME - ); - return textureSamples(texture); - } - - return 1; - } - - private static int textureSamples(int texture) { - if (texture == 0) return 1; - - int registeredSamples = MsaaTextureRegistry.getSamples(texture); - if (registeredSamples > 1) { - return registeredSamples; - } - - if (textureSamplesDsaProbeDisabled) { - return 1; - } - - GLCapabilities capabilities = GL.getCapabilities(); - if (capabilities == null || (!capabilities.OpenGL45 && !capabilities.GL_ARB_direct_state_access)) { - return 1; - } - - try { - clearGlErrorsSilently(); - int samples = capabilities.OpenGL45 - ? GL45C.glGetTextureLevelParameteri(texture, 0, GL32C.GL_TEXTURE_SAMPLES) - : ARBDirectStateAccess.glGetTextureLevelParameteri(texture, 0, GL32C.GL_TEXTURE_SAMPLES); - int error = GL11C.glGetError(); - if (error == GL11C.GL_NO_ERROR) { - return Math.max(1, samples); - } - - // Never keep probing a driver/path that rejects the target-free sample query. One - // failed probe is enough; repeated probes are exactly what creates scary KHR_debug spam. - textureSamplesDsaProbeDisabled = true; - } catch (Throwable ignored) { - textureSamplesDsaProbeDisabled = true; - } - clearGlErrorsSilently(); - return 1; - } - - private static int renderbufferSamples(int renderbuffer) { - if (renderbuffer == 0) return 1; - int previousRenderbuffer = GL11C.glGetInteger(GL30C.GL_RENDERBUFFER_BINDING); - try { - GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, renderbuffer); - int samples = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_SAMPLES); - return Math.max(1, samples); - } catch (Throwable ignored) { - clearGlErrorsSilently(); - return 1; - } finally { - GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, previousRenderbuffer); - } - } - - private static void clearGlErrorsSilently() { - int guard = 0; - while (GL11C.glGetError() != GL11C.GL_NO_ERROR && guard++ < 16) { - // drain previous error state before optional capability probes - } - } - - private static void checkGlErrors(String where) { - int guard = 0; - int error; - while ((error = GL11C.glGetError()) != GL11C.GL_NO_ERROR && guard++ < 16) { - DebugLog.errorOnChange("shapeclip.framebuffer.gl.error", where + "|" + error, "[ShapeClip/GL] gl error at %s: 0x%s", where, Integer.toHexString(error)); - } + public GlStencilFramebufferSupport() { + this.gl = new GlStencilGl(); + this.probe = new GlStencilSampleProbe(this.gl); + this.manager = new GlStencilAttachmentManager(this.gl, this.probe); } boolean supported() { @@ -190,477 +31,132 @@ public final class GlStencilFramebufferSupport { } String lastFailure() { - return lastFailure; + return manager.lastFailure(); } String describe() { - return "attachments=" + attachments.size() - + ", warnedUnsupported=" + warnedUnsupported - + ", lastFailure='" + lastFailure + "'"; + return manager.describe(); } boolean ensureForMainFramebuffer() { Minecraft mc = Minecraft.getInstance(); - if (mc == null) return fail("MinecraftClient is null"); - RenderTarget framebuffer = mc.gameRenderer.mainRenderTarget(); - if (framebuffer == null) return fail("main framebuffer is null"); - - /* - * UI batches in LoVisual are rendered into the main color target without passing the - * framebuffer depth attachment to the render pass. GlTextureView keeps separate FBOs for - * (color, null-depth) and (color, depthTexture). The shape-clip mask must be prepared and - * cleared on the same color-only FBO that Renderer2D will use; otherwise the mask is written - * to one framebuffer while the actual module rows are tested against another stencil buffer. - */ - return ensure(framebuffer.getColorTextureView(), null); + if (mc == null) { + return manager.fail("MinecraftClient is null"); + } + RenderTarget fb = mc.gameRenderer.mainRenderTarget(); + if (fb == null) { + return manager.fail("main framebuffer is null"); + } + return ensure(fb.getColorTextureView(), null); } boolean clearMainFramebufferStencil() { Minecraft mc = Minecraft.getInstance(); - if (mc == null) return fail("MinecraftClient is null"); - RenderTarget framebuffer = mc.gameRenderer.mainRenderTarget(); - if (framebuffer == null) return fail("main framebuffer is null"); - - // Keep this in lockstep with ensureForMainFramebuffer(): Renderer2D UI uses color-only FBOs. - return clear(framebuffer.getColorTextureView(), null); + if (mc == null) { + return manager.fail("MinecraftClient is null"); + } + RenderTarget fb = mc.gameRenderer.mainRenderTarget(); + if (fb == null) { + return manager.fail("main framebuffer is null"); + } + return clear(fb.getColorTextureView(), null); } boolean ensure(@Nullable GpuTextureView colorView, @Nullable GpuTextureView depthView) { IGlBackendInfo backend = GlBackendAccess.current(); if (backend == null) { - return fail("RenderSystem device is not GlBackend"); + return manager.fail("RenderSystem device is not GlBackend"); } if (colorView == null) { - return fail("color view is null"); + return manager.fail("color view is null"); } if (!(colorView instanceof GlTextureView glColorView)) { - return fail("color view is not GlTextureView: " + colorView.getClass().getName()); + return manager.fail("color view is not GlTextureView: " + colorView.getClass().getName()); } if (colorView.isClosed()) { - return fail("color view is closed"); + return manager.fail("color view is closed"); } - try { - int framebuffer = framebufferFor(backend, glColorView, depthView); + int framebuffer = gl.framebufferFor(backend, glColorView, depthView); int width = Math.max(1, colorView.getWidth(0)); int height = Math.max(1, colorView.getHeight(0)); - return ensureAttachment(framebuffer, width, height); + return manager.ensureAttachment(framebuffer, width, height); } catch (Throwable t) { - return fail("exception while preparing framebuffer: " + t.getClass().getSimpleName() + ": " + t.getMessage()); + return manager.fail("exception while preparing framebuffer: " + t.getClass().getSimpleName() + ": " + t.getMessage()); } } boolean clear(@Nullable GpuTextureView colorView, @Nullable GpuTextureView depthView) { - if (!ensure(colorView, depthView)) return false; + if (!ensure(colorView, depthView)) { + return false; + } IGlBackendInfo backend = GlBackendAccess.current(); if (backend == null || !(colorView instanceof GlTextureView glColorView)) { - return fail("clear target is not backed by GlTextureView"); + return manager.fail("clear target is not backed by GlTextureView"); } - - int previousReadFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); - int previousDrawFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); - boolean scissorWasEnabled = GL11C.glIsEnabled(GL11C.GL_SCISSOR_TEST); - int previousStencilMask = GL11C.glGetInteger(GL11C.GL_STENCIL_WRITEMASK); + int prevRead = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); + int prevDraw = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); + boolean scissor = GL11C.glIsEnabled(GL11C.GL_SCISSOR_TEST); + int mask = GL11C.glGetInteger(GL11C.GL_STENCIL_WRITEMASK); try { - int framebuffer = framebufferFor(backend, glColorView, depthView); - GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, framebuffer); + int fb = gl.framebufferFor(backend, glColorView, depthView); + GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, fb); int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); if (status != GL30C.GL_FRAMEBUFFER_COMPLETE) { - // Do not issue glClear on an incomplete FBO: that is what produces the scary - // GL_INVALID_FRAMEBUFFER_OPERATION debug spam on stricter drivers. Rebuild the - // attachment once because the owning GlTextureView may have recreated its backing FBO. - invalidateAttachment(framebuffer, "clear preflight incomplete status=0x" + Integer.toHexString(status)); + manager.invalidateAttachment(fb, "clear preflight incomplete status=0x" + Integer.toHexString(status)); if (!ensure(colorView, depthView)) { return false; } - framebuffer = framebufferFor(backend, glColorView, depthView); - GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, framebuffer); + fb = gl.framebufferFor(backend, glColorView, depthView); + GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, fb); status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); if (status != GL30C.GL_FRAMEBUFFER_COMPLETE) { - return fail("clear target still incomplete after rebuild: status=0x" + Integer.toHexString(status) + ", fbo=" + framebuffer); + return manager.fail("clear target still incomplete after rebuild: status=0x" + Integer.toHexString(status) + ", fbo=" + fb); } } GL11C.glDisable(GL11C.GL_SCISSOR_TEST); GL11C.glStencilMask(0xFF); GL11C.glClearStencil(0); GL11C.glClear(GL11C.GL_STENCIL_BUFFER_BIT); - GL11C.glStencilMask(previousStencilMask); - checkGlErrors("clearStencil"); + GL11C.glStencilMask(mask); + gl.checkGlErrors("clearStencil"); return true; } catch (Throwable t) { - return fail("exception while clearing stencil: " + t.getClass().getSimpleName() + ": " + t.getMessage()); + return manager.fail("exception while clearing stencil: " + t.getClass().getSimpleName() + ": " + t.getMessage()); } finally { - if (scissorWasEnabled) { + if (scissor) { GL11C.glEnable(GL11C.GL_SCISSOR_TEST); } else { GL11C.glDisable(GL11C.GL_SCISSOR_TEST); } - restoreFramebufferBindings(previousReadFramebuffer, previousDrawFramebuffer); + gl.restoreFramebufferBindings(prevRead, prevDraw); } } void restoreActiveAttachments() { - int previousReadFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); - int previousDrawFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); + int prevRead = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); + int prevDraw = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); try { - for (Map.Entry entry : attachments.entrySet()) { - Attachment attachment = entry.getValue(); - if (!attachment.attached) continue; - GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, entry.getKey()); - attachment.restoreOriginalAttachments(); + for (Map.Entry e : manager.attachments().entrySet()) { + GlStencilAttachment a = e.getValue(); + if (!a.attached) { + continue; + } + GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, e.getKey()); + a.restoreOriginalAttachments(); } } catch (Throwable t) { - fail("exception while restoring framebuffer attachments: " + t.getClass().getSimpleName() + ": " + t.getMessage()); + manager.fail("exception while restoring framebuffer attachments: " + t.getClass().getSimpleName() + ": " + t.getMessage()); } finally { - restoreFramebufferBindings(previousReadFramebuffer, previousDrawFramebuffer); + gl.restoreFramebufferBindings(prevRead, prevDraw); } } void cleanupClosedOrResized() { - Iterator> it = attachments.entrySet().iterator(); - while (it.hasNext()) { - Attachment attachment = it.next().getValue(); - if (attachment.deleted) { - it.remove(); - } - } + manager.cleanupClosedOrResized(); } boolean hasWarnedUnsupported() { - return warnedUnsupported; - } - - private boolean ensureAttachment(int framebuffer, int width, int height) { - Attachment attachment = attachments.get(framebuffer); - int previousReadFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); - int previousDrawFramebuffer = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); - try { - GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, framebuffer); - int samples = detectBoundFramebufferSamples(); - - if (attachment == null || attachment.width != width || attachment.height != height || attachment.samples != samples || attachment.deleted) { - if (attachment != null) { - attachment.restoreOriginalAttachments(); - attachment.delete(); - } - attachment = new Attachment(width, height, samples); - attachments.put(framebuffer, attachment); - } - - if (attachment.attached) { - int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); - if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { - owner.setStencilAvailable(true); - lastFailure = "none"; - checkGlErrors("ensureAttachment(existing)"); - return true; - } - DebugLog.warnOnChange("shapeclip.attachment.stale", framebuffer + "|" + status + "|" + attachment.mode, "[ShapeClip/GL] existing stencil attachment stale/incomplete. fbo=%d status=0x%s mode=%s", framebuffer, Integer.toHexString(status), attachment.mode); - attachment.restoreOriginalAttachments(); - } - - attachment.captureOriginalAttachments(); - - if (!attachment.separateStencilRejected && tryAttachSeparateStencil(framebuffer, attachment)) { - owner.setStencilAvailable(true); - lastFailure = "none"; - return true; - } - - if (tryAttachPackedDepthStencil(framebuffer, attachment)) { - owner.setStencilAvailable(true); - lastFailure = "none"; - return true; - } - - attachment.restoreOriginalAttachments(); - markUnavailable(); - return false; - } finally { - restoreFramebufferBindings(previousReadFramebuffer, previousDrawFramebuffer); - } - } - - private void invalidateAttachment(int framebuffer, String reason) { - Attachment attachment = attachments.remove(framebuffer); - if (attachment == null) { - lastFailure = reason; - return; - } - try { - attachment.restoreOriginalAttachments(); - } catch (Throwable t) { - DebugLog.warnOnChange("shapeclip.attachment.restore.invalidate", framebuffer + "|" + t.getClass().getSimpleName(), "[ShapeClip/GL] failed to restore attachment while invalidating fbo=%d reason=%s error=%s: %s", framebuffer, reason, t.getClass().getSimpleName(), t.getMessage()); - } finally { - try { - attachment.delete(); - } catch (Throwable t) { - DebugLog.warnOnChange("shapeclip.attachment.delete.invalidate", framebuffer + "|" + t.getClass().getSimpleName(), "[ShapeClip/GL] failed to delete invalidated attachment fbo=%d error=%s: %s", framebuffer, t.getClass().getSimpleName(), t.getMessage()); - } - lastFailure = reason; - DebugLog.stencilOnChange("shapeclip.attachment.invalidated", framebuffer + "|" + reason, "[ShapeClip/GL] invalidated cached stencil attachment. fbo=%d reason=%s", framebuffer, reason); - } - } - - private boolean tryAttachSeparateStencil(int framebuffer, Attachment attachment) { - if (!attachment.separateStencilAllocationOk) { - attachment.separateStencilRejected = true; - lastFailure = "separate GL_STENCIL_INDEX8 allocation failed: fbo=" + framebuffer - + ", size=" + attachment.width + "x" + attachment.height - + ", samples=" + attachment.samples; - DebugLog.stencilOnce("shapeclip.separate.alloc.failed." + attachment.samples, "[ShapeClip/GL] separate stencil allocation failed; packed depth-stencil will be tried. %s", lastFailure); - return false; - } - GL30C.glFramebufferRenderbuffer( - GL30C.GL_FRAMEBUFFER, - GL30C.GL_STENCIL_ATTACHMENT, - GL30C.GL_RENDERBUFFER, - attachment.stencilRenderbuffer - ); - int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); - if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { - attachment.attached = true; - attachment.mode = AttachmentMode.SEPARATE_STENCIL; - DebugLog.stencilOnce("shapeclip.separate.stencil." + framebuffer, "[ShapeClip/GL] using separate GL_STENCIL_INDEX8. fbo=%d size=%dx%d samples=%d", framebuffer, attachment.width, attachment.height, attachment.samples); - checkGlErrors("tryAttachSeparateStencil(success)"); - return true; - } - - GL30C.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_STENCIL_ATTACHMENT, GL30C.GL_RENDERBUFFER, 0); - attachment.restoreOriginalAttachments(); - lastFailure = "separate GL_STENCIL_INDEX8 incomplete: status=0x" - + Integer.toHexString(status) - + ", fbo=" + framebuffer - + ", size=" + attachment.width + "x" + attachment.height - + ", samples=" + attachment.samples; - attachment.separateStencilRejected = true; - DebugLog.stencilOnce("shapeclip.separate.rejected", "[ShapeClip/GL] separate stencil attach failed, using packed depth-stencil path from now on. %s", lastFailure); - return false; - } - - private boolean tryAttachPackedDepthStencil(int framebuffer, Attachment attachment) { - if (!attachment.packedDepthStencilAllocationOk) { - lastFailure = "packed GL_DEPTH24_STENCIL8 allocation failed: fbo=" + framebuffer - + ", size=" + attachment.width + "x" + attachment.height - + ", samples=" + attachment.samples; - DebugLog.warnOnChange("shapeclip.packed.alloc.failed", framebuffer + "|" + attachment.samples, "[ShapeClip/GL] packed depth-stencil allocation failed. %s", lastFailure); - return false; - } - GL30C.glFramebufferRenderbuffer( - GL30C.GL_FRAMEBUFFER, - GL30C.GL_DEPTH_ATTACHMENT, - GL30C.GL_RENDERBUFFER, - attachment.depthStencilRenderbuffer - ); - GL30C.glFramebufferRenderbuffer( - GL30C.GL_FRAMEBUFFER, - GL30C.GL_STENCIL_ATTACHMENT, - GL30C.GL_RENDERBUFFER, - attachment.depthStencilRenderbuffer - ); - int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); - if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { - attachment.attached = true; - attachment.mode = AttachmentMode.PACKED_DEPTH_STENCIL; - if (!attachment.loggedPackedMode) { - attachment.loggedPackedMode = true; - DebugLog.stencilOnce("shapeclip.packed.depth.stencil." + framebuffer, "[ShapeClip/GL] using packed depth-stencil for shape clip. fbo=%d size=%dx%d samples=%d", - framebuffer, attachment.width, attachment.height, attachment.samples); - } - checkGlErrors("tryAttachPackedDepthStencil(success)"); - return true; - } - - lastFailure = "packed GL_DEPTH24_STENCIL8 incomplete: status=0x" - + Integer.toHexString(status) - + ", fbo=" + framebuffer - + ", size=" + attachment.width + "x" + attachment.height - + ", samples=" + attachment.samples; - return false; - } - - private boolean fail(String reason) { - lastFailure = reason; - markUnavailable(); - return false; - } - - private void markUnavailable() { - owner.setStencilAvailable(false); - warnedUnsupported = true; - } - - private enum AttachmentMode { - NONE, - SEPARATE_STENCIL, - PACKED_DEPTH_STENCIL - } - - private static final class Attachment { - final int stencilRenderbuffer; - final int depthStencilRenderbuffer; - final int width; - final int height; - final int samples; - final boolean separateStencilAllocationOk; - final boolean packedDepthStencilAllocationOk; - boolean deleted; - boolean attached; - boolean separateStencilRejected; - boolean loggedPackedMode; - AttachmentMode mode = AttachmentMode.NONE; - SavedAttachment originalDepth = SavedAttachment.none(); - SavedAttachment originalStencil = SavedAttachment.none(); - - Attachment(int width, int height, int samples) { - this.width = width; - this.height = height; - this.samples = Math.max(1, samples); - - int previousRenderbuffer = GL11C.glGetInteger(GL30C.GL_RENDERBUFFER_BINDING); - try { - this.stencilRenderbuffer = GL30C.glGenRenderbuffers(); - GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, stencilRenderbuffer); - this.separateStencilAllocationOk = allocateRenderbuffer(GL30C.GL_STENCIL_INDEX8, width, height, this.samples); - - this.depthStencilRenderbuffer = GL30C.glGenRenderbuffers(); - GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, depthStencilRenderbuffer); - this.packedDepthStencilAllocationOk = allocateRenderbuffer(GL30C.GL_DEPTH24_STENCIL8, width, height, this.samples); - } finally { - GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, previousRenderbuffer); - } - } - - private static boolean allocateRenderbuffer(int internalFormat, int width, int height, int samples) { - clearGlErrorsSilently(); - if (samples > 1) { - GL32C.glRenderbufferStorageMultisample(GL30C.GL_RENDERBUFFER, samples, internalFormat, width, height); - } else { - GL30C.glRenderbufferStorage(GL30C.GL_RENDERBUFFER, internalFormat, width, height); - } - int error = GL11C.glGetError(); - if (error != GL11C.GL_NO_ERROR) { - return false; - } - - int storedWidth = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_WIDTH); - int storedHeight = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_HEIGHT); - int storedSamples = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_SAMPLES); - return storedWidth == width && storedHeight == height && Math.max(1, storedSamples) == Math.max(1, samples); - } - - void captureOriginalAttachments() { - if (attached) return; - originalDepth = SavedAttachment.capture(GL30C.GL_DEPTH_ATTACHMENT); - originalStencil = SavedAttachment.capture(GL30C.GL_STENCIL_ATTACHMENT); - } - - void restoreOriginalAttachments() { - originalDepth.restore(GL30C.GL_DEPTH_ATTACHMENT); - originalStencil.restore(GL30C.GL_STENCIL_ATTACHMENT); - attached = false; - mode = AttachmentMode.NONE; - } - - void delete() { - if (deleted) return; - restoreOriginalAttachments(); - deleted = true; - GL30C.glDeleteRenderbuffers(stencilRenderbuffer); - GL30C.glDeleteRenderbuffers(depthStencilRenderbuffer); - } - } - - private record SavedAttachment(int type, int name, int level, int cubeMapFace, int layer, boolean layered) { - private static final int GL_FRAMEBUFFER_ATTACHMENT_LAYERED_SAFE = 0x8DA7; - - static SavedAttachment none() { - return new SavedAttachment(GL11C.GL_NONE, 0, 0, 0, 0, false); - } - - static SavedAttachment capture(int attachmentPoint) { - int type = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE - ); - if (type == GL11C.GL_NONE) return none(); - - int name = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME - ); - int level = 0; - int cubeMapFace = 0; - int layer = 0; - boolean layered = false; - if (type == GL11C.GL_TEXTURE) { - level = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL - ); - cubeMapFace = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE - ); - try { - layer = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER - ); - layered = GL30C.glGetFramebufferAttachmentParameteri( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL_FRAMEBUFFER_ATTACHMENT_LAYERED_SAFE - ) != 0; - } catch (Throwable ignored) { - clearGlErrorsSilently(); - } - } - return new SavedAttachment(type, name, level, cubeMapFace, layer, layered); - } - - void restore(int attachmentPoint) { - if (type == GL11C.GL_TEXTURE) { - if (cubeMapFace != 0) { - GL30C.glFramebufferTexture2D( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - cubeMapFace, - name, - level - ); - } else { - // Do not query a non-existent TEXTURE_TARGET token. glFramebufferTexture lets - // the driver restore ordinary 2D and 2D multisample attachments target-free. - GL32C.glFramebufferTexture( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - name, - level - ); - } - return; - } - if (type == GL30C.GL_RENDERBUFFER) { - GL30C.glFramebufferRenderbuffer( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_RENDERBUFFER, - name - ); - return; - } - GL30C.glFramebufferRenderbuffer( - GL30C.GL_FRAMEBUFFER, - attachmentPoint, - GL30C.GL_RENDERBUFFER, - 0 - ); - } + return manager.hasWarnedUnsupported(); } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java index a285f2b..5e471b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java @@ -1,68 +1,42 @@ -/* - * 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.render.engine.rhi.backend.gl.clip; import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.textures.GpuTextureView; import org.jetbrains.annotations.Nullable; -import org.lwjgl.opengl.GL11C; -import org.lwjgl.opengl.GL20C; +import dev.loki.lovisual.render.engine.rhi.backend.gl.clip.GlStencilNativeState.Mode; import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipBackend; import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; import dev.loki.lovisual.util.logging.DebugLog; -import dev.loki.lovisual.render.engine.rhi.backend.gl.clip.GlStencilFramebufferSupport; /** * GL implementation of shape clipping using a render-pass-bound stencil attachment. + * + *

    This class owns the clip-stack state machine and render-pass lifecycle; the + * low-level GL stencil emission, ambient depth/cull save-restore and GL error + * scanning live in {@link GlStencilNativeState}.

    */ public final class GlStencilShapeClipBackend implements ShapeClipBackend { - private final GlStencilFramebufferSupport framebuffers = new GlStencilFramebufferSupport(this); - private Mode mode = Mode.DISABLED; - private int reference; + final GlStencilFramebufferSupport framebuffers = new GlStencilFramebufferSupport(); + final GlStencilNativeState nativeState = new GlStencilNativeState(); + Mode mode = Mode.DISABLED; + int reference; private int compareReference; - private boolean stencilAvailable; - private boolean renderPassAttachmentRequired; - private boolean clearRequested; - private String attachmentReason = "none"; - private String clearReason = "none"; - private @Nullable GpuTextureView currentColorView; - private @Nullable GpuTextureView currentDepthView; - private String currentPassLabel = ""; - private boolean currentPassPrepared; - private ShapeClipRenderPassContract currentPipelineContract = ShapeClipRenderPassContract.NONE; - private String currentPipelineName = ""; - private boolean depthStateCaptured; - private boolean previousDepthTest; - private boolean previousDepthMask; - private boolean cullStateCaptured; - private boolean previousCullFace; - private String lastFailure = "none"; - private int offscreenPassBypassDepth; + boolean stencilAvailable; + boolean renderPassAttachmentRequired; + boolean clearRequested; + String attachmentReason = "none"; + String clearReason = "none"; + @Nullable GpuTextureView currentColorView; + @Nullable GpuTextureView currentDepthView; + String currentPassLabel = ""; + boolean currentPassPrepared; + ShapeClipRenderPassContract currentPipelineContract = ShapeClipRenderPassContract.NONE; + String currentPipelineName = ""; + String lastFailure = "none"; + int offscreenPassBypassDepth; - private static int clampRef(int value) { - if (value < 0) return 0; - return Math.min(255, value); - } - - private static String describeView(@Nullable GpuTextureView view) { - if (view == null) return "null"; - try { - return view.getClass().getSimpleName() - + "[" - + view.getWidth(0) - + "x" - + view.getHeight(0) - + ", closed=" - + view.isClosed() - + "]"; - } catch (Throwable t) { - return view.getClass().getSimpleName() + "[describe failed: " + t.getClass().getSimpleName() + "]"; - } + private int clampRef(int value) { + return GlStencilNativeState.clampRef(value); } @Override @@ -89,100 +63,36 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { @Override public void beginRenderPass(String label, @Nullable GpuTextureView colorView, @Nullable GpuTextureView depthView) { - currentPassLabel = label == null ? "" : label; - currentColorView = colorView; - currentDepthView = depthView; - currentPassPrepared = false; - - if (offscreenPassBypassDepth > 0) { - resetNativeStateOnly(); - if (DebugLog.isStencilDebugEnabled()) { - checkGlErrors("beginRenderPass(offscreen-bypass)"); - } - return; - } - - boolean shouldPrepare = renderPassAttachmentRequired || mode != Mode.DISABLED || clearRequested; - if (!shouldPrepare) { - // Hot path (every vanilla/Sodium pass with clip idle): glGetError syncs with the - // driver — skip it unless explicitly debugging stencil. - return; - } - - if (!prepareCurrentPassTarget("beginRenderPass")) { - DebugLog.errorOnChange("shapeclip.renderpass.prepare.failed", currentPassLabel + "|" + attachmentReason + "|" + lastFailureReason(), "[ShapeClip/GL] render pass stencil prepare failed: pass=%s reason=%s lastFailure=%s", currentPassLabel, attachmentReason, lastFailureReason()); - checkGlErrors("beginRenderPass(prepare-failed)"); - return; - } - - if (clearRequested) { - boolean cleared = framebuffers.clear(currentColorView, currentDepthView); - if (!cleared) { - lastFailure = "clear failed: " + framebuffers.lastFailure(); - DebugLog.errorOnChange("shapeclip.renderpass.clear.failed", currentPassLabel + "|" + clearReason + "|" + lastFailure, "[ShapeClip/GL] render pass stencil clear failed: pass=%s reason=%s lastFailure=%s", currentPassLabel, clearReason, lastFailure); - } - clearRequested = false; - clearReason = "none"; - } - - checkGlErrors("beginRenderPass"); + GlStencilPassLifecycle.beginRenderPass(this, label, colorView, depthView); } @Override public void endRenderPass() { - // Capture before endRenderPass resets renderPassAttachmentRequired below. - boolean errorsOfInterest = mode != Mode.DISABLED || renderPassAttachmentRequired - || DebugLog.isStencilDebugEnabled(); - resetNativeStateOnly(); - currentColorView = null; - currentDepthView = null; - currentPassLabel = ""; - currentPassPrepared = false; - currentPipelineContract = ShapeClipRenderPassContract.NONE; - currentPipelineName = ""; - if (offscreenPassBypassDepth == 0) { - renderPassAttachmentRequired = false; - attachmentReason = "none"; - } - if (errorsOfInterest) { - checkGlErrors(offscreenPassBypassDepth > 0 ? "endRenderPass(offscreen-bypass)" : "endRenderPass"); - } + GlStencilPassLifecycle.endRenderPass(this); } @Override public void bindPipeline(RenderPipeline pipeline, ShapeClipRenderPassContract contract) { - currentPipelineContract = contract == null ? ShapeClipRenderPassContract.NONE : contract; - currentPipelineName = pipeline == null || pipeline.getLocation() == null ? "" : pipeline.getLocation().toString(); - if (offscreenPassBypassDepth > 0) { - return; - } - if (mode != Mode.DISABLED && currentPipelineContract == ShapeClipRenderPassContract.NONE) { - DebugLog.warnOnce( - "shapeclip.stale.pipeline.contract." + currentPipelineName, - "[ShapeClip/GL] stale clip state risk: active mode=%s ref=%d but pipeline has no shape-clip contract. pipeline=%s pass=%s", - mode, - reference, - currentPipelineName, - currentPassLabel - ); - } - if (currentPipelineContract.requiresAttachment(mode != Mode.DISABLED || renderPassAttachmentRequired)) { - requireRenderPassAttachment("pipeline contract " + currentPipelineContract + " for " + currentPipelineName); - if (currentColorView != null && !currentPassPrepared) { - prepareCurrentPassTarget("bindPipeline"); - } - } + GlStencilPassLifecycle.bindPipeline(this, pipeline, contract); } @Override public boolean prepareMainTarget() { - return framebuffers.ensureForMainFramebuffer(); + boolean ok = framebuffers.ensureForMainFramebuffer(); + setStencilAvailable(ok); + if (!ok) { + lastFailure = framebuffers.lastFailure(); + } + return ok; } @Override public boolean prepare(@Nullable GpuTextureView colorView, @Nullable GpuTextureView depthView) { boolean ok = framebuffers.ensure(colorView, depthView); - if (!ok) lastFailure = framebuffers.lastFailure(); + setStencilAvailable(ok); + if (!ok) { + lastFailure = framebuffers.lastFailure(); + } return ok; } @@ -228,7 +138,7 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { public void pushOffscreenPassBypass() { offscreenPassBypassDepth++; if (offscreenPassBypassDepth == 1) { - resetNativeStateOnly(); + nativeState.resetNativeState(); } } @@ -240,7 +150,7 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { @Override public void disable() { - resetNativeStateOnly(); + nativeState.resetNativeState(); mode = Mode.DISABLED; reference = 0; compareReference = 0; @@ -257,14 +167,14 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { @Override public void applyNativeState() { if (offscreenPassBypassDepth > 0) { - resetNativeStateOnly(); + nativeState.resetNativeState(); if (DebugLog.isStencilDebugEnabled()) { checkGlErrors("applyNativeState(offscreen-bypass)"); } return; } if (mode == Mode.DISABLED) { - resetNativeStateOnly(); + nativeState.resetNativeState(); if (DebugLog.isStencilDebugEnabled()) { checkGlErrors("applyNativeState(disabled)"); } @@ -272,7 +182,7 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { } if (!prepareCurrentPassTarget("applyNativeState")) { - resetNativeStateOnly(); + nativeState.resetNativeState(); DebugLog.errorOnChange( "shapeclip.apply.prepare.failed", currentPassLabel + "|" + currentPipelineName + "|" + mode + "|" + reference + "|" + lastFailureReason(), @@ -288,120 +198,13 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { return; } - captureDepthStateIfNeeded(); - GL11C.glDisable(GL11C.GL_DEPTH_TEST); - GL11C.glDepthMask(false); - GL11C.glEnable(GL11C.GL_STENCIL_TEST); - - switch (mode) { - case WRITE -> { - captureCullStateIfNeeded(); - GL11C.glDisable(GL11C.GL_CULL_FACE); - GL11C.glStencilMask(0xFF); - GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0xFF); - int compareFunc = compareReference == 0 ? GL11C.GL_ALWAYS : GL11C.GL_EQUAL; - GL11C.glStencilFunc(compareFunc, compareReference, 0xFF); - GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, compareFunc, compareReference, 0xFF); - // The stack model stores child = parent + 1. GL_REPLACE cannot express - // "compare against parent but write child", because REPLACE writes the - // same ref value supplied to glStencilFunc. Increment/decrement keeps the - // compare ref independent from the value transition. - GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_INCR, GL11C.GL_INCR); - GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_INCR, GL11C.GL_INCR); - GL11C.glColorMask(false, false, false, false); - } - case RESTORE -> { - captureCullStateIfNeeded(); - GL11C.glDisable(GL11C.GL_CULL_FACE); - GL11C.glStencilMask(0xFF); - GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0xFF); - GL11C.glStencilFunc(GL11C.GL_EQUAL, compareReference, 0xFF); - GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_EQUAL, compareReference, 0xFF); - GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_DECR, GL11C.GL_DECR); - GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_DECR, GL11C.GL_DECR); - GL11C.glColorMask(false, false, false, false); - } - case TEST -> { - restoreCullStateIfNeeded(); - GL11C.glStencilMask(0x00); - GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0x00); - GL11C.glStencilFunc(GL11C.GL_EQUAL, reference, 0xFF); - GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_EQUAL, reference, 0xFF); - GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); - GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); - GL11C.glColorMask(true, true, true, true); - } - case DISABLED -> resetNativeStateOnly(); - } + nativeState.apply(mode, reference, compareReference); checkGlErrors("applyNativeState(" + mode + ")"); } - private boolean prepareCurrentPassTarget(String source) { - if (currentColorView == null) { - lastFailure = "no current render pass target while " + source + " requested stencil state"; - DebugLog.warnOnChange("shapeclip.no.current.pass", lastFailure, "[ShapeClip/GL] stale state: %s", lastFailure); - return false; - } - if (currentPassPrepared && stencilAvailable) return true; - boolean ok = framebuffers.ensure(currentColorView, currentDepthView); - currentPassPrepared = ok; - stencilAvailable = ok; - if (!ok) { - lastFailure = framebuffers.lastFailure(); - DebugLog.errorOnChange("shapeclip.prepare.current.target.failed", source + "|" + currentPassLabel + "|" + describeView(currentColorView) + "|" + describeView(currentDepthView) + "|" + lastFailure, "[ShapeClip/GL] prepareCurrentPassTarget failed from=%s pass=%s targetColor=%s targetDepth=%s failure=%s", - source, currentPassLabel, describeView(currentColorView), describeView(currentDepthView), lastFailure); - return false; - } - lastFailure = "none"; - return true; - } - - private void resetNativeStateOnly() { - GL11C.glDisable(GL11C.GL_STENCIL_TEST); - GL11C.glStencilMask(0x00); - GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0x00); - GL11C.glStencilFunc(GL11C.GL_ALWAYS, 0, 0xFF); - GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_ALWAYS, 0, 0xFF); - GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); - GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); - GL11C.glColorMask(true, true, true, true); - restoreDepthStateIfNeeded(); - restoreCullStateIfNeeded(); - } - - private void captureDepthStateIfNeeded() { - if (depthStateCaptured) return; - previousDepthTest = GL11C.glIsEnabled(GL11C.GL_DEPTH_TEST); - previousDepthMask = GL11C.glGetBoolean(GL11C.GL_DEPTH_WRITEMASK); - depthStateCaptured = true; - } - - private void restoreDepthStateIfNeeded() { - if (!depthStateCaptured) return; - if (previousDepthTest) { - GL11C.glEnable(GL11C.GL_DEPTH_TEST); - } else { - GL11C.glDisable(GL11C.GL_DEPTH_TEST); - } - GL11C.glDepthMask(previousDepthMask); - depthStateCaptured = false; - } - - private void captureCullStateIfNeeded() { - if (cullStateCaptured) return; - previousCullFace = GL11C.glIsEnabled(GL11C.GL_CULL_FACE); - cullStateCaptured = true; - } - - private void restoreCullStateIfNeeded() { - if (!cullStateCaptured) return; - if (previousCullFace) { - GL11C.glEnable(GL11C.GL_CULL_FACE); - } else { - GL11C.glDisable(GL11C.GL_CULL_FACE); - } - cullStateCaptured = false; + boolean prepareCurrentPassTarget(String source) { + return GlStencilPassLifecycle.prepareCurrentPassTarget(this, source); } @Override @@ -438,18 +241,7 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { return lastFailureReason(); } - private void checkGlErrors(String where) { - int guard = 0; - int error; - while ((error = GL11C.glGetError()) != GL11C.GL_NO_ERROR && guard++ < 16) { - DebugLog.errorOnChange("shapeclip.gl.error", where + "|" + error, "[ShapeClip/GL] gl error at %s: 0x%s state=%s", where, Integer.toHexString(error), this); - } - } - - private enum Mode { - DISABLED, - WRITE, - RESTORE, - TEST + void checkGlErrors(String where) { + GlStencilNativeState.checkGlErrors(where, this::toString); } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilNativeState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilNativeState.java new file mode 100644 index 0000000..a4ecd70 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilNativeState.java @@ -0,0 +1,162 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import com.mojang.blaze3d.textures.GpuTextureView; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL20C; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.function.Supplier; + +/** + * Low-level GL plumbing for {@link GlStencilShapeClipBackend}: emits the stencil + * state for a given clip mode, saves/restores the ambient depth and cull state it + * temporarily overrides, and scans for GL errors. Pure "how" — the backend keeps + * the "when" (mode/reference decisions and render-pass lifecycle). Package-private + * helper of {@link GlStencilShapeClipBackend} (impl subfolder, {@code gl.clip} package). + */ +final class GlStencilNativeState { + + /** Logical clip-stack phase driven by the backend state machine. */ + enum Mode { + DISABLED, + WRITE, + RESTORE, + TEST + } + + private boolean depthStateCaptured; + private boolean previousDepthTest; + private boolean previousDepthMask; + private boolean cullStateCaptured; + private boolean previousCullFace; + + static int clampRef(int value) { + if (value < 0) return 0; + return Math.min(255, value); + } + + static String describeView(@Nullable GpuTextureView view) { + if (view == null) return "null"; + try { + return view.getClass().getSimpleName() + + "[" + + view.getWidth(0) + + "x" + + view.getHeight(0) + + ", closed=" + + view.isClosed() + + "]"; + } catch (Throwable t) { + return view.getClass().getSimpleName() + "[describe failed: " + t.getClass().getSimpleName() + "]"; + } + } + + static void checkGlErrors(String where, Supplier state) { + int guard = 0; + int error; + while ((error = GL11C.glGetError()) != GL11C.GL_NO_ERROR && guard++ < 16) { + DebugLog.errorOnChange("shapeclip.gl.error", where + "|" + error, + "[ShapeClip/GL] gl error at %s: 0x%s state=%s", where, Integer.toHexString(error), state.get()); + } + } + + /** + * Emit the native stencil state for the requested clip phase. Assumes the caller + * has already prepared the pass target and handled the DISABLED/bypass short-circuits. + */ + void apply(Mode mode, int reference, int compareReference) { + captureDepthStateIfNeeded(); + GL11C.glDisable(GL11C.GL_DEPTH_TEST); + GL11C.glDepthMask(false); + GL11C.glEnable(GL11C.GL_STENCIL_TEST); + + switch (mode) { + case WRITE -> { + captureCullStateIfNeeded(); + GL11C.glDisable(GL11C.GL_CULL_FACE); + GL11C.glStencilMask(0xFF); + GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0xFF); + int compareFunc = compareReference == 0 ? GL11C.GL_ALWAYS : GL11C.GL_EQUAL; + GL11C.glStencilFunc(compareFunc, compareReference, 0xFF); + GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, compareFunc, compareReference, 0xFF); + // The stack model stores child = parent + 1. GL_REPLACE cannot express + // "compare against parent but write child", because REPLACE writes the + // same ref value supplied to glStencilFunc. Increment/decrement keeps the + // compare ref independent from the value transition. + GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_INCR, GL11C.GL_INCR); + GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_INCR, GL11C.GL_INCR); + GL11C.glColorMask(false, false, false, false); + } + case RESTORE -> { + captureCullStateIfNeeded(); + GL11C.glDisable(GL11C.GL_CULL_FACE); + GL11C.glStencilMask(0xFF); + GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0xFF); + GL11C.glStencilFunc(GL11C.GL_EQUAL, compareReference, 0xFF); + GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_EQUAL, compareReference, 0xFF); + GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_DECR, GL11C.GL_DECR); + GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_DECR, GL11C.GL_DECR); + GL11C.glColorMask(false, false, false, false); + } + case TEST -> { + restoreCullStateIfNeeded(); + GL11C.glStencilMask(0x00); + GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0x00); + GL11C.glStencilFunc(GL11C.GL_EQUAL, reference, 0xFF); + GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_EQUAL, reference, 0xFF); + GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); + GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); + GL11C.glColorMask(true, true, true, true); + } + case DISABLED -> resetNativeState(); + } + } + + void resetNativeState() { + GL11C.glDisable(GL11C.GL_STENCIL_TEST); + GL11C.glStencilMask(0x00); + GL20C.glStencilMaskSeparate(GL11C.GL_FRONT_AND_BACK, 0x00); + GL11C.glStencilFunc(GL11C.GL_ALWAYS, 0, 0xFF); + GL20C.glStencilFuncSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_ALWAYS, 0, 0xFF); + GL11C.glStencilOp(GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); + GL20C.glStencilOpSeparate(GL11C.GL_FRONT_AND_BACK, GL11C.GL_KEEP, GL11C.GL_KEEP, GL11C.GL_KEEP); + GL11C.glColorMask(true, true, true, true); + restoreDepthStateIfNeeded(); + restoreCullStateIfNeeded(); + } + + private void captureDepthStateIfNeeded() { + if (depthStateCaptured) return; + previousDepthTest = GL11C.glIsEnabled(GL11C.GL_DEPTH_TEST); + previousDepthMask = GL11C.glGetBoolean(GL11C.GL_DEPTH_WRITEMASK); + depthStateCaptured = true; + } + + private void restoreDepthStateIfNeeded() { + if (!depthStateCaptured) return; + if (previousDepthTest) { + GL11C.glEnable(GL11C.GL_DEPTH_TEST); + } else { + GL11C.glDisable(GL11C.GL_DEPTH_TEST); + } + GL11C.glDepthMask(previousDepthMask); + depthStateCaptured = false; + } + + private void captureCullStateIfNeeded() { + if (cullStateCaptured) return; + previousCullFace = GL11C.glIsEnabled(GL11C.GL_CULL_FACE); + cullStateCaptured = true; + } + + private void restoreCullStateIfNeeded() { + if (!cullStateCaptured) return; + if (previousCullFace) { + GL11C.glEnable(GL11C.GL_CULL_FACE); + } else { + GL11C.glDisable(GL11C.GL_CULL_FACE); + } + cullStateCaptured = false; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilPassLifecycle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilPassLifecycle.java new file mode 100644 index 0000000..ba583a6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/impl/GlStencilPassLifecycle.java @@ -0,0 +1,128 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.textures.GpuTextureView; +import org.jetbrains.annotations.Nullable; +import dev.loki.lovisual.render.engine.rhi.backend.gl.clip.GlStencilNativeState.Mode; +import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; +import dev.loki.lovisual.util.logging.DebugLog; + +/** + * Render-pass and pipeline attachment bookkeeping for + * {@link GlStencilShapeClipBackend}: tracks the currently-bound pass target, + * prepares its stencil framebuffer, applies pipeline contracts and drains the + * requested clears. Operates on the backend's package-private state; the backend + * keeps the clip-stack mode/reference decisions and the native GL emission. + * Package-private helper of {@link GlStencilShapeClipBackend} (impl subfolder, + * {@code gl.clip} package). + */ +final class GlStencilPassLifecycle { + + private GlStencilPassLifecycle() { + } + + static void beginRenderPass(GlStencilShapeClipBackend m, String label, + @Nullable GpuTextureView colorView, @Nullable GpuTextureView depthView) { + m.currentPassLabel = label == null ? "" : label; + m.currentColorView = colorView; + m.currentDepthView = depthView; + m.currentPassPrepared = false; + + if (m.offscreenPassBypassDepth > 0) { + m.nativeState.resetNativeState(); + if (DebugLog.isStencilDebugEnabled()) { + m.checkGlErrors("beginRenderPass(offscreen-bypass)"); + } + return; + } + + boolean shouldPrepare = m.renderPassAttachmentRequired || m.mode != Mode.DISABLED || m.clearRequested; + if (!shouldPrepare) { + // Hot path (every vanilla/Sodium pass with clip idle): glGetError syncs with the + // driver — skip it unless explicitly debugging stencil. + return; + } + + if (!prepareCurrentPassTarget(m, "beginRenderPass")) { + DebugLog.errorOnChange("shapeclip.renderpass.prepare.failed", m.currentPassLabel + "|" + m.attachmentReason + "|" + m.lastFailureReason(), "[ShapeClip/GL] render pass stencil prepare failed: pass=%s reason=%s lastFailure=%s", m.currentPassLabel, m.attachmentReason, m.lastFailureReason()); + m.checkGlErrors("beginRenderPass(prepare-failed)"); + return; + } + + if (m.clearRequested) { + boolean cleared = m.framebuffers.clear(m.currentColorView, m.currentDepthView); + if (!cleared) { + m.lastFailure = "clear failed: " + m.framebuffers.lastFailure(); + DebugLog.errorOnChange("shapeclip.renderpass.clear.failed", m.currentPassLabel + "|" + m.clearReason + "|" + m.lastFailure, "[ShapeClip/GL] render pass stencil clear failed: pass=%s reason=%s lastFailure=%s", m.currentPassLabel, m.clearReason, m.lastFailure); + } + m.clearRequested = false; + m.clearReason = "none"; + } + + m.checkGlErrors("beginRenderPass"); + } + + static void endRenderPass(GlStencilShapeClipBackend m) { + // Capture before endRenderPass resets renderPassAttachmentRequired below. + boolean errorsOfInterest = m.mode != Mode.DISABLED || m.renderPassAttachmentRequired + || DebugLog.isStencilDebugEnabled(); + m.nativeState.resetNativeState(); + m.currentColorView = null; + m.currentDepthView = null; + m.currentPassLabel = ""; + m.currentPassPrepared = false; + m.currentPipelineContract = ShapeClipRenderPassContract.NONE; + m.currentPipelineName = ""; + if (m.offscreenPassBypassDepth == 0) { + m.renderPassAttachmentRequired = false; + m.attachmentReason = "none"; + } + if (errorsOfInterest) { + m.checkGlErrors(m.offscreenPassBypassDepth > 0 ? "endRenderPass(offscreen-bypass)" : "endRenderPass"); + } + } + + static void bindPipeline(GlStencilShapeClipBackend m, RenderPipeline pipeline, ShapeClipRenderPassContract contract) { + m.currentPipelineContract = contract == null ? ShapeClipRenderPassContract.NONE : contract; + m.currentPipelineName = pipeline == null || pipeline.getLocation() == null ? "" : pipeline.getLocation().toString(); + if (m.offscreenPassBypassDepth > 0) { + return; + } + if (m.mode != Mode.DISABLED && m.currentPipelineContract == ShapeClipRenderPassContract.NONE) { + DebugLog.warnOnce( + "shapeclip.stale.pipeline.contract." + m.currentPipelineName, + "[ShapeClip/GL] stale clip state risk: active mode=%s ref=%d but pipeline has no shape-clip contract. pipeline=%s pass=%s", + m.mode, + m.reference, + m.currentPipelineName, + m.currentPassLabel + ); + } + if (m.currentPipelineContract.requiresAttachment(m.mode != Mode.DISABLED || m.renderPassAttachmentRequired)) { + m.requireRenderPassAttachment("pipeline contract " + m.currentPipelineContract + " for " + m.currentPipelineName); + if (m.currentColorView != null && !m.currentPassPrepared) { + prepareCurrentPassTarget(m, "bindPipeline"); + } + } + } + + static boolean prepareCurrentPassTarget(GlStencilShapeClipBackend m, String source) { + if (m.currentColorView == null) { + m.lastFailure = "no current render pass target while " + source + " requested stencil state"; + DebugLog.warnOnChange("shapeclip.no.current.pass", m.lastFailure, "[ShapeClip/GL] stale state: %s", m.lastFailure); + return false; + } + if (m.currentPassPrepared && m.stencilAvailable) return true; + boolean ok = m.framebuffers.ensure(m.currentColorView, m.currentDepthView); + m.currentPassPrepared = ok; + m.stencilAvailable = ok; + if (!ok) { + m.lastFailure = m.framebuffers.lastFailure(); + DebugLog.errorOnChange("shapeclip.prepare.current.target.failed", source + "|" + m.currentPassLabel + "|" + GlStencilNativeState.describeView(m.currentColorView) + "|" + GlStencilNativeState.describeView(m.currentDepthView) + "|" + m.lastFailure, "[ShapeClip/GL] prepareCurrentPassTarget failed from=%s pass=%s targetColor=%s targetDepth=%s failure=%s", + source, m.currentPassLabel, GlStencilNativeState.describeView(m.currentColorView), GlStencilNativeState.describeView(m.currentDepthView), m.lastFailure); + return false; + } + m.lastFailure = "none"; + return true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachment.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachment.java new file mode 100644 index 0000000..e983090 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachment.java @@ -0,0 +1,191 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL30C; +import org.lwjgl.opengl.GL32C; + +final class GlStencilAttachment { + + final int stencilRenderbuffer; + final int depthStencilRenderbuffer; + final int width; + final int height; + final int samples; + final boolean separateStencilAllocationOk; + final boolean packedDepthStencilAllocationOk; + boolean deleted; + boolean attached; + boolean separateStencilRejected; + boolean loggedPackedMode; + AttachmentMode mode = AttachmentMode.NONE; + SavedAttachment originalDepth = createNoneAttachment(); + SavedAttachment originalStencil = createNoneAttachment(); + private final GlStencilGl gl; + + GlStencilAttachment(int width, int height, int samples, GlStencilGl gl) { + this.width = width; + this.height = height; + this.samples = Math.max(1, samples); + this.gl = gl; + int previousRenderbuffer = GL11C.glGetInteger(GL30C.GL_RENDERBUFFER_BINDING); + int stencilRb = 0; + int depthStencilRb = 0; + boolean separateOk; + boolean packedOk; + try { + stencilRb = GL30C.glGenRenderbuffers(); + GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, stencilRb); + separateOk = allocateRenderbuffer(GL30C.GL_STENCIL_INDEX8, width, height, this.samples); + depthStencilRb = GL30C.glGenRenderbuffers(); + GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, depthStencilRb); + packedOk = allocateRenderbuffer(GL30C.GL_DEPTH24_STENCIL8, width, height, this.samples); + } finally { + GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, previousRenderbuffer); + } + this.stencilRenderbuffer = stencilRb; + this.depthStencilRenderbuffer = depthStencilRb; + this.separateStencilAllocationOk = separateOk; + this.packedDepthStencilAllocationOk = packedOk; + } + + boolean allocateRenderbuffer(int internalFormat, int width, int height, int samples) { + gl.clearGlErrorsSilently(); + if (samples > 1) { + GL32C.glRenderbufferStorageMultisample(GL30C.GL_RENDERBUFFER, samples, internalFormat, width, height); + } else { + GL30C.glRenderbufferStorage(GL30C.GL_RENDERBUFFER, internalFormat, width, height); + } + int error = GL11C.glGetError(); + if (error != GL11C.GL_NO_ERROR) { + return false; + } + int storedWidth = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_WIDTH); + int storedHeight = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_HEIGHT); + int storedSamples = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_SAMPLES); + return storedWidth == width && storedHeight == height + && Math.max(1, storedSamples) == Math.max(1, samples); + } + + void captureOriginalAttachments() { + if (attached) { + return; + } + originalDepth = captureAttachment(GL30C.GL_DEPTH_ATTACHMENT); + originalStencil = captureAttachment(GL30C.GL_STENCIL_ATTACHMENT); + } + + SavedAttachment createNoneAttachment() { + return new SavedAttachment(GL11C.GL_NONE, 0, 0, 0, 0, false); + } + + SavedAttachment captureAttachment(int attachmentPoint) { + int type = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE + ); + if (type == GL11C.GL_NONE) { + return createNoneAttachment(); + } + int name = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME + ); + int level = 0; + int cubeMapFace = 0; + int layer = 0; + boolean layered = false; + if (type == GL11C.GL_TEXTURE) { + level = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL + ); + cubeMapFace = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE + ); + try { + layer = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER + ); + int layeredInt = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + 0x8DA7 + ); + layered = layeredInt != 0; + } catch (Throwable ignored) { + gl.clearGlErrorsSilently(); + } + } + return new SavedAttachment(type, name, level, cubeMapFace, layer, layered); + } + + void restoreOriginalAttachments() { + originalDepth.restore(GL30C.GL_DEPTH_ATTACHMENT); + originalStencil.restore(GL30C.GL_STENCIL_ATTACHMENT); + attached = false; + mode = AttachmentMode.NONE; + } + + void delete() { + if (deleted) { + return; + } + restoreOriginalAttachments(); + deleted = true; + GL30C.glDeleteRenderbuffers(stencilRenderbuffer); + GL30C.glDeleteRenderbuffers(depthStencilRenderbuffer); + } + + enum AttachmentMode { + NONE, + SEPARATE_STENCIL, + PACKED_DEPTH_STENCIL + } + + record SavedAttachment(int type, int name, int level, int cubeMapFace, int layer, boolean layered) { + + void restore(int attachmentPoint) { + if (type == GL11C.GL_TEXTURE) { + if (cubeMapFace != 0) { + GL30C.glFramebufferTexture2D( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + cubeMapFace, + name, + level + ); + } else { + GL32C.glFramebufferTexture( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + name, + level + ); + } + return; + } + if (type == GL30C.GL_RENDERBUFFER) { + GL30C.glFramebufferRenderbuffer( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_RENDERBUFFER, + name + ); + return; + } + GL30C.glFramebufferRenderbuffer( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_RENDERBUFFER, + 0 + ); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachmentManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachmentManager.java new file mode 100644 index 0000000..485984e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilAttachmentManager.java @@ -0,0 +1,191 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import com.mojang.blaze3d.opengl.GlStateManager; +import com.mojang.blaze3d.opengl.GlConst; +import dev.loki.lovisual.util.logging.DebugLog; +import org.lwjgl.opengl.GL30C; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +final class GlStencilAttachmentManager { + private final Map attachments; + private final GlStencilGl gl; + private final GlStencilSampleProbe probe; + private boolean warnedUnsupported; + private String lastFailure; + GlStencilAttachmentManager(GlStencilGl gl, GlStencilSampleProbe probe) { + this.attachments = new HashMap<>(); + this.gl = gl; + this.probe = probe; + this.lastFailure = "none"; + } + boolean hasWarnedUnsupported() { + return warnedUnsupported; + } + String lastFailure() { + return lastFailure; + } + String describe() { + return "attachments=" + attachments.size() + + ", warnedUnsupported=" + warnedUnsupported + + ", lastFailure='" + lastFailure + "'"; + } + Map attachments() { + return attachments; + } + + boolean ensureAttachment(int framebuffer, int width, int height) { + GlStencilAttachment attachment = attachments.get(framebuffer); + int prevRead = GlStateManager.getFrameBuffer(GlConst.GL_READ_FRAMEBUFFER); + int prevDraw = GlStateManager.getFrameBuffer(GlConst.GL_DRAW_FRAMEBUFFER); + try { + GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, framebuffer); + int samples = probe.detectBoundFramebufferSamples(); + if (attachment == null || attachment.width != width || attachment.height != height + || attachment.samples != samples || attachment.deleted) { + if (attachment != null) { + attachment.restoreOriginalAttachments(); + attachment.delete(); + } + attachment = new GlStencilAttachment(width, height, samples, gl); + attachments.put(framebuffer, attachment); + } + if (attachment.attached) { + int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); + if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { + lastFailure = "none"; + gl.checkGlErrors("ensureAttachment(existing)"); + return true; + } + DebugLog.warnOnChange("shapeclip.attachment.stale", + framebuffer + "|" + status + "|" + attachment.mode, + "[ShapeClip/GL] existing stencil attachment stale/incomplete. fbo=%d status=0x%s mode=%s", + framebuffer, Integer.toHexString(status), attachment.mode); + attachment.restoreOriginalAttachments(); + } + attachment.captureOriginalAttachments(); + if (!attachment.separateStencilRejected && tryAttachSeparateStencil(framebuffer, attachment)) { + lastFailure = "none"; + return true; + } + if (tryAttachPackedDepthStencil(framebuffer, attachment)) { + lastFailure = "none"; + return true; + } + attachment.restoreOriginalAttachments(); + markUnavailable(); + return false; + } finally { + gl.restoreFramebufferBindings(prevRead, prevDraw); + } + } + + void invalidateAttachment(int framebuffer, String reason) { + GlStencilAttachment attachment = attachments.remove(framebuffer); + if (attachment == null) { + lastFailure = reason; + return; + } + try { + attachment.restoreOriginalAttachments(); + } catch (Throwable t) { + DebugLog.warnOnChange("shapeclip.attachment.restore.invalidate", + framebuffer + "|" + t.getClass().getSimpleName(), + "[ShapeClip/GL] failed to restore attachment while invalidating fbo=%d reason=%s error=%s: %s", + framebuffer, reason, t.getClass().getSimpleName(), t.getMessage()); + } finally { + try { + attachment.delete(); + } catch (Throwable t) { + DebugLog.warnOnChange("shapeclip.attachment.delete.invalidate", + framebuffer + "|" + t.getClass().getSimpleName(), + "[ShapeClip/GL] failed to delete invalidated attachment fbo=%d error=%s: %s", + framebuffer, t.getClass().getSimpleName(), t.getMessage()); + } + lastFailure = reason; + DebugLog.stencilOnChange("shapeclip.attachment.invalidated", framebuffer + "|" + reason, + "[ShapeClip/GL] invalidated cached stencil attachment. fbo=%d reason=%s", framebuffer, reason); + } + } + + boolean tryAttachSeparateStencil(int framebuffer, GlStencilAttachment attachment) { + if (!attachment.separateStencilAllocationOk) { + attachment.separateStencilRejected = true; + lastFailure = "separate GL_STENCIL_INDEX8 allocation failed: fbo=" + framebuffer + + ", size=" + attachment.width + "x" + attachment.height + ", samples=" + attachment.samples; + DebugLog.stencilOnce("shapeclip.separate.alloc.failed." + attachment.samples, + "[ShapeClip/GL] separate stencil allocation failed; packed depth-stencil will be tried. %s", lastFailure); + return false; + } + GL30C.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_STENCIL_ATTACHMENT, + GL30C.GL_RENDERBUFFER, attachment.stencilRenderbuffer); + int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); + if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { + attachment.attached = true; + attachment.mode = GlStencilAttachment.AttachmentMode.SEPARATE_STENCIL; + DebugLog.stencilOnce("shapeclip.separate.stencil." + framebuffer, + "[ShapeClip/GL] using separate GL_STENCIL_INDEX8. fbo=%d size=%dx%d samples=%d", + framebuffer, attachment.width, attachment.height, attachment.samples); + gl.checkGlErrors("tryAttachSeparateStencil(success)"); + return true; + } + GL30C.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_STENCIL_ATTACHMENT, GL30C.GL_RENDERBUFFER, 0); + attachment.restoreOriginalAttachments(); + lastFailure = "separate GL_STENCIL_INDEX8 incomplete: status=0x" + Integer.toHexString(status) + + ", fbo=" + framebuffer + ", size=" + attachment.width + "x" + attachment.height + ", samples=" + attachment.samples; + attachment.separateStencilRejected = true; + DebugLog.stencilOnce("shapeclip.separate.rejected", + "[ShapeClip/GL] separate stencil attach failed, using packed depth-stencil path from now on. %s", lastFailure); + return false; + } + + boolean tryAttachPackedDepthStencil(int framebuffer, GlStencilAttachment attachment) { + if (!attachment.packedDepthStencilAllocationOk) { + lastFailure = "packed GL_DEPTH24_STENCIL8 allocation failed: fbo=" + framebuffer + + ", size=" + attachment.width + "x" + attachment.height + ", samples=" + attachment.samples; + DebugLog.warnOnChange("shapeclip.packed.alloc.failed", framebuffer + "|" + attachment.samples, + "[ShapeClip/GL] packed depth-stencil allocation failed. %s", lastFailure); + return false; + } + GL30C.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_DEPTH_ATTACHMENT, + GL30C.GL_RENDERBUFFER, attachment.depthStencilRenderbuffer); + GL30C.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_STENCIL_ATTACHMENT, + GL30C.GL_RENDERBUFFER, attachment.depthStencilRenderbuffer); + int status = GL30C.glCheckFramebufferStatus(GL30C.GL_FRAMEBUFFER); + if (status == GL30C.GL_FRAMEBUFFER_COMPLETE) { + attachment.attached = true; + attachment.mode = GlStencilAttachment.AttachmentMode.PACKED_DEPTH_STENCIL; + if (!attachment.loggedPackedMode) { + attachment.loggedPackedMode = true; + DebugLog.stencilOnce("shapeclip.packed.depth.stencil." + framebuffer, + "[ShapeClip/GL] using packed depth-stencil for shape clip. fbo=%d size=%dx%d samples=%d", + framebuffer, attachment.width, attachment.height, attachment.samples); + } + gl.checkGlErrors("tryAttachPackedDepthStencil(success)"); + return true; + } + lastFailure = "packed GL_DEPTH24_STENCIL8 incomplete: status=0x" + Integer.toHexString(status) + + ", fbo=" + framebuffer + ", size=" + attachment.width + "x" + attachment.height + ", samples=" + attachment.samples; + return false; + } + + boolean fail(String reason) { + lastFailure = reason; + warnedUnsupported = true; + return false; + } + void markUnavailable() { + warnedUnsupported = true; + } + void cleanupClosedOrResized() { + Iterator> it = attachments.entrySet().iterator(); + while (it.hasNext()) { + GlStencilAttachment a = it.next().getValue(); + if (a.deleted) { + it.remove(); + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilGl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilGl.java new file mode 100644 index 0000000..a5f82f4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilGl.java @@ -0,0 +1,57 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import com.mojang.blaze3d.opengl.GlConst; +import com.mojang.blaze3d.opengl.GlStateManager; +import com.mojang.blaze3d.opengl.GlTextureView; +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.mixininterface.device.IGlBackendInfo; +import dev.loki.lovisual.util.logging.DebugLog; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL30C; + +import java.util.List; +import com.mojang.blaze3d.opengl.FrameBufferAttachment; + +final class GlStencilGl { + + void restoreFramebufferBindings(int readFramebuffer, int drawFramebuffer) { + if (readFramebuffer == drawFramebuffer) { + GlStateManager._glBindFramebuffer(GlConst.GL_FRAMEBUFFER, readFramebuffer); + return; + } + GlStateManager._glBindFramebuffer(GlConst.GL_READ_FRAMEBUFFER, readFramebuffer); + GlStateManager._glBindFramebuffer(GlConst.GL_DRAW_FRAMEBUFFER, drawFramebuffer); + } + + int framebufferFor(IGlBackendInfo backend, GlTextureView colorView, @Nullable GpuTextureView depthView) { + FrameBufferAttachment depthAttachment = null; + if (depthView != null) { + if (!(depthView instanceof GlTextureView glDepthView)) { + throw new IllegalArgumentException("depth view is not GlTextureView: " + depthView.getClass().getName()); + } + depthAttachment = glDepthView; + } + return backend.lovisual$frameBufferCache().getFbo( + backend.lovisual$directStateAccess(), + List.of(colorView), + depthAttachment + ); + } + + void clearGlErrorsSilently() { + int guard = 0; + while (GL11C.glGetError() != GL11C.GL_NO_ERROR && guard++ < 16) { + // drain + } + } + + void checkGlErrors(String where) { + int guard = 0; + int error; + while ((error = GL11C.glGetError()) != GL11C.GL_NO_ERROR && guard++ < 16) { + DebugLog.errorOnChange("shapeclip.framebuffer.gl.error", where + "|" + error, + "[ShapeClip/GL] gl error at %s: 0x%s", where, Integer.toHexString(error)); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilSampleProbe.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilSampleProbe.java new file mode 100644 index 0000000..474ced1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/stencil/GlStencilSampleProbe.java @@ -0,0 +1,108 @@ +package dev.loki.lovisual.render.engine.rhi.backend.gl.clip; + +import dev.loki.lovisual.render.engine.msaa.MsaaTextureRegistry; +import org.lwjgl.opengl.ARBDirectStateAccess; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL30C; +import org.lwjgl.opengl.GL32C; +import org.lwjgl.opengl.GL45C; +import org.lwjgl.opengl.GLCapabilities; + +final class GlStencilSampleProbe { + + private final GlStencilGl gl; + private boolean textureSamplesDsaProbeDisabled; + + GlStencilSampleProbe(GlStencilGl gl) { + this.gl = gl; + } + + int detectBoundFramebufferSamples() { + int colorSamples = attachmentSamples(GL30C.GL_COLOR_ATTACHMENT0); + int depthSamples = attachmentSamples(GL30C.GL_DEPTH_ATTACHMENT); + int stencilSamples = attachmentSamples(GL30C.GL_STENCIL_ATTACHMENT); + int max = Math.max(colorSamples, Math.max(depthSamples, stencilSamples)); + return Math.max(1, max); + } + + int attachmentSamples(int attachmentPoint) { + int type = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE + ); + if (type == GL11C.GL_NONE) { + return 1; + } + if (type == GL30C.GL_RENDERBUFFER) { + int renderbuffer = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME + ); + return renderbufferSamples(renderbuffer); + } + if (type == GL11C.GL_TEXTURE) { + int texture = GL30C.glGetFramebufferAttachmentParameteri( + GL30C.GL_FRAMEBUFFER, + attachmentPoint, + GL30C.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME + ); + return textureSamples(texture); + } + return 1; + } + + int textureSamples(int texture) { + if (texture == 0) { + return 1; + } + int registeredSamples = MsaaTextureRegistry.getSamples(texture); + if (registeredSamples > 1) { + return registeredSamples; + } + if (textureSamplesDsaProbeDisabled) { + return 1; + } + GLCapabilities capabilities = GL.getCapabilities(); + if (capabilities == null || (!capabilities.OpenGL45 && !capabilities.GL_ARB_direct_state_access)) { + return 1; + } + try { + gl.clearGlErrorsSilently(); + int samples; + if (capabilities.OpenGL45) { + samples = GL45C.glGetTextureLevelParameteri(texture, 0, GL32C.GL_TEXTURE_SAMPLES); + } else { + samples = ARBDirectStateAccess.glGetTextureLevelParameteri(texture, 0, GL32C.GL_TEXTURE_SAMPLES); + } + int error = GL11C.glGetError(); + if (error == GL11C.GL_NO_ERROR) { + return Math.max(1, samples); + } + textureSamplesDsaProbeDisabled = true; + } catch (Throwable ignored) { + textureSamplesDsaProbeDisabled = true; + } + gl.clearGlErrorsSilently(); + return 1; + } + + int renderbufferSamples(int renderbuffer) { + if (renderbuffer == 0) { + return 1; + } + int previousRenderbuffer = GL11C.glGetInteger(GL30C.GL_RENDERBUFFER_BINDING); + try { + GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, renderbuffer); + int samples = GL30C.glGetRenderbufferParameteri(GL30C.GL_RENDERBUFFER, GL30C.GL_RENDERBUFFER_SAMPLES); + return Math.max(1, samples); + } catch (Throwable ignored) { + gl.clearGlErrorsSilently(); + return 1; + } finally { + GL30C.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, previousRenderbuffer); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java index c167886..8859cca 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java @@ -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.render.engine.rhi.backend.gl.state; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java index cba5e82..63f2bfd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java @@ -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.render.engine.rhi.backend.vulkan; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java index aa09827..b12728c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java @@ -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.render.engine.rhi.backend.vulkan.clip; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java index 00c98ce..449b938 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java @@ -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.render.engine.rhi.backend.vulkan.clip; import com.mojang.blaze3d.systems.RenderPassDescriptor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java index b34111b..08063e3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java @@ -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.render.engine.rhi.backend.vulkan.clip; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java index f813ade..c2c393d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java @@ -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.render.engine.rhi.backend.vulkan.msaa; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java index 65d9376..13b57e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java @@ -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.render.engine.rhi.backend.vulkan.msaa; import com.mojang.blaze3d.pipeline.RenderTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java index 30bbfdc..371ff99 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java @@ -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.render.engine.rhi.backend.vulkan.state; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanPipelineVariantKey.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanPipelineVariantKey.java new file mode 100644 index 0000000..cf73628 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanPipelineVariantKey.java @@ -0,0 +1,69 @@ +package dev.loki.lovisual.render.engine.rhi.backend.vulkan.util; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge.StencilMode; + +/** + * Cache key identifying a concrete {@code VkPipeline} variant: the base pipeline + * plus the active render-pass sample count, stencil-attachment presence, effective + * stencil mode and shape-clip participation. Extracted from + * {@link VulkanRenderStateBridge}'s nested types; referenced as a value object by + * the Vulkan device pipeline-variant cache. + */ +public final class VulkanPipelineVariantKey { + private final RenderPipeline pipeline; + private final int pipelineIdentity; + private final int samples; + private final boolean stencilFormat; + private final StencilMode stencilMode; + private final boolean shapeClipParticipant; + + public VulkanPipelineVariantKey(RenderPipeline pipeline, int samples, boolean stencilFormat, StencilMode stencilMode, boolean shapeClipParticipant) { + this.pipeline = pipeline; + this.pipelineIdentity = System.identityHashCode(pipeline); + this.samples = samples; + this.stencilFormat = stencilFormat; + this.stencilMode = stencilMode == null ? StencilMode.DISABLED : stencilMode; + this.shapeClipParticipant = shapeClipParticipant; + } + + public boolean usesMsaa() { + return samples > 1; + } + + public boolean usesStencil() { + return stencilFormat; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof VulkanPipelineVariantKey other)) return false; + return pipeline == other.pipeline + && samples == other.samples + && stencilFormat == other.stencilFormat + && stencilMode == other.stencilMode + && shapeClipParticipant == other.shapeClipParticipant; + } + + @Override + public int hashCode() { + int result = pipelineIdentity; + result = 31 * result + samples; + result = 31 * result + (stencilFormat ? 1 : 0); + result = 31 * result + stencilMode.hashCode(); + result = 31 * result + (shapeClipParticipant ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PipelineVariantKey{" + + "pipeline=" + pipeline + + ", samples=" + samples + + ", stencilFormat=" + stencilFormat + + ", stencilMode=" + stencilMode + + ", shapeClipParticipant=" + shapeClipParticipant + + '}'; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java index 5bd3c44..4dbd55c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java @@ -1,29 +1,20 @@ -/* - * 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.render.engine.rhi.backend.vulkan.util; import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.systems.RenderPassDescriptor; import com.mojang.blaze3d.textures.GpuTextureView; import org.jetbrains.annotations.Nullable; -import dev.loki.lovisual.mixininterface.device.IMsaaTexture; -import dev.loki.lovisual.mixininterface.render.IRenderPipeline; -import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; -import dev.loki.lovisual.util.logging.DebugLog; - -import java.util.List; - -import static org.lwjgl.vulkan.VK12.*; -import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRuntimeGuards; /** * Vulkan-side fixed pipeline state bridge for LoVisual's GL-era dynamic state. * + *

    This is the stable public entry point used by the Vulkan mixins and backends. + * The concrete state lives in two package-private domain helpers: + * {@link VulkanMsaaState} (sample counts and MSAA capabilities) and + * {@link VulkanStencilState} (stencil clip stack, per-pass attachment latch and + * pipeline-variant derivation). The render-pass begin/end orchestration that spans + * both domains stays here.

    + * *

    Important Vulkan rule: do not destroy/recreate cached VkPipelines while the current * command buffer may already reference them. Stencil modes are therefore represented through a * separate per-mode pipeline variant cache in VulkanDeviceMixin, not by calling @@ -32,48 +23,32 @@ import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRuntimeGuar public enum VulkanRenderStateBridge { ; - public static boolean forceDisableStencil() { - return runtimeStencilDisabled; + private static volatile boolean vulkanBackendActive; + + public static void setVulkanBackendActive(boolean active) { + vulkanBackendActive = active; + if (!active) { + VulkanMsaaState.resetForInactiveBackend(); + VulkanStencilState.resetForInactiveBackend(); + } } - public static String stencilDisableReason() { - return runtimeStencilDisableReason; + public static boolean vulkanBackendActive() { + return vulkanBackendActive; } - public static void disableStencilAfterFailure(String reason, Throwable t) { - runtimeStencilDisabled = true; - runtimeStencilDisableReason = reason == null ? "runtime-failure" : reason; - stencilMode = StencilMode.DISABLED; - stencilReference = 0; - stencilCompareReference = 0; - stencilClearRequested = false; - currentRenderPassStencilAttachment = false; - stencilOffscreenBypassDepth = 0; - String key = runtimeStencilDisableReason + "|" + (t == null ? "null" : t.getClass().getSimpleName()); - DebugLog.errorOnChange("shapeclip.vulkan.disabled.after.failure", key, - "[ShapeClip/Vulkan] disabling stencil after failure: reason=%s error=%s: %s", - runtimeStencilDisableReason, - t == null ? "none" : t.getClass().getSimpleName(), - t == null ? "" : t.getMessage()); - } + // ---- MSAA / sample-count domain (delegates to VulkanMsaaState) ---- public static boolean forceDisableMsaa() { - return runtimeMsaaDisabled; + return VulkanMsaaState.forceDisableMsaa(); } public static String msaaDisableReason() { - return runtimeMsaaDisableReason; + return VulkanMsaaState.msaaDisableReason(); } public static void disableMsaaAfterFailure(String reason, Throwable t) { - runtimeMsaaDisabled = true; - runtimeMsaaDisableReason = reason == null ? "runtime-failure" : reason; - String key = runtimeMsaaDisableReason + "|" + (t == null ? "null" : t.getClass().getSimpleName()); - DebugLog.errorOnChange("msaa.vulkan.disabled.after.failure", key, - "[MSAA/Vulkan] disabling MSAA for future render passes: reason=%s error=%s: %s", - runtimeMsaaDisableReason, - t == null ? "none" : t.getClass().getSimpleName(), - t == null ? "" : t.getMessage()); + VulkanMsaaState.disableMsaaAfterFailure(reason, t); } public static boolean forceDisableCheckpoints() { @@ -84,314 +59,159 @@ public enum VulkanRenderStateBridge { return VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_DEBUG_LABELS; } - private static final ThreadLocal REQUESTED_TEXTURE_SAMPLES = ThreadLocal.withInitial(() -> 1); - private static final ThreadLocal COMPILING_PIPELINE = new ThreadLocal<>(); - private static volatile boolean vulkanBackendActive; - private static volatile int currentRenderPassSamples = 1; - private static volatile StencilMode stencilMode = StencilMode.DISABLED; - private static volatile int stencilReference; - private static volatile int stencilCompareReference; - private static volatile boolean stencilClearRequested; - private static volatile boolean currentRenderPassStencilAttachment; - private static volatile int stencilOffscreenBypassDepth; - private static volatile boolean runtimeStencilDisabled; - private static volatile String runtimeStencilDisableReason = "none"; - private static volatile boolean runtimeMsaaDisabled; - private static volatile String runtimeMsaaDisableReason = "none"; - private static volatile int supportedFramebufferSampleCounts = VK_SAMPLE_COUNT_1_BIT; - private static volatile int supportedDepthResolveModes; - private static volatile boolean independentResolveNone; - - public static void setVulkanBackendActive(boolean active) { - vulkanBackendActive = active; - if (!active) { - currentRenderPassSamples = 1; - stencilMode = StencilMode.DISABLED; - stencilReference = 0; - stencilCompareReference = 0; - stencilClearRequested = false; - currentRenderPassStencilAttachment = false; - stencilOffscreenBypassDepth = 0; - runtimeStencilDisabled = false; - runtimeStencilDisableReason = "none"; - runtimeMsaaDisabled = false; - runtimeMsaaDisableReason = "none"; - COMPILING_PIPELINE.remove(); - } - } - - public static boolean vulkanBackendActive() { - return vulkanBackendActive; - } - public static int requestedTextureSamples() { - if (forceDisableMsaa()) return 1; - return selectSupportedSamples(REQUESTED_TEXTURE_SAMPLES.get()); + return VulkanMsaaState.requestedTextureSamples(); } public static T withTextureSamples(int samples, java.util.function.Supplier factory) { - int previous = REQUESTED_TEXTURE_SAMPLES.get(); - REQUESTED_TEXTURE_SAMPLES.set(forceDisableMsaa() ? 1 : selectSupportedSamples(samples)); - try { - return factory.get(); - } finally { - REQUESTED_TEXTURE_SAMPLES.set(previous); - } + return VulkanMsaaState.withTextureSamples(samples, factory); } public static int normalizeSamples(int samples) { - return forceDisableMsaa() ? 1 : selectSupportedSamples(samples); + return VulkanMsaaState.normalizeSamples(samples); } public static int currentRenderPassSamples() { - return Math.max(1, currentRenderPassSamples); + return VulkanMsaaState.currentRenderPassSamples(); } public static int currentVkSampleCount() { - return Math.max(1, currentRenderPassSamples); + return VulkanMsaaState.currentVkSampleCount(); } public static void configureMsaaCapabilities(int framebufferSampleCounts, int depthResolveModes, boolean supportsIndependentResolveNone) { - supportedFramebufferSampleCounts = framebufferSampleCounts | VK_SAMPLE_COUNT_1_BIT; - supportedDepthResolveModes = depthResolveModes; - independentResolveNone = supportsIndependentResolveNone; + VulkanMsaaState.configureMsaaCapabilities(framebufferSampleCounts, depthResolveModes, supportsIndependentResolveNone); } public static boolean msaaSupported() { - return !forceDisableMsaa() - && (supportedFramebufferSampleCounts & ~VK_SAMPLE_COUNT_1_BIT) != 0 - && supportedDepthResolveModes != VK_RESOLVE_MODE_NONE; + return VulkanMsaaState.msaaSupported(); } public static int depthResolveMode() { - if ((supportedDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT) != 0) { - return VK_RESOLVE_MODE_SAMPLE_ZERO_BIT; - } - if ((supportedDepthResolveModes & VK_RESOLVE_MODE_MAX_BIT) != 0) { - return VK_RESOLVE_MODE_MAX_BIT; - } - throw new IllegalStateException("Vulkan device exposes no supported depth resolve mode"); + return VulkanMsaaState.depthResolveMode(); } public static boolean independentResolveNone() { - return independentResolveNone; - } - - private static int selectSupportedSamples(int requested) { - if (requested <= 1) return 1; - int capped = Math.min(requested, 16); - for (int candidate = 16; candidate >= 2; candidate >>= 1) { - if (candidate <= capped && (supportedFramebufferSampleCounts & candidate) != 0) { - return candidate; - } - } - return 1; + return VulkanMsaaState.independentResolveNone(); } public static void beginRenderPass(RenderPassDescriptor descriptor) { if (!vulkanBackendActive) return; - int samples = validateAndGetSamples(descriptor); - currentRenderPassSamples = samples; - currentRenderPassStencilAttachment = !forceDisableStencil() && stencilAttachmentRequiredForNextPass(); - DebugLog.stencilOnChange("vulkan.renderpass.compat", - samples + "|" + currentRenderPassStencilAttachment + "|" + stencilMode, - "[Vulkan/RHI] render pass compatibility: samples=%d stencilAttachment=%s stencilMode=%s", - samples, currentRenderPassStencilAttachment, stencilMode); + int samples = VulkanMsaaState.beginRenderPass(descriptor); + VulkanStencilState.beginRenderPass(samples); } public static void endRenderPass() { - currentRenderPassSamples = 1; - currentRenderPassStencilAttachment = false; - COMPILING_PIPELINE.remove(); - } - - private static boolean stencilAttachmentRequiredForNextPass() { - if (forceDisableStencil() || stencilOffscreenBypassDepth > 0) return false; - return stencilMode != StencilMode.DISABLED || stencilClearRequested; - } - - public static boolean currentRenderPassNeedsStencilAttachment() { - return vulkanBackendActive && !forceDisableStencil() && currentRenderPassStencilAttachment; - } - - public static void markCurrentRenderPassStencilAttachment(boolean attached) { - currentRenderPassStencilAttachment = vulkanBackendActive && !forceDisableStencil() && attached; - } - - public static boolean currentRenderPassHasStencilAttachment() { - return vulkanBackendActive && !forceDisableStencil() && currentRenderPassStencilAttachment; + VulkanMsaaState.endRenderPass(); + VulkanStencilState.endRenderPass(); } public static int samplesFor(@Nullable RenderPassDescriptor descriptor) { - return validateAndGetSamples(descriptor); - } - - private static int validateAndGetSamples(@Nullable RenderPassDescriptor descriptor) { - if (descriptor == null) return 1; - int samples = 0; - List>> colors = descriptor.colorAttachments(); - if (colors != null) { - for (RenderPassDescriptor.Attachment attachment : colors) { - samples = mergeAttachmentSamples(samples, samplesFor(attachment)); - } - } - samples = mergeAttachmentSamples(samples, samplesFor(descriptor.depthAttachment())); - if (samples == 0) samples = 1; - if (samples > 1 && (forceDisableMsaa() || (supportedFramebufferSampleCounts & samples) == 0)) { - throw new IllegalStateException("Unsupported Vulkan render-pass sample count: " + samples - + " (supported mask=0x" + Integer.toHexString(supportedFramebufferSampleCounts) + ")"); - } - return samples; - } - - private static int mergeAttachmentSamples(int current, int attachmentSamples) { - if (attachmentSamples <= 0) return current; - if (current == 0) return attachmentSamples; - if (current != attachmentSamples) { - throw new IllegalStateException("Vulkan render-pass attachment sample mismatch: " - + current + "x versus " + attachmentSamples + "x"); - } - return current; - } - - private static int samplesFor(@Nullable RenderPassDescriptor.Attachment attachment) { - if (attachment == null || attachment.textureView() == null || attachment.textureView().texture() == null) { - return 0; - } - if (attachment.textureView().texture() instanceof IMsaaTexture texture && texture.lovisual$isMsaa()) { - return Math.max(1, texture.lovisual$getSamples()); - } - return 1; + return VulkanMsaaState.samplesFor(descriptor); } public static int samplesFor(@Nullable GpuTextureView view) { - if (view == null || view.texture() == null) return 1; - if (view.texture() instanceof IMsaaTexture texture && texture.lovisual$isMsaa()) { - return Math.max(1, texture.lovisual$getSamples()); - } - return 1; + return VulkanMsaaState.samplesFor(view); + } + + // ---- Stencil / shape-clip domain (delegates to VulkanStencilState) ---- + + public static boolean forceDisableStencil() { + return VulkanStencilState.forceDisableStencil(); + } + + public static String stencilDisableReason() { + return VulkanStencilState.stencilDisableReason(); + } + + public static void disableStencilAfterFailure(String reason, Throwable t) { + VulkanStencilState.disableStencilAfterFailure(reason, t); + } + + public static boolean currentRenderPassNeedsStencilAttachment() { + return VulkanStencilState.currentRenderPassNeedsStencilAttachment(); + } + + public static void markCurrentRenderPassStencilAttachment(boolean attached) { + VulkanStencilState.markCurrentRenderPassStencilAttachment(attached); + } + + public static boolean currentRenderPassHasStencilAttachment() { + return VulkanStencilState.currentRenderPassHasStencilAttachment(); } public static void pushStencilOffscreenBypass() { - stencilOffscreenBypassDepth++; + VulkanStencilState.pushStencilOffscreenBypass(); } public static void popStencilOffscreenBypass() { - if (stencilOffscreenBypassDepth <= 0) return; - stencilOffscreenBypassDepth--; + VulkanStencilState.popStencilOffscreenBypass(); } public static boolean stencilOffscreenBypassed() { - return stencilOffscreenBypassDepth > 0; + return VulkanStencilState.stencilOffscreenBypassed(); } public static void setStencilMode(StencilMode mode, int reference, int compareReference) { - if (forceDisableStencil()) { - stencilMode = StencilMode.DISABLED; - stencilReference = 0; - stencilCompareReference = 0; - stencilClearRequested = false; - currentRenderPassStencilAttachment = false; - return; - } - stencilMode = mode == null ? StencilMode.DISABLED : mode; - stencilReference = clampStencilReference(reference); - stencilCompareReference = clampStencilReference(compareReference); + VulkanStencilState.setStencilMode(mode, reference, compareReference); } public static void requestStencilClear() { - if (forceDisableStencil()) return; - stencilClearRequested = true; + VulkanStencilState.requestStencilClear(); } public static boolean consumeStencilClearRequested() { - if (forceDisableStencil()) { - stencilClearRequested = false; - return false; - } - boolean requested = stencilClearRequested; - stencilClearRequested = false; - return requested; + return VulkanStencilState.consumeStencilClearRequested(); } public static StencilMode stencilMode() { - return stencilMode; + return VulkanStencilState.stencilMode(); } public static int stencilReference() { - return stencilReference; + return VulkanStencilState.stencilReference(); } public static int stencilCompareReference() { - return stencilCompareReference; + return VulkanStencilState.stencilCompareReference(); } public static boolean pipelineParticipatesInShapeClip(RenderPipeline pipeline) { - if (forceDisableStencil()) return false; - if (!(pipeline instanceof IRenderPipeline lovisualPipeline)) return false; - ShapeClipRenderPassContract contract = lovisualPipeline.lovisual$getShapeClipContract(); - return contract != null && contract.participates(); + return VulkanStencilState.pipelineParticipatesInShapeClip(pipeline); } - /** - * Dynamic rendering compatibility is per active render pass, not per logical clip contract. - * If pStencilAttachment is present, every graphics pipeline used in that dynamic rendering scope - * must be compiled with the same stencilAttachmentFormat. Non-participating pipelines simply keep - * stencilTestEnable=false. - */ public static boolean pipelineNeedsStencilFormat(RenderPipeline pipeline) { - return currentRenderPassHasStencilAttachment(); + return VulkanStencilState.pipelineNeedsStencilFormat(pipeline); } public static boolean stencilTestEnabledForCurrentPipeline(RenderPipeline pipeline) { - return !stencilOffscreenBypassed() - && currentRenderPassHasStencilAttachment() - && pipelineParticipatesInShapeClip(pipeline) - && stencilMode != StencilMode.DISABLED; + return VulkanStencilState.stencilTestEnabledForCurrentPipeline(pipeline); } public static boolean suppressColorWrites() { - RenderPipeline pipeline = COMPILING_PIPELINE.get(); - return !stencilOffscreenBypassed() - && currentRenderPassHasStencilAttachment() - && pipeline != null - && pipelineParticipatesInShapeClip(pipeline) - && (stencilMode == StencilMode.WRITE || stencilMode == StencilMode.RESTORE); + return VulkanStencilState.suppressColorWrites(); } public static void beginPipelineCompile(RenderPipeline pipeline) { - COMPILING_PIPELINE.set(pipeline); + VulkanStencilState.beginPipelineCompile(pipeline); } public static void endPipelineCompile() { - COMPILING_PIPELINE.remove(); + VulkanStencilState.endPipelineCompile(); } public static boolean needsPipelineVariant(RenderPipeline pipeline) { - if (!vulkanBackendActive) return false; - return currentRenderPassHasStencilAttachment() || currentRenderPassSamples() != 1; + return VulkanStencilState.needsPipelineVariant(pipeline); } - public static PipelineVariantKey pipelineVariantKey(RenderPipeline pipeline) { - StencilMode effectiveMode = stencilTestEnabledForCurrentPipeline(pipeline) ? stencilMode : StencilMode.DISABLED; - boolean stencilFormat = currentRenderPassHasStencilAttachment(); - return new PipelineVariantKey( - pipeline, - currentRenderPassSamples(), - stencilFormat, - effectiveMode, - pipelineParticipatesInShapeClip(pipeline) - ); + public static VulkanPipelineVariantKey pipelineVariantKey(RenderPipeline pipeline) { + return VulkanStencilState.pipelineVariantKey(pipeline); } public static int dynamicStencilReference() { - return stencilCompareReference; - } - - private static int clampStencilReference(int value) { - return Math.max(0, Math.min(255, value)); + return VulkanStencilState.dynamicStencilReference(); } public enum StencilMode { @@ -400,62 +220,4 @@ public enum VulkanRenderStateBridge { RESTORE, TEST } - - public static final class PipelineVariantKey { - private final RenderPipeline pipeline; - private final int pipelineIdentity; - private final int samples; - private final boolean stencilFormat; - private final StencilMode stencilMode; - private final boolean shapeClipParticipant; - - public PipelineVariantKey(RenderPipeline pipeline, int samples, boolean stencilFormat, StencilMode stencilMode, boolean shapeClipParticipant) { - this.pipeline = pipeline; - this.pipelineIdentity = System.identityHashCode(pipeline); - this.samples = samples; - this.stencilFormat = stencilFormat; - this.stencilMode = stencilMode == null ? StencilMode.DISABLED : stencilMode; - this.shapeClipParticipant = shapeClipParticipant; - } - - public boolean usesMsaa() { - return samples > 1; - } - - public boolean usesStencil() { - return stencilFormat; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof PipelineVariantKey other)) return false; - return pipeline == other.pipeline - && samples == other.samples - && stencilFormat == other.stencilFormat - && stencilMode == other.stencilMode - && shapeClipParticipant == other.shapeClipParticipant; - } - - @Override - public int hashCode() { - int result = pipelineIdentity; - result = 31 * result + samples; - result = 31 * result + (stencilFormat ? 1 : 0); - result = 31 * result + stencilMode.hashCode(); - result = 31 * result + (shapeClipParticipant ? 1 : 0); - return result; - } - - @Override - public String toString() { - return "PipelineVariantKey{" + - "pipeline=" + pipeline + - ", samples=" + samples + - ", stencilFormat=" + stencilFormat + - ", stencilMode=" + stencilMode + - ", shapeClipParticipant=" + shapeClipParticipant + - '}'; - } - } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java index 206d8bd..92a325e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java @@ -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.render.engine.rhi.backend.vulkan.util; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanMsaaState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanMsaaState.java new file mode 100644 index 0000000..bcad0a3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanMsaaState.java @@ -0,0 +1,184 @@ +package dev.loki.lovisual.render.engine.rhi.backend.vulkan.util; + +import com.mojang.blaze3d.systems.RenderPassDescriptor; +import com.mojang.blaze3d.textures.GpuTextureView; +import org.jetbrains.annotations.Nullable; +import dev.loki.lovisual.mixininterface.device.IMsaaTexture; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.List; + +import static org.lwjgl.vulkan.VK12.*; + +/** + * MSAA / sample-count domain of {@link VulkanRenderStateBridge}: tracks the + * requested and current-pass sample counts, the device's supported sample/resolve + * capabilities, the runtime MSAA-disable latch, and render-pass attachment sample + * validation. Package-private helper (impl subfolder, {@code vulkan.util} package); + * the facade exposes it through its public static API. + */ +final class VulkanMsaaState { + + private static final ThreadLocal REQUESTED_TEXTURE_SAMPLES = ThreadLocal.withInitial(() -> 1); + private static volatile int currentRenderPassSamples = 1; + private static volatile boolean runtimeMsaaDisabled; + private static volatile String runtimeMsaaDisableReason = "none"; + private static volatile int supportedFramebufferSampleCounts = VK_SAMPLE_COUNT_1_BIT; + private static volatile int supportedDepthResolveModes; + private static volatile boolean independentResolveNone; + + private VulkanMsaaState() { + } + + static void resetForInactiveBackend() { + currentRenderPassSamples = 1; + runtimeMsaaDisabled = false; + runtimeMsaaDisableReason = "none"; + } + + static boolean forceDisableMsaa() { + return runtimeMsaaDisabled; + } + + static String msaaDisableReason() { + return runtimeMsaaDisableReason; + } + + static void disableMsaaAfterFailure(String reason, Throwable t) { + runtimeMsaaDisabled = true; + runtimeMsaaDisableReason = reason == null ? "runtime-failure" : reason; + String key = runtimeMsaaDisableReason + "|" + (t == null ? "null" : t.getClass().getSimpleName()); + DebugLog.errorOnChange("msaa.vulkan.disabled.after.failure", key, + "[MSAA/Vulkan] disabling MSAA for future render passes: reason=%s error=%s: %s", + runtimeMsaaDisableReason, + t == null ? "none" : t.getClass().getSimpleName(), + t == null ? "" : t.getMessage()); + } + + static int requestedTextureSamples() { + if (forceDisableMsaa()) return 1; + return selectSupportedSamples(REQUESTED_TEXTURE_SAMPLES.get()); + } + + static T withTextureSamples(int samples, java.util.function.Supplier factory) { + int previous = REQUESTED_TEXTURE_SAMPLES.get(); + REQUESTED_TEXTURE_SAMPLES.set(forceDisableMsaa() ? 1 : selectSupportedSamples(samples)); + try { + return factory.get(); + } finally { + REQUESTED_TEXTURE_SAMPLES.set(previous); + } + } + + static int normalizeSamples(int samples) { + return forceDisableMsaa() ? 1 : selectSupportedSamples(samples); + } + + static int currentRenderPassSamples() { + return Math.max(1, currentRenderPassSamples); + } + + static int currentVkSampleCount() { + return Math.max(1, currentRenderPassSamples); + } + + static void configureMsaaCapabilities(int framebufferSampleCounts, + int depthResolveModes, + boolean supportsIndependentResolveNone) { + supportedFramebufferSampleCounts = framebufferSampleCounts | VK_SAMPLE_COUNT_1_BIT; + supportedDepthResolveModes = depthResolveModes; + independentResolveNone = supportsIndependentResolveNone; + } + + static boolean msaaSupported() { + return !forceDisableMsaa() + && (supportedFramebufferSampleCounts & ~VK_SAMPLE_COUNT_1_BIT) != 0 + && supportedDepthResolveModes != VK_RESOLVE_MODE_NONE; + } + + static int depthResolveMode() { + if ((supportedDepthResolveModes & VK_RESOLVE_MODE_SAMPLE_ZERO_BIT) != 0) { + return VK_RESOLVE_MODE_SAMPLE_ZERO_BIT; + } + if ((supportedDepthResolveModes & VK_RESOLVE_MODE_MAX_BIT) != 0) { + return VK_RESOLVE_MODE_MAX_BIT; + } + throw new IllegalStateException("Vulkan device exposes no supported depth resolve mode"); + } + + static boolean independentResolveNone() { + return independentResolveNone; + } + + private static int selectSupportedSamples(int requested) { + if (requested <= 1) return 1; + int capped = Math.min(requested, 16); + for (int candidate = 16; candidate >= 2; candidate >>= 1) { + if (candidate <= capped && (supportedFramebufferSampleCounts & candidate) != 0) { + return candidate; + } + } + return 1; + } + + /** Validate the descriptor's attachments and latch the current-pass sample count. */ + static int beginRenderPass(RenderPassDescriptor descriptor) { + int samples = validateAndGetSamples(descriptor); + currentRenderPassSamples = samples; + return samples; + } + + static void endRenderPass() { + currentRenderPassSamples = 1; + } + + static int samplesFor(@Nullable RenderPassDescriptor descriptor) { + return validateAndGetSamples(descriptor); + } + + private static int validateAndGetSamples(@Nullable RenderPassDescriptor descriptor) { + if (descriptor == null) return 1; + int samples = 0; + List>> colors = descriptor.colorAttachments(); + if (colors != null) { + for (RenderPassDescriptor.Attachment attachment : colors) { + samples = mergeAttachmentSamples(samples, samplesFor(attachment)); + } + } + samples = mergeAttachmentSamples(samples, samplesFor(descriptor.depthAttachment())); + if (samples == 0) samples = 1; + if (samples > 1 && (forceDisableMsaa() || (supportedFramebufferSampleCounts & samples) == 0)) { + throw new IllegalStateException("Unsupported Vulkan render-pass sample count: " + samples + + " (supported mask=0x" + Integer.toHexString(supportedFramebufferSampleCounts) + ")"); + } + return samples; + } + + private static int mergeAttachmentSamples(int current, int attachmentSamples) { + if (attachmentSamples <= 0) return current; + if (current == 0) return attachmentSamples; + if (current != attachmentSamples) { + throw new IllegalStateException("Vulkan render-pass attachment sample mismatch: " + + current + "x versus " + attachmentSamples + "x"); + } + return current; + } + + private static int samplesFor(@Nullable RenderPassDescriptor.Attachment attachment) { + if (attachment == null || attachment.textureView() == null || attachment.textureView().texture() == null) { + return 0; + } + if (attachment.textureView().texture() instanceof IMsaaTexture texture && texture.lovisual$isMsaa()) { + return Math.max(1, texture.lovisual$getSamples()); + } + return 1; + } + + static int samplesFor(@Nullable GpuTextureView view) { + if (view == null || view.texture() == null) return 1; + if (view.texture() instanceof IMsaaTexture texture && texture.lovisual$isMsaa()) { + return Math.max(1, texture.lovisual$getSamples()); + } + return 1; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanStencilState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanStencilState.java new file mode 100644 index 0000000..7f8895a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/impl/VulkanStencilState.java @@ -0,0 +1,218 @@ +package dev.loki.lovisual.render.engine.rhi.backend.vulkan.util; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.mixininterface.render.IRenderPipeline; +import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge.StencilMode; +import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipRenderPassContract; +import dev.loki.lovisual.util.logging.DebugLog; + +/** + * Stencil / shape-clip domain of {@link VulkanRenderStateBridge}: the logical clip + * stack (mode + references), the per-pass stencil-attachment latch, the offscreen + * bypass depth, the runtime stencil-disable latch, the pipeline-compile context and + * the pipeline-variant key derivation. Package-private helper (impl subfolder, + * {@code vulkan.util} package); the facade exposes it through its public static API. + */ +final class VulkanStencilState { + + private static final ThreadLocal COMPILING_PIPELINE = new ThreadLocal<>(); + private static volatile StencilMode stencilMode = StencilMode.DISABLED; + private static volatile int stencilReference; + private static volatile int stencilCompareReference; + private static volatile boolean stencilClearRequested; + private static volatile boolean currentRenderPassStencilAttachment; + private static volatile int stencilOffscreenBypassDepth; + private static volatile boolean runtimeStencilDisabled; + private static volatile String runtimeStencilDisableReason = "none"; + + private VulkanStencilState() { + } + + static void resetForInactiveBackend() { + stencilMode = StencilMode.DISABLED; + stencilReference = 0; + stencilCompareReference = 0; + stencilClearRequested = false; + currentRenderPassStencilAttachment = false; + stencilOffscreenBypassDepth = 0; + runtimeStencilDisabled = false; + runtimeStencilDisableReason = "none"; + COMPILING_PIPELINE.remove(); + } + + static boolean forceDisableStencil() { + return runtimeStencilDisabled; + } + + static String stencilDisableReason() { + return runtimeStencilDisableReason; + } + + static void disableStencilAfterFailure(String reason, Throwable t) { + runtimeStencilDisabled = true; + runtimeStencilDisableReason = reason == null ? "runtime-failure" : reason; + stencilMode = StencilMode.DISABLED; + stencilReference = 0; + stencilCompareReference = 0; + stencilClearRequested = false; + currentRenderPassStencilAttachment = false; + stencilOffscreenBypassDepth = 0; + String key = runtimeStencilDisableReason + "|" + (t == null ? "null" : t.getClass().getSimpleName()); + DebugLog.errorOnChange("shapeclip.vulkan.disabled.after.failure", key, + "[ShapeClip/Vulkan] disabling stencil after failure: reason=%s error=%s: %s", + runtimeStencilDisableReason, + t == null ? "none" : t.getClass().getSimpleName(), + t == null ? "" : t.getMessage()); + } + + /** Latch the per-pass stencil-attachment flag for the pass whose sample count was just set. */ + static void beginRenderPass(int samples) { + currentRenderPassStencilAttachment = !forceDisableStencil() && stencilAttachmentRequiredForNextPass(); + DebugLog.stencilOnChange("vulkan.renderpass.compat", + samples + "|" + currentRenderPassStencilAttachment + "|" + stencilMode, + "[Vulkan/RHI] render pass compatibility: samples=%d stencilAttachment=%s stencilMode=%s", + samples, currentRenderPassStencilAttachment, stencilMode); + } + + static void endRenderPass() { + currentRenderPassStencilAttachment = false; + COMPILING_PIPELINE.remove(); + } + + private static boolean stencilAttachmentRequiredForNextPass() { + if (forceDisableStencil() || stencilOffscreenBypassDepth > 0) return false; + return stencilMode != StencilMode.DISABLED || stencilClearRequested; + } + + static boolean currentRenderPassNeedsStencilAttachment() { + return VulkanRenderStateBridge.vulkanBackendActive() && !forceDisableStencil() && currentRenderPassStencilAttachment; + } + + static void markCurrentRenderPassStencilAttachment(boolean attached) { + currentRenderPassStencilAttachment = VulkanRenderStateBridge.vulkanBackendActive() && !forceDisableStencil() && attached; + } + + static boolean currentRenderPassHasStencilAttachment() { + return VulkanRenderStateBridge.vulkanBackendActive() && !forceDisableStencil() && currentRenderPassStencilAttachment; + } + + static void pushStencilOffscreenBypass() { + stencilOffscreenBypassDepth++; + } + + static void popStencilOffscreenBypass() { + if (stencilOffscreenBypassDepth <= 0) return; + stencilOffscreenBypassDepth--; + } + + static boolean stencilOffscreenBypassed() { + return stencilOffscreenBypassDepth > 0; + } + + static void setStencilMode(StencilMode mode, int reference, int compareReference) { + if (forceDisableStencil()) { + stencilMode = StencilMode.DISABLED; + stencilReference = 0; + stencilCompareReference = 0; + stencilClearRequested = false; + currentRenderPassStencilAttachment = false; + return; + } + stencilMode = mode == null ? StencilMode.DISABLED : mode; + stencilReference = clampStencilReference(reference); + stencilCompareReference = clampStencilReference(compareReference); + } + + static void requestStencilClear() { + if (forceDisableStencil()) return; + stencilClearRequested = true; + } + + static boolean consumeStencilClearRequested() { + if (forceDisableStencil()) { + stencilClearRequested = false; + return false; + } + boolean requested = stencilClearRequested; + stencilClearRequested = false; + return requested; + } + + static StencilMode stencilMode() { + return stencilMode; + } + + static int stencilReference() { + return stencilReference; + } + + static int stencilCompareReference() { + return stencilCompareReference; + } + + static boolean pipelineParticipatesInShapeClip(RenderPipeline pipeline) { + if (forceDisableStencil()) return false; + if (!(pipeline instanceof IRenderPipeline lovisualPipeline)) return false; + ShapeClipRenderPassContract contract = lovisualPipeline.lovisual$getShapeClipContract(); + return contract != null && contract.participates(); + } + + /** + * Dynamic rendering compatibility is per active render pass, not per logical clip contract. + * If pStencilAttachment is present, every graphics pipeline used in that dynamic rendering scope + * must be compiled with the same stencilAttachmentFormat. Non-participating pipelines simply keep + * stencilTestEnable=false. + */ + static boolean pipelineNeedsStencilFormat(RenderPipeline pipeline) { + return currentRenderPassHasStencilAttachment(); + } + + static boolean stencilTestEnabledForCurrentPipeline(RenderPipeline pipeline) { + return !stencilOffscreenBypassed() + && currentRenderPassHasStencilAttachment() + && pipelineParticipatesInShapeClip(pipeline) + && stencilMode != StencilMode.DISABLED; + } + + static boolean suppressColorWrites() { + RenderPipeline pipeline = COMPILING_PIPELINE.get(); + return !stencilOffscreenBypassed() + && currentRenderPassHasStencilAttachment() + && pipeline != null + && pipelineParticipatesInShapeClip(pipeline) + && (stencilMode == StencilMode.WRITE || stencilMode == StencilMode.RESTORE); + } + + static void beginPipelineCompile(RenderPipeline pipeline) { + COMPILING_PIPELINE.set(pipeline); + } + + static void endPipelineCompile() { + COMPILING_PIPELINE.remove(); + } + + static boolean needsPipelineVariant(RenderPipeline pipeline) { + if (!VulkanRenderStateBridge.vulkanBackendActive()) return false; + return currentRenderPassHasStencilAttachment() || VulkanMsaaState.currentRenderPassSamples() != 1; + } + + static VulkanPipelineVariantKey pipelineVariantKey(RenderPipeline pipeline) { + StencilMode effectiveMode = stencilTestEnabledForCurrentPipeline(pipeline) ? stencilMode : StencilMode.DISABLED; + boolean stencilFormat = currentRenderPassHasStencilAttachment(); + return new VulkanPipelineVariantKey( + pipeline, + VulkanMsaaState.currentRenderPassSamples(), + stencilFormat, + effectiveMode, + pipelineParticipatesInShapeClip(pipeline) + ); + } + + static int dynamicStencilReference() { + return stencilCompareReference; + } + + private static int clampStencilReference(int value) { + return Math.max(0, Math.min(255, value)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java index 41b6768..101afb0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java @@ -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.render.engine.rhi.blit; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java index f7d6382..8675ccf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java @@ -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.render.engine.rhi.blit; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java index ab1c763..b4f00db 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java @@ -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.render.engine.rhi.clip; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java index 1958e28..3e1567d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java @@ -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.render.engine.rhi.clip; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java index 37f9284..6c9fba9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java @@ -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.render.engine.rhi.command; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java index ab1df50..58fc0be 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java @@ -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.render.engine.rhi.command; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java index 7b34777..e1e2ce4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java @@ -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.render.engine.rhi.command; import com.mojang.blaze3d.textures.GpuSampler; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java index 8dfaa94..90ce83d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java @@ -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.render.engine.rhi.command; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java index 0f6a37f..5dc7beb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java @@ -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.render.engine.rhi.fullscreen; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java index 4600284..ce30ab2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java @@ -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.render.engine.rhi.fullscreen; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java index 7fe0f50..e3ccd6c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java @@ -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.render.engine.rhi.msaa; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java index dc3d8d6..8a09780 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java @@ -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.render.engine.rhi.msaa; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java index 585ce15..829e6ff 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java @@ -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.render.engine.rhi.pipeline.depth; public enum BlendPolicy { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java index cff5a1c..ac40a5e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java @@ -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.render.engine.rhi.pipeline.depth; public enum CullPolicy { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java index 5fdca68..e6ff903 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java @@ -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.render.engine.rhi.pipeline.depth; public enum DepthPolicy { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java index 48eca63..a762c9f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java @@ -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.render.engine.rhi.pipeline.depth; public enum FogPolicy { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java index de8a158..1f2b10c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java @@ -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.render.engine.rhi.pipeline.layout; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java index 2eb0a40..719e7c3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java @@ -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.render.engine.rhi.pipeline.layout; import java.util.LinkedHashSet; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java index 15c4a88..008ed1d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java @@ -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.render.engine.rhi.pipeline.layout; import com.mojang.blaze3d.vertex.VertexFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java index ef82e63..1adfdef 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java @@ -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.render.engine.rhi.pipeline.pipeline; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java index 396e431..da9a74c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java @@ -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.render.engine.rhi.pipeline.pipeline; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java index b6c00e0..c0d6626 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java @@ -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.render.engine.rhi.pipeline.pipeline; import dev.loki.lovisual.render.engine.rhi.pipeline.pipeline.PipelineDomain; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java index 56fbdb0..4b7b9b4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java @@ -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.render.engine.rhi.pipeline.pipeline; import dev.loki.lovisual.render.engine.core.frame.RenderPhase; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java index e2144bc..b5ac2c8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java @@ -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.render.engine.rhi.resource; import com.mojang.blaze3d.IndexType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java index 00b9ebd..9a03b0e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java @@ -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.render.engine.rhi.resource; import dev.loki.lovisual.render.engine.rhi.blit.TextureBlitter; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java index bd6969b..72bc98c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java @@ -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.render.engine.rhi.resource; public enum MeshOwnership { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java index 8de9e99..0cfc2cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java @@ -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.render.engine.rhi.resource.atlas; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java index 2148e8b..ea3a5dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java @@ -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.render.engine.rhi.resource.atlas; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java index 541536c..b7107df 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java @@ -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.render.engine.rhi.resource.manager; import java.util.ArrayDeque; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java index 93c25a4..d08b14f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java @@ -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.render.engine.rhi.resource.manager; import com.mojang.blaze3d.pipeline.TextureTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java index 6ea4fd8..b8e8710 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java @@ -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.render.engine.rhi.resource.manager; public record RenderResourceStatsSnapshot( diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java index 24db858..425ebba 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java @@ -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.render.engine.rhi.resource.manager; import java.util.Collections; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java index 4d6e88b..e156ee2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java @@ -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.render.engine.rhi.resource.pool; import dev.loki.lovisual.render.engine.rhi.resource.manager.RenderResourceManager; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java index 1e4f198..d41b258 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java @@ -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.render.engine.rhi.resource.pool; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java index 5c20bdb..71c4845 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java @@ -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.render.engine.rhi.resource.pool; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java index 56d481f..ea05835 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java @@ -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.render.engine.rhi.resource.pool; import com.mojang.blaze3d.textures.GpuTexture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java index cd4e374..dcd6971 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java @@ -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.render.engine.rhi.scissor; import com.mojang.blaze3d.systems.RenderPass; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java index 813cf87..a61ad73 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java @@ -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.render.engine.rhi.state; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java index bc0fd14..4fbdac6 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java @@ -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.render.engine.rhi.stats; import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java index a8b1f98..d6555cd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java @@ -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.render.engine.rhi.stats; import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java index accb673..261d0ae 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java @@ -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.render.engine.rhi.uniform; public interface FrameResource { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java index e948d18..1f2d7f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java @@ -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.render.engine.rhi.uniform; import dev.loki.lovisual.render.engine.rhi.uniform.FrameResource; import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java index f74e773..c8093e1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java @@ -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.render.engine.rhi.uniform; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java index c435f7b..25b575d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java @@ -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.render.engine.rhi.uniform.stats; import dev.loki.lovisual.render.engine.rhi.uniform.stats.UniformAllocatorStatsSnapshot; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java index 9ada112..464aadf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java @@ -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.render.engine.rhi.uniform.stats; public record UniformAllocatorStatsSnapshot(long frameId, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java index 42ecf41..e7e635d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java @@ -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.render.engine.rhi.upload; import dev.loki.lovisual.render.engine.rhi.resource.GpuMeshHandle; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java index 77ab734..2444b3a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java @@ -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.render.engine.rhi.upload.blaze3d; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java index f978f71..f0d351f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java @@ -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.render.engine.rhi.upload.blaze3d; import com.mojang.blaze3d.buffers.GpuFence; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java index d50a2b4..2645fe7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java @@ -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.render.engine.rhi.upload.blaze3d; import dev.loki.lovisual.render.engine.rhi.resource.GpuMeshHandle; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java index 6a8eaf4..2cf2d6f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java @@ -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.render.engine.rhi.upload.blaze3d; import com.mojang.blaze3d.buffers.GpuBuffer; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java index a96a188..fae9f29 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java @@ -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.render.engine.shader; import com.mojang.blaze3d.preprocessor.GlslPreprocessor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java index 7cd4634..42750a1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java @@ -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.render.engine.svg.backend; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java index 6b7625c..045d088 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java @@ -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.render.engine.svg.backend; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java index 0b7a80e..1b31ce4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java @@ -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.render.engine.svg.model; public enum SvgColorMode { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java index 256b85b..357b11e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java @@ -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.render.engine.svg.model; import dev.loki.lovisual.render.engine.svg.model.SvgColorMode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java index dfcb8e1..7cee965 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java @@ -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.render.engine.svg.model; import org.jetbrains.annotations.Nullable; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java index b28e7e1..60a1b57 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java @@ -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.render.engine.svg.model; import org.w3c.dom.Element; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java index 603aaa8..51b8b42 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java @@ -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.render.engine.svg.parser; import com.google.gson.JsonObject; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java index 60a9ad4..d6037ac 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java @@ -1,30 +1,29 @@ -/* - * 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.render.engine.svg.parser; import java.awt.geom.Path2D; +/** + * Parses an SVG path {@code d} attribute into a {@link Path2D.Double}. This class + * owns the scan cursor and the current/start/last-control-point state and drives the + * command dispatch loop; the individual command handlers live in + * {@link SvgPathCommands} and {@link SvgPathCurves}, and arc-to-Bezier conversion in + * {@link SvgArcGeometry}. + */ public final class SvgPathParser { - private static final double EPS = 1e-7; private final String data; private int index; private char command; - private double cx; - private double cy; - private double sx; - private double sy; - private double lastCubicX; - private double lastCubicY; - private double lastQuadX; - private double lastQuadY; - private char prevCmd; + double cx; + double cy; + double sx; + double sy; + double lastCubicX; + double lastCubicY; + double lastQuadX; + double lastQuadY; + char prevCmd; private SvgPathParser(String data) { this.data = data; @@ -35,117 +34,6 @@ public final class SvgPathParser { return new SvgPathParser(data).parseInternal(); } - private static void arcTo(Path2D.Double path, - double x1, double y1, - double x2, double y2, - double rx, double ry, - double rotDeg, - boolean largeArc, - boolean sweep) { - if (Math.abs(x1 - x2) < EPS && Math.abs(y1 - y2) < EPS) return; - rx = Math.abs(rx); - ry = Math.abs(ry); - if (rx < EPS || ry < EPS) { - path.lineTo(x2, y2); - return; - } - - double phi = Math.toRadians(rotDeg % 360.0); - double cosPhi = Math.cos(phi); - double sinPhi = Math.sin(phi); - - double dx2 = (x1 - x2) * 0.5; - double dy2 = (y1 - y2) * 0.5; - double x1p = cosPhi * dx2 + sinPhi * dy2; - double y1p = -sinPhi * dx2 + cosPhi * dy2; - - double rxSq = rx * rx; - double rySq = ry * ry; - double x1pSq = x1p * x1p; - double y1pSq = y1p * y1p; - double lambda = x1pSq / rxSq + y1pSq / rySq; - if (lambda > 1.0) { - double s = Math.sqrt(lambda); - rx *= s; - ry *= s; - rxSq = rx * rx; - rySq = ry * ry; - } - - double sign = (largeArc == sweep) ? -1.0 : 1.0; - double num = rxSq * rySq - rxSq * y1pSq - rySq * x1pSq; - double den = rxSq * y1pSq + rySq * x1pSq; - double coef = den <= EPS ? 0.0 : sign * Math.sqrt(Math.max(0.0, num / den)); - - double cxp = coef * (rx * y1p / ry); - double cyp = coef * (-ry * x1p / rx); - double cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) * 0.5; - double cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) * 0.5; - - double ux = (x1p - cxp) / rx; - double uy = (y1p - cyp) / ry; - double vx = (-x1p - cxp) / rx; - double vy = (-y1p - cyp) / ry; - double theta1 = angleBetween(1.0, 0.0, ux, uy); - double dTheta = angleBetween(ux, uy, vx, vy); - if (!sweep && dTheta > 0.0) dTheta -= Math.PI * 2.0; - else if (sweep && dTheta < 0.0) dTheta += Math.PI * 2.0; - - int segments = (int) Math.ceil(Math.abs(dTheta) / (Math.PI / 2.0)); - double seg = dTheta / segments; - double t = theta1; - for (int i = 0; i < segments; i++) { - double t2 = t + seg; - appendArcCubic(path, cx, cy, rx, ry, cosPhi, sinPhi, t, t2); - t = t2; - } - } - - private static void appendArcCubic(Path2D.Double path, - double cx, double cy, - double rx, double ry, - double cosPhi, double sinPhi, - double t1, double t2) { - double dt = t2 - t1; - double alpha = Math.sin(dt) * (Math.sqrt(4.0 + 3.0 * Math.pow(Math.tan(dt * 0.5), 2.0)) - 1.0) / 3.0; - double cosT1 = Math.cos(t1); - double sinT1 = Math.sin(t1); - double cosT2 = Math.cos(t2); - double sinT2 = Math.sin(t2); - double c1x = cosT1 - alpha * sinT1; - double c1y = sinT1 + alpha * cosT1; - double c2x = cosT2 + alpha * sinT2; - double c2y = sinT2 - alpha * cosT2; - double[] p1 = map(cx, cy, rx, ry, cosPhi, sinPhi, c1x, c1y); - double[] p2 = map(cx, cy, rx, ry, cosPhi, sinPhi, c2x, c2y); - double[] p = map(cx, cy, rx, ry, cosPhi, sinPhi, cosT2, sinT2); - path.curveTo(p1[0], p1[1], p2[0], p2[1], p[0], p[1]); - } - - private static double[] map(double cx, double cy, double rx, double ry, double cosPhi, double sinPhi, double x, double y) { - double px = cx + rx * cosPhi * x - ry * sinPhi * y; - double py = cy + rx * sinPhi * x + ry * cosPhi * y; - return new double[]{px, py}; - } - - private static double angleBetween(double ux, double uy, double vx, double vy) { - double dot = ux * vx + uy * vy; - double len = Math.hypot(ux, uy) * Math.hypot(vx, vy); - if (len <= EPS) return 0.0; - double cos = Math.max(-1.0, Math.min(1.0, dot / len)); - double angle = Math.acos(cos); - double cross = ux * vy - uy * vx; - return cross < 0 ? -angle : angle; - } - - private static boolean isCommand(char c) { - return switch (c) { - case 'M', 'm', 'Z', 'z', 'L', 'l', 'H', 'h', 'V', 'v', - 'C', 'c', 'S', 's', 'Q', 'q', 'T', 't', 'A', 'a' -> true; - default -> false; - }; - } - private Path2D.Double parseInternal() { Path2D.Double out = new Path2D.Double(Path2D.WIND_NON_ZERO); @@ -154,7 +42,7 @@ public final class SvgPathParser { if (eof()) break; char c = data.charAt(index); - if (isCommand(c)) { + if (SvgPathCommands.isCommand(c)) { command = c; index++; } else if (command == 0) { @@ -163,21 +51,21 @@ public final class SvgPathParser { boolean rel = Character.isLowerCase(command); switch (Character.toLowerCase(command)) { - case 'm' -> parseMove(out, rel); + case 'm' -> SvgPathCommands.parseMove(this, out, rel); case 'z' -> { out.closePath(); cx = sx; cy = sy; prevCmd = 'z'; } - case 'l' -> parseLine(out, rel); - case 'h' -> parseHorizontal(out, rel); - case 'v' -> parseVertical(out, rel); - case 'c' -> parseCubic(out, rel); - case 's' -> parseSmoothCubic(out, rel); - case 'q' -> parseQuadratic(out, rel); - case 't' -> parseSmoothQuadratic(out, rel); - case 'a' -> parseArc(out, rel); + case 'l' -> SvgPathCommands.parseLine(this, out, rel); + case 'h' -> SvgPathCommands.parseHorizontal(this, out, rel); + case 'v' -> SvgPathCommands.parseVertical(this, out, rel); + case 'c' -> SvgPathCurves.parseCubic(this, out, rel); + case 's' -> SvgPathCurves.parseSmoothCubic(this, out, rel); + case 'q' -> SvgPathCurves.parseQuadratic(this, out, rel); + case 't' -> SvgPathCurves.parseSmoothQuadratic(this, out, rel); + case 'a' -> SvgPathCurves.parseArc(this, out, rel); default -> { return out; } @@ -187,203 +75,14 @@ public final class SvgPathParser { return out; } - private void parseMove(Path2D.Double out, boolean rel) { - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x += cx; - y += cy; - } - out.moveTo(x, y); - cx = sx = x; - cy = sy = y; - prevCmd = 'm'; - - while (hasNumber()) { - double lx = readNumber(); - double ly = readNumber(); - if (Double.isNaN(lx) || Double.isNaN(ly)) return; - if (rel) { - lx += cx; - ly += cy; - } - out.lineTo(lx, ly); - cx = lx; - cy = ly; - prevCmd = 'l'; - } - } - - private void parseLine(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x += cx; - y += cy; - } - out.lineTo(x, y); - cx = x; - cy = y; - prevCmd = 'l'; - } - } - - private void parseHorizontal(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x = readNumber(); - if (Double.isNaN(x)) return; - if (rel) x += cx; - out.lineTo(x, cy); - cx = x; - prevCmd = 'h'; - } - } - - private void parseVertical(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double y = readNumber(); - if (Double.isNaN(y)) return; - if (rel) y += cy; - out.lineTo(cx, y); - cy = y; - prevCmd = 'v'; - } - } - - private void parseCubic(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x1 = readNumber(); - double y1 = readNumber(); - double x2 = readNumber(); - double y2 = readNumber(); - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x1) || Double.isNaN(y1) || Double.isNaN(x2) || Double.isNaN(y2) || Double.isNaN(x) || Double.isNaN(y)) - return; - if (rel) { - x1 += cx; - y1 += cy; - x2 += cx; - y2 += cy; - x += cx; - y += cy; - } - out.curveTo(x1, y1, x2, y2, x, y); - lastCubicX = x2; - lastCubicY = y2; - cx = x; - cy = y; - prevCmd = 'c'; - } - } - - private void parseSmoothCubic(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x2 = readNumber(); - double y2 = readNumber(); - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x2) || Double.isNaN(y2) || Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x2 += cx; - y2 += cy; - x += cx; - y += cy; - } - double x1 = cx; - double y1 = cy; - if (prevCmd == 'c' || prevCmd == 's') { - x1 = 2.0 * cx - lastCubicX; - y1 = 2.0 * cy - lastCubicY; - } - out.curveTo(x1, y1, x2, y2, x, y); - lastCubicX = x2; - lastCubicY = y2; - cx = x; - cy = y; - prevCmd = 's'; - } - } - - private void parseQuadratic(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x1 = readNumber(); - double y1 = readNumber(); - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x1) || Double.isNaN(y1) || Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x1 += cx; - y1 += cy; - x += cx; - y += cy; - } - out.quadTo(x1, y1, x, y); - lastQuadX = x1; - lastQuadY = y1; - cx = x; - cy = y; - prevCmd = 'q'; - } - } - - private void parseSmoothQuadratic(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x += cx; - y += cy; - } - double x1 = cx; - double y1 = cy; - if (prevCmd == 'q' || prevCmd == 't') { - x1 = 2.0 * cx - lastQuadX; - y1 = 2.0 * cy - lastQuadY; - } - out.quadTo(x1, y1, x, y); - lastQuadX = x1; - lastQuadY = y1; - cx = x; - cy = y; - prevCmd = 't'; - } - } - - private void parseArc(Path2D.Double out, boolean rel) { - while (hasNumber()) { - double rx = readNumber(); - double ry = readNumber(); - double angle = readNumber(); - double largeArc = readNumber(); - double sweep = readNumber(); - double x = readNumber(); - double y = readNumber(); - if (Double.isNaN(rx) || Double.isNaN(ry) || Double.isNaN(angle) || Double.isNaN(largeArc) - || Double.isNaN(sweep) || Double.isNaN(x) || Double.isNaN(y)) return; - if (rel) { - x += cx; - y += cy; - } - arcTo(out, cx, cy, x, y, rx, ry, angle, largeArc != 0.0, sweep != 0.0); - cx = x; - cy = y; - prevCmd = 'a'; - } - } - - private boolean hasNumber() { + boolean hasNumber() { skipDelimiters(); if (eof()) return false; char c = data.charAt(index); return c == '+' || c == '-' || c == '.' || Character.isDigit(c); } - private double readNumber() { + double readNumber() { skipDelimiters(); if (eof()) return Double.NaN; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java index c40dcaf..48e11ef 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java @@ -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.render.engine.svg.parser; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgArcGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgArcGeometry.java new file mode 100644 index 0000000..b044c87 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgArcGeometry.java @@ -0,0 +1,120 @@ +package dev.loki.lovisual.render.engine.svg.parser; + +import java.awt.geom.Path2D; + +/** + * Elliptical-arc geometry for {@link SvgPathParser}: converts an SVG arc segment + * into cubic Bezier approximations appended to the target path. Pure static + * helpers - no parser cursor state is touched. Package-private helper + * (impl subfolder, {@code parser} package). + */ +final class SvgArcGeometry { + + static final double EPS = 1e-7; + + private SvgArcGeometry() { + } + + static void arcTo(Path2D.Double path, + double x1, double y1, + double x2, double y2, + double rx, double ry, + double rotDeg, + boolean largeArc, + boolean sweep) { + if (Math.abs(x1 - x2) < EPS && Math.abs(y1 - y2) < EPS) return; + rx = Math.abs(rx); + ry = Math.abs(ry); + if (rx < EPS || ry < EPS) { + path.lineTo(x2, y2); + return; + } + + double phi = Math.toRadians(rotDeg % 360.0); + double cosPhi = Math.cos(phi); + double sinPhi = Math.sin(phi); + + double dx2 = (x1 - x2) * 0.5; + double dy2 = (y1 - y2) * 0.5; + double x1p = cosPhi * dx2 + sinPhi * dy2; + double y1p = -sinPhi * dx2 + cosPhi * dy2; + + double rxSq = rx * rx; + double rySq = ry * ry; + double x1pSq = x1p * x1p; + double y1pSq = y1p * y1p; + double lambda = x1pSq / rxSq + y1pSq / rySq; + if (lambda > 1.0) { + double s = Math.sqrt(lambda); + rx *= s; + ry *= s; + rxSq = rx * rx; + rySq = ry * ry; + } + + double sign = (largeArc == sweep) ? -1.0 : 1.0; + double num = rxSq * rySq - rxSq * y1pSq - rySq * x1pSq; + double den = rxSq * y1pSq + rySq * x1pSq; + double coef = den <= EPS ? 0.0 : sign * Math.sqrt(Math.max(0.0, num / den)); + + double cxp = coef * (rx * y1p / ry); + double cyp = coef * (-ry * x1p / rx); + double cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) * 0.5; + double cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) * 0.5; + + double ux = (x1p - cxp) / rx; + double uy = (y1p - cyp) / ry; + double vx = (-x1p - cxp) / rx; + double vy = (-y1p - cyp) / ry; + double theta1 = angleBetween(1.0, 0.0, ux, uy); + double dTheta = angleBetween(ux, uy, vx, vy); + if (!sweep && dTheta > 0.0) dTheta -= Math.PI * 2.0; + else if (sweep && dTheta < 0.0) dTheta += Math.PI * 2.0; + + int segments = (int) Math.ceil(Math.abs(dTheta) / (Math.PI / 2.0)); + double seg = dTheta / segments; + double t = theta1; + for (int i = 0; i < segments; i++) { + double t2 = t + seg; + appendArcCubic(path, cx, cy, rx, ry, cosPhi, sinPhi, t, t2); + t = t2; + } + } + + private static void appendArcCubic(Path2D.Double path, + double cx, double cy, + double rx, double ry, + double cosPhi, double sinPhi, + double t1, double t2) { + double dt = t2 - t1; + double alpha = Math.sin(dt) * (Math.sqrt(4.0 + 3.0 * Math.pow(Math.tan(dt * 0.5), 2.0)) - 1.0) / 3.0; + double cosT1 = Math.cos(t1); + double sinT1 = Math.sin(t1); + double cosT2 = Math.cos(t2); + double sinT2 = Math.sin(t2); + double c1x = cosT1 - alpha * sinT1; + double c1y = sinT1 + alpha * cosT1; + double c2x = cosT2 + alpha * sinT2; + double c2y = sinT2 - alpha * cosT2; + double[] p1 = map(cx, cy, rx, ry, cosPhi, sinPhi, c1x, c1y); + double[] p2 = map(cx, cy, rx, ry, cosPhi, sinPhi, c2x, c2y); + double[] p = map(cx, cy, rx, ry, cosPhi, sinPhi, cosT2, sinT2); + path.curveTo(p1[0], p1[1], p2[0], p2[1], p[0], p[1]); + } + + private static double[] map(double cx, double cy, double rx, double ry, double cosPhi, double sinPhi, double x, double y) { + double px = cx + rx * cosPhi * x - ry * sinPhi * y; + double py = cy + rx * sinPhi * x + ry * cosPhi * y; + return new double[]{px, py}; + } + + private static double angleBetween(double ux, double uy, double vx, double vy) { + double dot = ux * vx + uy * vy; + double len = Math.hypot(ux, uy) * Math.hypot(vx, vy); + if (len <= EPS) return 0.0; + double cos = Math.max(-1.0, Math.min(1.0, dot / len)); + double angle = Math.acos(cos); + double cross = ux * vy - uy * vx; + return cross < 0 ? -angle : angle; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCommands.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCommands.java new file mode 100644 index 0000000..5cacb04 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCommands.java @@ -0,0 +1,90 @@ +package dev.loki.lovisual.render.engine.svg.parser; + +import java.awt.geom.Path2D; + +/** + * Linear SVG path commands for {@link SvgPathParser}: token classification plus + * move/line/horizontal/vertical segment handlers. Each handler reads numbers via + * the parser cursor and mutates its current/start point and previous-command state. + * Curve and arc handlers live in {@link SvgPathCurves}. Package-private helper + * (impl subfolder, {@code parser} package). + */ +final class SvgPathCommands { + + private SvgPathCommands() { + } + + static boolean isCommand(char c) { + return switch (c) { + case 'M', 'm', 'Z', 'z', 'L', 'l', 'H', 'h', 'V', 'v', + 'C', 'c', 'S', 's', 'Q', 'q', 'T', 't', 'A', 'a' -> true; + default -> false; + }; + } + + static void parseMove(SvgPathParser p, Path2D.Double out, boolean rel) { + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x += p.cx; + y += p.cy; + } + out.moveTo(x, y); + p.cx = p.sx = x; + p.cy = p.sy = y; + p.prevCmd = 'm'; + + while (p.hasNumber()) { + double lx = p.readNumber(); + double ly = p.readNumber(); + if (Double.isNaN(lx) || Double.isNaN(ly)) return; + if (rel) { + lx += p.cx; + ly += p.cy; + } + out.lineTo(lx, ly); + p.cx = lx; + p.cy = ly; + p.prevCmd = 'l'; + } + } + + static void parseLine(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x += p.cx; + y += p.cy; + } + out.lineTo(x, y); + p.cx = x; + p.cy = y; + p.prevCmd = 'l'; + } + } + + static void parseHorizontal(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x = p.readNumber(); + if (Double.isNaN(x)) return; + if (rel) x += p.cx; + out.lineTo(x, p.cy); + p.cx = x; + p.prevCmd = 'h'; + } + } + + static void parseVertical(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double y = p.readNumber(); + if (Double.isNaN(y)) return; + if (rel) y += p.cy; + out.lineTo(p.cx, y); + p.cy = y; + p.prevCmd = 'v'; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCurves.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCurves.java new file mode 100644 index 0000000..2152194 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/impl/SvgPathCurves.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.render.engine.svg.parser; + +import java.awt.geom.Path2D; + +/** + * Curve and arc SVG path commands for {@link SvgPathParser}: cubic / smooth-cubic / + * quadratic / smooth-quadratic / elliptical-arc handlers. Each reads numbers via the + * parser cursor and updates the control-point reflection and previous-command state. + * Linear handlers live in {@link SvgPathCommands}; arc-to-Bezier math in + * {@link SvgArcGeometry}. Package-private helper (impl subfolder, {@code parser} package). + */ +final class SvgPathCurves { + + private SvgPathCurves() { + } + + static void parseCubic(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x1 = p.readNumber(); + double y1 = p.readNumber(); + double x2 = p.readNumber(); + double y2 = p.readNumber(); + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x1) || Double.isNaN(y1) || Double.isNaN(x2) || Double.isNaN(y2) || Double.isNaN(x) || Double.isNaN(y)) + return; + if (rel) { + x1 += p.cx; + y1 += p.cy; + x2 += p.cx; + y2 += p.cy; + x += p.cx; + y += p.cy; + } + out.curveTo(x1, y1, x2, y2, x, y); + p.lastCubicX = x2; + p.lastCubicY = y2; + p.cx = x; + p.cy = y; + p.prevCmd = 'c'; + } + } + + static void parseSmoothCubic(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x2 = p.readNumber(); + double y2 = p.readNumber(); + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x2) || Double.isNaN(y2) || Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x2 += p.cx; + y2 += p.cy; + x += p.cx; + y += p.cy; + } + double x1 = p.cx; + double y1 = p.cy; + if (p.prevCmd == 'c' || p.prevCmd == 's') { + x1 = 2.0 * p.cx - p.lastCubicX; + y1 = 2.0 * p.cy - p.lastCubicY; + } + out.curveTo(x1, y1, x2, y2, x, y); + p.lastCubicX = x2; + p.lastCubicY = y2; + p.cx = x; + p.cy = y; + p.prevCmd = 's'; + } + } + + static void parseQuadratic(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x1 = p.readNumber(); + double y1 = p.readNumber(); + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x1) || Double.isNaN(y1) || Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x1 += p.cx; + y1 += p.cy; + x += p.cx; + y += p.cy; + } + out.quadTo(x1, y1, x, y); + p.lastQuadX = x1; + p.lastQuadY = y1; + p.cx = x; + p.cy = y; + p.prevCmd = 'q'; + } + } + + static void parseSmoothQuadratic(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x += p.cx; + y += p.cy; + } + double x1 = p.cx; + double y1 = p.cy; + if (p.prevCmd == 'q' || p.prevCmd == 't') { + x1 = 2.0 * p.cx - p.lastQuadX; + y1 = 2.0 * p.cy - p.lastQuadY; + } + out.quadTo(x1, y1, x, y); + p.lastQuadX = x1; + p.lastQuadY = y1; + p.cx = x; + p.cy = y; + p.prevCmd = 't'; + } + } + + static void parseArc(SvgPathParser p, Path2D.Double out, boolean rel) { + while (p.hasNumber()) { + double rx = p.readNumber(); + double ry = p.readNumber(); + double angle = p.readNumber(); + double largeArc = p.readNumber(); + double sweep = p.readNumber(); + double x = p.readNumber(); + double y = p.readNumber(); + if (Double.isNaN(rx) || Double.isNaN(ry) || Double.isNaN(angle) || Double.isNaN(largeArc) + || Double.isNaN(sweep) || Double.isNaN(x) || Double.isNaN(y)) return; + if (rel) { + x += p.cx; + y += p.cy; + } + SvgArcGeometry.arcTo(out, p.cx, p.cy, x, y, rx, ry, angle, largeArc != 0.0, sweep != 0.0); + p.cx = x; + p.cy = y; + p.prevCmd = 'a'; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java index 1de8933..0346ede 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java @@ -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.render.engine.text.backend.atlas; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java index 1a2033a..7e30726 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java @@ -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.render.engine.text.backend.atlas; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java index 3b459fb..b88ad6b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java @@ -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.render.engine.text.backend.atlas; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java index 26cbd2d..40531e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java @@ -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.render.engine.text.backend.atlas; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java index 7e616fe..8b0682e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java @@ -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.render.engine.text.backend.backend; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java index 64b8a9f..0464393 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java @@ -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.render.engine.text.backend.backend; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java index 0bbcbff..7050acb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java @@ -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.render.engine.text.backend.backend; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java index dee54ab..0c7c4ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java @@ -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.render.engine.text.backend.backend; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java index d92119e..aa5be52 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java @@ -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.render.engine.text.backend.placement; import dev.loki.lovisual.render.engine.text.backend.placement.TextRenderDomain; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java index 6cc43ee..fc0d6f4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java @@ -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.render.engine.text.backend.placement; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java index 5e2ef1e..c156d9b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java @@ -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.render.engine.text.backend.placement; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java index 649bdbc..f0ccbe4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java @@ -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.render.engine.text.backend.placement; public enum TextRenderDomain { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java index 49346a0..cab2f84 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java @@ -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.render.engine.text.command; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java index d2c4706..06a5f91 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java @@ -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.render.engine.text.command; import dev.loki.lovisual.render.engine.text.font.info.TextBackendPreference; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java index 0db3f31..9bd5782 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java @@ -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.render.engine.text.command; public record TextCommandStatsSnapshot(int recorded, diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java index 6950e96..aced632 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java @@ -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.render.engine.text.command; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java index 38ab566..75e62e7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java @@ -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.render.engine.text.font; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java index b95d58f..055e403 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java @@ -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.render.engine.text.font; import java.io.InputStream; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java index f0a76b8..ddf6f18 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java @@ -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.render.engine.text.font; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java index aebee4a..8d983c0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java @@ -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.render.engine.text.font; import java.io.File; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java index 49e850e..b0490ad 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java @@ -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.render.engine.text.font.faces; import java.io.InputStream; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java index 8eb1ff6..7a906ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java @@ -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.render.engine.text.font.faces; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java index 8755217..be1667f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java @@ -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.render.engine.text.font.faces; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java index bd3eacd..7226c19 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java @@ -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.render.engine.text.font.faces; import java.io.InputStream; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java index a597d38..230e82a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java @@ -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.render.engine.text.font.info; import java.util.concurrent.atomic.AtomicInteger; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java index daff703..cfd9e58 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java @@ -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.render.engine.text.font.info; public record FontInfo(String family, Type type) { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java index 8089ffd..3a64608 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java @@ -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.render.engine.text.font.info; import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java index a6d08e3..cfbdb83 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java @@ -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.render.engine.text.font.info; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java index 7a82eaf..be3ff69 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java @@ -1,20 +1,10 @@ -/* - * 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.render.engine.text.glyph; -import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode; import dev.loki.lovisual.render.engine.text.font.Font; import dev.loki.lovisual.render.engine.text.font.FontFace; import dev.loki.lovisual.render.engine.text.glyph.custom.CustomFontSet; -import dev.loki.lovisual.render.engine.text.render.TextRenderSystem; import dev.loki.lovisual.render.engine.text.render.TextRenderer; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; @@ -112,25 +102,14 @@ public class CustomTextRenderer implements TextRenderer { } double shadowPass(RenderColor color, boolean alphaFromColor, GlyphRun shadowGlyphs, GlyphRun mainGlyphs) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) ((alphaFromColor ? color.a / 255.0 : mesh.alpha) * preShadowA); - double ignored = shadowGlyphs.run(); - double width = mainGlyphs.run(); - SHADOW_COLOR.a = preShadowA; - return width; + return GradientTexts.shadowPass(this, color, alphaFromColor, shadowGlyphs, mainGlyphs); } @Override public double render(String text, double x, double y, RenderColor color, boolean shadow) { return autoRun(() -> { ensureMeshStarted(); - double offset = fontScale * scale / 1.5; - if (shadow) { - return shadowPass(color, true, - () -> font.render(mesh, text, x + offset, y + offset, SHADOW_COLOR, scale / 1.5), - () -> font.render(mesh, text, x, y, color, scale / 1.5)); - } - return font.render(mesh, text, x, y, color, scale / 1.5); + return GradientTexts.drawText(this, text, x, y, color, shadow); }); } @@ -146,13 +125,7 @@ public class CustomTextRenderer implements TextRenderer { return autoRun(() -> { liquidGlassText = true; ensureMeshStarted(); - double offset = fontScale * scale / 1.5; - if (shadow) { - return shadowPass(color, true, - () -> font.render(mesh, text, x + offset, y + offset, SHADOW_COLOR, scale / 1.5), - () -> font.render(mesh, text, x, y, color, scale / 1.5)); - } - return font.render(mesh, text, x, y, color, scale / 1.5); + return GradientTexts.drawText(this, text, x, y, color, shadow); }); } @@ -192,7 +165,7 @@ public class CustomTextRenderer implements TextRenderer { applySelection(fontSet.select(scale * fontScale, false)); } if (font.isReady()) { - submitMesh(); + GradientTexts.submitMesh(this); } } @@ -202,19 +175,6 @@ public class CustomTextRenderer implements TextRenderer { scale = 1; } - private void submitMesh() { - RenderPipeline pipeline = liquidGlassText - ? (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS_MSDF : LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS) - : (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_MSDF : LoVisualRenderPipelines.UI_TEXT); - TextRenderSystem.submitGlyphMesh( - liquidGlassText ? "LoVisual UI Liquid Glass Text" : "LoVisual UI Text", - font, - mesh, - pipeline, - TextPlacementMode.UI - ); - } - void ensureMeshStarted() { if (meshStarted) { return; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java index 50b7963..e5dbdea 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java @@ -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.render.engine.text.glyph; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java index 88de745..3d473e8 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java @@ -1,26 +1,65 @@ -/* - * 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.render.engine.text.glyph; +import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode; import dev.loki.lovisual.render.engine.text.font.Font; import dev.loki.lovisual.render.engine.text.glyph.custom.HorizontalFade; +import dev.loki.lovisual.render.engine.text.render.TextRenderSystem; import dev.loki.lovisual.render.helpers.clip.ScissorFunction; /** - * Градиентные текстовые прогоны {@link CustomTextRenderer}: линейный градиент, - * квад-градиент и горизонтальный fade (опционально под scissor-клипом). - * Тела 1:1 с прежними методами CustomTextRenderer. + * Text-render helpers backing {@link CustomTextRenderer}: the shadow/main + * two-pass glyph sequence, single-text drawing, glyph-mesh submission and the + * gradient / horizontal-fade runs. Tiers 1:1 with the former inline methods. */ final class GradientTexts { private GradientTexts() { } + /** + * Runs the shadow pass then the main pass, temporarily folding the mesh + * alpha (or the colour alpha) into {@code SHADOW_COLOR.a}. + */ + static double shadowPass(CustomTextRenderer r, RenderColor color, boolean alphaFromColor, + CustomTextRenderer.GlyphRun shadowGlyphs, CustomTextRenderer.GlyphRun mainGlyphs) { + int preShadowA = CustomTextRenderer.SHADOW_COLOR.a; + CustomTextRenderer.SHADOW_COLOR.a = + (int) ((alphaFromColor ? color.a / 255.0 : r.mesh.alpha) * preShadowA); + double ignored = shadowGlyphs.run(); + double width = mainGlyphs.run(); + CustomTextRenderer.SHADOW_COLOR.a = preShadowA; + return width; + } + + /** Draws one text at (x, y); when {@code shadow}, a SHADOW_COLOR copy is offset behind it. */ + static double drawText(CustomTextRenderer r, String text, double x, double y, + RenderColor color, boolean shadow) { + double offset = r.fontScale * r.scale / 1.5; + if (shadow) { + return shadowPass(r, color, true, + () -> r.font.render(r.mesh, text, x + offset, y + offset, CustomTextRenderer.SHADOW_COLOR, r.scale / 1.5), + () -> r.font.render(r.mesh, text, x, y, color, r.scale / 1.5)); + } + return r.font.render(r.mesh, text, x, y, color, r.scale / 1.5); + } + + /** Submits the built glyph mesh through {@link TextRenderSystem}, picking the pipeline for the run. */ + static void submitMesh(CustomTextRenderer r) { + boolean msdf = r.font.isMsdf(); + RenderPipeline pipeline = r.liquidGlassText + ? (msdf ? LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS_MSDF : LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS) + : (msdf ? LoVisualRenderPipelines.UI_TEXT_MSDF : LoVisualRenderPipelines.UI_TEXT); + TextRenderSystem.submitGlyphMesh( + r.liquidGlassText ? "LoVisual UI Liquid Glass Text" : "LoVisual UI Text", + r.font, + r.mesh, + pipeline, + TextPlacementMode.UI + ); + } + static double gradient(CustomTextRenderer r, String text, double x, double y, Font.GlyphGradient gradient, boolean shadow) { return r.autoRun(() -> { diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java index 70996bd..1161df1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java @@ -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.render.engine.text.glyph; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java index 1ae9e73..e566c5c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java @@ -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.render.engine.text.glyph.custom; import dev.loki.lovisual.render.engine.text.font.Font; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java index eb19bf8..1e47dab 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java @@ -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.render.engine.text.glyph.custom; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java index 431ddf1..ad251f2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java @@ -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.render.engine.text.glyph.custom; import java.util.Set; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java index 27e60b6..5dc58fd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java @@ -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.render.engine.text.msdf; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java index 3d77ffb..70ab22d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java @@ -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.render.engine.text.msdf; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java index a8dddf2..21f7a08 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java @@ -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.render.engine.text.msdf; import net.minecraft.client.renderer.texture.AbstractTexture; @@ -14,6 +7,7 @@ import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.uniform.impl.ui.MsdfTextUniforms; import dev.loki.lovisual.render.engine.text.glyph.CustomTextRenderer; @@ -60,7 +54,7 @@ public enum WorldTextRenderer { : Renderer3D.DepthMode.MAIN; Renderer3D.DepthMode effectiveDepth = requestedDepth; - Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() + Renderer3DBatchBindings bindings = Renderer3DBatchBindings.none() .withSampler("u_Texture", texture.getTextureView(), texture.getSampler()); var pipeline = font.isMsdf() diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java index 6219962..dfa94be 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java @@ -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.render.engine.text.msdf.atlas; import dev.loki.lovisual.render.engine.texture.Texture; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java index 216677b..ca987e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java @@ -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.render.engine.text.msdf.atlas; import com.google.gson.JsonArray; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java index 051199d..5e77aab 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java @@ -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.render.engine.text.msdf.atlas; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java index 3b02dd6..054dbfe 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java @@ -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.render.engine.text.msdf.atlas; import com.google.gson.JsonElement; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java index a26af00..8206f6a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java @@ -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.render.engine.text.render; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java index ebd7801..096b054 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java @@ -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.render.engine.text.render; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java index 318f3e5..eb2b8c5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java @@ -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.render.engine.texture; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java index a5f44d8..a9f0a97 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java @@ -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.render.engine.texture; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java index a291702..d78721c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java @@ -1,49 +1,25 @@ -/* - * 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.render.engine.uniform; import com.mojang.blaze3d.buffers.GpuBuffer; import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.VertexFormat; -import net.minecraft.client.Minecraft; import net.minecraft.world.phys.Vec3; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; -import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarp; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.Objects; -import static org.lwjgl.system.MemoryUtil.memPutByte; -import static org.lwjgl.system.MemoryUtil.memPutFloat; -import static org.lwjgl.system.MemoryUtil.memPutInt; - /** * Reusable CPU-side mesh builder backed by native direct buffers. - * - *

    Geometry is written between {@link #begin()} and {@link #end()}, then consumed through - * {@link #vertexBufferView()} and {@link #indexBufferView()} by the RHI dynamic-mesh upload path. - * The builder owns only its CPU buffers; GPU buffer allocation and lifetime are managed by the renderer.

    - * - *

    ФАСАД (исключение Фазы 8): горячий путь записи атрибутов живёт здесь; рост буферов, - * debug-валидация и слияние мешей — в package-private хелперах `uniform/mesh/` - * (MeshBufferGrowth / MeshDebugChecks / MeshMerge, тот же package).

    + * Фасад ≤200: атрибуты → MeshWrites, примитивы/views/lifecycle → MeshOps, + * рост/валидация/merge → mesh/* хелперы (тот же package). */ public final class MeshBuilder implements AutoCloseable { final VertexFormat format; final int vertexStride; final int primitiveIndicesCount; - private final double[] warped2 = new double[2]; + final double[] warped2 = new double[2]; public double alpha = 1.0; ByteBuffer vertices; long verticesPtrStart, verticesPtr; @@ -52,8 +28,8 @@ public final class MeshBuilder implements AutoCloseable { int vertexI; int indicesCount; boolean building; - private boolean worldCameraAnchored; - private double cameraX, cameraZ; + boolean worldCameraAnchored; + double cameraX, cameraZ; public MeshBuilder(RenderPipeline pipeline) { this(Objects.requireNonNull(pipeline.getVertexFormatBinding(0)), pipeline.getPrimitiveTopology()); @@ -71,377 +47,63 @@ public final class MeshBuilder implements AutoCloseable { } public void begin() { - if (RenderState.rendering3D) { - beginWorld(resolveRenderCameraPos()); - } else { - beginScreen(); - } + if (RenderState.rendering3D) beginWorld(MeshOps.resolveRenderCameraPos()); + else beginScreen(); } - /** - * Begin a 2D/screen-space mesh. No world camera anchoring is applied. - */ - public void beginScreen() { - beginInternal(false, Vec3.ZERO); - } + public void beginScreen() { beginInternal(false, Vec3.ZERO); } - /** - * Begin a world-space mesh with an explicit camera anchor. - * - *

    World vertices keep full Y, but X/Z are written camera-relative to avoid - * float precision loss. The RHI world pass then applies the matching -cameraY - * translation immediately before writing the MeshData UBO. Do not make this - * depend on ambient RenderState in world command paths: the caller must pass - * the camera that belongs to the current frame.

    - */ public void beginWorld(Vec3 cameraPos) { - if (cameraPos == null) { - throw new IllegalArgumentException("MeshBuilder.beginWorld() requires a camera position"); - } + if (cameraPos == null) throw new IllegalArgumentException("MeshBuilder.beginWorld() requires a camera position"); beginInternal(true, cameraPos); } - private void beginInternal(boolean worldCameraAnchored, Vec3 cameraPos) { - if (building) throw new IllegalStateException("Mesh.begin() called while already building."); - - if (vertices == null || indices == null) { - reserve(MeshBufferGrowth.DEFAULT_VERTEX_CAPACITY, MeshBufferGrowth.DEFAULT_INDEX_CAPACITY); - } - - verticesPtr = verticesPtrStart; - vertexI = 0; - indicesCount = 0; - - this.worldCameraAnchored = worldCameraAnchored; - this.cameraX = worldCameraAnchored ? cameraPos.x : 0.0; - this.cameraZ = worldCameraAnchored ? cameraPos.z : 0.0; - - building = true; - } - - private static Vec3 resolveRenderCameraPos() { - if (RenderState.cameraPos != null && RenderState.cameraPos != Vec3.ZERO) { - return RenderState.cameraPos; - } - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { - return mc.gameRenderer.mainCamera().position(); - } - return Vec3.ZERO; - } - - public MeshBuilder vec3(double x, double y, double z) { - MeshDebugChecks.vertexWrite(this, 12, "Position3"); - - long p = verticesPtr; - - memPutFloat(p, (float) (x - cameraX)); - memPutFloat(p + 4, (float) y); - memPutFloat(p + 8, (float) (z - cameraZ)); - - verticesPtr += 12; - return this; - } - - public MeshBuilder vec2(double x, double y) { - MeshDebugChecks.vertexWrite(this, 8, "Position2"); - - RenderWarp warp = RenderWarpStack.current(); - if (warp.active()) { - warp.map(x, y, warped2); - x = warped2[0]; - y = warped2[1]; - } - - long p = verticesPtr; - - memPutFloat(p, (float) x); - memPutFloat(p + 4, (float) y); - - verticesPtr += 8; - return this; - } - - public MeshBuilder raw2(double x, double y) { - MeshDebugChecks.vertexWrite(this, 8, "Raw2"); - - long p = verticesPtr; - - memPutFloat(p, (float) x); - memPutFloat(p + 4, (float) y); - - verticesPtr += 8; - return this; - } - - public MeshBuilder local2(double x, double y) { - MeshDebugChecks.vertexWrite(this, 16, "Local"); - - RenderWarp warp = RenderWarpStack.current(); - double invW = warp.active() ? warp.interpolationInvW(x, y) : 1.0; - - long p = verticesPtr; - - memPutFloat(p, (float) (x * invW)); - memPutFloat(p + 4, (float) (y * invW)); - memPutFloat(p + 8, (float) invW); - memPutFloat(p + 12, 0.0f); - - verticesPtr += 16; - return this; - } - - public MeshBuilder vec4(float x, float y, float z, float w) { - MeshDebugChecks.vertexWrite(this, 16, "Vec4"); - - long p = verticesPtr; - - memPutFloat(p, x); - memPutFloat(p + 4, y); - memPutFloat(p + 8, z); - memPutFloat(p + 12, w); - - verticesPtr += 16; - return this; - } - - public MeshBuilder vec4(double x, double y, double z, double w) { - return vec4((float) x, (float) y, (float) z, (float) w); - } - - /** - * Color as bytes 0..255 - */ - public MeshBuilder color(int r, int g, int b, int a) { - MeshDebugChecks.vertexWrite(this, 4, "Color"); - - long p = verticesPtr; - - memPutByte(p, (byte) r); - memPutByte(p + 1, (byte) g); - memPutByte(p + 2, (byte) b); - memPutByte(p + 3, (byte) Math.max(0, Math.min(255, (int) (a * alpha)))); - - verticesPtr += 4; - return this; - } - - /** - * Color as floats 0..1 - */ - public MeshBuilder color(float r, float g, float b, float a) { - return color( - (int) (r * 255f), - (int) (g * 255f), - (int) (b * 255f), - (int) (a * 255f) - ); - } - - /** - * Color from RenderColor (0..255). - */ - public MeshBuilder color(RenderColor color) { - return color(color.r, color.g, color.b, color.a); - } - - public int next() { - MeshDebugChecks.completedVertex(this); - return vertexI++; - } - - public void line(int i1, int i2) { - MeshDebugChecks.indexWrite(this, 2, "line"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - - indicesCount += 2; - } - - public void triangle(int i1, int i2, int i3) { - MeshDebugChecks.indexWrite(this, 3, "triangle"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - memPutInt(p + 8, i3); - - indicesCount += 3; - } - - public void quad(int i1, int i2, int i3, int i4) { - MeshDebugChecks.indexWrite(this, 6, "quad"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - memPutInt(p + 8, i3); - - memPutInt(p + 12, i3); - memPutInt(p + 16, i4); - memPutInt(p + 20, i1); - - indicesCount += 6; - } - - public void ensureLineCapacity() { - ensureCapacity(2, 2); - } - - public void ensureTriCapacity() { - ensureCapacity(3, 3); - } - - public void ensureQuadCapacity() { - ensureCapacity(4, 6); - } - - public void ensureCapacity(int addVertices, int addIndices) { - MeshBufferGrowth.ensure(this, addVertices, addIndices); - } - - /** - * Ensures that the CPU-side direct buffers can hold at least the requested amount of vertices/indices. - * - *

    This method is intentionally separate from {@link #begin()}: render code that owns a long-lived - * MeshBuilder can pre-warm it during initialization instead of paying ByteBuffer.allocateDirect on the - * render-thread hot path. If the current buffers are already large enough, this is a no-op.

    - */ - public void reserve(int vertexCount, int indexCount) { - MeshBufferGrowth.reserve(this, vertexCount, indexCount); - } - - public void end() { - validateComplete("MeshBuilder.end"); - building = false; - } - - public boolean isBuilding() { - return building; - } - - /** - * Append an already-built mesh into this builder, rebasing INT indices by the current vertex count. - * This is used by higher level batchers (UI/text) to preserve draw order without flushing per element. - */ - public void appendMesh(MeshBuilder source) { - MeshMerge.append(this, source); - } - - public ByteBuffer vertexBufferView() { - validateComplete("MeshBuilder.vertexBufferView"); - ByteBuffer copy = vertices.duplicate().order(ByteOrder.nativeOrder()); - copy.position(0); - copy.limit(getVerticesOffset()); - return copy.slice().order(ByteOrder.nativeOrder()); - } - - public ByteBuffer indexBufferView() { - validateComplete("MeshBuilder.indexBufferView"); - ByteBuffer copy = indices.duplicate().order(ByteOrder.nativeOrder()); - copy.position(0); - copy.limit(indicesCount * Integer.BYTES); - return copy.slice().order(ByteOrder.nativeOrder()); - } - - public int getVertexBytes() { - return getVerticesOffset(); - } - - public int getVertexStride() { - return vertexStride; - } - - public boolean isWorldCameraAnchored() { - return worldCameraAnchored; - } - - public double cameraAnchorX() { - return cameraX; - } - - public double cameraAnchorZ() { - return cameraZ; - } - - public int getIndexBytes() { - return indicesCount * Integer.BYTES; - } - - /** - * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production - * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. - */ - @Deprecated - public GpuBuffer getVertexBuffer() { - RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getVertexBuffer"); - return RenderSystem.getDevice().createBuffer( - () -> "lovisual-immediate-vertices", - GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, - vertexBufferView() - ); - } - - /** - * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production - * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. - */ - @Deprecated - public GpuBuffer getIndexBuffer() { - RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getIndexBuffer"); - return RenderSystem.getDevice().createBuffer( - () -> "lovisual-immediate-indices", - GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, - indexBufferView() - ); - } - - public com.mojang.blaze3d.IndexType getIndexType() { - // we write ints in indices() - return com.mojang.blaze3d.IndexType.INT; - } - - public int getIndicesCount() { - return indicesCount; - } - - public int getVertexCount() { - return vertexI; - } - - int getVerticesOffset() { - return (int) (verticesPtr - verticesPtrStart); - } - - public void validateComplete(String owner) { - MeshDebugChecks.validateComplete(this, owner); - } - - /** - * Releases CPU-side native buffers owned by this builder. The builder may be reused later; - * the next begin()/reserve() call will allocate fresh buffers. - */ - @Override - public void close() { - if (building) { - building = false; - } - MeshBufferGrowth.freeDirect(vertices); - MeshBufferGrowth.freeDirect(indices); - vertices = null; - indices = null; - verticesPtrStart = 0L; - verticesPtr = 0L; - indicesPtr = 0L; - vertexI = 0; - indicesCount = 0; - worldCameraAnchored = false; - cameraX = 0.0; - cameraZ = 0.0; - } + private void beginInternal(boolean worldCameraAnchored, Vec3 cameraPos) { MeshOps.beginInternal(this, worldCameraAnchored, cameraPos); } + + public MeshBuilder vec3(double x, double y, double z) { return MeshWrites.vec3(this, x, y, z); } + public MeshBuilder vec2(double x, double y) { return MeshWrites.vec2(this, x, y); } + public MeshBuilder raw2(double x, double y) { return MeshWrites.raw2(this, x, y); } + public MeshBuilder local2(double x, double y) { return MeshWrites.local2(this, x, y); } + public MeshBuilder vec4(float x, float y, float z, float w) { return MeshWrites.vec4(this, x, y, z, w); } + public MeshBuilder vec4(double x, double y, double z, double w) { return vec4((float) x, (float) y, (float) z, (float) w); } + + public MeshBuilder color(int r, int g, int b, int a) { return MeshWrites.color(this, r, g, b, a); } + public MeshBuilder color(float r, float g, float b, float a) { return color((int) (r * 255f), (int) (g * 255f), (int) (b * 255f), (int) (a * 255f)); } + public MeshBuilder color(RenderColor color) { return color(color.r, color.g, color.b, color.a); } + + public int next() { return MeshOps.next(this); } + public void line(int i1, int i2) { MeshOps.line(this, i1, i2); } + public void triangle(int i1, int i2, int i3) { MeshOps.triangle(this, i1, i2, i3); } + public void quad(int i1, int i2, int i3, int i4) { MeshOps.quad(this, i1, i2, i3, i4); } + + public void ensureLineCapacity() { ensureCapacity(2, 2); } + public void ensureTriCapacity() { ensureCapacity(3, 3); } + public void ensureQuadCapacity() { ensureCapacity(4, 6); } + public void ensureCapacity(int addVertices, int addIndices) { MeshBufferGrowth.ensure(this, addVertices, addIndices); } + public void reserve(int vertexCount, int indexCount) { MeshBufferGrowth.reserve(this, vertexCount, indexCount); } + + public void end() { validateComplete("MeshBuilder.end"); building = false; } + public boolean isBuilding() { return building; } + public void appendMesh(MeshBuilder source) { MeshMerge.append(this, source); } + + public ByteBuffer vertexBufferView() { return MeshOps.vertexBufferView(this); } + public ByteBuffer indexBufferView() { return MeshOps.indexBufferView(this); } + + public int getVertexBytes() { return getVerticesOffset(); } + public int getVertexStride() { return vertexStride; } + public boolean isWorldCameraAnchored() { return worldCameraAnchored; } + public double cameraAnchorX() { return cameraX; } + public double cameraAnchorZ() { return cameraZ; } + public int getIndexBytes() { return indicesCount * Integer.BYTES; } + + @Deprecated public GpuBuffer getVertexBuffer() { return MeshOps.getVertexBuffer(this); } + @Deprecated public GpuBuffer getIndexBuffer() { return MeshOps.getIndexBuffer(this); } + + public com.mojang.blaze3d.IndexType getIndexType() { return com.mojang.blaze3d.IndexType.INT; } + public int getIndicesCount() { return indicesCount; } + public int getVertexCount() { return vertexI; } + int getVerticesOffset() { return (int) (verticesPtr - verticesPtrStart); } + public void validateComplete(String owner) { MeshDebugChecks.validateComplete(this, owner); } + + @Override public void close() { MeshOps.close(this); } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java index efa2c64..b1171ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java @@ -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.render.engine.uniform; import net.minecraft.client.renderer.DynamicUniformStorage; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshOps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshOps.java new file mode 100644 index 0000000..5fbdf51 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshOps.java @@ -0,0 +1,135 @@ +package dev.loki.lovisual.render.engine.uniform; + +import com.mojang.blaze3d.buffers.GpuBuffer; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; +import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.render.engine.RenderState; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static org.lwjgl.system.MemoryUtil.memPutInt; + +/** + * Примитивы, views, lifecycle и legacy upload для MeshBuilder. + */ +final class MeshOps { + + private MeshOps() {} + + static Vec3 resolveRenderCameraPos() { + if (RenderState.cameraPos != null && RenderState.cameraPos != Vec3.ZERO) { + return RenderState.cameraPos; + } + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { + return mc.gameRenderer.mainCamera().position(); + } + return Vec3.ZERO; + } + + static void beginInternal(MeshBuilder m, boolean worldCameraAnchored, Vec3 cameraPos) { + if (m.building) throw new IllegalStateException("Mesh.begin() called while already building."); + if (m.vertices == null || m.indices == null) { + MeshBufferGrowth.reserve(m, MeshBufferGrowth.DEFAULT_VERTEX_CAPACITY, MeshBufferGrowth.DEFAULT_INDEX_CAPACITY); + } + m.verticesPtr = m.verticesPtrStart; + m.vertexI = 0; + m.indicesCount = 0; + m.worldCameraAnchored = worldCameraAnchored; + m.cameraX = worldCameraAnchored ? cameraPos.x : 0.0; + m.cameraZ = worldCameraAnchored ? cameraPos.z : 0.0; + m.building = true; + } + + static int next(MeshBuilder m) { + MeshDebugChecks.completedVertex(m); + return m.vertexI++; + } + + static void line(MeshBuilder m, int i1, int i2) { + MeshDebugChecks.indexWrite(m, 2, "line"); + long p = m.indicesPtr + m.indicesCount * 4L; + memPutInt(p, i1); + memPutInt(p + 4, i2); + m.indicesCount += 2; + } + + static void triangle(MeshBuilder m, int i1, int i2, int i3) { + MeshDebugChecks.indexWrite(m, 3, "triangle"); + long p = m.indicesPtr + m.indicesCount * 4L; + memPutInt(p, i1); + memPutInt(p + 4, i2); + memPutInt(p + 8, i3); + m.indicesCount += 3; + } + + static void quad(MeshBuilder m, int i1, int i2, int i3, int i4) { + MeshDebugChecks.indexWrite(m, 6, "quad"); + long p = m.indicesPtr + m.indicesCount * 4L; + memPutInt(p, i1); + memPutInt(p + 4, i2); + memPutInt(p + 8, i3); + memPutInt(p + 12, i3); + memPutInt(p + 16, i4); + memPutInt(p + 20, i1); + m.indicesCount += 6; + } + + static ByteBuffer vertexBufferView(MeshBuilder m) { + m.validateComplete("MeshBuilder.vertexBufferView"); + ByteBuffer copy = m.vertices.duplicate().order(ByteOrder.nativeOrder()); + copy.position(0); + copy.limit(m.getVerticesOffset()); + return copy.slice().order(ByteOrder.nativeOrder()); + } + + static ByteBuffer indexBufferView(MeshBuilder m) { + m.validateComplete("MeshBuilder.indexBufferView"); + ByteBuffer copy = m.indices.duplicate().order(ByteOrder.nativeOrder()); + copy.position(0); + copy.limit(m.indicesCount * Integer.BYTES); + return copy.slice().order(ByteOrder.nativeOrder()); + } + + @Deprecated + static GpuBuffer getVertexBuffer(MeshBuilder m) { + RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getVertexBuffer"); + return RenderSystem.getDevice().createBuffer( + () -> "lovisual-immediate-vertices", + GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, + vertexBufferView(m) + ); + } + + @Deprecated + static GpuBuffer getIndexBuffer(MeshBuilder m) { + RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getIndexBuffer"); + return RenderSystem.getDevice().createBuffer( + () -> "lovisual-immediate-indices", + GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, + indexBufferView(m) + ); + } + + static void close(MeshBuilder m) { + if (m.building) { + m.building = false; + } + MeshBufferGrowth.freeDirect(m.vertices); + MeshBufferGrowth.freeDirect(m.indices); + m.vertices = null; + m.indices = null; + m.verticesPtrStart = 0L; + m.verticesPtr = 0L; + m.indicesPtr = 0L; + m.vertexI = 0; + m.indicesCount = 0; + m.worldCameraAnchored = false; + m.cameraX = 0.0; + m.cameraZ = 0.0; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java index d0a0f0a..c60b797 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java @@ -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.render.engine.uniform.impl.esp; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java index cc2f560..f11a594 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java @@ -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.render.engine.uniform.impl.esp; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java index 5367493..4ab1100 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java @@ -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.render.engine.uniform.impl.esp; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java index 86ed3f3..9e8f72a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java @@ -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.render.engine.uniform.impl.hand; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java index cacdad1..dfc7c08 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java @@ -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.render.engine.uniform.impl.hand; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java index 17b6eb9..ce95bb5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java @@ -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.render.engine.uniform.impl.hand.effects; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java index cfe5e84..f472e3b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java @@ -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.render.engine.uniform.impl.hand.effects; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java index fee620b..5ecd44a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java @@ -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.render.engine.uniform.impl.hand.effects; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java index 0a49d74..ddbdd52 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java @@ -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.render.engine.uniform.impl.post; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java index ffc441a..301b62b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java @@ -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.render.engine.uniform.impl.post; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java index f588ef7..425adb6 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java @@ -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.render.engine.uniform.impl.post; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java index 2ef6896..568e620 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java @@ -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.render.engine.uniform.impl.post; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java index c2964f2..e4ac13c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java @@ -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.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java index 87ce38f..a0f1039 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java @@ -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.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java index 915c106..30edfa1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java @@ -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.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java index 6d3b517..48c0237 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java @@ -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.render.engine.uniform.impl.sky.skybox; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java index b9ab8ff..55e7ec1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java @@ -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.render.engine.uniform.impl.sky.sun; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java index e8f37b6..4af46df 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java @@ -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.render.engine.uniform.impl.ui; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java index 5af48c0..e92cfc1 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java @@ -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.render.engine.uniform.impl.ui; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java index 5ed49d4..4cce96b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java @@ -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.render.engine.uniform.impl.ui; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java index efcfdf0..ef898a0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java @@ -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.render.engine.uniform.impl.ui; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java index 1595f1b..0b8394a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java @@ -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.render.engine.uniform.impl.world; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java index eb64dcb..32bd91f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java @@ -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.render.engine.uniform.impl.world; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java index 87a3ad3..2b17e98 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java @@ -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.render.engine.uniform.impl.world; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java index 6ac0e57..90ac485 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java @@ -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.render.engine.uniform.impl.world.liquid; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java index 6280e68..250e1b7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java @@ -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.render.engine.uniform.impl.world.liquid; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshWrites.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshWrites.java new file mode 100644 index 0000000..37784ec --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshWrites.java @@ -0,0 +1,84 @@ +package dev.loki.lovisual.render.engine.uniform; + +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarp; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; + +import static org.lwjgl.system.MemoryUtil.memPutByte; +import static org.lwjgl.system.MemoryUtil.memPutFloat; + +/** + * Вершинные записи MeshBuilder: позиции, warp, цвета. + */ +final class MeshWrites { + + private MeshWrites() {} + + static MeshBuilder vec3(MeshBuilder m, double x, double y, double z) { + MeshDebugChecks.vertexWrite(m, 12, "Position3"); + long p = m.verticesPtr; + memPutFloat(p, (float) (x - m.cameraX)); + memPutFloat(p + 4, (float) y); + memPutFloat(p + 8, (float) (z - m.cameraZ)); + m.verticesPtr += 12; + return m; + } + + static MeshBuilder vec2(MeshBuilder m, double x, double y) { + MeshDebugChecks.vertexWrite(m, 8, "Position2"); + RenderWarp warp = RenderWarpStack.current(); + if (warp.active()) { + warp.map(x, y, m.warped2); + x = m.warped2[0]; + y = m.warped2[1]; + } + long p = m.verticesPtr; + memPutFloat(p, (float) x); + memPutFloat(p + 4, (float) y); + m.verticesPtr += 8; + return m; + } + + static MeshBuilder raw2(MeshBuilder m, double x, double y) { + MeshDebugChecks.vertexWrite(m, 8, "Raw2"); + long p = m.verticesPtr; + memPutFloat(p, (float) x); + memPutFloat(p + 4, (float) y); + m.verticesPtr += 8; + return m; + } + + static MeshBuilder local2(MeshBuilder m, double x, double y) { + MeshDebugChecks.vertexWrite(m, 16, "Local"); + RenderWarp warp = RenderWarpStack.current(); + double invW = warp.active() ? warp.interpolationInvW(x, y) : 1.0; + long p = m.verticesPtr; + memPutFloat(p, (float) (x * invW)); + memPutFloat(p + 4, (float) (y * invW)); + memPutFloat(p + 8, (float) invW); + memPutFloat(p + 12, 0.0f); + m.verticesPtr += 16; + return m; + } + + static MeshBuilder vec4(MeshBuilder m, float x, float y, float z, float w) { + MeshDebugChecks.vertexWrite(m, 16, "Vec4"); + long p = m.verticesPtr; + memPutFloat(p, x); + memPutFloat(p + 4, y); + memPutFloat(p + 8, z); + memPutFloat(p + 12, w); + m.verticesPtr += 16; + return m; + } + + static MeshBuilder color(MeshBuilder m, int r, int g, int b, int a) { + MeshDebugChecks.vertexWrite(m, 4, "Color"); + long p = m.verticesPtr; + memPutByte(p, (byte) r); + memPutByte(p + 1, (byte) g); + memPutByte(p + 2, (byte) b); + memPutByte(p + 3, (byte) Math.max(0, Math.min(255, (int) (a * m.alpha)))); + m.verticesPtr += 4; + return m; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java index ff8c99e..1e7ca87 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java @@ -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.render.engine.uniform.storage; import com.mojang.blaze3d.buffers.GpuBufferSlice; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java index 7805e6d..709e63a 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java @@ -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.render.engine.vertex; import com.mojang.blaze3d.GpuFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java index f510d49..41657d4 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java @@ -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.render.engine.vertex; import com.mojang.blaze3d.vertex.VertexFormat; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java index d790bac..92ec352 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java @@ -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.render.engine.visuals; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java index 8518850..e257fd5 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java @@ -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.render.engine.visuals; import com.mojang.blaze3d.textures.GpuTextureView; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java index ff67d31..e0a3bbd 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java @@ -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.render.engine.visuals; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java index fcc6df8..4f2f013 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java @@ -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.render.engine.visuals; import com.mojang.blaze3d.ProjectionType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java index f2efe23..7bc2ceb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java @@ -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.render.engine.world; import com.mojang.blaze3d.pipeline.RenderPipeline; @@ -13,6 +6,7 @@ import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.core.frame.RenderFrameContext; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import java.util.ArrayList; @@ -46,13 +40,13 @@ public final class WorldBatcher { public MeshBuilder batch(RenderPipeline pipeline, Renderer3D.DepthMode depthMode, float lineWidth, - Renderer3D.BatchBindings bindings) { + Renderer3DBatchBindings bindings) { if (pipeline == null) return null; int lineBits = isLineMode(pipeline) ? Float.floatToIntBits(lineWidth > 0.0f ? lineWidth : 1.0f) : 0; - Renderer3D.BatchBindings resolvedBindings = bindings != null ? bindings : Renderer3D.BatchBindings.none(); + Renderer3DBatchBindings resolvedBindings = bindings != null ? bindings : Renderer3DBatchBindings.none(); if (lastCommand != null && lastCommand.mesh().isBuilding()) { WorldDrawCommand candidate = new WorldDrawCommand(pipeline, depthMode, lineBits, resolvedBindings, lastCommand.mesh()); diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java index 0d036dc..579db65 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java @@ -1,14 +1,8 @@ -/* - * 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.render.engine.world; import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import java.util.List; @@ -26,7 +20,7 @@ public final class WorldCommandBuffer { public MeshBuilder batch(RenderPipeline pipeline, Renderer3D.DepthMode depthMode, float lineWidth, - Renderer3D.BatchBindings bindings) { + Renderer3DBatchBindings bindings) { return batcher.batch(pipeline, depthMode, lineWidth, bindings); } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java index ad8e26a..5ec908e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java @@ -1,14 +1,8 @@ -/* - * 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.render.engine.world; import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.renderer.Renderer3DBatchBindings; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.engine.rhi.command.RhiDrawCommand; @@ -19,16 +13,16 @@ import dev.loki.lovisual.render.engine.rhi.command.RhiDrawCommand; * WorldPassCompiler owns the transition from this command to RhiDrawCommand and dynamic mesh upload. */ public record WorldDrawCommand(RenderPipeline pipeline, Renderer3D.DepthMode depthMode, int lineWidthBits, - Renderer3D.BatchBindings bindings, MeshBuilder mesh) { + Renderer3DBatchBindings bindings, MeshBuilder mesh) { public WorldDrawCommand(RenderPipeline pipeline, Renderer3D.DepthMode depthMode, int lineWidthBits, - Renderer3D.BatchBindings bindings, + Renderer3DBatchBindings bindings, MeshBuilder mesh) { this.pipeline = pipeline; this.depthMode = depthMode != null ? depthMode : Renderer3D.DepthMode.MAIN; this.lineWidthBits = lineWidthBits; - this.bindings = bindings != null ? bindings : Renderer3D.BatchBindings.none(); + this.bindings = bindings != null ? bindings : Renderer3DBatchBindings.none(); this.mesh = mesh; } diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java index 1f3c676..8520c38 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java @@ -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.render.engine.world; import dev.loki.lovisual.render.engine.world.mesh.WorldWideLineMeshCompiler; import dev.loki.lovisual.render.engine.world.stats.WorldRenderStats; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java index 45dd96b..01a62e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java @@ -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.render.engine.world.mesh; import dev.loki.lovisual.render.engine.world.WorldDrawCommand; diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java index 5f8f919..9d0dfc7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java @@ -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.render.engine.world.stats; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java index 150c746..a01492b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java @@ -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.render.engine.world.stats; public record WorldRenderStatsSnapshot(long frameId, diff --git a/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java index c68bf03..2197c98 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java @@ -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.render.fade; import java.util.ArrayDeque; diff --git a/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java index e4116b1..4f71de2 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java @@ -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.render.fade; public interface ViewObstructionFadeState { diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java index 15c8a6a..802bfe7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java @@ -1,519 +1,146 @@ -/* - * 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.render.helpers.clip; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; -import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiChamferRadii; import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiRect; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerKind; import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerMode; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerRadii; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerSpec; import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape; import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShapeKind; import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipBackend; -import dev.loki.lovisual.util.logging.DebugLog; import java.util.ArrayDeque; import java.util.Deque; -import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; -import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiChamferRadii; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerKind; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerMode; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerRadii; -import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerSpec; -import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiRect; -import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape; -import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShapeKind; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; /** - * Higher-level clipping stack. - * - *

    Rectangles are routed to ScissorFunction. Non-rect primitives use the active RHI - * shape-clip backend. The GL backend currently implements this with stencil.

    + * Higher-level clipping stack: rectangles route to {@link ScissorFunction}; + * non-rect primitives use the active RHI shape-clip backend (GL → stencil). + * Geometry rasterization lives in {@link ClipGeometry}, diagnostics in {@link ClipDebug}. */ public enum ClipFunction { ; private static final int MAX_SHAPE_DEPTH = 250; - private static final int MASK_COLOR = 0xFFFFFFFF; private static final Deque STACK = new ArrayDeque<>(); - private static final double[] POINTS = new double[512]; - private static boolean warnedShapeUnsupported; - private static boolean warnedShapeDepth; - public static boolean pushRaw(float x, float y, float width, float height) { - return pushRect(x, y, width, height); - } + // ---- Public push API -------------------------------------------------------------------- + + public static boolean pushRaw(float x, float y, float width, float height) { return pushRect(x, y, width, height); } public static boolean pushRect(double x, double y, double width, double height) { boolean pushed = ScissorFunction.pushRaw((float) x, (float) y, (float) width, (float) height); - if (pushed) { - STACK.push(Layer.rect()); - } + if (pushed) STACK.push(Layer.rect()); return pushed; } - public static boolean pushRoundedRect(double x, double y, double width, double height, double radius) { - return push(UiShape.roundedRect(x, y, width, height, radius)); + public static boolean pushRoundedRect(double x, double y, double w, double h, double r) { return push(UiShape.roundedRect(x, y, w, h, r)); } + + public static boolean pushRoundedRect(double x, double y, double w, double h, double tl, double tr, double br, double bl) { + return push(UiShape.roundedRect(x, y, w, h, tl, tr, br, bl)); } - public static boolean pushRoundedRect(double x, double y, double width, double height, - double topLeft, double topRight, double bottomRight, double bottomLeft) { - return push(UiShape.roundedRect(x, y, width, height, topLeft, topRight, bottomRight, bottomLeft)); - } - - public static boolean pushChamferedRect(double x, double y, double width, double height, double chamfer) { - return push(UiShape.chamferedRect(x, y, width, height, chamfer, chamfer, chamfer, chamfer)); - } - - public static boolean pushCircle(double cx, double cy, double radius) { - return push(UiShape.circle(cx, cy, radius)); - } + public static boolean pushChamferedRect(double x, double y, double w, double h, double c) { return push(UiShape.chamferedRect(x, y, w, h, c, c, c, c)); } + public static boolean pushCircle(double cx, double cy, double r) { return push(UiShape.circle(cx, cy, r)); } + /** Push an arbitrary shape clip; plain rects fall through to scissor, others go through the stencil backend. */ public static boolean push(UiShape shape) { if (shape == null) return false; UiRect bounds = shape.bounds(); if (bounds == null || bounds.empty()) return false; - - if (isPlainRect(shape)) { - return pushRect(bounds.x(), bounds.y(), bounds.width(), bounds.height()); - } - - if (activeShapeDepth() >= MAX_SHAPE_DEPTH) { - warnShapeDepth(); - return false; - } - + if (isPlainRect(shape)) return pushRect(bounds.x(), bounds.y(), bounds.width(), bounds.height()); + if (activeShapeDepth() >= MAX_SHAPE_DEPTH) { ClipDebug.warnShapeDepth(MAX_SHAPE_DEPTH); return false; } Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - // A clipped liquid-glass consumer must refresh the shared blur before this - // shape installs its bounds scissor/stencil. The refresh remains on the normal - // shared Kawase chain and therefore never needs a clip-state bypass. Renderer2D.prepareLiquidGlassBlurBeforeShapeClipIfRequested(); ShapeClipBackend clip = clipBackend(); - if (!clip.supported()) { - warnShapeUnsupported(shape, clip, "backend unsupported"); - return false; - } - - boolean scissor = ScissorFunction.pushRaw(bounds.x(), bounds.y(), bounds.width(), bounds.height()); - if (!scissor) return false; - - int parent = currentShapeReference(); - int reference = parent + 1; + if (!clip.supported()) { ClipDebug.warnShapeUnsupported(shape, clip, "backend unsupported", activeShapeDepth()); return false; } + if (!ScissorFunction.pushRaw(bounds.x(), bounds.y(), bounds.width(), bounds.height())) return false; + int parent = currentShapeReference(), reference = parent + 1; boolean clear = activeShapeDepth() == 0; - String attachmentReason = "ClipFunction.push kind=" + shape.kind() + " bounds=" + bounds; - - if (Renderer2D.isDeferredExtractRecording()) { - Renderer2D.deferRenderThreadAction(() -> { - ShapeClipBackend renderClip = clipBackend(); - renderClip.requireRenderPassAttachment(attachmentReason); - if (clear) { - renderClip.requestClear("first shape layer"); - } - renderClip.beginWrite(parent, reference); - renderMaskShape(shape); - renderClip.beginTest(reference); - renderStencilDebugProbe(shape, reference, false); - }); - } else { - clip.requireRenderPassAttachment(attachmentReason); - if (clear) { - clip.requestClear("first shape layer"); - } - clip.beginWrite(parent, reference); - renderMaskShape(shape); - clip.beginTest(reference); - renderStencilDebugProbe(shape, reference, false); - } - + if (Renderer2D.isDeferredExtractRecording()) Renderer2D.deferRenderThreadAction(() -> doPushClipOps(clip, shape, parent, reference, clear)); + else doPushClipOps(clip, shape, parent, reference, clear); STACK.push(Layer.shape(shape, reference, parent)); return true; } - public static Scope scope(UiShape shape) { - boolean pushed = push(shape); - return new Scope(pushed); + private static void doPushClipOps(ShapeClipBackend clip, UiShape shape, int parent, int reference, boolean clear) { + clip.requireRenderPassAttachment("ClipFunction.push kind=" + shape.kind() + " bounds=" + shape.bounds()); + if (clear) clip.requestClear("first shape layer"); + clip.beginWrite(parent, reference); + ClipGeometry.renderMaskShape(shape); + clip.beginTest(reference); + ClipDebug.renderStencilDebugProbe(shape, reference, false); } - public static Scope rectScope(double x, double y, double width, double height) { - boolean pushed = pushRect(x, y, width, height); - return new Scope(pushed); - } + // ---- Pop ------------------------------------------------------------------------------------ public static void pop() { if (STACK.isEmpty()) return; Layer layer = STACK.pop(); - if (!layer.shape) { - ScissorFunction.pop(); - applyPreviousShapeTest(); - return; - } - + if (!layer.shape) { ScissorFunction.pop(); applyPreviousShapeTest(); return; } Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - int previousReference = currentShapeReference(); + int prevRef = currentShapeReference(); if (Renderer2D.isDeferredExtractRecording()) { - Renderer2D.deferRenderThreadAction(() -> { - renderStencilDebugProbe(layer.shapeValue, layer.reference, true); - Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - ShapeClipBackend renderClip = clipBackend(); - renderClip.beginRestore(layer.reference, layer.parentReference); - renderMaskShape(layer.shapeValue); - if (previousReference > 0) { - renderClip.beginTest(previousReference); - } else { - renderClip.disable(); - } - }); - ScissorFunction.pop(); - return; + Renderer2D.deferRenderThreadAction(() -> doPopClipOps(layer, prevRef)); + ScissorFunction.pop(); return; } - - ShapeClipBackend clip = clipBackend(); - renderStencilDebugProbe(layer.shapeValue, layer.reference, true); - Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - clip.beginRestore(layer.reference, layer.parentReference); - renderMaskShape(layer.shapeValue); + doPopClipOps(layer, prevRef); ScissorFunction.pop(); applyPreviousShapeTest(); } - public static int depth() { - return STACK.size(); + private static void doPopClipOps(Layer layer, int prevRef) { + ClipDebug.renderStencilDebugProbe(layer.shapeValue, layer.reference, true); + Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); + ShapeClipBackend clip = clipBackend(); + clip.beginRestore(layer.reference, layer.parentReference); + ClipGeometry.renderMaskShape(layer.shapeValue); + if (prevRef > 0) clip.beginTest(prevRef); else clip.disable(); } - public static boolean isShapeClipActive() { - return currentShapeReference() > 0; - } + // ---- Scopes / queries --------------------------------------------------------------------- - /** - * @deprecated use {@link #isShapeClipActive()} — the backend is no longer required to be stencil. - */ - @Deprecated - public static boolean isStencilActive() { - return isShapeClipActive(); - } + public static Scope scope(UiShape shape) { return new Scope(push(shape)); } + public static Scope rectScope(double x, double y, double w, double h) { return new Scope(pushRect(x, y, w, h)); } + public static int depth() { return STACK.size(); } + public static boolean isShapeClipActive() { return currentShapeReference() > 0; } - private static void warnShapeUnsupported(UiShape shape, ShapeClipBackend backend, String reason) { - if (warnedShapeUnsupported) return; - warnedShapeUnsupported = true; - UiRect bounds = shape.bounds(); - DebugLog.warnOnChange( - "clipfunction.shape.unsupported", - reason + "|" + shape.kind() + "|" + bounds + "|" + activeShapeDepth() + "|" + backend, - "ClipFunction: shape push rejected: %s. kind=%s bounds=%s depth=%d backend=%s", - reason, - shape.kind(), - bounds, - activeShapeDepth(), - backend - ); - } + /** @deprecated use {@link #isShapeClipActive()} — the backend is no longer required to be stencil. */ + @Deprecated public static boolean isStencilActive() { return isShapeClipActive(); } - private static void warnShapeDepth() { - if (warnedShapeDepth) return; - warnedShapeDepth = true; - DebugLog.warn("ClipFunction: shape clip stack exceeded %d layers. Clip push rejected.", MAX_SHAPE_DEPTH); - } + // ---- Internals ---------------------------------------------------------------------------- private static void applyPreviousShapeTest() { - int reference = currentShapeReference(); - if (reference > 0) { - clipBackend().beginTest(reference); - } else { - clipBackend().disable(); - } + int ref = currentShapeReference(); + if (ref > 0) clipBackend().beginTest(ref); else clipBackend().disable(); } - private static void renderStencilDebugProbe(UiShape shape, int reference, boolean overlay) { - if (!DebugLog.isStencilDebugEnabled()) return; - UiRect b = shape.bounds(); - if (b == null || b.empty()) return; - - int fill = overlay ? 0x5AFF2A2A : 0x2600FF7A; - int border = overlay ? 0xFFFFE600 : 0xC000FF7A; - boolean auto = !Renderer2D.isBatching(); - if (auto) Renderer2D.COLOR.begin(); - Renderer2D.COLOR.quad(b.x(), b.y(), b.width(), b.height(), fill); - Renderer2D.COLOR.boxLines(b.x(), b.y(), b.width(), b.height(), border); - if (auto) { - Renderer2D.COLOR.render(); - } else { - Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - } - DebugLog.stencilOnChange( - "clipfunction.debug.probe", - (overlay ? "overlay|" : "push|") + shape.kind() + "|" + reference + "|" + b, - "ClipFunction debug probe rendered through stencil ref=%d kind=%s overlay=%s bounds=%s", - reference, - shape.kind(), - overlay, - b - ); - } - - private static ShapeClipBackend clipBackend() { - return LoVisualRenderSystem.rhi().shapeClip(); - } + private static ShapeClipBackend clipBackend() { return LoVisualRenderSystem.rhi().shapeClip(); } private static int currentShapeReference() { - for (Layer layer : STACK) { - if (layer.shape) return layer.reference; - } + for (Layer l : STACK) if (l.shape) return l.reference; return 0; } private static int activeShapeDepth() { - int depth = 0; - for (Layer layer : STACK) { - if (layer.shape) depth++; - } - return depth; + int d = 0; + for (Layer l : STACK) if (l.shape) d++; + return d; } private static boolean isPlainRect(UiShape shape) { return shape.kind() == UiShapeKind.RECT && shape.cornerMode() == UiCornerMode.NONE; } - private static void renderMaskShape(UiShape shape) { - boolean auto = !Renderer2D.isBatching(); - if (auto) Renderer2D.COLOR.begin(); - emitMaskShape(Renderer2D.COLOR, shape); - if (auto) { - Renderer2D.COLOR.render(); - } else { - Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - } - } - - private static void emitMaskShape(Renderer2D renderer, UiShape shape) { - UiRect b = shape.bounds(); - switch (shape.kind()) { - case RECT -> emitRectMask(renderer, shape, b); - case CIRCLE -> emitCircleMask(renderer, b, shape.radius()); - case POLYGON -> emitPolygonMask(renderer, shape); - case FLEXIBLE_BOX -> emitBoxMask(renderer, shape.box(), b); - default -> renderer.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); - } - } - - private static void emitRectMask(Renderer2D renderer, UiShape shape, UiRect b) { - if (shape.cornerMode() == UiCornerMode.ROUNDED) { - int count = buildRoundedRectPolygon(b, shape.roundedRadii()); - renderer.polygon(POINTS, count, MASK_COLOR); - return; - } - if (shape.cornerMode() == UiCornerMode.CHAMFERED) { - int count = buildChamferedRectPolygon(b, shape.chamferRadii()); - renderer.polygon(POINTS, count, MASK_COLOR); - return; - } - renderer.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); - } - - private static void emitCircleMask(Renderer2D renderer, UiRect b, float radius) { - float cx = b.x() + b.width() * 0.5f; - float cy = b.y() + b.height() * 0.5f; - float r = radius > 0.0001f ? radius : Math.min(b.width(), b.height()) * 0.5f; - int count = buildEllipsePolygon(cx, cy, r, r, 40); - renderer.polygon(POINTS, count, MASK_COLOR); - } - - private static void emitPolygonMask(Renderer2D renderer, UiShape shape) { - double[] points = shape.points(); - int count = Math.min(shape.pointCount(), points != null ? points.length / 2 : 0); - if (!shape.closed() || count < 3) { - UiRect b = shape.bounds(); - renderer.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); - return; - } - renderer.polygon(points, count, MASK_COLOR); - } - - private static void emitBoxMask(Renderer2D renderer, UiBoxShape box, UiRect fallbackBounds) { - if (box == null || !box.hasOnlyStraightEdges()) { - renderer.quad(fallbackBounds.x(), fallbackBounds.y(), fallbackBounds.width(), fallbackBounds.height(), MASK_COLOR); - return; - } - int count = buildBoxPolygon(box); - if (count >= 3) { - renderer.polygon(POINTS, count, MASK_COLOR); - } else { - renderer.quad(fallbackBounds.x(), fallbackBounds.y(), fallbackBounds.width(), fallbackBounds.height(), MASK_COLOR); - } - } - - private static int buildRoundedRectPolygon(UiRect b, UiCornerRadii radii) { - resetPoints(); - float x = b.x(); - float y = b.y(); - float w = b.width(); - float h = b.height(); - float maxR = Math.max(0.0f, Math.min(w, h) * 0.5f); - float tl = Math.min(radii.topLeft(), maxR); - float tr = Math.min(radii.topRight(), maxR); - float br = Math.min(radii.bottomRight(), maxR); - float bl = Math.min(radii.bottomLeft(), maxR); - - int n = 0; - n = appendArc(n, x + tl, y + tl, tl, tl, 180.0, 270.0); - n = appendArc(n, x + w - tr, y + tr, tr, tr, 270.0, 360.0); - n = appendArc(n, x + w - br, y + h - br, br, br, 0.0, 90.0); - n = appendArc(n, x + bl, y + h - bl, bl, bl, 90.0, 180.0); - return n; - } - - private static int buildChamferedRectPolygon(UiRect b, UiChamferRadii radii) { - resetPoints(); - float x = b.x(); - float y = b.y(); - float w = b.width(); - float h = b.height(); - float tlX = clamp(radii.topLeftX(), 0, w * 0.5f); - float tlY = clamp(radii.topLeftY(), 0, h * 0.5f); - float trX = clamp(radii.topRightX(), 0, w * 0.5f); - float trY = clamp(radii.topRightY(), 0, h * 0.5f); - float brX = clamp(radii.bottomRightX(), 0, w * 0.5f); - float brY = clamp(radii.bottomRightY(), 0, h * 0.5f); - float blX = clamp(radii.bottomLeftX(), 0, w * 0.5f); - float blY = clamp(radii.bottomLeftY(), 0, h * 0.5f); - int n = 0; - n = addPoint(n, x + tlX, y); - n = addPoint(n, x + w - trX, y); - n = addPoint(n, x + w, y + trY); - n = addPoint(n, x + w, y + h - brY); - n = addPoint(n, x + w - brX, y + h); - n = addPoint(n, x + blX, y + h); - n = addPoint(n, x, y + h - blY); - n = addPoint(n, x, y + tlY); - return n; - } - - private static int buildEllipsePolygon(double cx, double cy, double rx, double ry, int requestedSegments) { - resetPoints(); - int segments = Math.max(12, Math.min(96, requestedSegments)); - int n = 0; - for (int i = 0; i < segments; i++) { - double angle = Math.PI * 2.0 * i / segments; - n = addPoint(n, cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry); - } - return n; - } - - private static int buildBoxPolygon(UiBoxShape box) { - resetPoints(); - UiRect b = box.bounds(); - float x = b.x(); - float y = b.y(); - float w = b.width(); - float h = b.height(); - int n = 0; - n = appendCorner(n, x, y, w, h, box.topLeft(), 0); - n = appendCorner(n, x, y, w, h, box.topRight(), 1); - n = appendCorner(n, x, y, w, h, box.bottomRight(), 2); - n = appendCorner(n, x, y, w, h, box.bottomLeft(), 3); - return n; - } - - private static int appendCorner(int n, float x, float y, float w, float h, UiCornerSpec corner, int index) { - UiCornerSpec c = corner != null ? corner : UiCornerSpec.SQUARE; - if (c.kind() == UiCornerKind.ROUNDED || c.kind() == UiCornerKind.CONCAVE_ROUNDED) { - float rx = Math.min(c.radiusX(), w * 0.5f); - float ry = Math.min(c.radiusY(), h * 0.5f); - return switch (index) { - case 0 -> appendArc(n, x + rx, y + ry, rx, ry, 180.0, 270.0); - case 1 -> appendArc(n, x + w - rx, y + ry, rx, ry, 270.0, 360.0); - case 2 -> appendArc(n, x + w - rx, y + h - ry, rx, ry, 0.0, 90.0); - default -> appendArc(n, x + rx, y + h - ry, rx, ry, 90.0, 180.0); - }; - } - if (c.kind() == UiCornerKind.CHAMFERED || c.kind() == UiCornerKind.NOTCHED) { - float cx = Math.min(c.cutX(), w * 0.5f); - float cy = Math.min(c.cutY(), h * 0.5f); - return switch (index) { - case 0 -> addChamfer(n, x + cx, y, x, y + cy); - case 1 -> addChamfer(n, x + w - cx, y, x + w, y + cy); - case 2 -> addChamfer(n, x + w, y + h - cy, x + w - cx, y + h); - default -> addChamfer(n, x + cx, y + h, x, y + h - cy); - }; - } - return switch (index) { - case 0 -> addPoint(n, x, y); - case 1 -> addPoint(n, x + w, y); - case 2 -> addPoint(n, x + w, y + h); - default -> addPoint(n, x, y + h); - }; - } - - private static int addChamfer(int n, double x0, double y0, double x1, double y1) { - n = addPoint(n, x0, y0); - return addPoint(n, x1, y1); - } - - private static int appendArc(int n, double cx, double cy, double rx, double ry, double startDeg, double endDeg) { - if (rx <= 0.0001 || ry <= 0.0001) { - return addPoint(n, cx, cy); - } - int segments = Math.max(3, Math.min(12, (int) Math.ceil(Math.max(rx, ry) / 4.0))); - for (int i = 0; i <= segments; i++) { - double t = i / (double) segments; - double angle = Math.toRadians(startDeg + (endDeg - startDeg) * t); - n = addPoint(n, cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry); - } - return n; - } - - private static int addPoint(int n, double x, double y) { - if (n <= 0 || POINTS[(n - 1) * 2] != x || POINTS[(n - 1) * 2 + 1] != y) { - if ((n + 1) * 2 > POINTS.length) return n; - POINTS[n * 2] = x; - POINTS[n * 2 + 1] = y; - return n + 1; - } - return n; - } - - private static void resetPoints() { - // The active count is returned by builders; old values are ignored. - } - - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); - } + // ---- Layer / Scope ----------------------------------------------------------------------- private record Layer(boolean shape, UiShape shapeValue, int reference, int parentReference) { - static Layer rect() { - return new Layer(false, null, 0, 0); - } - - static Layer shape(UiShape shape, int reference, int parentReference) { - return new Layer(true, shape, reference, parentReference); - } + static Layer rect() { return new Layer(false, null, 0, 0); } + static Layer shape(UiShape s, int ref, int parent) { return new Layer(true, s, ref, parent); } } + /** Auto-closeable scope; pops the clip on {@link #close()} if the push succeeded. */ public static final class Scope implements AutoCloseable { private boolean active; - - private Scope(boolean active) { - this.active = active; - } - - public boolean active() { - return active; - } - - @Override - public void close() { - if (!active) return; - active = false; - ClipFunction.pop(); - } + private Scope(boolean active) { this.active = active; } + public boolean active() { return active; } + @Override public void close() { if (!active) return; active = false; ClipFunction.pop(); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java index 56e29b7..b0e2714 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java @@ -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.render.helpers.clip; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java index 68696df..63f4634 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java @@ -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.render.helpers.clip; import net.minecraft.client.Camera; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java index 9d74986..ddc77ee 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java @@ -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.render.helpers.clip; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipDebug.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipDebug.java new file mode 100644 index 0000000..f5dec47 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipDebug.java @@ -0,0 +1,58 @@ +package dev.loki.lovisual.render.helpers.clip; + +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiRect; +import dev.loki.lovisual.render.engine.rhi.clip.ShapeClipBackend; +import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape; +import dev.loki.lovisual.util.logging.DebugLog; + +/** + * Diagnostic helpers for {@link ClipFunction}: one-shot warnings when the shape-clip + * backend is unsupported or the stack overflows, and an optional colored-stencil + * debug probe that draws a translucent overlay over the clip bounds. + * Sibling folder sharing the parent's package. + */ +final class ClipDebug { + + private static boolean warnedShapeUnsupported; + private static boolean warnedShapeDepth; + + private ClipDebug() { } + + /** Log-once warning when the active RHI backend rejects shape clips. */ + static void warnShapeUnsupported(UiShape shape, ShapeClipBackend backend, String reason, int depth) { + if (warnedShapeUnsupported) return; + warnedShapeUnsupported = true; + UiRect b = shape.bounds(); + DebugLog.warnOnChange("clipfunction.shape.unsupported", + reason + "|" + shape.kind() + "|" + b + "|" + depth + "|" + backend, + "ClipFunction: shape push rejected: %s. kind=%s bounds=%s depth=%d backend=%s", + reason, shape.kind(), b, depth, backend); + } + + /** Log-once warning when the clip shape stack exceeds the maximum depth. */ + static void warnShapeDepth(int maxDepth) { + if (warnedShapeDepth) return; + warnedShapeDepth = true; + DebugLog.warn("ClipFunction: shape clip stack exceeded %d layers. Clip push rejected.", maxDepth); + } + + /** Draws a translucent green/red quad + border over the shape bounds when stencil debugging is enabled. */ + static void renderStencilDebugProbe(UiShape shape, int reference, boolean overlay) { + if (!DebugLog.isStencilDebugEnabled()) return; + UiRect b = shape.bounds(); + if (b == null || b.empty()) return; + int fill = overlay ? 0x5AFF2A2A : 0x2600FF7A; + int border = overlay ? 0xFFFFE600 : 0xC000FF7A; + boolean auto = !Renderer2D.isBatching(); + if (auto) Renderer2D.COLOR.begin(); + Renderer2D.COLOR.quad(b.x(), b.y(), b.width(), b.height(), fill); + Renderer2D.COLOR.boxLines(b.x(), b.y(), b.width(), b.height(), border); + if (auto) Renderer2D.COLOR.render(); + else Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); + DebugLog.stencilOnChange("clipfunction.debug.probe", + (overlay ? "overlay|" : "push|") + shape.kind() + "|" + reference + "|" + b, + "ClipFunction debug probe rendered through stencil ref=%d kind=%s overlay=%s bounds=%s", + reference, shape.kind(), overlay, b); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipGeometry.java new file mode 100644 index 0000000..6b8471b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/impl/ClipGeometry.java @@ -0,0 +1,170 @@ +package dev.loki.lovisual.render.helpers.clip; + +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiBoxShape; +import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiChamferRadii; +import dev.loki.lovisual.render.engine.renderer.ui.draw.box.UiRect; +import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerKind; +import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerMode; +import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerRadii; +import dev.loki.lovisual.render.engine.renderer.ui.draw.corner.UiCornerSpec; +import dev.loki.lovisual.render.engine.renderer.ui.draw.shape.UiShape; + +/** + * Geometry rasterization for shape-clip masks. Builds vertex arrays from + * rounded / chamfered rects, ellipses, polygons and flexible box shapes, + * then emits them through {@link Renderer2D#COLOR}. Also drives the + * batched / auto-flush sequence required by the render pipeline when + * painting a mask into the stencil buffer. + * Sibling folder sharing the parent's package. + */ +final class ClipGeometry { + + static final int MASK_COLOR = 0xFFFFFFFF; + private static final double[] POINTS = new double[512]; + + private ClipGeometry() { } + + /** Rasterize {@code shape} into the stencil write buffer; handles batched and auto-flush modes. */ + static void renderMaskShape(UiShape shape) { + boolean auto = !Renderer2D.isBatching(); + if (auto) Renderer2D.COLOR.begin(); + emitMaskShape(Renderer2D.COLOR, shape); + if (auto) Renderer2D.COLOR.render(); + else Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); + } + + private static void emitMaskShape(Renderer2D r, UiShape shape) { + UiRect b = shape.bounds(); + switch (shape.kind()) { + case RECT -> emitRectMask(r, shape, b); + case CIRCLE -> emitCircleMask(r, b, shape.radius()); + case POLYGON -> emitPolygonMask(r, shape); + case FLEXIBLE_BOX -> emitBoxMask(r, shape.box(), b); + default -> r.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); + } + } + + private static void emitRectMask(Renderer2D r, UiShape shape, UiRect b) { + if (shape.cornerMode() == UiCornerMode.ROUNDED) { r.polygon(POINTS, buildRoundedRectPolygon(b, shape.roundedRadii()), MASK_COLOR); return; } + if (shape.cornerMode() == UiCornerMode.CHAMFERED) { r.polygon(POINTS, buildChamferedRectPolygon(b, shape.chamferRadii()), MASK_COLOR); return; } + r.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); + } + + private static void emitCircleMask(Renderer2D r, UiRect b, float radius) { + float cx = b.x() + b.width() * 0.5f, cy = b.y() + b.height() * 0.5f; + float rad = radius > 0.0001f ? radius : Math.min(b.width(), b.height()) * 0.5f; + r.polygon(POINTS, buildEllipsePolygon(cx, cy, rad, rad, 40), MASK_COLOR); + } + + private static void emitPolygonMask(Renderer2D r, UiShape shape) { + double[] pts = shape.points(); + int count = Math.min(shape.pointCount(), pts != null ? pts.length / 2 : 0); + if (!shape.closed() || count < 3) { UiRect b = shape.bounds(); r.quad(b.x(), b.y(), b.width(), b.height(), MASK_COLOR); return; } + r.polygon(pts, count, MASK_COLOR); + } + + private static void emitBoxMask(Renderer2D r, UiBoxShape box, UiRect fb) { + if (box == null || !box.hasOnlyStraightEdges()) { r.quad(fb.x(), fb.y(), fb.width(), fb.height(), MASK_COLOR); return; } + int count = buildBoxPolygon(box); + if (count >= 3) r.polygon(POINTS, count, MASK_COLOR); + else r.quad(fb.x(), fb.y(), fb.width(), fb.height(), MASK_COLOR); + } + + // ---- Polygon builders -------------------------------------------------------------------- + + private static int buildRoundedRectPolygon(UiRect b, UiCornerRadii radii) { + resetPoints(); + float x = b.x(), y = b.y(), w = b.width(), h = b.height(); + float maxR = Math.max(0f, Math.min(w, h) * 0.5f); + float tl = Math.min(radii.topLeft(), maxR), tr = Math.min(radii.topRight(), maxR); + float br = Math.min(radii.bottomRight(), maxR), bl = Math.min(radii.bottomLeft(), maxR); + int n = 0; + n = appendArc(n, x + tl, y + tl, tl, tl, 180.0, 270.0); + n = appendArc(n, x + w - tr, y + tr, tr, tr, 270.0, 360.0); + n = appendArc(n, x + w - br, y + h - br, br, br, 0.0, 90.0); + n = appendArc(n, x + bl, y + h - bl, bl, bl, 90.0, 180.0); + return n; + } + + private static int buildChamferedRectPolygon(UiRect b, UiChamferRadii radii) { + resetPoints(); + float x = b.x(), y = b.y(), w = b.width(), h = b.height(); + float tlX = clamp(radii.topLeftX(), 0, w * .5f), tlY = clamp(radii.topLeftY(), 0, h * .5f); + float trX = clamp(radii.topRightX(), 0, w * .5f), trY = clamp(radii.topRightY(), 0, h * .5f); + float brX = clamp(radii.bottomRightX(), 0, w * .5f), brY = clamp(radii.bottomRightY(), 0, h * .5f); + float blX = clamp(radii.bottomLeftX(), 0, w * .5f), blY = clamp(radii.bottomLeftY(), 0, h * .5f); + int n = 0; + n = addPoint(n, x + tlX, y); n = addPoint(n, x + w - trX, y); + n = addPoint(n, x + w, y + trY); n = addPoint(n, x + w, y + h - brY); + n = addPoint(n, x + w - brX, y + h); n = addPoint(n, x + blX, y + h); + n = addPoint(n, x, y + h - blY); n = addPoint(n, x, y + tlY); + return n; + } + + private static int buildEllipsePolygon(double cx, double cy, double rx, double ry, int requestedSegments) { + resetPoints(); + int segments = Math.max(12, Math.min(96, requestedSegments)), n = 0; + for (int i = 0; i < segments; i++) { double a = Math.PI * 2.0 * i / segments; n = addPoint(n, cx + Math.cos(a) * rx, cy + Math.sin(a) * ry); } + return n; + } + + private static int buildBoxPolygon(UiBoxShape box) { + resetPoints(); + UiRect b = box.bounds(); + float x = b.x(), y = b.y(), w = b.width(), h = b.height(); + int n = 0; + n = appendCorner(n, x, y, w, h, box.topLeft(), 0); + n = appendCorner(n, x, y, w, h, box.topRight(), 1); + n = appendCorner(n, x, y, w, h, box.bottomRight(), 2); + n = appendCorner(n, x, y, w, h, box.bottomLeft(), 3); + return n; + } + + private static int appendCorner(int n, float x, float y, float w, float h, UiCornerSpec corner, int index) { + UiCornerSpec c = corner != null ? corner : UiCornerSpec.SQUARE; + if (c.kind() == UiCornerKind.ROUNDED || c.kind() == UiCornerKind.CONCAVE_ROUNDED) { + float rx = Math.min(c.radiusX(), w * 0.5f), ry = Math.min(c.radiusY(), h * 0.5f); + return switch (index) { + case 0 -> appendArc(n, x + rx, y + ry, rx, ry, 180.0, 270.0); + case 1 -> appendArc(n, x + w - rx, y + ry, rx, ry, 270.0, 360.0); + case 2 -> appendArc(n, x + w - rx, y + h - ry, rx, ry, 0.0, 90.0); + default -> appendArc(n, x + rx, y + h - ry, rx, ry, 90.0, 180.0); + }; + } + if (c.kind() == UiCornerKind.CHAMFERED || c.kind() == UiCornerKind.NOTCHED) { + float cx = Math.min(c.cutX(), w * 0.5f), cy = Math.min(c.cutY(), h * 0.5f); + return switch (index) { + case 0 -> addChamfer(n, x + cx, y, x, y + cy); + case 1 -> addChamfer(n, x + w - cx, y, x + w, y + cy); + case 2 -> addChamfer(n, x + w, y + h - cy, x + w - cx, y + h); + default -> addChamfer(n, x + cx, y + h, x, y + h - cy); + }; + } + return switch (index) { + case 0 -> addPoint(n, x, y); case 1 -> addPoint(n, x + w, y); + case 2 -> addPoint(n, x + w, y + h); default -> addPoint(n, x, y + h); + }; + } + + private static int addChamfer(int n, double x0, double y0, double x1, double y1) { n = addPoint(n, x0, y0); return addPoint(n, x1, y1); } + + private static int appendArc(int n, double cx, double cy, double rx, double ry, double startDeg, double endDeg) { + if (rx <= 0.0001 || ry <= 0.0001) return addPoint(n, cx, cy); + int segments = Math.max(3, Math.min(12, (int) Math.ceil(Math.max(rx, ry) / 4.0))); + for (int i = 0; i <= segments; i++) { + double angle = Math.toRadians(startDeg + (endDeg - startDeg) * i / (double) segments); + n = addPoint(n, cx + Math.cos(angle) * rx, cy + Math.sin(angle) * ry); + } + return n; + } + + private static int addPoint(int n, double x, double y) { + if (n > 0 && POINTS[(n - 1) * 2] == x && POINTS[(n - 1) * 2 + 1] == y) return n; + if ((n + 1) * 2 > POINTS.length) return n; + POINTS[n * 2] = x; POINTS[n * 2 + 1] = y; return n + 1; + } + + private static void resetPoints() { /* active count returned by builders; stale values ignored */ } + private static float clamp(float v, float lo, float hi) { return Math.max(lo, Math.min(hi, v)); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java index ef23ba1..678d35c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java @@ -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.render.helpers.material; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java index 15ddf1a..1254a10 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java @@ -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.render.helpers.material; import dev.loki.lovisual.features.theme.Theme; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java index 744f6d0..0068468 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java @@ -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.render.helpers.material; public enum SodiumMaterialFlags { diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java index 11a243e..2fc6d5f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java @@ -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.render.helpers.material; import net.minecraft.tags.BlockTags; @@ -16,6 +9,7 @@ import net.minecraft.world.level.block.VegetationBlock; import net.minecraft.world.level.block.VineBlock; import net.minecraft.world.level.block.state.BlockState; import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; import dev.loki.lovisual.util.logging.DebugLog; @@ -36,7 +30,8 @@ public enum SodiumSurfaceFlagContext { WaveType waveType = WaveType.NONE; if (stateObj instanceof BlockState state) { softFade = NoRender.shouldFadeSoftBlock(state); - if (ReimaginedVisual.isWavyVegetationEnabledStatic()) { + ReimaginedVisual wavy = Modules.get(ReimaginedVisual.class); + if (wavy != null && wavy.isWavyVegetationEnabled()) { waveType = getVegetationWaveType(state); } if (softFade && !loggedSoftFadeState) { @@ -69,7 +64,8 @@ public enum SodiumSurfaceFlagContext { } if (flags.waveType != WaveType.NONE) { encoded |= SodiumMaterialFlags.SURFACE_FLAG_WAVY_VEGETATION; - encoded |= ReimaginedVisual.packWavyVegetationSettingsStatic(); + ReimaginedVisual wavyPack = Modules.get(ReimaginedVisual.class); + if (wavyPack != null) encoded |= wavyPack.packWavyVegetationSettings(); if (flags.waveType == WaveType.FREE) { encoded |= SodiumMaterialFlags.SURFACE_FLAG_WAVY_VEGETATION_FREE; } diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java index bdd32ad..f2dee55 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java @@ -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.render.helpers.particle; import dev.loki.lovisual.render.engine.color.RenderColor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java index 5b68985..871e621 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java @@ -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.render.helpers.player; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java index df317e2..f5fbb4c 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java @@ -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.render.helpers.player; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java index 8eac5b5..9f82565 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java @@ -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.render.helpers.player; import net.minecraft.client.gui.GuiGraphicsExtractor; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java index 523ae71..1415798 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java @@ -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.render.helpers.player; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java index ed29c07..dabfd03 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java @@ -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.render.helpers.util; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java index 2ac4b13..1d9af2b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java @@ -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.render.helpers.util; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java index 72aa91e..3110f2e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java @@ -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.render.helpers.util; import com.mojang.blaze3d.platform.Window; diff --git a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java index dc7e632..58cb6cb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java +++ b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java @@ -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.render.helpers.util; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java index 9b1e6c1..dd0c082 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java +++ b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java @@ -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.render.invisible; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java index d9d0a8d..ba9ad09 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java @@ -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.render.invisible; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java index 845f2ac..3b7caf3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java @@ -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.render.iris.compat; public enum IrisCompatibilityFeature { diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java index d497854..143cbbb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java @@ -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.render.iris.compat; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java index e52039c..b88a461 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java @@ -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.render.iris.compat; import java.util.EnumSet; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java index b42ee92..d1cb2b3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java @@ -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.render.iris.compat; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java index e31d28c..8ad409f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java @@ -1,9 +1,4 @@ -/* - * 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.render.iris.patch; import com.google.common.collect.ImmutableList; import java.io.IOException; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java index 406b2cc..602263b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java @@ -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.render.iris.patch; import com.google.common.collect.ImmutableList; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java index 0235790..0ac6d8b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java @@ -1,52 +1,37 @@ -/* - * 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.render.iris.patch; import com.google.common.collect.ImmutableList; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import dev.loki.lovisual.render.iris.patch.ShaderPatchRepo.Manifest; +import dev.loki.lovisual.render.iris.patch.ShaderPatchRepo.Repository; +import dev.loki.lovisual.render.iris.patch.ShaderPatchRepo.Target; import dev.loki.lovisual.util.logging.DebugLog; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import dev.loki.lovisual.render.iris.patch.ShaderPatchCompiler; +/** + * Higher-level orchestrator for shader-pack patches: manages the global + * {@link Repository}, per-pack {@link Session}s, manifest registration and + * diagnostics. JSON loading logic lives in {@link ShaderPatchRepo}. + */ public enum ShaderPatchEngine { ; - private static final String INDEX_RESOURCE = "assets/lovisual/shaders/iris-patches/index.json"; private static final CopyOnWriteArrayList EXTRA_MANIFEST_RESOURCES = new CopyOnWriteArrayList<>(); private static final AtomicReference REPOSITORY = new AtomicReference<>(Repository.load(List.of())); private static final Map DIAGNOSTICS = new ConcurrentHashMap<>(); private static final ThreadLocal LOADING_SHADER_PACK_NAME = new ThreadLocal<>(); private static final AtomicReference GLOBAL_LOADING_SHADER_PACK_NAME = new AtomicReference<>(); - /** - * Legacy validation session: all repository manifests are visible. Runtime Iris hooks should use - * {@link #newSession(String)} so LoVisual does not probe unrelated shaderpacks. - */ - public static Session newSession() { - Repository repository = repository(); - return new Session("", repository.targetsByPath, Set.copyOf(repository.manifestsById.keySet()), false); - } + /** Legacy validation session: all repository manifests are visible. */ + public static Session newSession() { Repository r = repository(); return new Session("", r.targetsByPath(), Set.copyOf(r.manifestsById().keySet()), false); } public static Session newSession(String shaderPackName) { - Repository.Selection selection = repository().select(shaderPackName); - return new Session(shaderPackName, selection.targetsByPath(), selection.manifestIds(), selection.nameGated()); + ShaderPatchRepo.Selection sel = repository().select(shaderPackName); + return new Session(shaderPackName, sel.targetsByPath(), sel.manifestIds(), sel.nameGated()); } public static boolean registerManifestResource(String manifestResourcePath) { @@ -60,10 +45,7 @@ public enum ShaderPatchEngine { } public static void beginShaderPackLoad(String shaderPackName) { - if (shaderPackName == null || shaderPackName.isBlank()) { - LOADING_SHADER_PACK_NAME.remove(); - return; - } + if (shaderPackName == null || shaderPackName.isBlank()) { LOADING_SHADER_PACK_NAME.remove(); return; } LOADING_SHADER_PACK_NAME.set(shaderPackName); GLOBAL_LOADING_SHADER_PACK_NAME.set(shaderPackName); DebugLog.info("[IrisPatch] shaderpack load begin: '%s'", shaderPackName); @@ -71,79 +53,38 @@ public enum ShaderPatchEngine { public static void endShaderPackLoad(String shaderPackName) { String active = LOADING_SHADER_PACK_NAME.get(); - if (active != null && (shaderPackName == null || active.equals(shaderPackName))) { - LOADING_SHADER_PACK_NAME.remove(); - } + if (active != null && (shaderPackName == null || active.equals(shaderPackName))) LOADING_SHADER_PACK_NAME.remove(); String global = GLOBAL_LOADING_SHADER_PACK_NAME.get(); - if (global != null && (shaderPackName == null || global.equals(shaderPackName))) { - GLOBAL_LOADING_SHADER_PACK_NAME.compareAndSet(global, null); - } + if (global != null && (shaderPackName == null || global.equals(shaderPackName))) GLOBAL_LOADING_SHADER_PACK_NAME.compareAndSet(global, null); DebugLog.info("[IrisPatch] shaderpack load end: '%s'", shaderPackName == null ? "" : shaderPackName); } public static String loadingShaderPackName() { - String threadLocal = LOADING_SHADER_PACK_NAME.get(); - if (threadLocal != null && !threadLocal.isBlank()) { - return threadLocal; - } - return GLOBAL_LOADING_SHADER_PACK_NAME.get(); + String t = LOADING_SHADER_PACK_NAME.get(); + return (t != null && !t.isBlank()) ? t : GLOBAL_LOADING_SHADER_PACK_NAME.get(); } public static List diagnostics() { - return DIAGNOSTICS.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(entry -> entry.getKey() + ": " + entry.getValue()) - .toList(); + return DIAGNOSTICS.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(e -> e.getKey() + ": " + e.getValue()).toList(); } - public static List targetPaths() { - return repository().targetsByPath.keySet().stream().sorted().toList(); - } - - public static List targetPaths(String shaderPackName) { - return repository().select(shaderPackName).targetsByPath.keySet().stream().sorted().toList(); - } - - private static Repository repository() { - return REPOSITORY.get(); - } - - private static void setDiagnostic(Target target, String value) { - DIAGNOSTICS.put(diagnosticKey(target), value); - } - - private static boolean contains(List lines, String marker) { - if (marker == null || marker.isBlank()) { - return false; - } - for (String line : lines) { - if (line.contains(marker)) { - return true; - } - } - return false; - } - - private static String diagnosticKey(Target target) { - return target.manifestId + " " + target.path; - } - - private static String normalizePath(String path) { - String normalized = path.replace('\\', '/'); - return normalized.startsWith("/") ? normalized : "/" + normalized; - } + public static List targetPaths() { return repository().targetsByPath().keySet().stream().sorted().toList(); } + public static List targetPaths(String shaderPackName) { return repository().select(shaderPackName).targetsByPath().keySet().stream().sorted().toList(); } + + private static Repository repository() { return REPOSITORY.get(); } + private static void setDiagnostic(Target target, String value) { DIAGNOSTICS.put(target.manifestId() + " " + target.path(), value); } + private static boolean contains(List lines, String marker) { return marker != null && !marker.isBlank() && lines.stream().anyMatch(l -> l.contains(marker)); } private static String normalizeResourcePath(String path) { if (path == null || path.isBlank()) return ""; - String normalized = path.trim().replace('\\', '/'); - return normalized.startsWith("/") ? normalized.substring(1) : normalized; + String n = path.trim().replace('\\', '/'); + return n.startsWith("/") ? n.substring(1) : n; } @FunctionalInterface - public interface SourceProvider { - ImmutableList load(String path); - } + public interface SourceProvider { ImmutableList load(String path); } + /** Per-shaderpack patch session; holds a filtered view of the repository and applies patches to GLSL source. */ public static final class Session { private final String shaderPackName; private final Map> targetsByPath; @@ -153,354 +94,53 @@ public enum ShaderPatchEngine { private final boolean nameGated; private boolean announced; - private Session(String shaderPackName, - Map> targetsByPath, - Set activeManifestIds, - boolean nameGated) { + private Session(String shaderPackName, Map> targetsByPath, Set activeManifestIds, boolean nameGated) { this.shaderPackName = shaderPackName == null ? "" : shaderPackName; this.targetsByPath = targetsByPath; this.activeManifestIds = activeManifestIds; this.nameGated = nameGated; } - public boolean isActive() { - return !targetsByPath.isEmpty(); - } - - public ImmutableList patch(String path, ImmutableList source, SourceProvider sourceProvider) { - if (path == null || source == null || preflighting.get()) { - return source; - } + public boolean isActive() { return !targetsByPath.isEmpty(); } + public ImmutableList patch(String path, ImmutableList source, SourceProvider sp) { + if (path == null || source == null || preflighting.get()) return source; announceOnce(); - List candidates = targetsByPath.get(normalizePath(path)); - if (candidates == null || candidates.isEmpty()) { - return source; - } - - DebugLog.renderThread("[IrisPatch] path=%s candidates=%d sourceLines=%d", normalizePath(path), candidates.size(), source.size()); + List candidates = targetsByPath.get(ShaderPatchRepo.normalizePath(path)); + if (candidates == null || candidates.isEmpty()) return source; for (Target target : candidates) { - if (contains(source, target.marker)) { - setDiagnostic(target, "already applied"); - DebugLog.renderThread("[IrisPatch] %s %s already applied marker=%s", target.manifestId, target.path, target.marker); - return source; - } - boolean applicable = manifestApplicability.computeIfAbsent( - target.manifestId, - ignored -> preflight(target.manifestId, sourceProvider) - ); - if (!applicable) { - DebugLog.renderThread("[IrisPatch] %s %s skipped: manifest preflight rejected earlier", target.manifestId, target.path); - continue; - } + if (contains(source, target.marker())) { setDiagnostic(target, "already applied"); return source; } + if (!manifestApplicability.computeIfAbsent(target.manifestId(), id -> preflight(id, sp))) continue; try { - ShaderPatchCompiler.CompiledPatch compiled = ShaderPatchCompiler.compile( - target.program, - source, - ShaderPatchCompiler.CompileMode.APPLICATION, - target.stages - ); - ImmutableList patched = ShaderPatchCompiler.apply(compiled, source); + ShaderPatchCompiler.CompiledPatch compiled = ShaderPatchCompiler.compile(target.program(), source, ShaderPatchCompiler.CompileMode.APPLICATION, target.stages()); setDiagnostic(target, "applied edits=" + compiled.edits().size()); - DebugLog.info("[IrisPatch] %s %s applied: schema=%d stages=%s edits=%d marker=%s", - target.manifestId, - target.path, - target.program.schemaVersion(), - target.stages, - compiled.edits().size(), - target.marker); - return patched; - } catch (ShaderPatchCompiler.CompileException e) { - // Do not poison the whole manifest here. Actual source can already contain LoVisual-owned - // includes from earlier targets, while preflight is the compatibility decision point. - setDiagnostic(target, "rejected after preflight: " + e.getMessage()); - DebugLog.warn("[IrisPatch] %s %s rejected after preflight: %s", target.manifestId, target.path, e.getMessage()); - } + DebugLog.info("[IrisPatch] %s %s applied edits=%d", target.manifestId(), target.path(), compiled.edits().size()); + return ShaderPatchCompiler.apply(compiled, source); + } catch (ShaderPatchCompiler.CompileException e) { setDiagnostic(target, "rejected: " + e.getMessage()); } } - - DebugLog.renderThread("[IrisPatch] path=%s no candidate applied", normalizePath(path)); return source; } private void announceOnce() { - if (announced) { - return; - } + if (announced) return; announced = true; - if (targetsByPath.isEmpty()) { - DebugLog.info("[IrisPatch] session inactive: shaderPack='%s' nameGated=%s matchedManifests=0", shaderPackName, nameGated); - } else { - DebugLog.info("[IrisPatch] session active: shaderPack='%s' nameGated=%s manifests=%d targetPaths=%d targets=%d", - shaderPackName, - nameGated, - activeManifestIds.size(), - targetsByPath.size(), - targetsByPath.values().stream().mapToInt(List::size).sum()); - } + DebugLog.info("[IrisPatch] session %s: shaderPack='%s' nameGated=%s manifests=%d paths=%d", + targetsByPath.isEmpty() ? "inactive" : "active", shaderPackName, nameGated, activeManifestIds.size(), targetsByPath.size()); } - private boolean preflight(String manifestId, SourceProvider sourceProvider) { - Manifest manifest = repository().manifestsById.get(manifestId); - if (manifest == null) { - DebugLog.warn("[IrisPatch] preflight rejected: missing manifest %s", manifestId); - return false; - } - if (!activeManifestIds.contains(manifestId)) { - DebugLog.renderThread("[IrisPatch] preflight skipped: manifest=%s not selected for shaderPack='%s'", manifestId, shaderPackName); - return false; - } - - DebugLog.info("[IrisPatch] preflight start: manifest=%s shaderPack='%s' targets=%d", manifestId, shaderPackName, manifest.targets.size()); + private boolean preflight(String manifestId, SourceProvider sp) { + Manifest manifest = repository().manifestsById().get(manifestId); + if (manifest == null || !activeManifestIds.contains(manifestId)) return false; preflighting.set(true); try { - for (Target target : manifest.targets) { - ImmutableList source = sourceProvider.load(target.path); - if (source == null) { - setDiagnostic(target, "preflight rejected: source unavailable"); - DebugLog.warn("[IrisPatch] %s %s preflight rejected: source unavailable", target.manifestId, target.path); - return false; - } - try { - ShaderPatchCompiler.CompiledPatch compiled = ShaderPatchCompiler.compile( - target.program, - source, - ShaderPatchCompiler.CompileMode.PREFLIGHT, - target.stages - ); - setDiagnostic(target, "preflight passed edits=" + compiled.edits().size()); - DebugLog.renderThread("[IrisPatch] %s %s preflight passed: schema=%d stages=%s sourceLines=%d edits=%d", - target.manifestId, - target.path, - target.program.schemaVersion(), - target.stages, - source.size(), - compiled.edits().size()); - } catch (ShaderPatchCompiler.CompileException e) { - setDiagnostic(target, "preflight rejected: " + e.getMessage()); - DebugLog.warn("[IrisPatch] %s %s preflight rejected: %s", target.manifestId, target.path, e.getMessage()); - return false; - } + for (Target target : manifest.targets()) { + ImmutableList source = sp.load(target.path()); + if (source == null) { setDiagnostic(target, "preflight rejected: source unavailable"); return false; } + try { ShaderPatchCompiler.compile(target.program(), source, ShaderPatchCompiler.CompileMode.PREFLIGHT, target.stages()); } + catch (ShaderPatchCompiler.CompileException e) { setDiagnostic(target, "preflight rejected: " + e.getMessage()); return false; } } - DebugLog.info("[IrisPatch] preflight accepted: manifest=%s shaderPack='%s'", manifestId, shaderPackName); return true; - } finally { - preflighting.set(false); - } - } - } - - private record Target( - String manifestId, - int priority, - String path, - String marker, - Set stages, - ShaderPatchCompiler.PatchProgram program - ) { - } - - private record Manifest(String id, int priority, Identity identity, List targets) { - } - - private record Identity(String family, String profileId, String policy, Pattern packNamePattern) { - private boolean matches(String shaderPackName) { - if (shaderPackName == null || shaderPackName.isBlank()) { - return false; - } - if (packNamePattern != null) { - return packNamePattern.matcher(shaderPackName).matches(); - } - String normalized = shaderPackName.toLowerCase(Locale.ROOT); - return normalized.contains(family.toLowerCase(Locale.ROOT)) || normalized.contains(profileId.toLowerCase(Locale.ROOT)); - } - } - - private record Repository(Map> targetsByPath, Map manifestsById) { - - private static Repository load(List extraManifestResources) { - Map> targets = new HashMap<>(); - Map manifests = new HashMap<>(); - try { - JsonObject index = readObject(INDEX_RESOURCE); - requireSchema(index, INDEX_RESOURCE); - for (JsonElement element : requireArray(index, "manifests")) { - addManifest(targets, manifests, element.getAsString()); - } - if (extraManifestResources != null) { - for (String manifestResource : extraManifestResources) { - addManifest(targets, manifests, manifestResource); - } - } - } catch (Exception e) { - DebugLog.error("[IrisPatch] Failed to load shader patch repository", e); - } - targets.replaceAll((path, list) -> list.stream() - .sorted(Comparator.comparingInt(Target::priority).reversed()) - .toList()); - return new Repository(Map.copyOf(targets), Map.copyOf(manifests)); - } - - private static void addManifest(Map> targets, - Map manifests, - String manifestResource) - throws IOException, ShaderPatchCompiler.CompileException { - Manifest manifest = loadManifest(manifestResource); - if (manifests.putIfAbsent(manifest.id, manifest) != null) { - throw new IllegalArgumentException("Duplicate shader patch manifest " + manifest.id); - } - for (Target target : manifest.targets) { - targets.computeIfAbsent(target.path, ignored -> new ArrayList<>()).add(target); - } - } - - private static Manifest loadManifest(String manifestPath) - throws IOException, ShaderPatchCompiler.CompileException { - JsonObject manifestJson = readObject(manifestPath); - requireSchema(manifestJson, manifestPath); - String manifestId = requireString(manifestJson, "id"); - int priority = manifestJson.has("priority") ? manifestJson.get("priority").getAsInt() : 0; - Identity identity = readIdentity(requireObject(manifestJson, "identity")); - - // Compatibility is descriptive. Runtime patchability is decided by preflight against the active pack. - requireObject(manifestJson, "compatibility"); - - String base = parent(manifestPath); - ArrayList targets = new ArrayList<>(); - for (JsonElement element : requireArray(manifestJson, "targets")) { - JsonObject targetJson = element.getAsJsonObject(); - String path = normalizePath(requireString(targetJson, "path")); - String patchPath = resolve(base, requireString(targetJson, "patch")); - ShaderPatchCompiler.PatchProgram program; - try (InputStreamReader reader = readResource(patchPath)) { - program = ShaderPatchCompiler.parse(reader); - } - targets.add(new Target( - manifestId, - priority, - path, - requireString(targetJson, "marker"), - readStages(targetJson), - program - )); - } - if (targets.isEmpty()) { - throw new IllegalArgumentException("Shader patch manifest has no targets: " + manifestId); - } - return new Manifest(manifestId, priority, identity, List.copyOf(targets)); - } - - private static Set readStages(JsonObject targetJson) - throws ShaderPatchCompiler.CompileException { - if (targetJson.has("stage")) { - return Set.of(ShaderPatchCompiler.ShaderStage.parse(targetJson.get("stage").getAsString())); - } - if (!targetJson.has("stages")) { - return Set.of(); - } - HashSet stages = new HashSet<>(); - JsonArray array = requireArray(targetJson, "stages"); - for (JsonElement element : array) { - stages.add(ShaderPatchCompiler.ShaderStage.parse(element.getAsString())); - } - return Set.copyOf(stages); - } - - private static Identity readIdentity(JsonObject identity) { - String family = requireString(identity, "family"); - String profile = requireString(identity, "profile"); - String policy = requireString(identity, "policy"); - Pattern namePattern = null; - if (identity.has("packNameRegex")) { - try { - namePattern = Pattern.compile(identity.get("packNameRegex").getAsString(), Pattern.CASE_INSENSITIVE); - } catch (PatternSyntaxException e) { - throw new IllegalArgumentException("Invalid shaderpack identity regex", e); - } - } - return new Identity(family, profile, policy, namePattern); - } - - private static JsonObject readObject(String resource) throws IOException { - try (InputStreamReader reader = readResource(resource)) { - return JsonParser.parseReader(reader).getAsJsonObject(); - } - } - - private static InputStreamReader readResource(String resource) throws IOException { - InputStream stream = ShaderPatchEngine.class.getClassLoader().getResourceAsStream(resource); - if (stream == null) { - throw new IOException("Missing resource " + resource); - } - return new InputStreamReader(stream, StandardCharsets.UTF_8); - } - - private static void requireSchema(JsonObject object, String resource) { - if (!object.has("schemaVersion") || object.get("schemaVersion").getAsInt() != 1) { - throw new IllegalArgumentException("Unsupported shader patch manifest schema in " + resource); - } - } - - private static JsonObject requireObject(JsonObject object, String key) { - if (!object.has(key) || !object.get(key).isJsonObject()) { - throw new IllegalArgumentException("Missing object " + key); - } - return object.getAsJsonObject(key); - } - - private static JsonArray requireArray(JsonObject object, String key) { - if (!object.has(key) || !object.get(key).isJsonArray()) { - throw new IllegalArgumentException("Missing array " + key); - } - return object.getAsJsonArray(key); - } - - private static String requireString(JsonObject object, String key) { - if (!object.has(key) || !object.get(key).isJsonPrimitive()) { - throw new IllegalArgumentException("Missing string " + key); - } - return object.get(key).getAsString(); - } - - private static String parent(String path) { - int separator = path.lastIndexOf('/'); - return separator < 0 ? "" : path.substring(0, separator + 1); - } - - private static String resolve(String base, String path) { - return path.startsWith("/") ? path.substring(1) : base + path; - } - - private Selection select(String shaderPackName) { - if (shaderPackName == null || shaderPackName.isBlank()) { - return new Selection(Map.of(), Set.of(), true); - } - - HashSet selectedManifests = new HashSet<>(); - for (Manifest manifest : manifestsById.values()) { - if (manifest.identity.matches(shaderPackName)) { - selectedManifests.add(manifest.id); - } - } - if (selectedManifests.isEmpty()) { - return new Selection(Map.of(), Set.of(), true); - } - - Map> selectedTargets = new HashMap<>(); - for (Map.Entry> entry : targetsByPath.entrySet()) { - ArrayList list = new ArrayList<>(); - for (Target target : entry.getValue()) { - if (selectedManifests.contains(target.manifestId)) { - list.add(target); - } - } - if (!list.isEmpty()) { - selectedTargets.put(entry.getKey(), List.copyOf(list)); - } - } - return new Selection(Map.copyOf(selectedTargets), Set.copyOf(selectedManifests), true); - } - - private record Selection(Map> targetsByPath, Set manifestIds, boolean nameGated) { + } finally { preflighting.set(false); } } } } diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java index 6afb426..c6a7b9d 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java @@ -1,9 +1,4 @@ -/* - * 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.render.iris.patch; import com.google.common.collect.ImmutableList; import java.security.MessageDigest; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java index d73f578..c918a53 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java @@ -1,9 +1,4 @@ -/* - * 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.render.iris.patch; import com.google.common.collect.ImmutableList; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/impl/ShaderPatchRepo.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/impl/ShaderPatchRepo.java new file mode 100644 index 0000000..fa04f96 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/impl/ShaderPatchRepo.java @@ -0,0 +1,158 @@ +package dev.loki.lovisual.render.iris.patch; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Repository that loads and indexes shader-patch manifests from JSON resources. + * Extracted from {@link ShaderPatchEngine} to keep the facade under the file-size cap. + * Sibling folder sharing the parent's package. + */ +final class ShaderPatchRepo { + + static final String INDEX_RESOURCE = "assets/lovisual/shaders/iris-patches/index.json"; + + private ShaderPatchRepo() { } + + record Target(String manifestId, int priority, String path, String marker, + Set stages, ShaderPatchCompiler.PatchProgram program) { } + + record Manifest(String id, int priority, Identity identity, List targets) { } + + record Identity(String family, String profileId, String policy, Pattern packNamePattern) { + boolean matches(String shaderPackName) { + if (shaderPackName == null || shaderPackName.isBlank()) return false; + if (packNamePattern != null) return packNamePattern.matcher(shaderPackName).matches(); + String n = shaderPackName.toLowerCase(Locale.ROOT); + return n.contains(family.toLowerCase(Locale.ROOT)) || n.contains(profileId.toLowerCase(Locale.ROOT)); + } + } + + record Selection(Map> targetsByPath, Set manifestIds, boolean nameGated) { } + + record Repository(Map> targetsByPath, Map manifestsById) { + + static Repository load(List extraManifestResources) { + Map> targets = new HashMap<>(); + Map manifests = new HashMap<>(); + try { + JsonObject index = readObject(INDEX_RESOURCE); + requireSchema(index, INDEX_RESOURCE); + for (JsonElement el : requireArray(index, "manifests")) addManifest(targets, manifests, el.getAsString()); + if (extraManifestResources != null) for (String r : extraManifestResources) addManifest(targets, manifests, r); + } catch (Exception e) { DebugLog.error("[IrisPatch] Failed to load shader patch repository", e); } + targets.replaceAll((path, list) -> list.stream().sorted(Comparator.comparingInt(Target::priority).reversed()).toList()); + return new Repository(Map.copyOf(targets), Map.copyOf(manifests)); + } + + Selection select(String shaderPackName) { + if (shaderPackName == null || shaderPackName.isBlank()) return new Selection(Map.of(), Set.of(), true); + HashSet selected = new HashSet<>(); + for (Manifest m : manifestsById.values()) if (m.identity.matches(shaderPackName)) selected.add(m.id); + if (selected.isEmpty()) return new Selection(Map.of(), Set.of(), true); + Map> filtered = new HashMap<>(); + for (Map.Entry> e : targetsByPath.entrySet()) { + List list = e.getValue().stream().filter(t -> selected.contains(t.manifestId)).toList(); + if (!list.isEmpty()) filtered.put(e.getKey(), list); + } + return new Selection(Map.copyOf(filtered), Set.copyOf(selected), true); + } + } + + // ---- JSON loading helpers ---------------------------------------------------------------- + + static String normalizePath(String path) { + String n = path.replace('\\', '/'); + return n.startsWith("/") ? n : "/" + n; + } + + private static void addManifest(Map> targets, Map manifests, String resource) + throws IOException, ShaderPatchCompiler.CompileException { + Manifest manifest = loadManifest(resource); + if (manifests.putIfAbsent(manifest.id, manifest) != null) throw new IllegalArgumentException("Duplicate shader patch manifest " + manifest.id); + for (Target t : manifest.targets) targets.computeIfAbsent(t.path, ignored -> new ArrayList<>()).add(t); + } + + private static Manifest loadManifest(String manifestPath) throws IOException, ShaderPatchCompiler.CompileException { + JsonObject json = readObject(manifestPath); + requireSchema(json, manifestPath); + String id = requireString(json, "id"); + int priority = json.has("priority") ? json.get("priority").getAsInt() : 0; + Identity identity = readIdentity(requireObject(json, "identity")); + requireObject(json, "compatibility"); + String base = parent(manifestPath); + ArrayList targets = new ArrayList<>(); + for (JsonElement el : requireArray(json, "targets")) { + JsonObject tj = el.getAsJsonObject(); + String path = normalizePath(requireString(tj, "path")); + String patchPath = resolve(base, requireString(tj, "patch")); + ShaderPatchCompiler.PatchProgram program; + try (InputStreamReader reader = readResource(patchPath)) { program = ShaderPatchCompiler.parse(reader); } + targets.add(new Target(id, priority, path, requireString(tj, "marker"), readStages(tj), program)); + } + if (targets.isEmpty()) throw new IllegalArgumentException("Shader patch manifest has no targets: " + id); + return new Manifest(id, priority, identity, List.copyOf(targets)); + } + + private static Set readStages(JsonObject tj) throws ShaderPatchCompiler.CompileException { + if (tj.has("stage")) return Set.of(ShaderPatchCompiler.ShaderStage.parse(tj.get("stage").getAsString())); + if (!tj.has("stages")) return Set.of(); + HashSet stages = new HashSet<>(); + for (JsonElement el : requireArray(tj, "stages")) stages.add(ShaderPatchCompiler.ShaderStage.parse(el.getAsString())); + return Set.copyOf(stages); + } + + private static Identity readIdentity(JsonObject obj) { + String family = requireString(obj, "family"), profile = requireString(obj, "profile"), policy = requireString(obj, "policy"); + Pattern namePattern = null; + if (obj.has("packNameRegex")) { + try { namePattern = Pattern.compile(obj.get("packNameRegex").getAsString(), Pattern.CASE_INSENSITIVE); } + catch (PatternSyntaxException e) { throw new IllegalArgumentException("Invalid shaderpack identity regex", e); } + } + return new Identity(family, profile, policy, namePattern); + } + + private static JsonObject readObject(String resource) throws IOException { + try (InputStreamReader reader = readResource(resource)) { return JsonParser.parseReader(reader).getAsJsonObject(); } + } + + private static InputStreamReader readResource(String resource) throws IOException { + InputStream s = ShaderPatchRepo.class.getClassLoader().getResourceAsStream(resource); + if (s == null) throw new IOException("Missing resource " + resource); + return new InputStreamReader(s, StandardCharsets.UTF_8); + } + + private static void requireSchema(JsonObject obj, String resource) { + if (!obj.has("schemaVersion") || obj.get("schemaVersion").getAsInt() != 1) + throw new IllegalArgumentException("Unsupported shader patch manifest schema in " + resource); + } + + private static JsonObject requireObject(JsonObject obj, String key) { + if (!obj.has(key) || !obj.get(key).isJsonObject()) throw new IllegalArgumentException("Missing object " + key); + return obj.getAsJsonObject(key); + } + + private static JsonArray requireArray(JsonObject obj, String key) { + if (!obj.has(key) || !obj.get(key).isJsonArray()) throw new IllegalArgumentException("Missing array " + key); + return obj.getAsJsonArray(key); + } + + private static String requireString(JsonObject obj, String key) { + if (!obj.has(key) || !obj.get(key).isJsonPrimitive()) throw new IllegalArgumentException("Missing string " + key); + return obj.get(key).getAsString(); + } + + private static String parent(String path) { int i = path.lastIndexOf('/'); return i < 0 ? "" : path.substring(0, i + 1); } + private static String resolve(String base, String path) { return path.startsWith("/") ? path.substring(1) : base + path; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java index 6c89f09..16518d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java @@ -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.render.iris.patch; import java.io.BufferedReader; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java index 1fafa28..ee5bd91 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java @@ -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.render.iris.patch; import java.io.BufferedReader; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java index 89e6222..d8f4dcb 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java @@ -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.render.iris.runtime; import net.fabricmc.loader.api.FabricLoader; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java index fcfbe09..7dbb805 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java @@ -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.render.iris.runtime; import com.mojang.blaze3d.pipeline.RenderPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java index 95ef1ff..c156ff7 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java @@ -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.render.iris.runtime; import dev.loki.lovisual.render.iris.compat.IrisCompatibilityProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java index c2d64e7..f902d32 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java @@ -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.render.iris.runtime; import net.irisshaders.iris.gl.uniform.UniformHolder; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java index 9ef2417..55c653f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java @@ -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.render.iris.scene; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java index 2b0ba61..41ea22e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java @@ -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.render.iris.scene; import com.mojang.blaze3d.systems.RenderSystem; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java index 602981a..628f31e 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java @@ -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.render.iris.scene; import net.irisshaders.iris.pipeline.WorldRenderingPipeline; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java index 0a720a7..01c9a95 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java @@ -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.render.iris.state; import com.mojang.blaze3d.vertex.PoseStack; diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java index aa71a37..845c5d3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java @@ -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.render.iris.state; import net.minecraft.client.DeltaTracker; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java index 2b04ce4..c4fa429 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java @@ -1,55 +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.render.sky; import com.mojang.blaze3d.buffers.GpuBufferSlice; -import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Camera; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.client.renderer.GameRenderer; import net.minecraft.client.renderer.SkyRenderer; -import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.client.renderer.state.level.SkyRenderState; import net.minecraft.util.ARGB; import net.minecraft.util.Mth; -import net.minecraft.world.effect.MobEffects; -import net.minecraft.world.phys.Vec3; import org.joml.Matrix4f; import org.joml.Matrix4fStack; import org.joml.Quaternionf; -import org.joml.Vector3f; import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; -import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky; import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; -import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; import dev.loki.lovisual.render.engine.renderer.MeshRenderer; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.ShaderSkyUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.sky.sun.SkySunUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.SkyboxShaderUniforms; +/** + * Custom sky renderer entry point. Orchestrates the vanilla sky disc, the optional Reimagined shader + * skybox / procedural ShaderSky passes and the shader sun, restoring vanilla sun/moon and dark-disc + * rendering for whatever it does not replace. + * + *

    Projection math lives in {@code impl.SkyboxProjection}, the fullscreen sky passes in + * {@code impl.SkyboxShaderPasses} and the sun billboard in {@code impl.SkyboxSunRenderer}.

    + */ public enum CustomSkyboxRenderer { ; private static final float MIN_TWILIGHT_SKYBOX_ALPHA = 0.38f; - private static final double SKY_SIZE = 160.0; private static float lastSunAngle; private static boolean lastSunAngleValid; @@ -74,7 +55,7 @@ public enum CustomSkyboxRenderer { float tickDelta = tickCounter != null ? tickCounter.getGameTimeDeltaPartialTick(true) : 0.0f; float fov = camera.getFov(); - Matrix4f projection = buildWorldProjection(mc, camera, fov, tickDelta); + Matrix4f projection = SkyboxProjection.build(mc, camera, fov, tickDelta); Matrix4f previousProjection = MeshRenderer.projection(); MeshRenderer.setProjection(projection); @@ -95,19 +76,19 @@ public enum CustomSkyboxRenderer { boolean useShaderSkybox = !useShaderSky && visual != null && visual.isShaderSkyboxEnabled(); if (useShaderSky) { - renderShaderSky(mc, mc.gameRenderer.mainRenderTarget(), camera, Theme.theme().accent() & 0x00FFFFFF, skyboxAlpha, tickDelta, fov, projection); + SkyboxShaderPasses.renderSky(mc, mc.gameRenderer.mainRenderTarget(), camera, Theme.theme().accent() & 0x00FFFFFF, skyboxAlpha, tickDelta, fov, projection); } else if (useShaderSkybox) { - renderShaderSkybox(mc, mc.gameRenderer.mainRenderTarget(), camera, fog, sky, skyboxAlpha, tickDelta, fov, projection); + SkyboxShaderPasses.renderSkybox(mc, mc.gameRenderer.mainRenderTarget(), camera, fog, sky, skyboxAlpha, tickDelta, fov, projection); } - if (shouldRenderShaderSun()) { - renderShaderSun(mc, mc.gameRenderer.mainRenderTarget(), camera, sky, tickDelta); + if (SkyboxSunRenderer.shouldRender()) { + SkyboxSunRenderer.render(mc, mc.gameRenderer.mainRenderTarget(), camera, sky, tickDelta); } mv.popMatrix(); MeshRenderer.setProjection(previousProjection); RenderState.rendering3D = prevRendering3D; - if (!shouldRenderShaderSun()) { + if (!SkyboxSunRenderer.shouldRender()) { PoseStack skyMatrices = new PoseStack(); skyRendering.renderSunriseAndSunset(skyMatrices, sky.sunAngle, sky.sunriseAndSunsetColor); } @@ -116,208 +97,6 @@ public enum CustomSkyboxRenderer { } } - private static void renderShaderSkybox(Minecraft mc, - com.mojang.blaze3d.pipeline.RenderTarget framebuffer, - Camera camera, - GpuBufferSlice fog, - SkyRenderState sky, - float alpha, - float tickDelta, - float fov, - Matrix4f worldProjection) { - if (alpha <= 0.001f || fog == null) return; - ReimaginedVisual visual = Modules.get(ReimaginedVisual.class); - if (visual == null || !visual.isShaderSkyboxEnabled()) return; - - float time = AnimationUtility.time(0.001f); - float width = mc.getWindow() != null ? mc.getWindow().getWidth() : 1.0f; - float height = mc.getWindow() != null ? mc.getWindow().getHeight() : 1.0f; - float yawRad = (float) Math.toRadians(-camera.yRot()); - float pitchRad = (float) Math.toRadians(camera.xRot()); - float shaderFov = extractProjectionFov(worldProjection, fov); - SkyboxShaderUniforms.update( - visual.getShaderSkyboxColor(), - sky.skyColor & 0x00FFFFFF, - alpha, - visual.getSkyboxSkyFogBlend(), - time, - visual.getSkyboxShaderMotionSpeed(), - 5.0f, - 0.01f, - width, - height, - yawRad, - pitchRad, - shaderFov, - visual.isSkyboxShaderAuroraEnabled() ? 1.0f : 0.0f, - visual.getSkyboxShaderAuroraIntensity(), - visual.getSkyboxShaderAuroraSpeed(), - visual.getSkyboxShaderSmallStars(), - visual.getSkyboxShaderDustStars(), - visual.getSkyboxShaderMediumStars(), - visual.getSkyboxShaderLargeStars(), - visual.getSkyboxShaderStarBrightness(), - visual.getSkyboxShaderTwinkleStrength(), - visual.getSkyboxShaderLayerMask() - ); - - FullScreenRenderer.ensureInit(); - Matrix4fStack mv = RenderSystem.getModelViewStack(); - mv.pushMatrix(); - mv.identity(); - MeshRenderer.setProjection(new Matrix4f()); - try { - MeshRenderer.begin() - .attachments(framebuffer) - .pipeline(LoVisualRenderPipelines.WORLD_REIMAGINED_SKYBOX_SHADER) - .mesh(FullScreenRenderer.vbo, FullScreenRenderer.ibo) - .uniform("Fog", fog) - .uniform("SkyboxShader", SkyboxShaderUniforms.get()) - .end(); - } finally { - MeshRenderer.setProjection(worldProjection); - mv.popMatrix(); - } - } - - private static void renderShaderSky(Minecraft mc, - com.mojang.blaze3d.pipeline.RenderTarget framebuffer, - Camera camera, - int skyColor, - float alpha, - float tickDelta, - float fov, - Matrix4f worldProjection) { - if (alpha <= 0.001f) return; - ShaderSky skyModule = Modules.get(ShaderSky.class); - if (skyModule == null || !skyModule.isEnabled()) return; - - RenderPipeline pipeline = pipelineForMode(skyModule.getMode()); - if (pipeline == null) return; - - float time = AnimationUtility.time(0.001f); - float width = mc.getWindow() != null ? mc.getWindow().getWidth() : 1.0f; - float height = mc.getWindow() != null ? mc.getWindow().getHeight() : 1.0f; - float yawRad = (float) Math.toRadians(-camera.yRot()); - float pitchRad = (float) Math.toRadians(camera.xRot()); - float shaderFov = extractProjectionFov(worldProjection, fov); - ShaderSkyUniforms.update( - skyColor, - alpha * skyModule.getAlpha(), - time, - skyModule.getSpeed(), - skyModule.getScale(), - skyModule.getIntensity(), - width, - height, - yawRad, - pitchRad, - shaderFov, - skyModule.getThunderInterval(), - skyModule.getThunderChance(), - skyModule.getThunderGlow() - ); - - FullScreenRenderer.ensureInit(); - Matrix4fStack mv = RenderSystem.getModelViewStack(); - mv.pushMatrix(); - mv.identity(); - MeshRenderer.setProjection(new Matrix4f()); - try { - MeshRenderer.begin() - .attachments(framebuffer) - .pipeline(pipeline) - .mesh(FullScreenRenderer.vbo, FullScreenRenderer.ibo) - .uniform("ShaderSky", ShaderSkyUniforms.get()) - .end(); - } finally { - MeshRenderer.setProjection(worldProjection); - mv.popMatrix(); - } - } - - private static RenderPipeline pipelineForMode(String mode) { - return switch (mode) { - case "Pulsar" -> LoVisualRenderPipelines.WORLD_SHADER_SKY_PULSAR; - default -> LoVisualRenderPipelines.WORLD_SHADER_SKY_THUNDER; - }; - } - - private static float extractProjectionFov(Matrix4f projection, float fallbackFov) { - if (projection == null) { - return fallbackFov; - } - float m11 = Math.abs(projection.m11()); - if (!Float.isFinite(m11) || m11 <= 1.0e-4f) { - return fallbackFov; - } - float fov = (float) Math.toDegrees(2.0 * Math.atan(1.0 / m11)); - return Float.isFinite(fov) ? Mth.clamp(fov, 1.0f, 179.0f) : fallbackFov; - } - - private static Matrix4f buildWorldProjection(Minecraft mc, Camera camera, float fov, float tickDelta) { - Matrix4f capturedProjection = LoVisualWorldMatrices.renderProjectionMatrix(); - if (capturedProjection != null) { - return capturedProjection; - } - - GameRenderer renderer = mc.gameRenderer; - GameRendererAccessor accessor = (GameRendererAccessor) renderer; - CameraRenderState cameraRenderState = new CameraRenderState(); - camera.extractRenderState(cameraRenderState, fov); - Matrix4f projection = new Matrix4f(cameraRenderState.projectionMatrix); - - PoseStack viewEffects = new PoseStack(); - NoRender noRender = Modules.get(NoRender.class); - if (noRender == null || !noRender.off("camera_shake")) { - accessor.invokeTiltViewWhenHurt(cameraRenderState, viewEffects); - } - - boolean freecamEnabled = Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled(); - if (mc.options.bobView().get() - && !freecamEnabled - && (noRender == null || !noRender.off("view_bob"))) { - accessor.invokeBobView(cameraRenderState, viewEffects); - } - - projection.mul(viewEffects.last().pose()); - applyNauseaProjection(mc, accessor, projection, tickDelta); - return projection; - } - - private static void applyNauseaProjection(Minecraft mc, - GameRendererAccessor accessor, - Matrix4f projection, - float tickDelta) { - LocalPlayer player = mc.player; - if (player == null) return; - - float distortionScale = mc.options.screenEffectScale().get().floatValue(); - if (distortionScale <= 0.0f) return; - - float nauseaIntensity = 0.0f; - if (player instanceof LocalPlayerAccessor playerAccessor) { - nauseaIntensity = Mth.lerp( - tickDelta, - playerAccessor.lovisual$getLastNauseaIntensity(), - playerAccessor.lovisual$getNauseaIntensity() - ); - } - - float nauseaEffect = player.getEffectBlendFactor(MobEffects.NAUSEA, tickDelta); - float strength = Math.max(nauseaIntensity, nauseaEffect) * (distortionScale * distortionScale); - if (strength <= 0.0f) return; - - float scale = 5.0f / (strength * strength + 5.0f) - strength * 0.04f; - scale *= scale; - Vector3f axis = new Vector3f(0.0f, Mth.SQRT_OF_TWO / 2.0f, Mth.SQRT_OF_TWO / 2.0f); - float angle = (accessor.lovisual$getNauseaEffectTime() + tickDelta * accessor.lovisual$getNauseaEffectSpeed()) - * Mth.DEG_TO_RAD; - projection.rotate(angle, axis); - projection.scale(1.0f / scale, 1.0f, 1.0f); - projection.rotate(-angle, axis); - } - private static float smoothstep(float x) { float t = Mth.clamp((x - (float) 0.0), 0.0f, 1.0f); return t * t * (3.0f - 2.0f * t); @@ -327,101 +106,4 @@ public enum CustomSkyboxRenderer { float sunriseAlpha = ARGB.alphaFloat(sky.sunriseAndSunsetColor); return smoothstep(sunriseAlpha); } - - private static boolean shouldRenderShaderSun() { - return ReimaginedVisual.isWorldSunEnabledStatic(); - } - - private static void renderShaderSun(Minecraft mc, com.mojang.blaze3d.pipeline.RenderTarget framebuffer, Camera camera, SkyRenderState sky, float tickDelta) { - if (mc.level == null) return; - - Vec3 sunDir = new Vec3(-Math.sin(sky.sunAngle), Math.cos(sky.sunAngle), 0.0).normalize(); - float sunVisibility = Mth.clamp(((float) sunDir.y + 0.0625f) / 0.125f, 0.0f, 1.0f); - if (sunVisibility <= 0.001f) return; - - float dayFactor = Mth.clamp((float) sunDir.y, 0.0f, 1.0f); - float rainFactor = mc.level.getRainLevel(tickDelta); - float rainFactor2 = rainFactor * rainFactor; - float rainFade = 1.0f - rainFactor * 0.7f; - if (rainFade <= 0.001f) return; - - float sunSizeSetting = ReimaginedVisual.getWorldSunSizeStatic(); - float sunGlowSetting = ReimaginedVisual.getWorldSunGlowStatic(); - float sunIntensitySetting = ReimaginedVisual.getWorldSunIntensityStatic(); - - float yawRad = camera.yRot() * ((float) Math.PI / 180.0f); - float pitchRad = camera.xRot() * ((float) Math.PI / 180.0f); - Vec3 forward = new Vec3( - -Math.sin(yawRad) * Math.cos(pitchRad), - -Math.sin(pitchRad), - Math.cos(yawRad) * Math.cos(pitchRad) - ).normalize(); - Vec3 right = new Vec3(0.0, 1.0, 0.0).cross(forward); - if (right.lengthSqr() < 1.0E-6) { - right = new Vec3(1.0, 0.0, 0.0); - } else { - right = right.normalize(); - } - Vec3 up = forward.cross(right).normalize(); - - double viewDot = sunDir.dot(forward); - if (viewDot <= -0.2) return; - - double distance = SKY_SIZE * 0.92; - double coreRadius = SKY_SIZE * (0.045 + sunSizeSetting * 0.95); - double baseGlowScale = 1.08 + sunGlowSetting * 0.55; - float lookFactor = Mth.clamp((float) ((viewDot - 0.45) / 0.50), 0.0f, 1.0f); - lookFactor = lookFactor * lookFactor * (3.0f - 2.0f * lookFactor); - float glareFactor = ReimaginedVisual.isWorldSunGlareEnabledStatic() ? lookFactor : 0.0f; - double glowRadius = coreRadius * (baseGlowScale + glareFactor * 1.35); - double radius = glowRadius; - Vec3 center = sunDir.scale(distance); - - double angularRadius = Math.max(coreRadius / distance, 0.012); - float sunrise = 1.0f - Math.abs(dayFactor * 2.0f - 1.0f); - float noonFactor = 1.0f - sunrise; - float shadowTimeVar1 = Math.abs(sunVisibility - 0.5f) * 2.0f; - float shadowTimeVar2 = shadowTimeVar1 * shadowTimeVar1; - float shadowTime = shadowTimeVar2 * shadowTimeVar2; - - int red = 255; - int green = Mth.clamp((int) ((0.70f + 0.20f * dayFactor + 0.06f * sunrise) * 255.0f), 0, 255); - int blue = Mth.clamp((int) ((0.38f + 0.48f * dayFactor - 0.08f * sunrise) * 255.0f), 0, 255); - float alphaFactor = Mth.clamp(rainFade, 0.0f, 1.0f); - int alpha = Mth.clamp((int) (alphaFactor * 255.0f), 0, 255); - - float innerRatio = (float) Mth.clamp(coreRadius / glowRadius, 0.08, 0.9); - float haloStrength = Mth.clamp( - (0.08f + sunGlowSetting * 0.16f + glareFactor * 0.42f) - * sunIntensitySetting * rainFade, - 0.0f, - 1.10f - ); - float coreStrength = Mth.clamp(1.10f * sunIntensitySetting * rainFade, 0.0f, 2.2f); - float rayStrength = 0.0f; - - MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.WORLD_SKY_SUN); - mesh.begin(); - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(center.x - right.x * radius - up.x * radius, center.y - right.y * radius - up.y * radius, center.z - right.z * radius - up.z * radius) - .vec2(0.0, 0.0).color(red, green, blue, alpha).next(); - int i2 = mesh.vec3(center.x + right.x * radius - up.x * radius, center.y + right.y * radius - up.y * radius, center.z + right.z * radius - up.z * radius) - .vec2(1.0, 0.0).color(red, green, blue, alpha).next(); - int i3 = mesh.vec3(center.x + right.x * radius + up.x * radius, center.y + right.y * radius + up.y * radius, center.z + right.z * radius + up.z * radius) - .vec2(1.0, 1.0).color(red, green, blue, alpha).next(); - int i4 = mesh.vec3(center.x - right.x * radius + up.x * radius, center.y - right.y * radius + up.y * radius, center.z - right.z * radius + up.z * radius) - .vec2(0.0, 1.0).color(red, green, blue, alpha).next(); - mesh.quad(i1, i2, i3, i4); - mesh.end(); - - SkySunUniforms.update(innerRatio, haloStrength, coreStrength, rayStrength); - MeshRenderer.begin() - .attachments(framebuffer) - .pipeline(LoVisualRenderPipelines.WORLD_SKY_SUN) - .mesh(mesh) - .uniform("SkySun", SkySunUniforms.get()) - .end(); - - } - } diff --git a/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxProjection.java b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxProjection.java new file mode 100644 index 0000000..4725c2a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxProjection.java @@ -0,0 +1,103 @@ +package dev.loki.lovisual.render.sky; + +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.util.Mth; +import net.minecraft.world.effect.MobEffects; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; +import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; +import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; + +/** + * World projection helpers for {@link CustomSkyboxRenderer}: builds the camera projection matrix + * (reusing the captured frame projection when available, else re-deriving hurt-tilt / view-bob / + * nausea effects) and extracts the vertical FOV from a projection matrix. + */ +final class SkyboxProjection { + private SkyboxProjection() { + } + + static Matrix4f build(Minecraft mc, Camera camera, float fov, float tickDelta) { + Matrix4f capturedProjection = LoVisualWorldMatrices.renderProjectionMatrix(); + if (capturedProjection != null) { + return capturedProjection; + } + + GameRenderer renderer = mc.gameRenderer; + GameRendererAccessor accessor = (GameRendererAccessor) renderer; + CameraRenderState cameraRenderState = new CameraRenderState(); + camera.extractRenderState(cameraRenderState, fov); + Matrix4f projection = new Matrix4f(cameraRenderState.projectionMatrix); + + PoseStack viewEffects = new PoseStack(); + NoRender noRender = Modules.get(NoRender.class); + if (noRender == null || !noRender.off("camera_shake")) { + accessor.invokeTiltViewWhenHurt(cameraRenderState, viewEffects); + } + + boolean freecamEnabled = Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled(); + if (mc.options.bobView().get() + && !freecamEnabled + && (noRender == null || !noRender.off("view_bob"))) { + accessor.invokeBobView(cameraRenderState, viewEffects); + } + + projection.mul(viewEffects.last().pose()); + applyNauseaProjection(mc, accessor, projection, tickDelta); + return projection; + } + + static float extractFov(Matrix4f projection, float fallbackFov) { + if (projection == null) { + return fallbackFov; + } + float m11 = Math.abs(projection.m11()); + if (!Float.isFinite(m11) || m11 <= 1.0e-4f) { + return fallbackFov; + } + float fov = (float) Math.toDegrees(2.0 * Math.atan(1.0 / m11)); + return Float.isFinite(fov) ? Mth.clamp(fov, 1.0f, 179.0f) : fallbackFov; + } + + private static void applyNauseaProjection(Minecraft mc, + GameRendererAccessor accessor, + Matrix4f projection, + float tickDelta) { + LocalPlayer player = mc.player; + if (player == null) return; + + float distortionScale = mc.options.screenEffectScale().get().floatValue(); + if (distortionScale <= 0.0f) return; + + float nauseaIntensity = 0.0f; + if (player instanceof LocalPlayerAccessor playerAccessor) { + nauseaIntensity = Mth.lerp( + tickDelta, + playerAccessor.lovisual$getLastNauseaIntensity(), + playerAccessor.lovisual$getNauseaIntensity() + ); + } + + float nauseaEffect = player.getEffectBlendFactor(MobEffects.NAUSEA, tickDelta); + float strength = Math.max(nauseaIntensity, nauseaEffect) * (distortionScale * distortionScale); + if (strength <= 0.0f) return; + + float scale = 5.0f / (strength * strength + 5.0f) - strength * 0.04f; + scale *= scale; + Vector3f axis = new Vector3f(0.0f, Mth.SQRT_OF_TWO / 2.0f, Mth.SQRT_OF_TWO / 2.0f); + float angle = (accessor.lovisual$getNauseaEffectTime() + tickDelta * accessor.lovisual$getNauseaEffectSpeed()) + * Mth.DEG_TO_RAD; + projection.rotate(angle, axis); + projection.scale(1.0f / scale, 1.0f, 1.0f); + projection.rotate(-angle, axis); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxShaderPasses.java b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxShaderPasses.java new file mode 100644 index 0000000..99ee05a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxShaderPasses.java @@ -0,0 +1,158 @@ +package dev.loki.lovisual.render.sky; + +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.state.level.SkyRenderState; +import org.joml.Matrix4f; +import org.joml.Matrix4fStack; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; +import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; +import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.ShaderSkyUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.SkyboxShaderUniforms; + +/** + * Fullscreen shader sky passes for {@link CustomSkyboxRenderer}: the Reimagined shader skybox and + * the procedural ShaderSky (Pulsar/Thunder). Each uploads its uniform block, then draws the shared + * fullscreen quad through the deferred {@link MeshRenderer} while temporarily flattening projection + * and the model-view stack. + */ +final class SkyboxShaderPasses { + private SkyboxShaderPasses() { + } + + static void renderSkybox(Minecraft mc, + RenderTarget framebuffer, + Camera camera, + GpuBufferSlice fog, + SkyRenderState sky, + float alpha, + float tickDelta, + float fov, + Matrix4f worldProjection) { + if (alpha <= 0.001f || fog == null) return; + ReimaginedVisual visual = Modules.get(ReimaginedVisual.class); + if (visual == null || !visual.isShaderSkyboxEnabled()) return; + + float time = AnimationUtility.time(0.001f); + float width = mc.getWindow() != null ? mc.getWindow().getWidth() : 1.0f; + float height = mc.getWindow() != null ? mc.getWindow().getHeight() : 1.0f; + float yawRad = (float) Math.toRadians(-camera.yRot()); + float pitchRad = (float) Math.toRadians(camera.xRot()); + float shaderFov = SkyboxProjection.extractFov(worldProjection, fov); + SkyboxShaderUniforms.update( + visual.getShaderSkyboxColor(), + sky.skyColor & 0x00FFFFFF, + alpha, + visual.getSkyboxSkyFogBlend(), + time, + visual.getSkyboxShaderMotionSpeed(), + 5.0f, + 0.01f, + width, + height, + yawRad, + pitchRad, + shaderFov, + visual.isSkyboxShaderAuroraEnabled() ? 1.0f : 0.0f, + visual.getSkyboxShaderAuroraIntensity(), + visual.getSkyboxShaderAuroraSpeed(), + visual.getSkyboxShaderSmallStars(), + visual.getSkyboxShaderDustStars(), + visual.getSkyboxShaderMediumStars(), + visual.getSkyboxShaderLargeStars(), + visual.getSkyboxShaderStarBrightness(), + visual.getSkyboxShaderTwinkleStrength(), + visual.getSkyboxShaderLayerMask() + ); + + FullScreenRenderer.ensureInit(); + Matrix4fStack mv = RenderSystem.getModelViewStack(); + mv.pushMatrix(); + mv.identity(); + MeshRenderer.setProjection(new Matrix4f()); + try { + MeshRenderer.begin() + .attachments(framebuffer) + .pipeline(LoVisualRenderPipelines.WORLD_REIMAGINED_SKYBOX_SHADER) + .mesh(FullScreenRenderer.vbo, FullScreenRenderer.ibo) + .uniform("Fog", fog) + .uniform("SkyboxShader", SkyboxShaderUniforms.get()) + .end(); + } finally { + MeshRenderer.setProjection(worldProjection); + mv.popMatrix(); + } + } + + static void renderSky(Minecraft mc, + RenderTarget framebuffer, + Camera camera, + int skyColor, + float alpha, + float tickDelta, + float fov, + Matrix4f worldProjection) { + if (alpha <= 0.001f) return; + ShaderSky skyModule = Modules.get(ShaderSky.class); + if (skyModule == null || !skyModule.isEnabled()) return; + + RenderPipeline pipeline = pipelineForMode(skyModule.getMode()); + if (pipeline == null) return; + + float time = AnimationUtility.time(0.001f); + float width = mc.getWindow() != null ? mc.getWindow().getWidth() : 1.0f; + float height = mc.getWindow() != null ? mc.getWindow().getHeight() : 1.0f; + float yawRad = (float) Math.toRadians(-camera.yRot()); + float pitchRad = (float) Math.toRadians(camera.xRot()); + float shaderFov = SkyboxProjection.extractFov(worldProjection, fov); + ShaderSkyUniforms.update( + skyColor, + alpha * skyModule.getAlpha(), + time, + skyModule.getSpeed(), + skyModule.getScale(), + skyModule.getIntensity(), + width, + height, + yawRad, + pitchRad, + shaderFov, + skyModule.getThunderInterval(), + skyModule.getThunderChance(), + skyModule.getThunderGlow() + ); + + FullScreenRenderer.ensureInit(); + Matrix4fStack mv = RenderSystem.getModelViewStack(); + mv.pushMatrix(); + mv.identity(); + MeshRenderer.setProjection(new Matrix4f()); + try { + MeshRenderer.begin() + .attachments(framebuffer) + .pipeline(pipeline) + .mesh(FullScreenRenderer.vbo, FullScreenRenderer.ibo) + .uniform("ShaderSky", ShaderSkyUniforms.get()) + .end(); + } finally { + MeshRenderer.setProjection(worldProjection); + mv.popMatrix(); + } + } + + private static RenderPipeline pipelineForMode(String mode) { + return switch (mode) { + case "Pulsar" -> LoVisualRenderPipelines.WORLD_SHADER_SKY_PULSAR; + default -> LoVisualRenderPipelines.WORLD_SHADER_SKY_THUNDER; + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxSunRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxSunRenderer.java new file mode 100644 index 0000000..0a21936 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/sky/impl/SkyboxSunRenderer.java @@ -0,0 +1,125 @@ +package dev.loki.lovisual.render.sky; + +import com.mojang.blaze3d.pipeline.RenderTarget; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.state.level.SkyRenderState; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.sky.sun.SkySunUniforms; + +/** + * Procedural world-sun pass for {@link CustomSkyboxRenderer}. Computes the sun disc orientation, + * color and glow from the sun angle / rain / look direction, bakes a billboard quad through + * {@link MeshBuilder} and draws it with the {@code WORLD_SKY_SUN} pipeline. + */ +final class SkyboxSunRenderer { + private static final double SKY_SIZE = 160.0; + + private SkyboxSunRenderer() { + } + + static boolean shouldRender() { + ReimaginedVisual m = Modules.get(ReimaginedVisual.class); + return m != null && m.isWorldSunEnabled(); + } + + static void render(Minecraft mc, RenderTarget framebuffer, Camera camera, SkyRenderState sky, float tickDelta) { + if (mc.level == null) return; + + Vec3 sunDir = new Vec3(-Math.sin(sky.sunAngle), Math.cos(sky.sunAngle), 0.0).normalize(); + float sunVisibility = Mth.clamp(((float) sunDir.y + 0.0625f) / 0.125f, 0.0f, 1.0f); + if (sunVisibility <= 0.001f) return; + + float dayFactor = Mth.clamp((float) sunDir.y, 0.0f, 1.0f); + float rainFactor = mc.level.getRainLevel(tickDelta); + float rainFactor2 = rainFactor * rainFactor; + float rainFade = 1.0f - rainFactor * 0.7f; + if (rainFade <= 0.001f) return; + + ReimaginedVisual sunModule = Modules.get(ReimaginedVisual.class); + float sunSizeSetting = sunModule != null ? sunModule.getWorldSunSize() : 0.06f; + float sunGlowSetting = sunModule != null ? sunModule.getWorldSunGlow() : 0.18f; + float sunIntensitySetting = sunModule != null ? sunModule.getWorldSunIntensity() : 1.0f; + + float yawRad = camera.yRot() * ((float) Math.PI / 180.0f); + float pitchRad = camera.xRot() * ((float) Math.PI / 180.0f); + Vec3 forward = new Vec3( + -Math.sin(yawRad) * Math.cos(pitchRad), + -Math.sin(pitchRad), + Math.cos(yawRad) * Math.cos(pitchRad) + ).normalize(); + Vec3 right = new Vec3(0.0, 1.0, 0.0).cross(forward); + if (right.lengthSqr() < 1.0E-6) { + right = new Vec3(1.0, 0.0, 0.0); + } else { + right = right.normalize(); + } + Vec3 up = forward.cross(right).normalize(); + + double viewDot = sunDir.dot(forward); + if (viewDot <= -0.2) return; + + double distance = SKY_SIZE * 0.92; + double coreRadius = SKY_SIZE * (0.045 + sunSizeSetting * 0.95); + double baseGlowScale = 1.08 + sunGlowSetting * 0.55; + float lookFactor = Mth.clamp((float) ((viewDot - 0.45) / 0.50), 0.0f, 1.0f); + lookFactor = lookFactor * lookFactor * (3.0f - 2.0f * lookFactor); + ReimaginedVisual glareModule = Modules.get(ReimaginedVisual.class); + float glareFactor = glareModule != null && glareModule.isWorldSunGlareEnabled() ? lookFactor : 0.0f; + double glowRadius = coreRadius * (baseGlowScale + glareFactor * 1.35); + double radius = glowRadius; + Vec3 center = sunDir.scale(distance); + + double angularRadius = Math.max(coreRadius / distance, 0.012); + float sunrise = 1.0f - Math.abs(dayFactor * 2.0f - 1.0f); + float noonFactor = 1.0f - sunrise; + float shadowTimeVar1 = Math.abs(sunVisibility - 0.5f) * 2.0f; + float shadowTimeVar2 = shadowTimeVar1 * shadowTimeVar1; + float shadowTime = shadowTimeVar2 * shadowTimeVar2; + + int red = 255; + int green = Mth.clamp((int) ((0.70f + 0.20f * dayFactor + 0.06f * sunrise) * 255.0f), 0, 255); + int blue = Mth.clamp((int) ((0.38f + 0.48f * dayFactor - 0.08f * sunrise) * 255.0f), 0, 255); + float alphaFactor = Mth.clamp(rainFade, 0.0f, 1.0f); + int alpha = Mth.clamp((int) (alphaFactor * 255.0f), 0, 255); + + float innerRatio = (float) Mth.clamp(coreRadius / glowRadius, 0.08, 0.9); + float haloStrength = Mth.clamp( + (0.08f + sunGlowSetting * 0.16f + glareFactor * 0.42f) + * sunIntensitySetting * rainFade, + 0.0f, + 1.10f + ); + float coreStrength = Mth.clamp(1.10f * sunIntensitySetting * rainFade, 0.0f, 2.2f); + float rayStrength = 0.0f; + + MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.WORLD_SKY_SUN); + mesh.begin(); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(center.x - right.x * radius - up.x * radius, center.y - right.y * radius - up.y * radius, center.z - right.z * radius - up.z * radius) + .vec2(0.0, 0.0).color(red, green, blue, alpha).next(); + int i2 = mesh.vec3(center.x + right.x * radius - up.x * radius, center.y + right.y * radius - up.y * radius, center.z + right.z * radius - up.z * radius) + .vec2(1.0, 0.0).color(red, green, blue, alpha).next(); + int i3 = mesh.vec3(center.x + right.x * radius + up.x * radius, center.y + right.y * radius + up.y * radius, center.z + right.z * radius + up.z * radius) + .vec2(1.0, 1.0).color(red, green, blue, alpha).next(); + int i4 = mesh.vec3(center.x - right.x * radius + up.x * radius, center.y - right.y * radius + up.y * radius, center.z - right.z * radius + up.z * radius) + .vec2(0.0, 1.0).color(red, green, blue, alpha).next(); + mesh.quad(i1, i2, i3, i4); + mesh.end(); + + SkySunUniforms.update(innerRatio, haloStrength, coreStrength, rayStrength); + MeshRenderer.begin() + .attachments(framebuffer) + .pipeline(LoVisualRenderPipelines.WORLD_SKY_SUN) + .mesh(mesh) + .uniform("SkySun", SkySunUniforms.get()) + .end(); + + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java index 43531a5..d572eca 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java @@ -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.render.sodium.bridge; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java index c140531..68045c0 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java @@ -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.render.sodium.bridge; import dev.loki.lovisual.render.sodium.interop.SodiumTerrainInterop; import dev.loki.lovisual.render.sodium.visibility.SodiumSectionVisibilityProvider; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java index 88fdba9..fb79246 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java @@ -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.render.sodium.bridge; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java index faaa5ac..3d924f3 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java @@ -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.render.sodium.interop; import dev.loki.lovisual.render.sodium.bridge.SodiumRenderBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java index 646a3f2..f62b396 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java @@ -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.render.sodium.interop; public record SodiumTerrainInteropStatsSnapshot(long terrainUpdatesScheduled, diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java index aca660d..f6df474 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java @@ -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.render.sodium.terrain; /** diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java index 0b22556..18bf6db 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java @@ -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.render.sodium.terrain; import dev.loki.lovisual.render.sodium.terrain.vertex.LoVisualModelVertexType; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java index 3727324..b0ed1cc 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java @@ -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.render.sodium.terrain.vertex; public interface LoVisualChunkVertexExtension { diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java index 9f11106..5eb46bf 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java @@ -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.render.sodium.terrain.vertex; import dev.loki.lovisual.render.sodium.terrain.LoVisualChunkMeshAttributes; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java index cd79261..ad3d05b 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java @@ -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.render.sodium.terrain.vertex; import net.caffeinemc.mods.sodium.api.util.ColorARGB; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java index f72bb2c..c1ca33f 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java @@ -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.render.sodium.visibility; import dev.loki.lovisual.render.sodium.bridge.SodiumRenderBridge; diff --git a/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java index 7f84271..6882663 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java @@ -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.render.sodium.visibility; public record SodiumVisibilityStatsSnapshot(long queries, diff --git a/mod/src/main/java/dev/loki/lovisual/util/FastFps.java b/mod/src/main/java/dev/loki/lovisual/util/FastFps.java index d1b82ad..4371801 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/FastFps.java +++ b/mod/src/main/java/dev/loki/lovisual/util/FastFps.java @@ -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.util; import lombok.Getter; diff --git a/mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java b/mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java index 25ba3c5..96b84a5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java +++ b/mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java @@ -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.util; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java b/mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java index 0d2e1b2..ceba524 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java @@ -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.util; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java b/mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java index 8e3787a..ec72b3b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java +++ b/mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java @@ -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.util; import net.minecraft.client.player.LocalPlayer; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java index 39c0f79..3d7db9b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java @@ -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.util.aiming; import dev.loki.lovisual.util.aiming.rotation.RotationUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java index f4a8788..12821d9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java @@ -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.util.aiming; import dev.loki.lovisual.features.module.core.Module; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java index f9164af..cc5458f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java @@ -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.util.aiming; public final class RestrictedSingleUseAction { diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java index ee97c18..d4b5ba5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java @@ -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.util.aiming.data; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java index b3f3646..ecf1de2 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java @@ -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.util.aiming.data; public record RotationDelta(float deltaYaw, float deltaPitch) { diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java index 4077c74..4d419e3 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java @@ -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.util.aiming.data; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java index 4aade46..aa36228 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java @@ -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.util.aiming.features; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java index e0ada2d..2c90bee 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java @@ -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.util.aiming.features.processors; import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java index 1a5c9b0..8f8e9a8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java @@ -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.util.aiming.features.processors; import dev.loki.lovisual.util.aiming.rotation.RotationTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java index 7aab61e..0ebd547 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth; import dev.loki.lovisual.util.aiming.rotation.RotationTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java index 9f46917..329783f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth; import dev.loki.lovisual.util.aiming.rotation.RotationTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java index 8858bbc..7504172 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth; import dev.loki.lovisual.util.aiming.rotation.RotationTarget; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java index f1cc6c2..5ea4f92 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl; import dev.loki.lovisual.config.values.primitive.BooleanValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java index 7650a7d..abf9d58 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java index d74c1e9..0510141 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java index 6f6efaa..eff26ae 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl.curves; import dev.loki.lovisual.config.values.primitive.NumberValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java index 6a13b64..25d9058 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl.curves; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java index 3685ea4..21dc95d 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java @@ -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.util.aiming.features.processors.anglesmooth.impl.curves; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java index 7baa695..f2ac0a7 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java @@ -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.util.aiming.features.processors.impl; import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java index 47be756..52250fd 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java @@ -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.util.aiming.features.processors.impl; import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java index 15f8087..b2aae33 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java @@ -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.util.aiming.features.processors.impl; import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java index 61741bf..48fe3b9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java @@ -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.util.aiming.features.processors.impl; import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointGeometry.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointGeometry.java new file mode 100644 index 0000000..df2c10b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointGeometry.java @@ -0,0 +1,193 @@ +package dev.loki.lovisual.util.aiming.point; + +import net.minecraft.util.Mth; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.util.aiming.data.Rotation; +import dev.loki.lovisual.util.raycast.RaycastUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Pure geometry, scoring and small random helpers shared by {@link PointTracker} + * and {@link PointProcessor}. Package-private: not part of the public aiming API. + */ +final class PointGeometry { + private PointGeometry() {} + /** Composite cost used by {@link PointTracker#findPoint} to rank candidate aim points. */ + static float scoreCandidate(Vec3 eyes, Rotation initialRotation, Rotation candidateRotation, + Vec3 point, Vec3 center, AABB box, Vec3 aimDir, + Vec3 movement, double movementStrength) { + float rotationScore = initialRotation.angleTo(candidateRotation) * 1.15f; + double halfX = Math.max(1.0E-4, box.getXsize() * 0.5); + double halfY = Math.max(1.0E-4, box.getYsize() * 0.5); + double halfZ = Math.max(1.0E-4, box.getZsize() * 0.5); + double nx = Math.abs(point.x - center.x) / halfX; + double ny = Math.abs(point.y - center.y) / halfY; + double nz = Math.abs(point.z - center.z) / halfZ; + double edgeFactor = Math.max(Math.max(nx, nz), ny * 0.65); + double centerPenalty = (1.0 - Mth.clamp(edgeFactor, 0.0, 1.0)) * (movementStrength > 0.035 ? 6.0 : 4.0); + double rayDistance = distanceToRay(eyes, aimDir, point); + double rayScale = Math.max(0.08, Math.max(box.getXsize(), Math.max(box.getYsize(), box.getZsize())) * 0.5); + double rayScore = (rayDistance / rayScale) * (movementStrength > 0.035 ? 5.5 : 4.0); + double movementBenefit = movingPointBenefit(point, center, box, movement, movementStrength); + return rotationScore + (float) rayScore + (float) centerPenalty - (float) movementBenefit; + } + /** Drops candidates that sit too close to {@code reference}, encouraging aim diversity. */ + static List filterDiverse(List candidates, Vec3 reference, AABB box, boolean movingTarget) { + List out = new ArrayList<>(); + if (reference == null) return out; + double minAxis = Math.min(box.getXsize(), Math.min(box.getYsize(), box.getZsize())); + double minDistance = Math.max(movingTarget ? 0.085 : 0.055, minAxis * (movingTarget ? 0.26 : 0.18)); + double minDistanceSq = minDistance * minDistance; + for (Candidate candidate : candidates) { + if (candidate.point().distanceToSqr(reference) >= minDistanceSq) out.add(candidate); + } + return out; + } + /** Softmax-like weighted pick among close-scoring candidates to avoid deterministic aim. */ + static Candidate selectWeighted(List candidates, float bestScore) { + if (candidates.size() == 1) return candidates.get(0); + double total = 0.0; + double[] weights = new double[candidates.size()]; + for (int i = 0; i < candidates.size(); i++) { + double delta = Math.max(0.0, candidates.get(i).score() - bestScore); + double weight = 1.0 / (1.0 + delta * delta * 0.08); + weights[i] = weight; + total += weight; + } + double pick = ThreadLocalRandom.current().nextDouble(total); + for (int i = 0; i < candidates.size(); i++) { + pick -= weights[i]; + if (pick <= 0.0) return candidates.get(i); + } + return candidates.get(candidates.size() - 1); + } + /** Bonus for leading a moving target: reward points on the forward / lateral sides of the box. */ + static double movingPointBenefit(Vec3 point, Vec3 center, AABB box, Vec3 movement, double movementStrength) { + if (movementStrength <= 0.035) return 0.0; + double halfX = Math.max(1.0E-4, box.getXsize() * 0.5); + double halfZ = Math.max(1.0E-4, box.getZsize() * 0.5); + double dirX = movement.x / movementStrength; + double dirZ = movement.z / movementStrength; + double offX = Mth.clamp((point.x - center.x) / halfX, -1.0, 1.0); + double offZ = Mth.clamp((point.z - center.z) / halfZ, -1.0, 1.0); + double lead = Mth.clamp(offX * dirX + offZ * dirZ, -1.0, 1.0); + double sideStrength = Math.min(1.0, Math.hypot(offX, offZ)); + return Math.max(0.0, lead) * 4.0 + sideStrength * 1.4; + } + static double distanceToRay(Vec3 origin, Vec3 direction, Vec3 point) { + Vec3 diff = point.subtract(origin); + double t = Math.max(0.0, diff.dot(direction)); + return point.distanceTo(origin.add(direction.scale(t))); + } + /** Intersection of the aim ray with the target box, or {@code null} if the ray misses within {@code maxDistance}. */ + static Vec3 findRayHitPoint(Vec3 eyes, Vec3 aimDir, AABB box, double maxDistance) { + double range = maxDistance > 0.0 && maxDistance < Double.MAX_VALUE ? maxDistance : 6.0; + return box.clip(eyes, eyes.add(aimDir.scale(range))).orElse(null); + } + static double horizontalLength(Vec3 vec) { + return vec == null ? 0.0 : Math.hypot(vec.x, vec.z); + } + static boolean isValidPoint(Vec3 startPoint, Vec3 endPoint, double maxDistance, boolean ignoreWalls) { + if (startPoint == null || endPoint == null) return false; + if (maxDistance > 0.0 && startPoint.distanceToSqr(endPoint) > maxDistance * maxDistance) return false; + return ignoreWalls || RaycastUtil.hasLineOfSightPoint(startPoint, endPoint); + } + /** Sample a directional offset biased along the entity's motion (or perpendicular to aim when stationary). */ + static Vec3 randomInstabilityOffset(AABB box, Vec3 entityVelocity, Vec3 aimDirection, double intensity) { + double halfX = Math.max(0.001, box.getXsize() * 0.5); + double halfY = Math.max(0.001, box.getYsize() * 0.5); + double halfZ = Math.max(0.001, box.getZsize() * 0.5); + double speed = horizontalLength(entityVelocity); + double sideX, sideZ; + if (speed > 0.035) { + sideX = entityVelocity.x / speed; + sideZ = entityVelocity.z / speed; + } else { + Vec3 horizontalAim = new Vec3(aimDirection.x, 0.0, aimDirection.z); + double aimLen = Math.max(1.0E-4, horizontalLength(horizontalAim)); + sideX = -horizontalAim.z / aimLen; + sideZ = horizontalAim.x / aimLen; + } + double lateral = randomDouble(-1.0, 1.0) * (0.14 + 0.22 * intensity); + double lead = speed > 0.035 ? randomDouble(0.05, 0.24) * intensity : 0.0; + double vertical = randomDouble(-0.16, 0.14) * intensity; + return new Vec3( + sideX * halfX * lateral + sideX * halfX * lead, + halfY * vertical, + sideZ * halfZ * lateral + sideZ * halfZ * lead + ); + } + static Vec3 clampToBox(Vec3 point, AABB box) { + return new Vec3( + Mth.clamp(point.x, box.minX, box.maxX), + Mth.clamp(point.y, box.minY, box.maxY), + Mth.clamp(point.z, box.minZ, box.maxZ) + ); + } + static Vec3 getNearestPoint(AABB box, Vec3 eyes) { + return new Vec3( + Mth.clamp(eyes.x, box.minX, box.maxX), + Mth.clamp(eyes.y, box.minY, box.maxY), + Mth.clamp(eyes.z, box.minZ, box.maxZ) + ); + } + /** Sample a grid of surface (or interior, when eyes are inside) points on the target's bounding box. */ + static List projectPointsOnBox(Vec3 eyes, AABB box) { + List points = new ArrayList<>(); + double[] steps = proportions(); + if (!box.contains(eyes)) { + for (double y : steps) { + for (double z : steps) { + points.add(new Vec3(box.minX, box.minY + box.getYsize() * y, box.minZ + box.getZsize() * z)); + points.add(new Vec3(box.maxX, box.minY + box.getYsize() * y, box.minZ + box.getZsize() * z)); + } + } + for (double x : steps) { + for (double z : steps) { + points.add(new Vec3(box.minX + box.getXsize() * x, box.minY, box.minZ + box.getZsize() * z)); + points.add(new Vec3(box.minX + box.getXsize() * x, box.maxY, box.minZ + box.getZsize() * z)); + } + } + for (double x : steps) { + for (double y : steps) { + points.add(new Vec3(box.minX + box.getXsize() * x, box.minY + box.getYsize() * y, box.minZ)); + points.add(new Vec3(box.minX + box.getXsize() * x, box.minY + box.getYsize() * y, box.maxZ)); + } + } + return points; + } + for (double x : steps) { + for (double y : steps) { + for (double z : steps) { + points.add(new Vec3( + box.minX + box.getXsize() * x, + box.minY + box.getYsize() * y, + box.minZ + box.getZsize() * z + )); + } + } + } + return points; + } + static int randomInt(int min, int max) { + if (max <= min) return min; + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + static double randomDouble(double min, double max) { + if (max <= min) return min; + return ThreadLocalRandom.current().nextDouble(min, max); + } + private static double[] proportions() { + double[] steps = new double[13]; + for (int i = 0; i < steps.length; i++) { + steps[i] = (i + 0.5) / steps.length; + } + return steps; + } + /** Scored aim candidate used by {@link PointTracker} during multi-point selection. */ + record Candidate(Vec3 point, float score) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java index a2980b0..175f63f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java @@ -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.util.aiming.point; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointProcessor.java new file mode 100644 index 0000000..dff1206 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointProcessor.java @@ -0,0 +1,185 @@ +package dev.loki.lovisual.util.aiming.point; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.util.aiming.data.Rotation; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Holds tunable aim-behaviour options and applies the post-selection point pipeline + * (delay, lazy hold, gaussian jitter, motion instability) for a single tracked entity. + * Package-private: only {@link PointTracker} composes it. + */ +final class PointProcessor { + private boolean delayEnabled = true; + private int delayMin = 2; + private int delayMax = 4; + private boolean lazyEnabled = true; + private double lazyMin = 0.10; + private double lazyMax = 0.20; + private boolean gaussianEnabled; + private double gaussianYawFactor; + private double gaussianPitchFactor; + private int gaussianChance = 100; + private double gaussianSpeed = 0.15; + private PointInsideBox delayedPoint; + private int currentDelay = PointGeometry.randomInt(2, 4); + private PointInsideBox lazyPoint; + private double currentLazyThreshold; + private Vec3 currentOffset = Vec3.ZERO; + private Vec3 targetOffset = Vec3.ZERO; + private Vec3 lastEyes; + private Vec3 lastAimDirection; + private Vec3 instabilityOffset = Vec3.ZERO; + private Vec3 targetInstabilityOffset = Vec3.ZERO; + private int instabilityRetargetTicks; + /** + * Replaces all runtime options in one shot; clamps each field so min/max pairs stay consistent. + * Called by {@link PointTracker#setOptions}. + */ + void setOptions(boolean delayEnabled, int delayMin, int delayMax, + boolean lazyEnabled, double lazyMin, double lazyMax, + boolean gaussianEnabled, double gaussianYawFactor, double gaussianPitchFactor, + int gaussianChance, double gaussianSpeed) { + this.delayEnabled = delayEnabled; + this.delayMin = Math.max(0, Math.min(delayMin, delayMax)); + this.delayMax = Math.max(this.delayMin, delayMax); + this.lazyEnabled = lazyEnabled; + this.lazyMin = Math.max(0.0, Math.min(lazyMin, lazyMax)); + this.lazyMax = Math.max(this.lazyMin, lazyMax); + this.gaussianEnabled = gaussianEnabled; + this.gaussianYawFactor = Math.max(0.0, gaussianYawFactor); + this.gaussianPitchFactor = Math.max(0.0, gaussianPitchFactor); + this.gaussianChance = Math.max(0, Math.min(100, gaussianChance)); + this.gaussianSpeed = Math.max(0.01, Math.min(1.0, gaussianSpeed)); + } + /** Clears every per-target counter so a new entity starts with fresh delay / lazy / instability state. */ + void reset() { + delayedPoint = null; + currentDelay = PointGeometry.randomInt(delayMin, delayMax); + lazyPoint = null; + currentLazyThreshold = 0.0; + currentOffset = Vec3.ZERO; + targetOffset = Vec3.ZERO; + lastEyes = null; + lastAimDirection = null; + instabilityOffset = Vec3.ZERO; + targetInstabilityOffset = Vec3.ZERO; + instabilityRetargetTicks = 0; + } + /** Runs the selected point through delay -> lazy -> gaussian -> motion-instability filters. */ + PointInsideBox process(PointInsideBox point, Vec3 eyes, Entity entity, Rotation initialRotation) { + point = applyDelay(point); + point = applyLazy(point); + point = applyGaussian(point); + return applyMotionInstability(point, eyes, entity, initialRotation); + } + /** Keeps the previously emitted point for a random short window before switching to the new one. */ + private PointInsideBox applyDelay(PointInsideBox point) { + if (!delayEnabled) { + delayedPoint = point; + currentDelay = 0; + return point; + } + if (delayedPoint != null) { + delayedPoint = new PointInsideBox(PointGeometry.clampToBox(delayedPoint.pos(), point.box()), point.box()); + } + if (point.equals(delayedPoint)) return point; + if (delayedPoint == null) { + delayedPoint = point; + currentDelay = PointGeometry.randomInt(delayMin, delayMax); + return point; + } + PointInsideBox currentPoint = delayedPoint; + currentDelay--; + if (currentDelay > 0) return currentPoint; + delayedPoint = point; + currentDelay = PointGeometry.randomInt(delayMin, delayMax); + return currentPoint; + } + /** Snaps the emitted point back to the previous one until the raw movement exceeds a lazy threshold. */ + private PointInsideBox applyLazy(PointInsideBox point) { + if (!lazyEnabled) { + lazyPoint = point; + currentLazyThreshold = 0.0; + return point; + } + if (lazyPoint != null) { + lazyPoint = new PointInsideBox(PointGeometry.clampToBox(lazyPoint.pos(), point.box()), point.box()); + } + if (lazyPoint == null) { + lazyPoint = point; + currentLazyThreshold = PointGeometry.randomDouble(lazyMin, lazyMax); + return point; + } + PointInsideBox currentPoint = lazyPoint; + double thresholdSq = currentLazyThreshold * currentLazyThreshold; + if (point.pos().distanceToSqr(currentPoint.pos()) < thresholdSq) return currentPoint; + lazyPoint = point; + currentLazyThreshold = PointGeometry.randomDouble(lazyMin, lazyMax); + return currentPoint; + } + /** Adds a slowly-lerping Gaussian offset that occasionally re-targets (per {@code gaussianChance}% of ticks). */ + private PointInsideBox applyGaussian(PointInsideBox point) { + if (!gaussianEnabled || (gaussianYawFactor <= 0.0 && gaussianPitchFactor <= 0.0)) { + currentOffset = Vec3.ZERO; + targetOffset = Vec3.ZERO; + return point; + } + if (hasReachedOffsetTarget()) { + if (ThreadLocalRandom.current().nextInt(100) < gaussianChance) { + targetOffset = new Vec3( + ThreadLocalRandom.current().nextGaussian() * gaussianYawFactor, + ThreadLocalRandom.current().nextGaussian() * gaussianPitchFactor, + ThreadLocalRandom.current().nextGaussian() * gaussianYawFactor + ); + } else { + targetOffset = Vec3.ZERO; + } + } else { + currentOffset = new Vec3( + Mth.lerp(gaussianSpeed, currentOffset.x, targetOffset.x), + Mth.lerp(gaussianSpeed, currentOffset.y, targetOffset.y), + Mth.lerp(gaussianSpeed, currentOffset.z, targetOffset.z) + ); + } + return new PointInsideBox(point.pos().add(currentOffset), point.box()); + } + /** Biases the aim point outward when the player turns fast, moves, or the target itself is in motion. */ + private PointInsideBox applyMotionInstability(PointInsideBox point, Vec3 eyes, Entity entity, Rotation initialRotation) { + Vec3 aimDirection = initialRotation.directionVector().normalize(); + double angleChange = lastAimDirection != null + ? Math.toDegrees(Math.acos(Mth.clamp(lastAimDirection.dot(aimDirection), -1.0, 1.0))) + : 0.0; + double playerMove = lastEyes != null ? eyes.distanceTo(lastEyes) : 0.0; + Vec3 entityVelocity = entity != null ? entity.getDeltaMovement() : Vec3.ZERO; + double entityMove = PointGeometry.horizontalLength(entityVelocity) + Math.abs(entityVelocity.y) * 0.45; + lastEyes = eyes; + lastAimDirection = aimDirection; + double intensity = Mth.clamp(angleChange / 14.0 + playerMove / 0.14 + entityMove / 0.11, 0.0, 1.0); + if (intensity < 0.12) { + instabilityOffset = instabilityOffset.scale(0.72); + targetInstabilityOffset = targetInstabilityOffset.scale(0.72); + return new PointInsideBox(PointGeometry.clampToBox(point.pos().add(instabilityOffset), point.box()), point.box()); + } + instabilityRetargetTicks--; + if (instabilityRetargetTicks <= 0 || instabilityOffset.distanceToSqr(targetInstabilityOffset) < 1.0E-4) { + targetInstabilityOffset = PointGeometry.randomInstabilityOffset(point.box(), entityVelocity, aimDirection, intensity); + instabilityRetargetTicks = PointGeometry.randomInt(2, intensity > 0.65 ? 4 : 6); + } + double blend = 0.14 + 0.18 * intensity; + instabilityOffset = new Vec3( + Mth.lerp(blend, instabilityOffset.x, targetInstabilityOffset.x), + Mth.lerp(blend, instabilityOffset.y, targetInstabilityOffset.y), + Mth.lerp(blend, instabilityOffset.z, targetInstabilityOffset.z) + ); + return new PointInsideBox(PointGeometry.clampToBox(point.pos().add(instabilityOffset), point.box()), point.box()); + } + private boolean hasReachedOffsetTarget() { + return Math.abs(currentOffset.x - targetOffset.x) < 0.01 + && Math.abs(currentOffset.y - targetOffset.y) < 0.01 + && Math.abs(currentOffset.z - targetOffset.z) < 0.01; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java index fd5c03f..9689ab4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java @@ -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.util.aiming.point; import net.minecraft.util.Mth; @@ -13,265 +6,25 @@ import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import dev.loki.lovisual.util.aiming.data.Rotation; import dev.loki.lovisual.util.entity.simulation.PositionExtrapolation; -import dev.loki.lovisual.util.raycast.RaycastUtil; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; +/** + * Chooses and stabilises an aim point on a target's hitbox across ticks. + * Public entry point of the point-tracking util: callers invoke {@link #findPoint} every tick + * and configure behaviour via {@link #setOptions}. + * + *

    Internal layout: pure geometry helpers live in {@link PointGeometry}; the stateful + * post-selection pipeline (delay, lazy hold, gaussian jitter, motion instability) lives in + * {@link PointProcessor}. This class keeps only cross-tick selection state and continuity.

    + */ public final class PointTracker { - + private final PointProcessor processor = new PointProcessor(); private int trackedEntityId = -1; - private PointInsideBox delayedPoint; - private int currentDelay = randomInt(2, 4); - private PointInsideBox lazyPoint; - private double currentLazyThreshold; - private Vec3 currentOffset = Vec3.ZERO; - private Vec3 targetOffset = Vec3.ZERO; private Vec3 smoothedPoint; private Vec3 lastSelectedPoint; - private Vec3 lastEyes; - private Vec3 lastAimDirection; - private Vec3 instabilityOffset = Vec3.ZERO; - private Vec3 targetInstabilityOffset = Vec3.ZERO; - private int instabilityRetargetTicks; - - private boolean delayEnabled = true; - private int delayMin = 2; - private int delayMax = 4; - private boolean lazyEnabled = true; - private double lazyMin = 0.10; - private double lazyMax = 0.20; - private boolean gaussianEnabled; - private double gaussianYawFactor; - private double gaussianPitchFactor; - private int gaussianChance = 100; - private double gaussianSpeed = 0.15; - - private static float scoreCandidate(Vec3 eyes, - Rotation initialRotation, - Rotation candidateRotation, - Vec3 point, - Vec3 center, - AABB box, - Vec3 aimDir, - Vec3 movement, - double movementStrength) { - float rotationScore = initialRotation.angleTo(candidateRotation) * 1.15f; - double halfX = Math.max(1.0E-4, box.getXsize() * 0.5); - double halfY = Math.max(1.0E-4, box.getYsize() * 0.5); - double halfZ = Math.max(1.0E-4, box.getZsize() * 0.5); - - double nx = Math.abs(point.x - center.x) / halfX; - double ny = Math.abs(point.y - center.y) / halfY; - double nz = Math.abs(point.z - center.z) / halfZ; - double edgeFactor = Math.max(Math.max(nx, nz), ny * 0.65); - double centerPenalty = (1.0 - Mth.clamp(edgeFactor, 0.0, 1.0)) - * (movementStrength > 0.035 ? 6.0 : 4.0); - - double rayDistance = distanceToRay(eyes, aimDir, point); - double rayScale = Math.max(0.08, Math.max(box.getXsize(), Math.max(box.getYsize(), box.getZsize())) * 0.5); - double rayScore = (rayDistance / rayScale) * (movementStrength > 0.035 ? 5.5 : 4.0); - - double movementBenefit = movingPointBenefit(point, center, box, movement, movementStrength); - return rotationScore + (float) rayScore + (float) centerPenalty - (float) movementBenefit; - } - - private static List filterDiverse(List candidates, Vec3 reference, AABB box, boolean movingTarget) { - List out = new ArrayList<>(); - if (reference == null) { - return out; - } - - double minAxis = Math.min(box.getXsize(), Math.min(box.getYsize(), box.getZsize())); - double minDistance = Math.max(movingTarget ? 0.085 : 0.055, minAxis * (movingTarget ? 0.26 : 0.18)); - double minDistanceSq = minDistance * minDistance; - for (Candidate candidate : candidates) { - if (candidate.point().distanceToSqr(reference) >= minDistanceSq) { - out.add(candidate); - } - } - return out; - } - - private static Candidate selectWeighted(List candidates, float bestScore) { - if (candidates.size() == 1) { - return candidates.get(0); - } - - double total = 0.0; - double[] weights = new double[candidates.size()]; - for (int i = 0; i < candidates.size(); i++) { - double delta = Math.max(0.0, candidates.get(i).score() - bestScore); - double weight = 1.0 / (1.0 + delta * delta * 0.08); - weights[i] = weight; - total += weight; - } - - double pick = ThreadLocalRandom.current().nextDouble(total); - for (int i = 0; i < candidates.size(); i++) { - pick -= weights[i]; - if (pick <= 0.0) { - return candidates.get(i); - } - } - return candidates.get(candidates.size() - 1); - } - - private static double movingPointBenefit(Vec3 point, Vec3 center, AABB box, Vec3 movement, double movementStrength) { - if (movementStrength <= 0.035) { - return 0.0; - } - - double halfX = Math.max(1.0E-4, box.getXsize() * 0.5); - double halfZ = Math.max(1.0E-4, box.getZsize() * 0.5); - double dirX = movement.x / movementStrength; - double dirZ = movement.z / movementStrength; - double offX = Mth.clamp((point.x - center.x) / halfX, -1.0, 1.0); - double offZ = Mth.clamp((point.z - center.z) / halfZ, -1.0, 1.0); - double lead = Mth.clamp(offX * dirX + offZ * dirZ, -1.0, 1.0); - double sideStrength = Math.min(1.0, Math.hypot(offX, offZ)); - - return Math.max(0.0, lead) * 4.0 + sideStrength * 1.4; - } - - private static double distanceToRay(Vec3 origin, Vec3 direction, Vec3 point) { - Vec3 diff = point.subtract(origin); - double t = Math.max(0.0, diff.dot(direction)); - Vec3 closest = origin.add(direction.scale(t)); - return point.distanceTo(closest); - } - - private static Vec3 findRayHitPoint(Vec3 eyes, Vec3 aimDir, AABB box, double maxDistance) { - double range = maxDistance > 0.0 && maxDistance < Double.MAX_VALUE ? maxDistance : 6.0; - return box.clip(eyes, eyes.add(aimDir.scale(range))).orElse(null); - } - - private static double horizontalLength(Vec3 vec) { - if (vec == null) { - return 0.0; - } - return Math.hypot(vec.x, vec.z); - } - - private static boolean isValidPoint(Vec3 startPoint, - Vec3 endPoint, - double maxDistance, - boolean ignoreWalls) { - if (startPoint == null || endPoint == null) { - return false; - } - - if (maxDistance > 0.0 && startPoint.distanceToSqr(endPoint) > maxDistance * maxDistance) { - return false; - } - - return ignoreWalls || RaycastUtil.hasLineOfSightPoint(startPoint, endPoint); - } - - private static Vec3 randomInstabilityOffset(AABB box, Vec3 entityVelocity, Vec3 aimDirection, double intensity) { - double halfX = Math.max(0.001, box.getXsize() * 0.5); - double halfY = Math.max(0.001, box.getYsize() * 0.5); - double halfZ = Math.max(0.001, box.getZsize() * 0.5); - - double speed = horizontalLength(entityVelocity); - double sideX; - double sideZ; - if (speed > 0.035) { - sideX = entityVelocity.x / speed; - sideZ = entityVelocity.z / speed; - } else { - Vec3 horizontalAim = new Vec3(aimDirection.x, 0.0, aimDirection.z); - double aimLen = Math.max(1.0E-4, horizontalLength(horizontalAim)); - sideX = -horizontalAim.z / aimLen; - sideZ = horizontalAim.x / aimLen; - } - - double lateral = randomDouble(-1.0, 1.0) * (0.14 + 0.22 * intensity); - double lead = speed > 0.035 ? randomDouble(0.05, 0.24) * intensity : 0.0; - double vertical = randomDouble(-0.16, 0.14) * intensity; - - return new Vec3( - sideX * halfX * lateral + sideX * halfX * lead, - halfY * vertical, - sideZ * halfZ * lateral + sideZ * halfZ * lead - ); - } - - private static Vec3 clampToBox(Vec3 point, AABB box) { - return new Vec3( - Mth.clamp(point.x, box.minX, box.maxX), - Mth.clamp(point.y, box.minY, box.maxY), - Mth.clamp(point.z, box.minZ, box.maxZ) - ); - } - - private static Vec3 getNearestPoint(AABB box, Vec3 eyes) { - return new Vec3( - Mth.clamp(eyes.x, box.minX, box.maxX), - Mth.clamp(eyes.y, box.minY, box.maxY), - Mth.clamp(eyes.z, box.minZ, box.maxZ) - ); - } - - private static List projectPointsOnBox(Vec3 eyes, AABB box) { - List points = new ArrayList<>(); - double[] steps = proportions(); - - if (!box.contains(eyes)) { - for (double y : steps) { - for (double z : steps) { - points.add(new Vec3(box.minX, box.minY + box.getYsize() * y, box.minZ + box.getZsize() * z)); - points.add(new Vec3(box.maxX, box.minY + box.getYsize() * y, box.minZ + box.getZsize() * z)); - } - } - for (double x : steps) { - for (double z : steps) { - points.add(new Vec3(box.minX + box.getXsize() * x, box.minY, box.minZ + box.getZsize() * z)); - points.add(new Vec3(box.minX + box.getXsize() * x, box.maxY, box.minZ + box.getZsize() * z)); - } - } - for (double x : steps) { - for (double y : steps) { - points.add(new Vec3(box.minX + box.getXsize() * x, box.minY + box.getYsize() * y, box.minZ)); - points.add(new Vec3(box.minX + box.getXsize() * x, box.minY + box.getYsize() * y, box.maxZ)); - } - } - return points; - } - - for (double x : steps) { - for (double y : steps) { - for (double z : steps) { - points.add(new Vec3( - box.minX + box.getXsize() * x, - box.minY + box.getYsize() * y, - box.minZ + box.getZsize() * z - )); - } - } - } - return points; - } - - private static int randomInt(int min, int max) { - if (max <= min) return min; - return ThreadLocalRandom.current().nextInt(min, max + 1); - } - - private static double randomDouble(double min, double max) { - if (max <= min) return min; - return ThreadLocalRandom.current().nextDouble(min, max); - } - - private static double[] proportions() { - double[] steps = new double[13]; - for (int i = 0; i < steps.length; i++) { - steps[i] = (i + 0.5) / steps.length; - } - return steps; - } - + /** Applies runtime tuning for the point-processing pipeline. See {@link PointProcessor#setOptions}. */ public void setOptions(boolean delayEnabled, int delayMin, int delayMax, @@ -283,19 +36,13 @@ public final class PointTracker { double gaussianPitchFactor, int gaussianChance, double gaussianSpeed) { - this.delayEnabled = delayEnabled; - this.delayMin = Math.max(0, Math.min(delayMin, delayMax)); - this.delayMax = Math.max(this.delayMin, delayMax); - this.lazyEnabled = lazyEnabled; - this.lazyMin = Math.max(0.0, Math.min(lazyMin, lazyMax)); - this.lazyMax = Math.max(this.lazyMin, lazyMax); - this.gaussianEnabled = gaussianEnabled; - this.gaussianYawFactor = Math.max(0.0, gaussianYawFactor); - this.gaussianPitchFactor = Math.max(0.0, gaussianPitchFactor); - this.gaussianChance = Math.max(0, Math.min(100, gaussianChance)); - this.gaussianSpeed = Math.max(0.01, Math.min(1.0, gaussianSpeed)); + processor.setOptions(delayEnabled, delayMin, delayMax, lazyEnabled, lazyMin, lazyMax, + gaussianEnabled, gaussianYawFactor, gaussianPitchFactor, gaussianChance, gaussianSpeed); } - + /** + * Computes the aim point for the entity's hitbox extrapolated {@code ticks} forward, + * honouring max-distance and wall visibility constraints. + */ public PointInsideBox findPoint(Vec3 eyes, Entity entity, int ticks, @@ -303,38 +50,42 @@ public final class PointTracker { double maxDistance, boolean ignoreWalls) { AABB box = PositionExtrapolation.getBestForEntity(entity).getBoxInTicks(ticks); - List points = projectPointsOnBox(eyes, box); - + List points = PointGeometry.projectPointsOnBox(eyes, box); if (trackedEntityId != entity.getId()) { - resetProcessors(); + reset(); trackedEntityId = entity.getId(); } - Vec3 bestPoint = selectMultiPoint(eyes, entity, box, initialRotation, maxDistance, ignoreWalls); if (bestPoint == null) { - Vec3 anchor = smoothedPoint != null ? clampToBox(smoothedPoint, box) : eyes; - bestPoint = points.isEmpty() ? getNearestPoint(box, anchor) : points.get(0); + Vec3 anchor = smoothedPoint != null ? PointGeometry.clampToBox(smoothedPoint, box) : eyes; + bestPoint = points.isEmpty() ? PointGeometry.getNearestPoint(box, anchor) : points.get(0); for (Vec3 point : points) { if (point.distanceToSqr(anchor) < bestPoint.distanceToSqr(anchor)) { bestPoint = point; } } } - - PointInsideBox point = new PointInsideBox(clampToBox(bestPoint, box), box); - point = applyDelay(point); - point = applyLazy(point); - point = applyGaussian(point); - point = applyMotionInstability(point, eyes, entity, initialRotation); + PointInsideBox point = new PointInsideBox(PointGeometry.clampToBox(bestPoint, box), box); + point = processor.process(point, eyes, entity, initialRotation); point = applyContinuity(point); smoothedPoint = point.pos(); - return new PointInsideBox(clampToBox(point.pos(), box), box); + return new PointInsideBox(PointGeometry.clampToBox(point.pos(), box), box); } - + /** Convenience overload with unlimited range and walls ignored. */ public PointInsideBox findPoint(Vec3 eyes, Entity entity, int ticks, Rotation initialRotation) { return findPoint(eyes, entity, ticks, initialRotation, Double.MAX_VALUE, true); } - + /** Forgets the tracked entity and every per-target counter (selection, delay, lazy, gaussian, instability). */ + public void reset() { + trackedEntityId = -1; + smoothedPoint = null; + lastSelectedPoint = null; + processor.reset(); + } + /** + * Scores every projected box point (plus the aim-ray hit) and picks one from a + * diverse, weighted pool. Falls back to {@code null} when no valid candidate exists. + */ private Vec3 selectMultiPoint(Vec3 eyes, Entity entity, AABB box, @@ -344,46 +95,41 @@ public final class PointTracker { if (eyes == null || box == null || initialRotation == null) { return null; } - - List candidates = new ArrayList<>(); + List candidates = new ArrayList<>(); float bestScore = Float.MAX_VALUE; Vec3 center = box.getCenter(); Vec3 aimDir = initialRotation.directionVector().normalize(); Vec3 movement = entity != null ? entity.getDeltaMovement() : Vec3.ZERO; - double movementStrength = horizontalLength(movement); - - Vec3 rayHit = findRayHitPoint(eyes, aimDir, box, maxDistance); - if (rayHit != null && isValidPoint(eyes, rayHit, maxDistance, ignoreWalls)) { + double movementStrength = PointGeometry.horizontalLength(movement); + Vec3 rayHit = PointGeometry.findRayHitPoint(eyes, aimDir, box, maxDistance); + if (rayHit != null && PointGeometry.isValidPoint(eyes, rayHit, maxDistance, ignoreWalls)) { Rotation rayRotation = Rotation.lookingAt(rayHit, eyes).normalize(); - float score = scoreCandidate(eyes, initialRotation, rayRotation, rayHit, center, box, + float score = PointGeometry.scoreCandidate(eyes, initialRotation, rayRotation, rayHit, center, box, aimDir, movement, movementStrength) - 2.0f; - candidates.add(new Candidate(rayHit, score)); + candidates.add(new PointGeometry.Candidate(rayHit, score)); bestScore = score; } - - for (Vec3 point : projectPointsOnBox(eyes, box)) { - if (!isValidPoint(eyes, point, maxDistance, ignoreWalls)) { + for (Vec3 point : PointGeometry.projectPointsOnBox(eyes, box)) { + if (!PointGeometry.isValidPoint(eyes, point, maxDistance, ignoreWalls)) { continue; } Rotation candidateRotation = Rotation.lookingAt(point, eyes).normalize(); - float score = scoreCandidate(eyes, initialRotation, candidateRotation, point, center, box, + float score = PointGeometry.scoreCandidate(eyes, initialRotation, candidateRotation, point, center, box, aimDir, movement, movementStrength); - candidates.add(new Candidate(point, score)); + candidates.add(new PointGeometry.Candidate(point, score)); if (score < bestScore) { bestScore = score; } } - if (candidates.isEmpty()) { return null; } - boolean movingTarget = movementStrength > 0.035 || Math.abs(movement.y) > 0.03; float threshold = movingTarget ? Math.max(7.0f, Math.min(28.0f, bestScore * 0.55f + 6.0f)) : Math.max(4.0f, Math.min(18.0f, bestScore * 0.35f + 3.0f)); - List pool = new ArrayList<>(); - for (Candidate candidate : candidates) { + List pool = new ArrayList<>(); + for (PointGeometry.Candidate candidate : candidates) { if (candidate.score() <= bestScore + threshold) { pool.add(candidate); } @@ -391,177 +137,26 @@ public final class PointTracker { if (pool.isEmpty()) { pool = candidates; } - Vec3 reference = lastSelectedPoint != null ? lastSelectedPoint : smoothedPoint; - List diversePool = filterDiverse(pool, reference, box, movingTarget); + List diversePool = PointGeometry.filterDiverse(pool, reference, box, movingTarget); if (!diversePool.isEmpty()) { pool = diversePool; } - - Candidate selected = selectWeighted(pool, bestScore); + PointGeometry.Candidate selected = PointGeometry.selectWeighted(pool, bestScore); lastSelectedPoint = selected.point(); return selected.point(); } - - public void reset() { - trackedEntityId = -1; - resetProcessors(); - } - - private void resetProcessors() { - delayedPoint = null; - currentDelay = randomInt(delayMin, delayMax); - lazyPoint = null; - currentLazyThreshold = 0.0; - currentOffset = Vec3.ZERO; - targetOffset = Vec3.ZERO; - smoothedPoint = null; - lastSelectedPoint = null; - lastEyes = null; - lastAimDirection = null; - instabilityOffset = Vec3.ZERO; - targetInstabilityOffset = Vec3.ZERO; - instabilityRetargetTicks = 0; - } - - private PointInsideBox applyDelay(PointInsideBox point) { - if (!delayEnabled) { - delayedPoint = point; - currentDelay = 0; - return point; - } - - if (delayedPoint != null) { - delayedPoint = new PointInsideBox(clampToBox(delayedPoint.pos(), point.box()), point.box()); - } - - if (point.equals(delayedPoint)) { - return point; - } - - if (delayedPoint == null) { - delayedPoint = point; - currentDelay = randomInt(delayMin, delayMax); - return point; - } - - PointInsideBox currentPoint = delayedPoint; - currentDelay--; - if (currentDelay > 0) { - return currentPoint; - } - - delayedPoint = point; - currentDelay = randomInt(delayMin, delayMax); - return currentPoint; - } - - private PointInsideBox applyLazy(PointInsideBox point) { - if (!lazyEnabled) { - lazyPoint = point; - currentLazyThreshold = 0.0; - return point; - } - - if (lazyPoint != null) { - lazyPoint = new PointInsideBox(clampToBox(lazyPoint.pos(), point.box()), point.box()); - } - - if (lazyPoint == null) { - lazyPoint = point; - currentLazyThreshold = randomDouble(lazyMin, lazyMax); - return point; - } - - PointInsideBox currentPoint = lazyPoint; - double thresholdSq = currentLazyThreshold * currentLazyThreshold; - if (point.pos().distanceToSqr(currentPoint.pos()) < thresholdSq) { - return currentPoint; - } - - lazyPoint = point; - currentLazyThreshold = randomDouble(lazyMin, lazyMax); - return currentPoint; - } - - private PointInsideBox applyGaussian(PointInsideBox point) { - if (!gaussianEnabled || (gaussianYawFactor <= 0.0 && gaussianPitchFactor <= 0.0)) { - currentOffset = Vec3.ZERO; - targetOffset = Vec3.ZERO; - return point; - } - - if (hasReachedOffsetTarget()) { - if (ThreadLocalRandom.current().nextInt(100) < gaussianChance) { - targetOffset = new Vec3( - ThreadLocalRandom.current().nextGaussian() * gaussianYawFactor, - ThreadLocalRandom.current().nextGaussian() * gaussianPitchFactor, - ThreadLocalRandom.current().nextGaussian() * gaussianYawFactor - ); - } else { - targetOffset = Vec3.ZERO; - } - } else { - currentOffset = new Vec3( - Mth.lerp(gaussianSpeed, currentOffset.x, targetOffset.x), - Mth.lerp(gaussianSpeed, currentOffset.y, targetOffset.y), - Mth.lerp(gaussianSpeed, currentOffset.z, targetOffset.z) - ); - } - - return new PointInsideBox(point.pos().add(currentOffset), point.box()); - } - - private PointInsideBox applyMotionInstability(PointInsideBox point, - Vec3 eyes, - Entity entity, - Rotation initialRotation) { - Vec3 aimDirection = initialRotation.directionVector().normalize(); - double angleChange = lastAimDirection != null - ? Math.toDegrees(Math.acos(Mth.clamp(lastAimDirection.dot(aimDirection), -1.0, 1.0))) - : 0.0; - double playerMove = lastEyes != null ? eyes.distanceTo(lastEyes) : 0.0; - Vec3 entityVelocity = entity != null ? entity.getDeltaMovement() : Vec3.ZERO; - double entityMove = horizontalLength(entityVelocity) + Math.abs(entityVelocity.y) * 0.45; - - lastEyes = eyes; - lastAimDirection = aimDirection; - - double intensity = Mth.clamp(angleChange / 14.0 + playerMove / 0.14 + entityMove / 0.11, 0.0, 1.0); - if (intensity < 0.12) { - instabilityOffset = instabilityOffset.scale(0.72); - targetInstabilityOffset = targetInstabilityOffset.scale(0.72); - return new PointInsideBox(clampToBox(point.pos().add(instabilityOffset), point.box()), point.box()); - } - - instabilityRetargetTicks--; - if (instabilityRetargetTicks <= 0 || instabilityOffset.distanceToSqr(targetInstabilityOffset) < 1.0E-4) { - targetInstabilityOffset = randomInstabilityOffset(point.box(), entityVelocity, aimDirection, intensity); - instabilityRetargetTicks = randomInt(2, intensity > 0.65 ? 4 : 6); - } - - double blend = 0.14 + 0.18 * intensity; - instabilityOffset = new Vec3( - Mth.lerp(blend, instabilityOffset.x, targetInstabilityOffset.x), - Mth.lerp(blend, instabilityOffset.y, targetInstabilityOffset.y), - Mth.lerp(blend, instabilityOffset.z, targetInstabilityOffset.z) - ); - - return new PointInsideBox(clampToBox(point.pos().add(instabilityOffset), point.box()), point.box()); - } - + /** Lerps the freshly processed point towards the previous smoothed position so aim does not teleport. */ private PointInsideBox applyContinuity(PointInsideBox point) { if (smoothedPoint == null) { return point; } - - Vec3 current = clampToBox(smoothedPoint, point.box()); - Vec3 target = clampToBox(point.pos(), point.box()); + Vec3 current = PointGeometry.clampToBox(smoothedPoint, point.box()); + Vec3 target = PointGeometry.clampToBox(point.pos(), point.box()); double distance = current.distanceTo(target); if (distance <= 1.0E-4) { return new PointInsideBox(current, point.box()); } - double blend = distance > 0.35 ? 0.42 : distance > 0.15 ? 0.28 : 0.18; Vec3 blended = new Vec3( Mth.lerp(blend, current.x, target.x), @@ -570,13 +165,4 @@ public final class PointTracker { ); return new PointInsideBox(blended, point.box()); } - - private boolean hasReachedOffsetTarget() { - return Math.abs(currentOffset.x - targetOffset.x) < 0.01 - && Math.abs(currentOffset.y - targetOffset.y) < 0.01 - && Math.abs(currentOffset.z - targetOffset.z) < 0.01; - } - - private record Candidate(Vec3 point, float score) { - } } diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java index fe03485..5fc8a53 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java @@ -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.util.aiming.preference; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java index 1006563..116f991 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java @@ -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.util.aiming.preference; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java index 20977b3..ad21e12 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java @@ -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.util.aiming.raytrace; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java index c0cfbd8..d25bec4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java @@ -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.util.aiming.rotation; import dev.loki.lovisual.util.aiming.RequestHandler; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java index 4677864..4c461ef 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java @@ -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.util.aiming.rotation; import dev.loki.lovisual.util.aiming.RestrictedSingleUseAction; diff --git a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java index 944dd53..87b8eb2 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java @@ -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.util.aiming.rotation; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java b/mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java index 0788c45..7e4d9be 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java @@ -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.util.animation; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java b/mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java index c145386..44eeb6e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java @@ -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.util.animation; import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems; diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java b/mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java index 1f0c442..83197ef 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java @@ -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.util.animation; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java b/mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java index 0ca5f5d..2b14e72 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java @@ -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.util.animation; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java index 32c92fd..4342b3b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java @@ -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.util.animation.hand; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java index 870dd13..eb46e12 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java @@ -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.util.animation.hand; import dev.loki.lovisual.util.animation.hand.state.HandAnimState; diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java index 5b4d0c3..e56b893 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java @@ -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.util.animation.hand; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java index 2501d28..22f3eb1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java @@ -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.util.animation.hand; import net.minecraft.tags.ItemTags; diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java index b6422bc..ede4a8e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java @@ -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.util.animation.hand.state; import dev.loki.lovisual.util.animation.SmoothValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java index 9510951..52d41ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java +++ b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java @@ -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.util.animation.hand.state; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java index 59cba63..64e5f1a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java @@ -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.util.block.bed; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java index 434412c..8cb7847 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java @@ -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.util.block.bed; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java index 885fac0..5894766 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java @@ -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.util.block.bed; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java index 5e94635..41ad5c8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java @@ -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.util.block.bed; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java index 8468e95..d4079b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java @@ -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.util.block.render; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java index 26c6221..4467d4c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java @@ -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.util.block.render; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java index 81d906d..1ee73aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java @@ -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.util.block.render; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java index 0abebcb..b5ef7b0 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java @@ -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.util.block.render; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java index ea21eb8..98c5385 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java @@ -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.util.block.scan; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java index 2889aeb..0daa0b7 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java @@ -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.util.block.scan; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java index afc4c58..7cfcb95 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java @@ -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.util.block.scan; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java index 77ae5e7..b61bd76 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java @@ -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.util.block.scan; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java b/mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java index a409872..fa1b7a1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java +++ b/mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java @@ -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.util.click; import net.minecraft.client.player.LocalPlayer; diff --git a/mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java b/mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java index 5213a61..3cfc5d7 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java +++ b/mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java @@ -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.util.click; import dev.loki.lovisual.config.values.primitive.BooleanMapValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java b/mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java index 165694e..ce44489 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java +++ b/mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java @@ -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.util.click; import java.util.Arrays; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java index 9b90227..4c737e1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java @@ -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.util.combat.attack; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java index 737e0da..70a10cf 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java @@ -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.util.combat.attack; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java index b38b226..eca71e2 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java @@ -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.util.combat.attack; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java b/mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java index 50fb5f2..bdb2e4c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java @@ -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.util.combat.hit; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java index 3acca45..9c6fdbd 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java @@ -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.util.combat.protocol; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java index c63117b..3cdf09c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java @@ -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.util.combat.protocol; import com.viaversion.viafabricplus.ViaFabricPlus; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java index 9dd808c..3daf1d9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java @@ -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.util.combat.protocol.heuristics; public enum CombatProtocolHeuristicSource { diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java index 01d3c27..8206c48 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java @@ -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.util.combat.protocol.heuristics; import dev.loki.lovisual.events.meta.EventHandler; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java index 77c98da..620ce73 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java @@ -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.util.combat.protocol.heuristics; import dev.loki.lovisual.config.definition.ConfigNameProvider; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java index 15b1a6e..a5153df 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java @@ -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.util.combat.protocol.vfp; import com.viaversion.viafabricplus.ViaFabricPlus; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java index a489507..153cb92 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java @@ -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.util.combat.protocol.vfp; import com.viaversion.viafabricplus.ViaFabricPlus; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java index 03cd71f..e9cb5aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java @@ -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.util.combat.rotation; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java index 08b1c97..e8cab97 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java @@ -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.util.combat.rotation; import net.minecraft.core.Holder; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java index 732eba2..ee7070c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java @@ -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.util.combat.rotation; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java index 6f0513f..cfcb977 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java @@ -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.util.combat.rotation; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java index 8df3c8a..17a3866 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java @@ -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.util.combat.tracking; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java index 94339da..d57734f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java +++ b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java @@ -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.util.combat.tracking; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java b/mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java index 91097d9..4410fca 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java @@ -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.util.entity; import net.minecraft.client.player.LocalPlayer; @@ -21,8 +14,6 @@ public enum EagleUtil { private static final double MIN_AXIS_COMPONENT = 0.18; private static final double MAX_RESCUE_LOOKAHEAD = 0.42; private static final double MIN_TICK_DELTA_LEAD = 0.25; - private static final double CENTER_DEAD_ANGLE_COS = 0.34; - private static final int DIAGONAL_RESCUE_TICKS = 2; public static EdgeCheck checkEdge(LocalPlayer player, MovementInputEvent event, double edgeDistance) { return checkEdge(player, event, edgeDistance, TickDelta.get()); @@ -173,196 +164,6 @@ public enum EagleUtil { return Math.max(0.0, Math.min(1.0, value)); } - public enum RecoveryMode { - STOP, - INVERT, - CENTER - } - - public static final class EdgeRecovery { - private Vec3 center; - private int overwriteTicks; - private int sneakTicks; - - private static Vec3 blockBottomCenter(LocalPlayer player) { - return new Vec3( - player.blockPosition().getX() + 0.5, - player.blockPosition().getY(), - player.blockPosition().getZ() + 0.5 - ); - } - - private static double horizontalDistanceSquared(Vec3 a, Vec3 b) { - double x = a.x - b.x; - double z = a.z - b.z; - return x * x + z * z; - } - - public boolean handleDiagonalRescue(LocalPlayer player, MovementInputEvent event, double edgeDistance) { - if (player == null || event == null) { - return false; - } - - updateSafeCenter(player); - - EdgeCheck edgeCheck = checkEdge(player, event, edgeDistance); - if (edgeCheck.diagonalRescue() && shouldOverwriteInput(player)) { - overwriteTicks = Math.max(overwriteTicks, DIAGONAL_RESCUE_TICKS); - } - - if (overwriteTicks <= 0) { - return false; - } - - overwriteTicks--; - applyInputTowardsCenter(player, event); - event.setJump(false); - event.setSneak(true); - event.setSprint(false); - - updateSafeCenter(player); - return true; - } - - public boolean handleOnEdge( - LocalPlayer player, - MovementInputEvent event, - double edgeDistance, - RecoveryMode mode, - int keepTicks, - int requestedSneakTicks, - boolean jump - ) { - if (player == null || event == null) { - return false; - } - - updateSafeCenter(player); - - double distance = Math.min(horizontalSpeed(player), Math.max(0.0, edgeDistance)); - EdgeCheck edgeCheck = checkEdge(player, event, distance); - if (edgeCheck.closeToEdge() && shouldOverwriteInput(player)) { - overwriteTicks = Math.max(overwriteTicks, Math.max(1, keepTicks)); - sneakTicks = Math.max(sneakTicks, Math.max(0, requestedSneakTicks)); - } - - boolean applied = false; - if (overwriteTicks > 0) { - overwriteTicks--; - applyRecoveryInput(player, event, mode); - event.setJump(jump); - event.setSprint(false); - applied = true; - } - - if (sneakTicks > 0) { - sneakTicks--; - event.setSneak(true); - applied = true; - } - - updateSafeCenter(player); - return applied; - } - - public void reset() { - center = null; - overwriteTicks = 0; - sneakTicks = 0; - } - - private boolean shouldOverwriteInput(LocalPlayer player) { - Vec3 center = this.center; - if (center == null) { - return true; - } - - Vec3 horizontalVelocity = new Vec3(player.getDeltaMovement().x, 0.0, player.getDeltaMovement().z); - if (horizontalVelocity.horizontalDistanceSqr() <= 0.02 * 0.02) { - return true; - } - - double currentDistance = horizontalDistanceSquared(center, player.position()); - double nextDistance = horizontalDistanceSquared(center, player.position().add(horizontalVelocity)); - return nextDistance > currentDistance; - } - - private void applyRecoveryInput(LocalPlayer player, MovementInputEvent event, RecoveryMode mode) { - RecoveryMode resolvedMode = mode != null ? mode : RecoveryMode.CENTER; - if (resolvedMode == RecoveryMode.INVERT) { - boolean forward = event.isForward(); - boolean backward = event.isBackward(); - boolean left = event.isLeft(); - boolean right = event.isRight(); - event.setForward(backward); - event.setBackward(forward); - event.setLeft(right); - event.setRight(left); - event.setJump(false); - return; - } - - if (resolvedMode == RecoveryMode.STOP && horizontalSpeed(player) <= 0.05) { - event.setForward(false); - event.setBackward(false); - event.setLeft(false); - event.setRight(false); - event.setJump(false); - return; - } - - applyInputTowardsCenter(player, event); - } - - private void applyInputTowardsCenter(LocalPlayer player, MovementInputEvent event) { - Vec3 center = this.center != null ? this.center : blockBottomCenter(player); - Vec3 toCenter = center.subtract(player.position()); - Vec3 horizontal = new Vec3(toCenter.x, 0.0, toCenter.z); - - if (horizontal.lengthSqr() < 0.015 * 0.015) { - event.setForward(false); - event.setBackward(false); - event.setLeft(false); - event.setRight(false); - return; - } - - Vec3 direction = horizontal.normalize(); - double rad = Math.toRadians(player.getYRot()); - double sin = Math.sin(rad); - double cos = Math.cos(rad); - double localForward = direction.z * cos - direction.x * sin; - double localSideways = direction.x * cos + direction.z * sin; - - boolean forward = localForward > CENTER_DEAD_ANGLE_COS; - boolean backward = localForward < -CENTER_DEAD_ANGLE_COS; - boolean left = localSideways < -CENTER_DEAD_ANGLE_COS; - boolean right = localSideways > CENTER_DEAD_ANGLE_COS; - - if (!forward && !backward && !left && !right) { - if (Math.abs(localForward) >= Math.abs(localSideways)) { - forward = localForward > 0.0; - backward = localForward < 0.0; - } else { - left = localSideways < 0.0; - right = localSideways > 0.0; - } - } - - event.setForward(forward); - event.setBackward(backward); - event.setLeft(left); - event.setRight(right); - } - - private void updateSafeCenter(LocalPlayer player) { - Vec3 blockCenter = blockBottomCenter(player); - if (!wouldBeCloseToFallOff(player, blockCenter)) { - center = blockCenter; - } - } - } - public record EdgeCheck(boolean closeToEdge, boolean diagonalRescue, Vec3 movementDirection) { private static EdgeCheck empty() { return new EdgeCheck(false, false, Vec3.ZERO); diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/EdgeRecovery.java b/mod/src/main/java/dev/loki/lovisual/util/entity/EdgeRecovery.java new file mode 100644 index 0000000..82128bc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/EdgeRecovery.java @@ -0,0 +1,204 @@ +package dev.loki.lovisual.util.entity; + +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.events.impl.movement.MovementInputEvent; + +/** + * Stateful edge-recovery controller extracted from {@link EagleUtil}. Tracks a safe block center and + * the remaining overwrite/sneak ticks, and rewrites {@link MovementInputEvent} input to rescue the + * player from walking off an edge. The pure geometry detection it consumes still lives in + * {@link EagleUtil}; behaviour is identical to the original nested implementation. + */ +public final class EdgeRecovery { + private static final double CENTER_DEAD_ANGLE_COS = 0.34; + private static final int DIAGONAL_RESCUE_TICKS = 2; + + public enum RecoveryMode { + STOP, + INVERT, + CENTER + } + + private Vec3 center; + private int overwriteTicks; + private int sneakTicks; + + private static Vec3 blockBottomCenter(LocalPlayer player) { + return new Vec3( + player.blockPosition().getX() + 0.5, + player.blockPosition().getY(), + player.blockPosition().getZ() + 0.5 + ); + } + + private static double horizontalDistanceSquared(Vec3 a, Vec3 b) { + double x = a.x - b.x; + double z = a.z - b.z; + return x * x + z * z; + } + + public boolean handleDiagonalRescue(LocalPlayer player, MovementInputEvent event, double edgeDistance) { + if (player == null || event == null) { + return false; + } + + updateSafeCenter(player); + + EagleUtil.EdgeCheck edgeCheck = EagleUtil.checkEdge(player, event, edgeDistance); + if (edgeCheck.diagonalRescue() && shouldOverwriteInput(player)) { + overwriteTicks = Math.max(overwriteTicks, DIAGONAL_RESCUE_TICKS); + } + + if (overwriteTicks <= 0) { + return false; + } + + overwriteTicks--; + applyInputTowardsCenter(player, event); + event.setJump(false); + event.setSneak(true); + event.setSprint(false); + + updateSafeCenter(player); + return true; + } + + public boolean handleOnEdge( + LocalPlayer player, + MovementInputEvent event, + double edgeDistance, + RecoveryMode mode, + int keepTicks, + int requestedSneakTicks, + boolean jump + ) { + if (player == null || event == null) { + return false; + } + + updateSafeCenter(player); + + double distance = Math.min(EagleUtil.horizontalSpeed(player), Math.max(0.0, edgeDistance)); + EagleUtil.EdgeCheck edgeCheck = EagleUtil.checkEdge(player, event, distance); + if (edgeCheck.closeToEdge() && shouldOverwriteInput(player)) { + overwriteTicks = Math.max(overwriteTicks, Math.max(1, keepTicks)); + sneakTicks = Math.max(sneakTicks, Math.max(0, requestedSneakTicks)); + } + + boolean applied = false; + if (overwriteTicks > 0) { + overwriteTicks--; + applyRecoveryInput(player, event, mode); + event.setJump(jump); + event.setSprint(false); + applied = true; + } + + if (sneakTicks > 0) { + sneakTicks--; + event.setSneak(true); + applied = true; + } + + updateSafeCenter(player); + return applied; + } + + public void reset() { + center = null; + overwriteTicks = 0; + sneakTicks = 0; + } + + private boolean shouldOverwriteInput(LocalPlayer player) { + Vec3 center = this.center; + if (center == null) { + return true; + } + + Vec3 horizontalVelocity = new Vec3(player.getDeltaMovement().x, 0.0, player.getDeltaMovement().z); + if (horizontalVelocity.horizontalDistanceSqr() <= 0.02 * 0.02) { + return true; + } + + double currentDistance = horizontalDistanceSquared(center, player.position()); + double nextDistance = horizontalDistanceSquared(center, player.position().add(horizontalVelocity)); + return nextDistance > currentDistance; + } + + private void applyRecoveryInput(LocalPlayer player, MovementInputEvent event, RecoveryMode mode) { + RecoveryMode resolvedMode = mode != null ? mode : RecoveryMode.CENTER; + if (resolvedMode == RecoveryMode.INVERT) { + boolean forward = event.isForward(); + boolean backward = event.isBackward(); + boolean left = event.isLeft(); + boolean right = event.isRight(); + event.setForward(backward); + event.setBackward(forward); + event.setLeft(right); + event.setRight(left); + event.setJump(false); + return; + } + + if (resolvedMode == RecoveryMode.STOP && EagleUtil.horizontalSpeed(player) <= 0.05) { + event.setForward(false); + event.setBackward(false); + event.setLeft(false); + event.setRight(false); + event.setJump(false); + return; + } + + applyInputTowardsCenter(player, event); + } + + private void applyInputTowardsCenter(LocalPlayer player, MovementInputEvent event) { + Vec3 center = this.center != null ? this.center : blockBottomCenter(player); + Vec3 toCenter = center.subtract(player.position()); + Vec3 horizontal = new Vec3(toCenter.x, 0.0, toCenter.z); + + if (horizontal.lengthSqr() < 0.015 * 0.015) { + event.setForward(false); + event.setBackward(false); + event.setLeft(false); + event.setRight(false); + return; + } + + Vec3 direction = horizontal.normalize(); + double rad = Math.toRadians(player.getYRot()); + double sin = Math.sin(rad); + double cos = Math.cos(rad); + double localForward = direction.z * cos - direction.x * sin; + double localSideways = direction.x * cos + direction.z * sin; + + boolean forward = localForward > CENTER_DEAD_ANGLE_COS; + boolean backward = localForward < -CENTER_DEAD_ANGLE_COS; + boolean left = localSideways < -CENTER_DEAD_ANGLE_COS; + boolean right = localSideways > CENTER_DEAD_ANGLE_COS; + + if (!forward && !backward && !left && !right) { + if (Math.abs(localForward) >= Math.abs(localSideways)) { + forward = localForward > 0.0; + backward = localForward < 0.0; + } else { + left = localSideways < 0.0; + right = localSideways > 0.0; + } + } + + event.setForward(forward); + event.setBackward(backward); + event.setLeft(left); + event.setRight(right); + } + + private void updateSafeCenter(LocalPlayer player) { + Vec3 blockCenter = blockBottomCenter(player); + if (!EagleUtil.wouldBeCloseToFallOff(player, blockCenter)) { + center = blockCenter; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java b/mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java index 6974723..02fc762 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java @@ -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.util.entity; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java index 08e5209..943639e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java @@ -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.util.entity.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java index 9ecb6de..2ddb720 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java @@ -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.util.entity.simulation; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java index 612130c..48c577b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java @@ -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.util.entity.simulation; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java index a95401d..d81bb85 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java +++ b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java @@ -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.util.entity.simulation; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java b/mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java index 7a9bea8..150a8c8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java @@ -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.util.input; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java b/mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java index c330591..20785ee 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java @@ -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.util.input; import org.lwjgl.glfw.GLFW; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java index 20782c5..0421855 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java @@ -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.util.item.enchant; import net.minecraft.client.resources.language.I18n; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java index 79f2bdc..39c6702 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java @@ -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.util.item.enchant; import net.minecraft.resources.ResourceKey; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java index 6c3449d..0d1c130 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java @@ -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.util.item.enchant; import net.minecraft.core.Holder; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java index 0fc005d..7614fba 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java @@ -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.util.item.enchant; import it.unimi.dsi.fastutil.objects.Object2IntMap; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java index 886a556..98d84f5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java @@ -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.util.item.food; import net.minecraft.core.component.DataComponents; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java index 2c61645..cd45e0a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java @@ -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.util.item.food; import it.unimi.dsi.fastutil.objects.Object2IntMap; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java index 2d2c496..70956dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java @@ -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.util.item.rarity; import net.minecraft.world.item.ItemStack; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java index d60d677..752f0ec 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java @@ -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.util.item.rarity; import net.minecraft.world.item.Rarity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java index 3cb7ed5..300eaae 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java @@ -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.util.item.rarity; import dev.loki.lovisual.features.module.modules.visuals.esp.overlay.DropESP; diff --git a/mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java index 6c04530..35becfc 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java +++ b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java @@ -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.util.logging; import org.slf4j.Logger; diff --git a/mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java index 2233008..64c339e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java @@ -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.util.logging; public enum DebugMode { diff --git a/mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java b/mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java index 637458e..579198b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java @@ -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.util.logging; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java b/mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java index f3995c9..44ae04c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java +++ b/mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java @@ -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.util.map; import com.google.gson.JsonArray; diff --git a/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java index 2ee844c..7e1176f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java +++ b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java @@ -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.util.mctiers; import com.google.gson.JsonObject; diff --git a/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java index 5a98264..3f00ae8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java +++ b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java @@ -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.util.mctiers; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java index 6228fd5..c20433c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java @@ -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.util.media.impl; import dev.loki.lovisual.util.media.session.IMediaSession; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java index 3637796..d0091bb 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java @@ -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.util.media.impl.linux; import org.freedesktop.dbus.connections.impl.DBusConnection; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java index d4dd7b5..4cc15aa 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java @@ -1,44 +1,21 @@ -/* - * 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.util.media.impl.linux; import org.freedesktop.dbus.ObjectPath; -import org.freedesktop.dbus.types.Variant; +import dev.loki.lovisual.util.media.impl.linux.LinuxMetadataReader.BaseState; import dev.loki.lovisual.util.media.impl.linux.dbus.Player; import dev.loki.lovisual.util.media.session.IMediaSession; import dev.loki.lovisual.util.media.info.MediaInfo; import dev.loki.lovisual.util.media.info.RepeatMode; -import java.io.InputStream; -import java.net.URL; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import dev.loki.lovisual.util.media.impl.linux.LinuxMediaPlayerInfo; -import dev.loki.lovisual.util.media.info.MediaPlayerInfo; - /** * Modified Java port for LoVisual by dev.loki.lovisual. * Based on Redstonecrafter0/MediaPlayerInfo. + * + *

    Thin MPR2 session facade: playback controls delegate straight to the D-Bus + * {@link Player}; property reads live in {@link LinuxMetadataReader} and the + * per-owner track/seek bookkeeping in {@link LinuxTrackState}.

    */ public final class LinuxMediaSession implements IMediaSession { - private static final long SEEK_OVERRIDE_TOLERANCE_SECONDS = 2L; - private static final long SEEK_OVERRIDE_TTL_MS = 15_000L; - - private static final ConcurrentHashMap LAST_TRACK_ID_BY_OWNER = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap LAST_FALLBACK_KEY_BY_OWNER = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap PENDING_TRACK_BY_OWNER = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap LAST_NUDGED_TRACK_ID_BY_OWNER = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap SEEK_OVERRIDE_BY_OWNER = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap ARTWORK_CACHE_BY_URL = new ConcurrentHashMap<>(); - private final Player dbus; private final String owner; private final MediaInfo media; @@ -50,58 +27,6 @@ public final class LinuxMediaSession implements IMediaSession { this.media = generateMediaInfo(); } - private static void rememberSeekOverride(String owner, String trackId, long targetPosition, boolean playing, long duration) { - SEEK_OVERRIDE_BY_OWNER.put(owner, new SeekOverrideState(trackId, targetPosition, System.currentTimeMillis(), playing, duration)); - } - - private static void clearSeekOverride(String owner) { - SEEK_OVERRIDE_BY_OWNER.remove(owner); - } - - private static long resolveVisiblePosition(String owner, - String trackId, - long rawPosition, - boolean playing, - long duration, - boolean pending) { - if (pending) return 0L; - - long now = System.currentTimeMillis(); - SeekOverrideState override = SEEK_OVERRIDE_BY_OWNER.get(owner); - if (override == null) return clampPosition(rawPosition, duration); - if (override.trackId != null && trackId != null && !override.trackId.equals(trackId)) { - clearSeekOverride(owner); - return clampPosition(rawPosition, duration); - } - if (now - override.appliedAtMs > SEEK_OVERRIDE_TTL_MS) { - clearSeekOverride(owner); - return clampPosition(rawPosition, duration); - } - - long expected = clampPosition(override.expectedPosition(now), duration); - long clampedRaw = clampPosition(rawPosition, duration); - long diff = Math.abs(clampedRaw - expected); - if (diff <= SEEK_OVERRIDE_TOLERANCE_SECONDS) { - clearSeekOverride(owner); - return clampedRaw; - } - if (clampedRaw > expected + SEEK_OVERRIDE_TOLERANCE_SECONDS) { - clearSeekOverride(owner); - return clampedRaw; - } - if (!playing && clampedRaw >= expected) { - clearSeekOverride(owner); - return clampedRaw; - } - return expected; - } - - private static long clampPosition(long position, long duration) { - if (position < 0L) return 0L; - if (duration > 0L) return Math.min(position, duration); - return position; - } - @Override public String getOwner() { return owner; @@ -144,12 +69,12 @@ public final class LinuxMediaSession implements IMediaSession { @Override public boolean supportsShuffle() { - return getShuffleValue() != null; + return LinuxMetadataReader.getShuffleValue(owner) != null; } @Override public boolean isShuffleActive() { - Boolean value = getShuffleValue(); + Boolean value = LinuxMetadataReader.getShuffleValue(owner); return value != null && value; } @@ -160,12 +85,12 @@ public final class LinuxMediaSession implements IMediaSession { @Override public boolean supportsRepeat() { - return getLoopStatus() != null; + return LinuxMetadataReader.getLoopStatus(owner) != null; } @Override public RepeatMode getRepeatMode() { - return RepeatMode.fromMpris(getLoopStatus()); + return RepeatMode.fromMpris(LinuxMetadataReader.getLoopStatus(owner)); } @Override @@ -180,268 +105,44 @@ public final class LinuxMediaSession implements IMediaSession { @Override public boolean supportsSeek() { - return getCanSeek(); + return LinuxMetadataReader.getCanSeek(owner); } @Override public void seekTo(long positionSeconds) { - if (!getCanSeek()) return; + if (!LinuxMetadataReader.getCanSeek(owner)) return; long targetSeconds = Math.max(0L, positionSeconds); long targetUs = targetSeconds * 1_000_000L; - BaseState state = readBaseState(); - String id = state.trackId != null ? state.trackId : trackId; + BaseState state = LinuxMetadataReader.readBaseState(owner); + String id = state.trackId() != null ? state.trackId() : trackId; trackId = id; try { if (id != null) { String source = "org.mpris.MediaPlayer2." + owner; dbus.SetPosition(new ObjectPath(source, id), targetUs); - rememberSeekOverride(owner, id, targetSeconds, state.playing, state.duration); + LinuxTrackState.rememberSeekOverride(owner, id, targetSeconds, state.playing(), state.duration()); return; } } catch (Exception ignored) { } try { - long currentUs = Math.max(0L, resolveSeekPosition(state)) * 1_000_000L; + long currentUs = Math.max(0L, LinuxTrackState.resolveVisiblePosition( + owner, state.trackId(), state.positionRaw(), state.playing(), state.duration(), false)) * 1_000_000L; dbus.Seek(targetUs - currentUs); - rememberSeekOverride(owner, id, targetSeconds, state.playing, state.duration); + LinuxTrackState.rememberSeekOverride(owner, id, targetSeconds, state.playing(), state.duration()); } catch (Exception ignored) { } } private MediaInfo generateMediaInfo() { - BaseState state = readBaseState(); - String title = state.title; - String artist = state.artist; - long durationRaw = state.duration; - String nextTrackId = state.trackId; - String fallbackKey = buildFallbackKey(title, artist, durationRaw); - boolean trackChanged = trackChanged(owner, nextTrackId, fallbackKey); - long positionRaw = state.positionRaw; - - if (trackChanged) { - PENDING_TRACK_BY_OWNER.put(owner, true); - clearSeekOverride(owner); - } - if (Boolean.TRUE.equals(PENDING_TRACK_BY_OWNER.get(owner)) && positionRaw <= 2L) { - PENDING_TRACK_BY_OWNER.remove(owner); - } - if (trackChanged && nextTrackId != null && positionRaw > 5L) { - String lastNudged = LAST_NUDGED_TRACK_ID_BY_OWNER.get(owner); - if (!Objects.equals(lastNudged, nextTrackId) && getCanSeek()) { - try { - String source = "org.mpris.MediaPlayer2." + owner; - dbus.SetPosition(new ObjectPath(source, nextTrackId), 0L); - positionRaw = 0L; - LAST_NUDGED_TRACK_ID_BY_OWNER.put(owner, nextTrackId); - rememberSeekOverride(owner, nextTrackId, 0L, state.playing, durationRaw); - } catch (Exception ignored) { - } - } - } - - boolean pending = Boolean.TRUE.equals(PENDING_TRACK_BY_OWNER.get(owner)); - long position = resolveVisiblePosition(owner, nextTrackId, positionRaw, state.playing, durationRaw, pending); - trackId = nextTrackId; - byte[] artwork = readArtworkBytes(state.metadata); - return new MediaInfo(title, artist, artwork, position, durationRaw, state.playing); - } - - @SuppressWarnings("unchecked") - private BaseState readBaseState() { - Map metadata; - try { - Object raw = LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Metadata"); - metadata = raw instanceof Map map ? (Map) map : Map.of(); - } catch (Exception ignored) { - metadata = Map.of(); - } - - boolean playing; - try { - playing = "Playing".equals(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "PlaybackStatus")); - } catch (Exception ignored) { - playing = false; - } - - return new BaseState( - metadata, - playing, - readTitle(metadata), - readArtist(metadata), - readDurationSeconds(metadata), - extractTrackId(metadata), - readPositionSeconds() - ); - } - - private boolean getCanSeek() { - try { - return switch (unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "CanSeek"))) { - case Boolean value -> value; - case Number value -> value.intValue() != 0; - case String value -> value.equalsIgnoreCase("true") || value.equals("1"); - case null, default -> false; - }; - } catch (Exception ignored) { - return false; - } - } - - private String extractTrackId(Map metadata) { - Object raw = metadata.get("mpris:trackid"); - Object value = unwrap(raw); - return value != null ? value.toString() : null; - } - - private Boolean getShuffleValue() { - try { - Object value = unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Shuffle")); - if (value instanceof Boolean bool) return bool; - if (value instanceof Number number) return number.doubleValue() >= 0.5; - if (value instanceof String string) { - try { - return Double.parseDouble(string) >= 0.5; - } catch (NumberFormatException ignored) { - return null; - } - } - return null; - } catch (Exception ignored) { - return null; - } - } - - private String getLoopStatus() { - try { - Object value = unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "LoopStatus")); - return value != null ? value.toString() : null; - } catch (Exception ignored) { - return null; - } - } - - private Object unwrap(Object value) { - return value instanceof Variant variant ? variant.getValue() : value; - } - - private Long toLong(Object value) { - if (value instanceof Number number) return number.longValue(); - if (value instanceof String string) { - try { - return Long.parseLong(string); - } catch (NumberFormatException ignored) { - return null; - } - } - return null; - } - - private long readPositionSeconds() { - Object raw; - try { - raw = LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Position"); - } catch (Exception ignored) { - return 0L; - } - Long positionUs = toLong(unwrap(raw)); - return positionUs != null ? positionUs / 1_000_000L : 0L; - } - - private long readDurationSeconds(Map metadata) { - Long durationUs = toLong(unwrap(metadata.get("mpris:length"))); - return durationUs != null ? durationUs / 1_000_000L : 0L; - } - - private String readTitle(Map metadata) { - Object raw = unwrap(metadata.get("xesam:title")); - return raw instanceof String string ? string : ""; - } - - private String readArtist(Map metadata) { - Object raw = unwrap(metadata.get("xesam:artist")); - if (raw instanceof String string) return string; - if (raw instanceof Object[] array) return joinObjects(Arrays.asList(array)); - if (raw instanceof List list) return joinObjects(list); - return ""; - } - - private String joinObjects(List values) { - StringBuilder builder = new StringBuilder(); - for (Object value : values) { - if (value == null) continue; - if (!builder.isEmpty()) builder.append(", "); - builder.append(value); - } - return builder.toString(); - } - - private byte[] readArtworkBytes(Map metadata) { - Object raw = unwrap(metadata.get("mpris:artUrl")); - if (!(raw instanceof String url) || url.isBlank()) return new byte[0]; - - byte[] cached = ARTWORK_CACHE_BY_URL.get(url); - if (cached != null) return cached; - - try { - URL connectionUrl = new URL(url); - var connection = connectionUrl.openConnection(); - connection.setConnectTimeout(3000); - connection.setReadTimeout(3000); - byte[] bytes; - try (InputStream stream = connection.getInputStream()) { - bytes = stream.readAllBytes(); - } - if (bytes.length > 0) { - ARTWORK_CACHE_BY_URL.put(url, bytes); - } - return bytes; - } catch (Exception ignored) { - return new byte[0]; - } - } - - private String buildFallbackKey(String title, String artist, long duration) { - if (title.isBlank() && artist.isBlank() && duration <= 0L) return null; - return title + "\u0000" + artist + "\u0000" + duration; - } - - private long resolveSeekPosition(BaseState state) { - return resolveVisiblePosition(owner, state.trackId, state.positionRaw, state.playing, state.duration, false); - } - - private boolean trackChanged(String owner, String trackId, String fallbackKey) { - if (trackId != null) { - String prev = LAST_TRACK_ID_BY_OWNER.put(owner, trackId); - if (fallbackKey != null) { - LAST_FALLBACK_KEY_BY_OWNER.put(owner, fallbackKey); - } - return prev != null && !prev.equals(trackId); - } - if (fallbackKey == null) return false; - String prev = LAST_FALLBACK_KEY_BY_OWNER.put(owner, fallbackKey); - return prev != null && !prev.equals(fallbackKey); - } - - private record BaseState(Map metadata, - boolean playing, - String title, - String artist, - long duration, - String trackId, - long positionRaw) { - } - - private record SeekOverrideState(String trackId, - long targetPositionSeconds, - long appliedAtMs, - boolean playing, - long duration) { - long expectedPosition(long nowMs) { - long elapsedSeconds = playing ? Math.max(0L, nowMs - appliedAtMs) / 1000L : 0L; - long position = targetPositionSeconds + elapsedSeconds; - return duration > 0L ? Math.min(position, duration) : position; - } + BaseState state = LinuxMetadataReader.readBaseState(owner); + String fallbackKey = LinuxTrackState.buildFallbackKey(state.title(), state.artist(), state.duration()); + boolean trackChanged = LinuxTrackState.trackChanged(owner, state.trackId(), fallbackKey); + long position = LinuxTrackState.resolveFramePosition(owner, dbus, state, trackChanged); + trackId = state.trackId(); + byte[] artwork = LinuxMetadataReader.readArtworkBytes(state.metadata()); + return new MediaInfo(state.title(), state.artist(), artwork, position, state.duration(), state.playing()); } } diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMetadataReader.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMetadataReader.java new file mode 100644 index 0000000..7a53ce6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMetadataReader.java @@ -0,0 +1,191 @@ +package dev.loki.lovisual.util.media.impl.linux; + +import org.freedesktop.dbus.types.Variant; + +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Package-private D-Bus metadata/property readers backing {@link LinuxMediaSession}: + * unwraps {@link Variant} values and turns the raw MPR2 metadata map plus the + * player's boolean/numeric properties into plain Java values. Pure reads only — + * no track-change or seek state lives here. + */ +final class LinuxMetadataReader { + private static final ConcurrentHashMap ARTWORK_CACHE_BY_URL = new ConcurrentHashMap<>(); + + private LinuxMetadataReader() { + } + + /** A snapshot of the player's raw state for one refresh tick. */ + record BaseState(Map metadata, + boolean playing, + String title, + String artist, + long duration, + String trackId, + long positionRaw) { + } + + static BaseState readBaseState(String owner) { + Map metadata; + try { + Object raw = LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Metadata"); + metadata = raw instanceof Map map ? castMap(map) : Map.of(); + } catch (Exception ignored) { + metadata = Map.of(); + } + + boolean playing; + try { + playing = "Playing".equals(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "PlaybackStatus")); + } catch (Exception ignored) { + playing = false; + } + + return new BaseState( + metadata, + playing, + readTitle(metadata), + readArtist(metadata), + readDurationSeconds(metadata), + extractTrackId(metadata), + readPositionSeconds(owner) + ); + } + + static boolean getCanSeek(String owner) { + try { + return switch (unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "CanSeek"))) { + case Boolean value -> value; + case Number value -> value.intValue() != 0; + case String value -> value.equalsIgnoreCase("true") || value.equals("1"); + case null, default -> false; + }; + } catch (Exception ignored) { + return false; + } + } + + static Boolean getShuffleValue(String owner) { + try { + Object value = unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Shuffle")); + if (value instanceof Boolean bool) return bool; + if (value instanceof Number number) return number.doubleValue() >= 0.5; + if (value instanceof String string) { + try { + return Double.parseDouble(string) >= 0.5; + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } catch (Exception ignored) { + return null; + } + } + + static String getLoopStatus(String owner) { + try { + Object value = unwrap(LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "LoopStatus")); + return value != null ? value.toString() : null; + } catch (Exception ignored) { + return null; + } + } + + static Object unwrap(Object value) { + return value instanceof Variant variant ? variant.getValue() : value; + } + + private static Long toLong(Object value) { + if (value instanceof Number number) return number.longValue(); + if (value instanceof String string) { + try { + return Long.parseLong(string); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + + private static long readPositionSeconds(String owner) { + Object raw; + try { + raw = LinuxMediaPlayerInfo.INSTANCE.getProperty(owner, "Position"); + } catch (Exception ignored) { + return 0L; + } + Long positionUs = toLong(unwrap(raw)); + return positionUs != null ? positionUs / 1_000_000L : 0L; + } + + private static long readDurationSeconds(Map metadata) { + Long durationUs = toLong(unwrap(metadata.get("mpris:length"))); + return durationUs != null ? durationUs / 1_000_000L : 0L; + } + + private static String extractTrackId(Map metadata) { + Object raw = metadata.get("mpris:trackid"); + Object value = unwrap(raw); + return value != null ? value.toString() : null; + } + + private static String readTitle(Map metadata) { + Object raw = unwrap(metadata.get("xesam:title")); + return raw instanceof String string ? string : ""; + } + + private static String readArtist(Map metadata) { + Object raw = unwrap(metadata.get("xesam:artist")); + if (raw instanceof String string) return string; + if (raw instanceof Object[] array) return joinObjects(Arrays.asList(array)); + if (raw instanceof List list) return joinObjects(list); + return ""; + } + + private static String joinObjects(List values) { + StringBuilder builder = new StringBuilder(); + for (Object value : values) { + if (value == null) continue; + if (!builder.isEmpty()) builder.append(", "); + builder.append(value); + } + return builder.toString(); + } + + static byte[] readArtworkBytes(Map metadata) { + Object raw = unwrap(metadata.get("mpris:artUrl")); + if (!(raw instanceof String url) || url.isBlank()) return new byte[0]; + + byte[] cached = ARTWORK_CACHE_BY_URL.get(url); + if (cached != null) return cached; + + try { + URL connectionUrl = new URL(url); + var connection = connectionUrl.openConnection(); + connection.setConnectTimeout(3000); + connection.setReadTimeout(3000); + byte[] bytes; + try (InputStream stream = connection.getInputStream()) { + bytes = stream.readAllBytes(); + } + if (bytes.length > 0) { + ARTWORK_CACHE_BY_URL.put(url, bytes); + } + return bytes; + } catch (Exception ignored) { + return new byte[0]; + } + } + + @SuppressWarnings("unchecked") + private static Map castMap(Map map) { + return (Map) map; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxTrackState.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxTrackState.java new file mode 100644 index 0000000..5aaccda --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxTrackState.java @@ -0,0 +1,145 @@ +package dev.loki.lovisual.util.media.impl.linux; + +import org.freedesktop.dbus.ObjectPath; +import dev.loki.lovisual.util.media.impl.linux.LinuxMetadataReader.BaseState; +import dev.loki.lovisual.util.media.impl.linux.dbus.Player; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Package-private per-owner track/seek bookkeeping for {@link LinuxMediaSession}: + * remembers the last seen track, pending-track and nudge markers, and the + * seek-override (a just-issued seek whose absolute position the player has not + * reported yet) so the visible position does not jump backwards between refresh + * ticks. All state is keyed by the D-Bus owner name and survives across the + * short-lived {@link LinuxMediaSession} instances the poller creates. + */ +final class LinuxTrackState { + private static final long SEEK_OVERRIDE_TOLERANCE_SECONDS = 2L; + private static final long SEEK_OVERRIDE_TTL_MS = 15_000L; + + private static final ConcurrentHashMap LAST_TRACK_ID_BY_OWNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap LAST_FALLBACK_KEY_BY_OWNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap PENDING_TRACK_BY_OWNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap LAST_NUDGED_TRACK_ID_BY_OWNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap SEEK_OVERRIDE_BY_OWNER = new ConcurrentHashMap<>(); + + private LinuxTrackState() { + } + + static void rememberSeekOverride(String owner, String trackId, long targetPosition, boolean playing, long duration) { + SEEK_OVERRIDE_BY_OWNER.put(owner, + new SeekOverrideState(trackId, targetPosition, System.currentTimeMillis(), playing, duration)); + } + + static void clearSeekOverride(String owner) { + SEEK_OVERRIDE_BY_OWNER.remove(owner); + } + + static long resolveVisiblePosition(String owner, + String trackId, + long rawPosition, + boolean playing, + long duration, + boolean pending) { + if (pending) return 0L; + + long now = System.currentTimeMillis(); + SeekOverrideState override = SEEK_OVERRIDE_BY_OWNER.get(owner); + if (override == null) return clampPosition(rawPosition, duration); + if (override.trackId != null && trackId != null && !override.trackId.equals(trackId)) { + clearSeekOverride(owner); + return clampPosition(rawPosition, duration); + } + if (now - override.appliedAtMs > SEEK_OVERRIDE_TTL_MS) { + clearSeekOverride(owner); + return clampPosition(rawPosition, duration); + } + + long expected = clampPosition(override.expectedPosition(now), duration); + long clampedRaw = clampPosition(rawPosition, duration); + long diff = Math.abs(clampedRaw - expected); + if (diff <= SEEK_OVERRIDE_TOLERANCE_SECONDS) { + clearSeekOverride(owner); + return clampedRaw; + } + if (clampedRaw > expected + SEEK_OVERRIDE_TOLERANCE_SECONDS) { + clearSeekOverride(owner); + return clampedRaw; + } + if (!playing && clampedRaw >= expected) { + clearSeekOverride(owner); + return clampedRaw; + } + return expected; + } + + private static long clampPosition(long position, long duration) { + if (position < 0L) return 0L; + if (duration > 0L) return Math.min(position, duration); + return position; + } + + static String buildFallbackKey(String title, String artist, long duration) { + if (title.isBlank() && artist.isBlank() && duration <= 0L) return null; + return title + "\u0000" + artist + "\u0000" + duration; + } + + static boolean trackChanged(String owner, String trackId, String fallbackKey) { + if (trackId != null) { + String prev = LAST_TRACK_ID_BY_OWNER.put(owner, trackId); + if (fallbackKey != null) { + LAST_FALLBACK_KEY_BY_OWNER.put(owner, fallbackKey); + } + return prev != null && !prev.equals(trackId); + } + if (fallbackKey == null) return false; + String prev = LAST_FALLBACK_KEY_BY_OWNER.put(owner, fallbackKey); + return prev != null && !prev.equals(fallbackKey); + } + + /** + * Advances the pending/nudge bookkeeping for one refresh and returns the + * visible position for {@code state}, replaying a just-issued seek when the + * player has not caught up. Nudges a freshly-started track back to zero. + */ + static long resolveFramePosition(String owner, Player dbus, BaseState state, boolean trackChanged) { + long positionRaw = state.positionRaw(); + if (trackChanged) { + PENDING_TRACK_BY_OWNER.put(owner, true); + clearSeekOverride(owner); + } + if (Boolean.TRUE.equals(PENDING_TRACK_BY_OWNER.get(owner)) && positionRaw <= 2L) { + PENDING_TRACK_BY_OWNER.remove(owner); + } + if (trackChanged && state.trackId() != null && positionRaw > 5L) { + String lastNudged = LAST_NUDGED_TRACK_ID_BY_OWNER.get(owner); + if (!Objects.equals(lastNudged, state.trackId()) && LinuxMetadataReader.getCanSeek(owner)) { + try { + String source = "org.mpris.MediaPlayer2." + owner; + dbus.SetPosition(new ObjectPath(source, state.trackId()), 0L); + positionRaw = 0L; + LAST_NUDGED_TRACK_ID_BY_OWNER.put(owner, state.trackId()); + rememberSeekOverride(owner, state.trackId(), 0L, state.playing(), state.duration()); + } catch (Exception ignored) { + } + } + } + + boolean pending = Boolean.TRUE.equals(PENDING_TRACK_BY_OWNER.get(owner)); + return resolveVisiblePosition(owner, state.trackId(), positionRaw, state.playing(), state.duration(), pending); + } + + private record SeekOverrideState(String trackId, + long targetPositionSeconds, + long appliedAtMs, + boolean playing, + long duration) { + long expectedPosition(long nowMs) { + long elapsedSeconds = playing ? Math.max(0L, nowMs - appliedAtMs) / 1000L : 0L; + long position = targetPositionSeconds + elapsedSeconds; + return duration > 0L ? Math.min(position, duration) : position; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java index a52b54f..71bf8cd 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java @@ -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.util.media.impl.linux.dbus; import org.freedesktop.dbus.ObjectPath; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java index b1f0489..4433c44 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java @@ -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.util.media.impl.win; import org.lwjgl.BufferUtils; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java index 882b02e..ef409c9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java @@ -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.util.media.impl.win; import dev.loki.lovisual.util.media.session.IMediaSession; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java index 3a88969..3ca9419 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java @@ -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.util.media.info; import java.util.Arrays; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java index 6a24108..3bec929 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java @@ -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.util.media.info; import dev.loki.lovisual.util.media.impl.DummyMediaPlayerInfo; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java index a8abba5..b077158 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java @@ -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.util.media.info; import java.util.Locale; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java index 065bc25..94239a8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java @@ -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.util.media.session; import dev.loki.lovisual.util.media.info.MediaInfo; import dev.loki.lovisual.util.media.info.MediaPlayerInfo; diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java index 9749d68..6425456 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java @@ -1,70 +1,38 @@ -/* - * 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.util.media.session; -import com.mojang.blaze3d.platform.NativeImage; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.util.logging.DebugLog; - -import java.io.ByteArrayInputStream; -import java.util.List; import java.util.Locale; +import net.minecraft.resources.Identifier; import dev.loki.lovisual.util.media.info.MediaInfo; -import dev.loki.lovisual.util.media.info.MediaPlayerInfo; import dev.loki.lovisual.util.media.info.RepeatMode; -import dev.loki.lovisual.util.media.session.IMediaSession; - public final class MediaSessionService { - private static final long SESSION_GRACE_MS = 1500L; - private static final Identifier ARTWORK_ID = Identifier.fromNamespaceAndPath("lovisual", "media_player/artwork"); private static final MediaSessionService INSTANCE = new MediaSessionService(); - private final MediaPlayerInfo mediaPlayerInfo = MediaPlayerInfo.system(); - private volatile List mediaSessions = List.of(); - private volatile boolean pollingRunning; - private Thread pollingThread; - - private String lastTrackKey; - private IMediaSession currentSession; - private MediaInfo currentMedia; - private Identifier artworkTexture; - private long lastSessionSeenMs; - private int lastArtworkSize; - - private long lastRawPosition; - private long lastRawDuration; - private boolean lastRawPlaying; - private long lastRawUpdateMs; - private long lastDebugLogMs; - private String lastDebugLogKey; - - private Snapshot snapshot = Snapshot.empty(isMediaAvailable()); - + private final MediaSessionClock clock; + private final MediaSessionArtwork artwork; + private final MediaSessionTracker tracker; + private final MediaSessionPoller poller; + private Snapshot snapshot; private MediaSessionService() { + MediaSessionClock createdClock = new MediaSessionClock(); + MediaSessionArtwork createdArtwork = new MediaSessionArtwork(); + MediaSessionTracker createdTracker = new MediaSessionTracker(createdClock, createdArtwork); + MediaSessionPoller createdPoller = new MediaSessionPoller(); + this.clock = createdClock; + this.artwork = createdArtwork; + this.tracker = createdTracker; + this.poller = createdPoller; + this.snapshot = Snapshot.empty(isMediaAvailable()); } public static MediaSessionService get() { return INSTANCE; } - private static NativeImage getArtworkAsNativeImage(MediaInfo mediaInfo) { - byte[] bytes = mediaInfo.artworkPng(); - if (bytes == null || bytes.length == 0) return null; - try { - return NativeImage.read(new ByteArrayInputStream(bytes)); - } catch (Exception e) { - DebugLog.errorOnce("media-artwork-native-image", "[MediaSessionService] Failed to read artwork NativeImage", e); - return null; - } + public static boolean isMediaAvailable() { + String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + return os.startsWith("windows") || os.equals("linux"); } private static String safe(String value) { @@ -73,30 +41,12 @@ public final class MediaSessionService { return v.isEmpty() ? "" : v; } - public static boolean isMediaAvailable() { - String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); - return os.startsWith("windows") || os.equals("linux"); - } - public synchronized void init() { - if (pollingThread != null && pollingThread.isAlive()) return; - pollingRunning = true; - pollingThread = new Thread(this::pollMediaSessions, "LoVisualMediaInfoPollingThread"); - pollingThread.setDaemon(true); - pollingThread.start(); + poller.init(); } public synchronized void shutdown() { - pollingRunning = false; - Thread thread = pollingThread; - if (thread == null) return; - try { - thread.join(1000L); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - pollingThread = null; - mediaSessions = List.of(); + poller.shutdown(); } public synchronized Snapshot snapshot() { @@ -105,512 +55,141 @@ public final class MediaSessionService { } public synchronized void onResourceReload() { - artworkTexture = null; - lastArtworkSize = 0; - lastTrackKey = null; - currentSession = null; - currentMedia = null; - lastSessionSeenMs = 0L; - lastDebugLogKey = null; + tracker.clearOnResourceReload(); snapshot = Snapshot.empty(isMediaAvailable()); } public synchronized void playPause() { - if (currentSession == null) return; - MediaInfo media = resolveCurrentMedia(); - if (media == null) return; + IMediaSession s = tracker.currentSession(); + if (s == null) return; + MediaInfo m = tracker.resolveCurrentMedia(); + if (m == null) return; try { - if (media.playing()) { - applyLocalPlaybackState(false, media); - currentSession.pause(); - } else { - applyLocalPlaybackState(true, media); - currentSession.play(); - } - } catch (Throwable ignored) { - } + if (m.playing()) { clock.applyLocalPlaybackState(false, m); s.pause(); } + else { clock.applyLocalPlaybackState(true, m); s.play(); } + } catch (Throwable ignored) {} } public synchronized void play() { - if (currentSession == null) return; - MediaInfo media = resolveCurrentMedia(); - if (media == null) return; - try { - applyLocalPlaybackState(true, media); - currentSession.play(); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null) return; + MediaInfo m = tracker.resolveCurrentMedia(); + if (m == null) return; + try { clock.applyLocalPlaybackState(true, m); s.play(); } catch (Throwable ignored) {} } public synchronized void pause() { - if (currentSession == null) return; - MediaInfo media = resolveCurrentMedia(); - if (media == null) return; - try { - applyLocalPlaybackState(false, media); - currentSession.pause(); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null) return; + MediaInfo m = tracker.resolveCurrentMedia(); + if (m == null) return; + try { clock.applyLocalPlaybackState(false, m); s.pause(); } catch (Throwable ignored) {} } public synchronized void stop() { - if (currentSession == null) return; - MediaInfo media = resolveCurrentMedia(); - if (media == null) return; - try { - applyLocalPlaybackState(false, media); - currentSession.stop(); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null) return; + MediaInfo m = tracker.resolveCurrentMedia(); + if (m == null) return; + try { clock.applyLocalPlaybackState(false, m); s.stop(); } catch (Throwable ignored) {} } public synchronized void previous() { - if (currentSession == null) return; - try { - currentSession.previous(); - resetPredictedAfterSkip(); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null) return; + try { s.previous(); clock.resetPredictedAfterSkip(); } catch (Throwable ignored) {} } public synchronized void next() { - if (currentSession == null) return; - try { - currentSession.next(); - resetPredictedAfterSkip(); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null) return; + try { s.next(); clock.resetPredictedAfterSkip(); } catch (Throwable ignored) {} } public synchronized boolean supportsShuffle() { - return currentSession != null && currentSession.supportsShuffle(); + IMediaSession s = tracker.currentSession(); + return s != null && s.supportsShuffle(); } public synchronized boolean isShuffleActive() { - return currentSession != null && currentSession.isShuffleActive(); + IMediaSession s = tracker.currentSession(); + return s != null && s.isShuffleActive(); } public synchronized void setShuffle(boolean active) { - if (currentSession == null || !currentSession.supportsShuffle()) return; - try { - currentSession.setShuffle(active); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null || !s.supportsShuffle()) return; + try { s.setShuffle(active); } catch (Throwable ignored) {} } public synchronized void toggleShuffle() { - if (currentSession == null || !currentSession.supportsShuffle()) return; - try { - currentSession.setShuffle(!currentSession.isShuffleActive()); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null || !s.supportsShuffle()) return; + try { s.setShuffle(!s.isShuffleActive()); } catch (Throwable ignored) {} } public synchronized boolean supportsRepeat() { - return currentSession != null && currentSession.supportsRepeat(); + IMediaSession s = tracker.currentSession(); + return s != null && s.supportsRepeat(); } public synchronized RepeatMode getRepeatMode() { - if (currentSession == null || !currentSession.supportsRepeat()) { - return RepeatMode.UNKNOWN; - } - try { - return currentSession.getRepeatMode(); - } catch (Throwable ignored) { - return RepeatMode.UNKNOWN; - } + IMediaSession s = tracker.currentSession(); + if (s == null || !s.supportsRepeat()) return RepeatMode.UNKNOWN; + try { return s.getRepeatMode(); } catch (Throwable ignored) { return RepeatMode.UNKNOWN; } } public synchronized void setRepeatMode(RepeatMode mode) { - if (currentSession == null || mode == null || !currentSession.supportsRepeat()) return; - try { - currentSession.setRepeatMode(mode); - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null || mode == null || !s.supportsRepeat()) return; + try { s.setRepeatMode(mode); } catch (Throwable ignored) {} } public synchronized RepeatMode cycleRepeatMode() { - RepeatMode current = getRepeatMode(); - RepeatMode next = switch (current) { - case OFF, UNKNOWN -> RepeatMode.ALL; - case ALL -> RepeatMode.ONE; - case ONE -> RepeatMode.OFF; - }; - setRepeatMode(next); - return next; + RepeatMode cur = getRepeatMode(); + RepeatMode next = switch (cur) { case OFF, UNKNOWN -> RepeatMode.ALL; case ALL -> RepeatMode.ONE; case ONE -> RepeatMode.OFF; }; + setRepeatMode(next); return next; } public synchronized boolean supportsSeek() { - return currentSession != null && currentSession.supportsSeek(); + IMediaSession s = tracker.currentSession(); + return s != null && s.supportsSeek(); } public synchronized void seekTo(long positionSeconds) { - if (currentSession == null || !currentSession.supportsSeek()) return; - try { - currentSession.seekTo(positionSeconds); - MediaInfo media = resolveCurrentMedia(); - if (media != null) { - rememberLocalSeek(positionSeconds, media); - } - } catch (Throwable ignored) { - } + IMediaSession s = tracker.currentSession(); + if (s == null || !s.supportsSeek()) return; + try { s.seekTo(positionSeconds); MediaInfo m = tracker.resolveCurrentMedia(); if (m != null) clock.rememberLocalSeek(positionSeconds, m); } catch (Throwable ignored) {} } private void refresh() { boolean mediaAvailable = isMediaAvailable(); - IMediaSession session = resolveSession(); - MediaInfo media = currentMedia; + IMediaSession session = tracker.resolveSession(poller.sessions()); + MediaInfo media = tracker.currentMedia(); if (session != null && media == null) { - media = resolveCurrentMedia(); - currentMedia = media; + media = tracker.resolveCurrentMedia(); + tracker.setCurrentMedia(media); } - boolean hasSession = session != null && media != null; - long predictedPositionMs = hasSession ? getPredictedPositionMs(media) : 0L; - long duration = hasSession ? lastRawDuration : 0L; - snapshot = new Snapshot(mediaAvailable, session, media, artworkTexture, predictedPositionMs, duration, hasSession); + long predictedMs = hasSession ? clock.getPredictedPositionMs(media) : 0L; + long duration = hasSession ? clock.duration() : 0L; + snapshot = new Snapshot(mediaAvailable, session, media, tracker.artworkTexture(), predictedMs, duration, hasSession); } - private IMediaSession resolveSession() { - List sessions = mediaSessions; - long now = Util.getMillis(); - if (sessions.isEmpty()) { - if (currentSession != null && now - lastSessionSeenMs < SESSION_GRACE_MS) { - return currentSession; - } - lastTrackKey = null; - artworkTexture = null; - lastArtworkSize = 0; - currentSession = null; - currentMedia = null; - return null; - } - - IMediaSession best = null; - MediaInfo bestMedia = null; - for (IMediaSession session : sessions) { - if (session == null) continue; - MediaInfo media; - try { - media = session.getMedia(); - } catch (Throwable ignored) { - continue; - } - if (best == null) { - best = session; - bestMedia = media; - } - if (media.playing()) { - best = session; - bestMedia = media; - break; - } - } - if (best == null) return null; - - currentSession = best; - lastSessionSeenMs = now; - MediaInfo media = bestMedia; - currentMedia = media; - if (media == null) { - return best; - } - - String key = buildTrackKey(best, media); - boolean keyChanged = !key.equals(lastTrackKey); - boolean forceReset = shouldForceTrackReset(media); - if (keyChanged || forceReset) { - resetPredictedClock(media); - if (keyChanged) { - lastTrackKey = key; - } - } - - maybeUpdateArtwork(media, keyChanged || forceReset); - debugLogMedia(best, media); - return best; - } - - private MediaInfo resolveCurrentMedia() { - if (currentSession == null) return null; - try { - return currentSession.getMedia(); - } catch (Throwable ignored) { - return currentMedia; - } - } - - private void updateArtwork(MediaInfo media) { - Minecraft mc = Minecraft.getInstance(); - artworkTexture = null; - if (mc == null || media == null || media.artworkPng() == null || media.artworkPng().length == 0) return; - NativeImage image = getArtworkAsNativeImage(media); - if (image == null) return; - DynamicTexture texture = new DynamicTexture(() -> "lovisual_media_artwork", image); - mc.getTextureManager().register(ARTWORK_ID, texture); - artworkTexture = ARTWORK_ID; - } - - // 300ms instead of 1s: MPRIS/SMTC position only updates when we ask for it (no - // PropertiesChanged push), so the raw sample is always stale by up to one poll - // interval. Polling faster shrinks that worst-case staleness window fourfold without - // adding network cost - these are local D-Bus/WinRT reads, not HTTP (artwork bytes are - // cached by URL, see LinuxMediaSession.readArtworkBytes, so this does not multiply - // network requests). - private static final long POLL_INTERVAL_MS = 300L; - - private void pollMediaSessions() { - while (pollingRunning) { - try { - mediaSessions = List.copyOf(mediaPlayerInfo.getMediaSessions()); - } catch (Throwable t) { - DebugLog.errorOnce("media-session-poll", "[MediaSessionService] Failed to fetch media sessions", t); - mediaSessions = List.of(); - } - - try { - Thread.sleep(POLL_INTERVAL_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - pollingRunning = false; - } - } - } - - private void maybeUpdateArtwork(MediaInfo media, boolean trackChanged) { - if (media == null) { - if (trackChanged) { - artworkTexture = null; - lastArtworkSize = 0; - } - return; - } - - byte[] artwork = media.artworkPng(); - int size = artwork != null ? artwork.length : 0; - if (size == 0) { - if (trackChanged) { - artworkTexture = null; - lastArtworkSize = 0; - } - return; - } - - if (trackChanged || artworkTexture == null || size != lastArtworkSize) { - updateArtwork(media); - lastArtworkSize = size; - } - } - - // Каноническая схема MPRIS-клиентов (playerctl, mpris-rs): якорь - // (позиция_плеера, момент_опроса) + экстраполяция в реальном времени, - // т.к. свойство Position не шлёт PropertiesChanged и обновляется раз в - // ~1с с квантизацией до секунд. Считаем в миллисекундах: целочисленное - // деление дельты на 1000 давало квантование предсказания до целых секунд. - private long getPredictedPositionMs(MediaInfo media) { - if (media == null) return 0L; - long now = Util.getMillis(); - long rawPos = Math.max(0L, media.position()); - long rawDur = Math.max(0L, media.duration()); - long[] normalized = normalizeTime(rawPos, rawDur); - rawPos = normalized[0]; - rawDur = normalized[1]; - boolean rawPlaying = media.playing(); - - boolean changed = rawPos != lastRawPosition || rawDur != lastRawDuration || rawPlaying != lastRawPlaying; - if (changed) { - lastRawPosition = rawPos; - lastRawDuration = rawDur; - lastRawPlaying = rawPlaying; - lastRawUpdateMs = now; - } - - long predictedMs = lastRawPosition * 1000L; - if (lastRawPlaying) { - predictedMs += Math.max(0L, now - lastRawUpdateMs); - } - if (lastRawDuration > 0L && predictedMs > lastRawDuration * 1000L) { - predictedMs = lastRawDuration * 1000L; - } - return predictedMs; - } - - private long getPredictedPosition(MediaInfo media) { - return getPredictedPositionMs(media) / 1000L; - } - - private void resetPredictedAfterSkip() { - long now = Util.getMillis(); - lastRawPosition = 0L; - lastRawDuration = 0L; - lastRawPlaying = true; - lastRawUpdateMs = now; - } - - private void applyLocalPlaybackState(boolean playing, MediaInfo media) { - if (media == null) return; - long now = Util.getMillis(); - long predicted = getPredictedPosition(media); - long rawDur = Math.max(0L, media.duration()); - long[] normalized = normalizeTime(predicted, rawDur); - lastRawPosition = normalized[0]; - lastRawDuration = normalized[1]; - lastRawPlaying = playing; - lastRawUpdateMs = now; - } - - private void rememberLocalSeek(long positionSeconds, MediaInfo media) { - long now = Util.getMillis(); - long rawDur = Math.max(0L, media.duration()); - long[] normalized = normalizeTime(Math.max(0L, positionSeconds), rawDur); - lastRawPosition = normalized[0]; - lastRawDuration = normalized[1]; - lastRawPlaying = media.playing(); - lastRawUpdateMs = now; - } - - private void resetPredictedClock(MediaInfo media) { - long now = Util.getMillis(); - if (media == null) { - lastRawPosition = 0L; - lastRawDuration = 0L; - lastRawPlaying = false; - lastRawUpdateMs = now; - return; - } - long rawPos = Math.max(0L, media.position()); - long rawDur = Math.max(0L, media.duration()); - long[] normalized = normalizeTime(rawPos, rawDur); - long pos = normalized[0]; - long dur = normalized[1]; - if (dur > 0L && pos > dur + 5L) { - pos = 0L; - } - lastRawPosition = pos; - lastRawDuration = dur; - lastRawPlaying = media.playing(); - lastRawUpdateMs = now; - } - - private boolean shouldForceTrackReset(MediaInfo media) { - if (media == null) return false; - long rawPos = Math.max(0L, media.position()); - if (rawPos > lastRawPosition) return false; - long backJump = lastRawPosition - rawPos; - return rawPos <= 2L && backJump >= 8L; - } - - private long[] normalizeTime(long position, long duration) { - long pos = Math.max(0L, position); - long dur = Math.max(0L, duration); - if (dur == 0L) return new long[]{pos, 0L}; - - for (int i = 0; i < 6; i++) { - if (pos > dur * 100L && pos >= 1000L) { - pos /= 1000L; - continue; - } - if (pos > 0L && dur > pos * 100L && dur >= 1000L) { - dur /= 1000L; - continue; - } - break; - } - return new long[]{pos, dur}; - } - - private String buildTrackKey(IMediaSession session, MediaInfo media) { - String owner = session != null ? safe(session.getOwner()) : ""; - String title = media != null ? safe(media.title()) : ""; - String artist = media != null ? safe(media.artist()) : ""; - String durationKey = ""; - if (media != null) { - long dur = Math.max(0L, media.duration()); - if (dur > 0L) durationKey = Long.toString(dur); - } - return owner + "\u0000" + title + "\u0000" + artist + "\u0000" + durationKey; - } - - private void debugLogMedia(IMediaSession session, MediaInfo media) { - if (!DebugLog.isEnabled()) return; - long now = Util.getMillis(); - if (now - lastDebugLogMs < 1000L) return; - lastDebugLogMs = now; - String owner = session != null ? safe(session.getOwner()) : ""; - String title = media != null ? safe(media.title()) : ""; - String artist = media != null ? safe(media.artist()) : ""; - long pos = media != null ? media.position() : -1L; - long dur = media != null ? media.duration() : -1L; - boolean playing = media != null && media.playing(); - String debugKey = owner + "\u0000" + title + "\u0000" + artist + "\u0000" + dur + "\u0000" + playing; - if (debugKey.equals(lastDebugLogKey)) return; - lastDebugLogKey = debugKey; - DebugLog.info("[MediaSessionService] owner=%s title=%s artist=%s pos=%d dur=%d playing=%s", - owner, title, artist, pos, dur, playing); - } - - public record Snapshot(boolean mediaAvailable, - IMediaSession session, - MediaInfo media, - Identifier artworkTexture, - long predictedPositionMs, - long durationSeconds, - boolean hasSession) { - public static Snapshot empty(boolean mediaAvailable) { - return new Snapshot(mediaAvailable, null, null, null, 0L, 0L, false); - } - - /** - * Whole-second position for UI widgets (mm:ss display, progress bars). Lyric sync must use - * {@link #predictedPositionMs()} directly instead — truncating to seconds here previously fed - * a staircase-quantized timestamp into KineticLyrics, even though the underlying prediction - * already extrapolates smoothly between MPRIS/SMTC polls. - */ - public long predictedPositionSeconds() { - return predictedPositionMs / 1000L; - } - - public boolean isPlaying() { - return media != null && media.playing(); - } - - public String owner() { - return session != null ? safe(session.getOwner()) : ""; - } - - public String title() { - return media != null ? safe(media.title()) : ""; - } - - public String artist() { - return media != null ? safe(media.artist()) : ""; - } - - public boolean supportsShuffle() { - return session != null && session.supportsShuffle(); - } - - public boolean isShuffleActive() { - return session != null && session.isShuffleActive(); - } - - public boolean supportsRepeat() { - return session != null && session.supportsRepeat(); - } - + public record Snapshot(boolean mediaAvailable, IMediaSession session, MediaInfo media, Identifier artworkTexture, long predictedPositionMs, long durationSeconds, boolean hasSession) { + public static Snapshot empty(boolean mediaAvailable) { return new Snapshot(mediaAvailable, null, null, null, 0L, 0L, false); } + public long predictedPositionSeconds() { return predictedPositionMs / 1000L; } + public boolean isPlaying() { return media != null && media.playing(); } + public String owner() { return session != null ? safe(session.getOwner()) : ""; } + public String title() { return media != null ? safe(media.title()) : ""; } + public String artist() { return media != null ? safe(media.artist()) : ""; } + public boolean supportsShuffle() { return session != null && session.supportsShuffle(); } + public boolean isShuffleActive() { return session != null && session.isShuffleActive(); } + public boolean supportsRepeat() { return session != null && session.supportsRepeat(); } public RepeatMode repeatMode() { - if (session == null || !session.supportsRepeat()) { - return RepeatMode.UNKNOWN; - } - try { - return session.getRepeatMode(); - } catch (Throwable ignored) { - return RepeatMode.UNKNOWN; - } - } - - public boolean supportsSeek() { - return session != null && session.supportsSeek(); + if (session == null || !session.supportsRepeat()) return RepeatMode.UNKNOWN; + try { return session.getRepeatMode(); } catch (Throwable ignored) { return RepeatMode.UNKNOWN; } } + public boolean supportsSeek() { return session != null && session.supportsSeek(); } } } diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionArtwork.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionArtwork.java new file mode 100644 index 0000000..f5b17c5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionArtwork.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.util.media.session; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.util.logging.DebugLog; +import java.io.ByteArrayInputStream; +import dev.loki.lovisual.util.media.info.MediaInfo; + +final class MediaSessionArtwork { + + private static final Identifier ARTWORK_ID = + Identifier.fromNamespaceAndPath("lovisual", "media_player/artwork"); + + private Identifier artworkTexture; + private int lastArtworkSize; + + Identifier texture() { + return artworkTexture; + } + + void clear() { + artworkTexture = null; + lastArtworkSize = 0; + } + + void clearIfTrackChanged(boolean trackChanged) { + if (trackChanged) { + artworkTexture = null; + lastArtworkSize = 0; + } + } + + void maybeUpdate(MediaInfo media, boolean trackChanged) { + if (media == null) { + if (trackChanged) { + artworkTexture = null; + lastArtworkSize = 0; + } + return; + } + byte[] artwork = media.artworkPng(); + int size = artwork != null ? artwork.length : 0; + if (size == 0) { + if (trackChanged) { + artworkTexture = null; + lastArtworkSize = 0; + } + return; + } + if (trackChanged || artworkTexture == null || size != lastArtworkSize) { + updateArtwork(media); + lastArtworkSize = size; + } + } + + private void updateArtwork(MediaInfo media) { + Minecraft mc = Minecraft.getInstance(); + artworkTexture = null; + if (mc == null || media == null || media.artworkPng() == null + || media.artworkPng().length == 0) { + return; + } + NativeImage image = getArtworkAsNativeImage(media); + if (image == null) { + return; + } + DynamicTexture texture = new DynamicTexture(() -> "lovisual_media_artwork", image); + mc.getTextureManager().register(ARTWORK_ID, texture); + artworkTexture = ARTWORK_ID; + } + + private static NativeImage getArtworkAsNativeImage(MediaInfo mediaInfo) { + byte[] bytes = mediaInfo.artworkPng(); + if (bytes == null || bytes.length == 0) { + return null; + } + try { + return NativeImage.read(new ByteArrayInputStream(bytes)); + } catch (Exception e) { + DebugLog.errorOnce("media-artwork-native-image", + "[MediaSessionService] Failed to read artwork NativeImage", e); + return null; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionClock.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionClock.java new file mode 100644 index 0000000..3a32c77 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionClock.java @@ -0,0 +1,132 @@ +package dev.loki.lovisual.util.media.session; + +import net.minecraft.util.Util; +import dev.loki.lovisual.util.media.info.MediaInfo; + +final class MediaSessionClock { + + private long lastRawPosition; + private long lastRawDuration; + private boolean lastRawPlaying; + private long lastRawUpdateMs; + + long duration() { + return lastRawDuration; + } + + long getPredictedPositionMs(MediaInfo media) { + if (media == null) { + return 0L; + } + long now = Util.getMillis(); + long rawPos = Math.max(0L, media.position()); + long rawDur = Math.max(0L, media.duration()); + long[] normalized = normalizeTime(rawPos, rawDur); + rawPos = normalized[0]; + rawDur = normalized[1]; + boolean rawPlaying = media.playing(); + boolean changed = rawPos != lastRawPosition + || rawDur != lastRawDuration || rawPlaying != lastRawPlaying; + if (changed) { + lastRawPosition = rawPos; + lastRawDuration = rawDur; + lastRawPlaying = rawPlaying; + lastRawUpdateMs = now; + } + long predictedMs = lastRawPosition * 1000L; + if (lastRawPlaying) { + predictedMs += Math.max(0L, now - lastRawUpdateMs); + } + if (lastRawDuration > 0L && predictedMs > lastRawDuration * 1000L) { + predictedMs = lastRawDuration * 1000L; + } + return predictedMs; + } + + void resetPredictedAfterSkip() { + long now = Util.getMillis(); + lastRawPosition = 0L; + lastRawDuration = 0L; + lastRawPlaying = true; + lastRawUpdateMs = now; + } + + void applyLocalPlaybackState(boolean playing, MediaInfo media) { + if (media == null) { + return; + } + long now = Util.getMillis(); + long predicted = getPredictedPositionMs(media) / 1000L; + long rawDur = Math.max(0L, media.duration()); + long[] normalized = normalizeTime(predicted, rawDur); + lastRawPosition = normalized[0]; + lastRawDuration = normalized[1]; + lastRawPlaying = playing; + lastRawUpdateMs = now; + } + + void rememberLocalSeek(long positionSeconds, MediaInfo media) { + long now = Util.getMillis(); + long rawDur = Math.max(0L, media.duration()); + long[] normalized = normalizeTime(Math.max(0L, positionSeconds), rawDur); + lastRawPosition = normalized[0]; + lastRawDuration = normalized[1]; + lastRawPlaying = media != null && media.playing(); + lastRawUpdateMs = now; + } + + void resetPredictedClock(MediaInfo media) { + long now = Util.getMillis(); + if (media == null) { + lastRawPosition = 0L; + lastRawDuration = 0L; + lastRawPlaying = false; + lastRawUpdateMs = now; + return; + } + long rawPos = Math.max(0L, media.position()); + long rawDur = Math.max(0L, media.duration()); + long[] normalized = normalizeTime(rawPos, rawDur); + long pos = normalized[0]; + long dur = normalized[1]; + if (dur > 0L && pos > dur + 5L) { + pos = 0L; + } + lastRawPosition = pos; + lastRawDuration = dur; + lastRawPlaying = media.playing(); + lastRawUpdateMs = now; + } + + boolean shouldForceTrackReset(MediaInfo media) { + if (media == null) { + return false; + } + long rawPos = Math.max(0L, media.position()); + if (rawPos > lastRawPosition) { + return false; + } + long backJump = lastRawPosition - rawPos; + return rawPos <= 2L && backJump >= 8L; + } + + long[] normalizeTime(long position, long duration) { + long pos = Math.max(0L, position); + long dur = Math.max(0L, duration); + if (dur == 0L) { + return new long[]{pos, 0L}; + } + for (int i = 0; i < 6; i++) { + if (pos > dur * 100L && pos >= 1000L) { + pos /= 1000L; + continue; + } + if (pos > 0L && dur > pos * 100L && dur >= 1000L) { + dur /= 1000L; + continue; + } + break; + } + return new long[]{pos, dur}; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionPoller.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionPoller.java new file mode 100644 index 0000000..181d85d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionPoller.java @@ -0,0 +1,63 @@ +package dev.loki.lovisual.util.media.session; + +import java.util.List; +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.media.info.MediaPlayerInfo; +import dev.loki.lovisual.util.media.session.IMediaSession; + +final class MediaSessionPoller { + + private static final long POLL_INTERVAL_MS = 300L; + + private final MediaPlayerInfo mediaPlayerInfo = MediaPlayerInfo.system(); + private volatile List mediaSessions = List.of(); + private volatile boolean pollingRunning; + private Thread pollingThread; + + List sessions() { + return mediaSessions; + } + + synchronized void init() { + if (pollingThread != null && pollingThread.isAlive()) { + return; + } + pollingRunning = true; + pollingThread = new Thread(this::pollMediaSessions, "LoVisualMediaInfoPollingThread"); + pollingThread.setDaemon(true); + pollingThread.start(); + } + + synchronized void shutdown() { + pollingRunning = false; + Thread thread = pollingThread; + if (thread == null) { + return; + } + try { + thread.join(1000L); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + pollingThread = null; + mediaSessions = List.of(); + } + + private void pollMediaSessions() { + while (pollingRunning) { + try { + mediaSessions = List.copyOf(mediaPlayerInfo.getMediaSessions()); + } catch (Throwable t) { + DebugLog.errorOnce("media-session-poll", + "[MediaSessionService] Failed to fetch media sessions", t); + mediaSessions = List.of(); + } + try { + Thread.sleep(POLL_INTERVAL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + pollingRunning = false; + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionTracker.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionTracker.java new file mode 100644 index 0000000..490b9c0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/media/session/service/MediaSessionTracker.java @@ -0,0 +1,168 @@ +package dev.loki.lovisual.util.media.session; + +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; +import dev.loki.lovisual.util.logging.DebugLog; +import java.util.List; +import dev.loki.lovisual.util.media.info.MediaInfo; +import dev.loki.lovisual.util.media.session.IMediaSession; + +final class MediaSessionTracker { + + private static final long SESSION_GRACE_MS = 1500L; + + private final MediaSessionClock clock; + private final MediaSessionArtwork artwork; + private IMediaSession currentSession; + private MediaInfo currentMedia; + private String lastTrackKey; + private long lastSessionSeenMs; + private long lastDebugLogMs; + private String lastDebugLogKey; + + MediaSessionTracker(MediaSessionClock clock, MediaSessionArtwork artwork) { + this.clock = clock; + this.artwork = artwork; + } + + IMediaSession currentSession() { + return currentSession; + } + + MediaInfo currentMedia() { + return currentMedia; + } + + Identifier artworkTexture() { + return artwork.texture(); + } + + void setCurrentMedia(MediaInfo media) { + currentMedia = media; + } + + MediaInfo resolveCurrentMedia() { + if (currentSession == null) { + return null; + } + try { + return currentSession.getMedia(); + } catch (Throwable ignored) { + return currentMedia; + } + } + + IMediaSession resolveSession(List sessions) { + long now = Util.getMillis(); + if (sessions.isEmpty()) { + if (currentSession != null && now - lastSessionSeenMs < SESSION_GRACE_MS) { + return currentSession; + } + lastTrackKey = null; + artwork.clear(); + currentSession = null; + currentMedia = null; + return null; + } + IMediaSession best = null; + MediaInfo bestMedia = null; + for (IMediaSession session : sessions) { + if (session == null) { + continue; + } + MediaInfo media; + try { + media = session.getMedia(); + } catch (Throwable ignored) { + continue; + } + if (best == null) { + best = session; + bestMedia = media; + } + if (media.playing()) { + best = session; + bestMedia = media; + break; + } + } + if (best == null) { + return null; + } + currentSession = best; + lastSessionSeenMs = now; + currentMedia = bestMedia; + if (bestMedia == null) { + return best; + } + String key = buildTrackKey(best, bestMedia); + boolean keyChanged = !key.equals(lastTrackKey); + boolean forceReset = clock.shouldForceTrackReset(bestMedia); + if (keyChanged || forceReset) { + clock.resetPredictedClock(bestMedia); + if (keyChanged) { + lastTrackKey = key; + } + } + artwork.maybeUpdate(bestMedia, keyChanged || forceReset); + debugLogMedia(best, bestMedia); + return best; + } + + void clearOnResourceReload() { + artwork.clear(); + lastTrackKey = null; + currentSession = null; + currentMedia = null; + lastSessionSeenMs = 0L; + lastDebugLogKey = null; + } + + private static String safe(String value) { + if (value == null) { + return ""; + } + String v = value.trim(); + return v.isEmpty() ? "" : v; + } + + private String buildTrackKey(IMediaSession session, MediaInfo media) { + String owner = session != null ? safe(session.getOwner()) : ""; + String title = media != null ? safe(media.title()) : ""; + String artist = media != null ? safe(media.artist()) : ""; + String durationKey = ""; + if (media != null) { + long dur = Math.max(0L, media.duration()); + if (dur > 0L) { + durationKey = Long.toString(dur); + } + } + return owner + "\u0000" + title + "\u0000" + artist + "\u0000" + durationKey; + } + + private void debugLogMedia(IMediaSession session, MediaInfo media) { + if (!DebugLog.isEnabled()) { + return; + } + long now = Util.getMillis(); + if (now - lastDebugLogMs < 1000L) { + return; + } + lastDebugLogMs = now; + String owner = session != null ? safe(session.getOwner()) : ""; + String title = media != null ? safe(media.title()) : ""; + String artist = media != null ? safe(media.artist()) : ""; + long pos = media != null ? media.position() : -1L; + long dur = media != null ? media.duration() : -1L; + boolean playing = media != null && media.playing(); + String debugKey = owner + "\u0000" + title + "\u0000" + artist + + "\u0000" + dur + "\u0000" + playing; + if (debugKey.equals(lastDebugLogKey)) { + return; + } + lastDebugLogKey = debugKey; + DebugLog.info( + "[MediaSessionService] owner=%s title=%s artist=%s pos=%d dur=%d playing=%s", + owner, title, artist, pos, dur, playing); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java index ec528d5..dd58763 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java @@ -1,126 +1,31 @@ -/* - * 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.util.network.control; -import net.minecraft.client.Minecraft; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; -import net.minecraft.network.protocol.common.ClientboundKeepAlivePacket; -import net.minecraft.network.protocol.common.ClientboundPingPacket; -import net.minecraft.network.protocol.game.*; -import net.minecraft.sounds.SoundEvents; +import net.minecraft.network.protocol.game.VecDeltaCodec; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.PositionMoveRotation; -import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.config.values.mode.EnumValue; import dev.loki.lovisual.events.meta.EventHandler; import dev.loki.lovisual.events.impl.entity.AttackEntityEvent; -import dev.loki.lovisual.events.impl.network.BlinkPacketEvent; import dev.loki.lovisual.events.impl.entity.EventTargetChanged; +import dev.loki.lovisual.events.impl.network.BlinkPacketEvent; import dev.loki.lovisual.events.impl.render.GameTickEvent; -import dev.loki.lovisual.util.target.TargetManager; -import dev.loki.lovisual.util.target.TargetingUtil; - -import java.util.concurrent.ThreadLocalRandom; -import dev.loki.lovisual.util.network.control.BlinkManager; -import dev.loki.lovisual.util.network.packet.TransferOrigin; +import dev.loki.lovisual.util.network.control.BacktrackModel.Config; public final class BacktrackController { public static final BacktrackController INSTANCE = new BacktrackController(); - private final VecDeltaCodec delayedPosition = new VecDeltaCodec(); - private volatile boolean enabled; - private Config config = Config.defaults(); - private Entity target; - private int currentDelayMs = randomBetween(config.minDelayMs(), config.maxDelayMs()); - private int currentChance = ThreadLocalRandom.current().nextInt(0, 100); - private long nextAllowedBacktrackMs; - private long trackingBufferUntilMs; - private long lastAttackMs; - private boolean shouldPause; + final VecDeltaCodec delayedPosition = new VecDeltaCodec(); + volatile boolean enabled; + Config config = Config.defaults(); + Entity target; + int currentDelayMs = BacktrackEngine.randomBetween(config.minDelayMs(), config.maxDelayMs()); + int currentChance = java.util.concurrent.ThreadLocalRandom.current().nextInt(0, 100); + long nextAllowedBacktrackMs; + long trackingBufferUntilMs; + long lastAttackMs; + boolean shouldPause; private BacktrackController() { } - private static boolean shouldAlwaysPass(Packet packet) { - return packet instanceof ServerboundChatPacket - || packet instanceof ServerboundChatCommandPacket - || packet instanceof ClientboundPingPacket - || packet instanceof ClientboundKeepAlivePacket - || packet instanceof ClientboundSystemChatPacket - || isPlayerHurtSound(packet); - } - - private static boolean isPlayerHurtSound(Packet packet) { - return packet instanceof ClientboundSoundPacket sound && sound.getSound().value() == SoundEvents.PLAYER_HURT - || packet instanceof ClientboundSoundEntityPacket entitySound - && entitySound.getSound().value() == SoundEvents.PLAYER_HURT; - } - - private static boolean shouldClearOnSafetyPacket(Packet packet) { - return packet instanceof ClientboundPlayerPositionPacket - || packet instanceof ClientboundDisconnectPacket - || packet instanceof ClientboundSetHealthPacket health && health.getHealth() <= 0.0f; - } - - private static boolean isTargetEntityPacket(ClientboundMoveEntityPacket packet, Entity target, Minecraft mc) { - if (packet == null || target == null || mc == null || mc.level == null) { - return false; - } - Entity packetEntity = packet.getEntity(mc.level); - return packetEntity != null && packetEntity.getId() == target.getId(); - } - - private static LivingEntity findEnemy(Minecraft mc, double minRange, double maxRange) { - if (mc == null || mc.level == null || mc.player == null) { - return null; - } - - double safeMin = Math.max(0.0, Math.min(minRange, maxRange)); - double safeMax = Math.max(safeMin, Math.max(minRange, maxRange)); - AABB searchBox = mc.player.getBoundingBox().inflate(safeMax); - double minRangeSq = safeMin * safeMin; - double maxRangeSq = safeMax * safeMax; - - LivingEntity best = null; - double bestDist = Double.MAX_VALUE; - for (Entity entity : mc.level.getEntities(mc.player, searchBox, e -> e instanceof LivingEntity)) { - LivingEntity living = (LivingEntity) entity; - if (!TargetingUtil.isValidCombatTarget(living)) { - continue; - } - - double dist = TargetingUtil.distanceToEntityBoxSq(mc.player.getEyePosition(), living); - if (dist >= minRangeSq && dist <= maxRangeSq && dist < bestDist) { - best = living; - bestDist = dist; - } - } - return best; - } - - private static double boxedDistanceSq(Entity entity, Vec3 position) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null || entity == null || position == null) { - return Double.MAX_VALUE; - } - - AABB movedBox = entity.getBoundingBox().move(position.subtract(entity.position())); - return TargetingUtil.distanceToBoxSq(mc.player.getEyePosition(), movedBox); - } - - private static int randomBetween(int a, int b) { - int min = Math.max(0, Math.min(a, b)); - int max = Math.max(min, Math.max(a, b)); - return ThreadLocalRandom.current().nextInt(min, max + 1); - } - public boolean isEnabled() { return enabled; } @@ -136,11 +41,11 @@ public final class BacktrackController { public void configure(Config config) { this.config = config != null ? config : Config.defaults(); - this.currentDelayMs = randomBetween(this.config.minDelayMs(), this.config.maxDelayMs()); + this.currentDelayMs = BacktrackEngine.randomBetween(this.config.minDelayMs(), this.config.maxDelayMs()); } public boolean isLagging() { - return enabled && hasQueuedIncoming(); + return enabled && BacktrackEngine.hasQueuedIncoming(); } public Vec3 getTrackedPosition() { @@ -152,285 +57,34 @@ public final class BacktrackController { } public void setTarget(Entity target) { - processTarget(target); + BacktrackTargeting.processTarget(this, target); } public boolean shouldRenderTrackedBody() { - Entity currentTarget = target; - if (!enabled - || !(currentTarget instanceof LivingEntity living) - || !living.isAlive() - || delayedPosition.getBase().equals(Vec3.ZERO)) { - return false; - } - - return config.targetMode() == TargetMode.RANGE - || hasQueuedIncoming() - || System.currentTimeMillis() - lastAttackMs <= config.lastAttackTimeToWorkMs(); + return BacktrackTargeting.shouldRenderTrackedBody(this); } @EventHandler public void onAttack(AttackEntityEvent event) { - if (!enabled || config.targetMode() != TargetMode.ATTACK || event == null) { - return; - } - - lastAttackMs = System.currentTimeMillis(); - currentChance = ThreadLocalRandom.current().nextInt(0, 100); - processTarget(event.getTarget()); + BacktrackTargeting.onAttack(this, event); } @EventHandler public void onTargetChanged(EventTargetChanged event) { - if (!enabled) { - return; - } - if (config.targetMode() != TargetMode.ATTACK) { - return; - } - if (event.currentSource == TargetManager.Source.ATTACK && event.current != null) { - lastAttackMs = System.currentTimeMillis(); - currentChance = ThreadLocalRandom.current().nextInt(0, 100); - processTarget(event.current); - } + BacktrackTargeting.onTargetChanged(this, event); } @EventHandler public void onTick(GameTickEvent event) { - if (!enabled) { - return; - } - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || mc.player == null) { - clear(true); - return; - } - - if (config.targetMode() == TargetMode.RANGE) { - LivingEntity enemy = findEnemy(mc, config.minRange(), config.maxRange()); - if (enemy != null) { - processTarget(enemy); - } else if (target != null || hasQueuedIncoming()) { - clear(true); - } - } - - boolean hadQueuedIncoming = hasQueuedIncoming(); - boolean shouldCancel = shouldCancelPackets(); - if (shouldCancel) { - long now = System.currentTimeMillis(); - BlinkManager.INSTANCE.flush(snapshot -> - snapshot.origin() == TransferOrigin.INCOMING - && snapshot.timestamp() <= now - currentDelayMs - ); - } else if (hadQueuedIncoming) { - BlinkManager.INSTANCE.flush(TransferOrigin.INCOMING); - } - - if (!hasQueuedIncoming()) { - currentDelayMs = randomBetween(config.minDelayMs(), config.maxDelayMs()); - } + BacktrackTargeting.onTick(this, event); } @EventHandler public void onBlinkPacket(BlinkPacketEvent event) { - if (!enabled || event == null || event.getOrigin() != TransferOrigin.INCOMING) { - return; - } - - Packet packet = event.getPacket(); - boolean shouldCancel = shouldCancelPackets(); - boolean hasQueuedIncoming = hasQueuedIncoming(); - - if (packet == null) { - if (shouldCancel || hasQueuedIncoming) { - event.setAction(BlinkManager.Action.PASS); - } - return; - } - - if (!hasQueuedIncoming && !shouldCancel) { - return; - } - - if (shouldAlwaysPass(packet)) { - event.setAction(BlinkManager.Action.PASS); - return; - } - - if (shouldClearOnSafetyPacket(packet)) { - clear(true); - event.setAction(BlinkManager.Action.PASS); - return; - } - - Entity currentTarget = target; - if (currentTarget == null) { - return; - } - - Vec3 nextTargetPos = getPacketTargetPosition(packet, currentTarget); - if (nextTargetPos == null) { - event.setAction(BlinkManager.Action.PASS); - return; - } - - if (boxedDistanceSq(currentTarget, nextTargetPos) < boxedDistanceSq(currentTarget, currentTarget.position())) { - event.setAction(BlinkManager.Action.FLUSH); - return; - } - - event.setAction(BlinkManager.Action.QUEUE); + BacktrackPacketFlow.onBlinkPacket(this, event); } public void clear(boolean handlePackets) { - clear(handlePackets, false, true); - } - - private void clear(boolean handlePackets, boolean clearOnly, boolean resetNextBacktrackDelay) { - if (handlePackets && !clearOnly) { - BlinkManager.INSTANCE.flush(TransferOrigin.INCOMING); - } else if (clearOnly) { - BlinkManager.INSTANCE.getPacketQueue().removeIf(snapshot -> snapshot.origin() == TransferOrigin.INCOMING); - } - - if (target != null && resetNextBacktrackDelay) { - nextAllowedBacktrackMs = System.currentTimeMillis() - + randomBetween(config.minNextBacktrackDelayMs(), config.maxNextBacktrackDelayMs()); - } - - target = null; - delayedPosition.setBase(Vec3.ZERO); - shouldPause = false; - } - - private void processTarget(Entity enemy) { - if (!(enemy instanceof LivingEntity living)) { - return; - } - - shouldPause = living.hurtTime >= config.pauseHurtTime(); - if (!shouldBacktrack(living)) { - return; - } - - if (enemy != target) { - clear(true, false, false); - delayedPosition.setBase(enemy.getPositionCodec().getBase()); - } - - target = enemy; - } - - private boolean shouldCancelPackets() { - return target instanceof LivingEntity living && living.isAlive() && shouldBacktrack(living); - } - - private boolean shouldBacktrack(LivingEntity entity) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null || !TargetingUtil.isValidCombatTarget(entity)) { - return false; - } - - long now = System.currentTimeMillis(); - boolean inRange = boxedDistanceSq(entity, entity.position()) >= config.minRange() * config.minRange() - && boxedDistanceSq(entity, entity.position()) <= config.maxRange() * config.maxRange(); - if (inRange) { - trackingBufferUntilMs = now + config.trackingBufferMs(); - } - - boolean attackWindow = config.targetMode() != TargetMode.ATTACK - || now - lastAttackMs <= config.lastAttackTimeToWorkMs(); - - return (inRange || now < trackingBufferUntilMs) - && mc.player.tickCount > 10 - && currentChance < config.chancePercent() - && now >= nextAllowedBacktrackMs - && !shouldPause() - && attackWindow; - } - - private boolean shouldPause() { - return config.pauseOnHurtTime() && shouldPause; - } - - private boolean hasQueuedIncoming() { - return BlinkManager.INSTANCE.getPacketQueue().stream().anyMatch(snapshot -> snapshot.origin() == TransferOrigin.INCOMING); - } - - private Vec3 getPacketTargetPosition(Packet packet, Entity target) { - Minecraft mc = Minecraft.getInstance(); - if (target == null || mc == null || mc.level == null) { - return null; - } - - if (packet instanceof ClientboundMoveEntityPacket entityPacket && isTargetEntityPacket(entityPacket, target, mc) && entityPacket.hasPosition()) { - Vec3 pos = delayedPosition.decode( - entityPacket.getXa(), - entityPacket.getYa(), - entityPacket.getZa() - ); - delayedPosition.setBase(pos); - return pos; - } - if (packet instanceof ClientboundTeleportEntityPacket positionPacket && positionPacket.id() == target.getId()) { - PositionMoveRotation current = new PositionMoveRotation( - delayedPosition.getBase(), - target.getDeltaMovement(), - target.getYRot(), - target.getXRot() - ); - Vec3 pos = PositionMoveRotation.calculateAbsolute(current, positionPacket.change(), positionPacket.relatives()).position(); - delayedPosition.setBase(pos); - return pos; - } - if (packet instanceof ClientboundEntityPositionSyncPacket syncPacket && syncPacket.id() == target.getId()) { - Vec3 pos = syncPacket.values().position(); - delayedPosition.setBase(pos); - return pos; - } - return null; - } - - public enum TargetMode implements EnumValue.IdProvider { - ATTACK("attack"), - RANGE("range"); - - private final String id; - - TargetMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public record Config( - double minRange, - double maxRange, - int minDelayMs, - int maxDelayMs, - int minNextBacktrackDelayMs, - int maxNextBacktrackDelayMs, - int trackingBufferMs, - int chancePercent, - boolean pauseOnHurtTime, - int pauseHurtTime, - int lastAttackTimeToWorkMs, - TargetMode targetMode - ) { - public Config { - if (targetMode == null) { - targetMode = TargetMode.ATTACK; - } - } - - public static Config defaults() { - return new Config(0.0, 3.0, 100, 150, 0, 10, 500, 100, false, 3, 1000, TargetMode.ATTACK); - } + BacktrackTargeting.clear(this, handlePackets); } } diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackModel.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackModel.java new file mode 100644 index 0000000..a3b2a82 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackModel.java @@ -0,0 +1,55 @@ +package dev.loki.lovisual.util.network.control; + +import dev.loki.lovisual.config.values.mode.EnumValue; + +/** + * Data types for {@link BacktrackController}: the target-acquisition mode and the + * immutable timing/range configuration. Kept out of the controller so the stateful + * machine stays focused on packet flow. + */ +public final class BacktrackModel { + + private BacktrackModel() { + } + + public enum TargetMode implements EnumValue.IdProvider { + ATTACK("attack"), + RANGE("range"); + + private final String id; + + TargetMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public record Config( + double minRange, + double maxRange, + int minDelayMs, + int maxDelayMs, + int minNextBacktrackDelayMs, + int maxNextBacktrackDelayMs, + int trackingBufferMs, + int chancePercent, + boolean pauseOnHurtTime, + int pauseHurtTime, + int lastAttackTimeToWorkMs, + TargetMode targetMode + ) { + public Config { + if (targetMode == null) { + targetMode = TargetMode.ATTACK; + } + } + + public static Config defaults() { + return new Config(0.0, 3.0, 100, 150, 0, 10, 500, 100, false, 3, 1000, TargetMode.ATTACK); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java index 3089006..d451759 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java @@ -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.util.network.control; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java index a17cfcf..3d0223f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java @@ -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.util.network.control; import dev.loki.lovisual.util.screen.ClientScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackEngine.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackEngine.java new file mode 100644 index 0000000..527ef65 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackEngine.java @@ -0,0 +1,106 @@ +package dev.loki.lovisual.util.network.control; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.network.protocol.common.ClientboundKeepAlivePacket; +import net.minecraft.network.protocol.common.ClientboundPingPacket; +import net.minecraft.network.protocol.game.*; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.util.network.packet.TransferOrigin; +import dev.loki.lovisual.util.target.TargetingUtil; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Stateless helpers for {@link BacktrackController}: packet classification, + * enemy search, boxed-distance math, delay randomization and the queued-incoming + * probe. No controller instance state is touched. Package-private helper (impl + * subfolder, {@code control} package). + */ +final class BacktrackEngine { + + private BacktrackEngine() { + } + + static boolean shouldAlwaysPass(Packet packet) { + return packet instanceof ServerboundChatPacket + || packet instanceof ServerboundChatCommandPacket + || packet instanceof ClientboundPingPacket + || packet instanceof ClientboundKeepAlivePacket + || packet instanceof ClientboundSystemChatPacket + || isPlayerHurtSound(packet); + } + + static boolean isPlayerHurtSound(Packet packet) { + return packet instanceof ClientboundSoundPacket sound && sound.getSound().value() == SoundEvents.PLAYER_HURT + || packet instanceof ClientboundSoundEntityPacket entitySound + && entitySound.getSound().value() == SoundEvents.PLAYER_HURT; + } + + static boolean shouldClearOnSafetyPacket(Packet packet) { + return packet instanceof ClientboundPlayerPositionPacket + || packet instanceof ClientboundDisconnectPacket + || packet instanceof ClientboundSetHealthPacket health && health.getHealth() <= 0.0f; + } + + static boolean isTargetEntityPacket(ClientboundMoveEntityPacket packet, Entity target, Minecraft mc) { + if (packet == null || target == null || mc == null || mc.level == null) { + return false; + } + Entity packetEntity = packet.getEntity(mc.level); + return packetEntity != null && packetEntity.getId() == target.getId(); + } + + static LivingEntity findEnemy(Minecraft mc, double minRange, double maxRange) { + if (mc == null || mc.level == null || mc.player == null) { + return null; + } + + double safeMin = Math.max(0.0, Math.min(minRange, maxRange)); + double safeMax = Math.max(safeMin, Math.max(minRange, maxRange)); + AABB searchBox = mc.player.getBoundingBox().inflate(safeMax); + double minRangeSq = safeMin * safeMin; + double maxRangeSq = safeMax * safeMax; + + LivingEntity best = null; + double bestDist = Double.MAX_VALUE; + for (Entity entity : mc.level.getEntities(mc.player, searchBox, e -> e instanceof LivingEntity)) { + LivingEntity living = (LivingEntity) entity; + if (!TargetingUtil.isValidCombatTarget(living)) { + continue; + } + + double dist = TargetingUtil.distanceToEntityBoxSq(mc.player.getEyePosition(), living); + if (dist >= minRangeSq && dist <= maxRangeSq && dist < bestDist) { + best = living; + bestDist = dist; + } + } + return best; + } + + static double boxedDistanceSq(Entity entity, Vec3 position) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || entity == null || position == null) { + return Double.MAX_VALUE; + } + + AABB movedBox = entity.getBoundingBox().move(position.subtract(entity.position())); + return TargetingUtil.distanceToBoxSq(mc.player.getEyePosition(), movedBox); + } + + static int randomBetween(int a, int b) { + int min = Math.max(0, Math.min(a, b)); + int max = Math.max(min, Math.max(a, b)); + return ThreadLocalRandom.current().nextInt(min, max + 1); + } + + static boolean hasQueuedIncoming() { + return BlinkManager.INSTANCE.getPacketQueue().stream().anyMatch(snapshot -> snapshot.origin() == TransferOrigin.INCOMING); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackPacketFlow.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackPacketFlow.java new file mode 100644 index 0000000..33d0240 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackPacketFlow.java @@ -0,0 +1,112 @@ +package dev.loki.lovisual.util.network.control; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; +import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.PositionMoveRotation; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.events.impl.network.BlinkPacketEvent; +import dev.loki.lovisual.util.network.packet.TransferOrigin; + +/** + * Blink packet interception for {@link BacktrackController}: decides whether an + * incoming packet should pass, be queued or be flushed, and decodes the target's + * tracked position from movement packets. Pure classification and geometry live in + * {@link BacktrackEngine}; cancellation eligibility in {@link BacktrackTargeting}. + * Package-private helper (impl subfolder, {@code control} package). + */ +final class BacktrackPacketFlow { + + private BacktrackPacketFlow() { + } + + static void onBlinkPacket(BacktrackController c, BlinkPacketEvent event) { + if (!c.enabled || event == null || event.getOrigin() != TransferOrigin.INCOMING) { + return; + } + + Packet packet = event.getPacket(); + boolean shouldCancel = BacktrackTargeting.shouldCancelPackets(c); + boolean hasQueuedIncoming = BacktrackEngine.hasQueuedIncoming(); + + if (packet == null) { + if (shouldCancel || hasQueuedIncoming) { + event.setAction(BlinkManager.Action.PASS); + } + return; + } + + if (!hasQueuedIncoming && !shouldCancel) { + return; + } + + if (BacktrackEngine.shouldAlwaysPass(packet)) { + event.setAction(BlinkManager.Action.PASS); + return; + } + + if (BacktrackEngine.shouldClearOnSafetyPacket(packet)) { + BacktrackTargeting.clear(c, true); + event.setAction(BlinkManager.Action.PASS); + return; + } + + Entity currentTarget = c.target; + if (currentTarget == null) { + return; + } + + Vec3 nextTargetPos = getPacketTargetPosition(c, packet, currentTarget); + if (nextTargetPos == null) { + event.setAction(BlinkManager.Action.PASS); + return; + } + + if (BacktrackEngine.boxedDistanceSq(currentTarget, nextTargetPos) + < BacktrackEngine.boxedDistanceSq(currentTarget, currentTarget.position())) { + event.setAction(BlinkManager.Action.FLUSH); + return; + } + + event.setAction(BlinkManager.Action.QUEUE); + } + + private static Vec3 getPacketTargetPosition(BacktrackController c, Packet packet, Entity target) { + Minecraft mc = Minecraft.getInstance(); + if (target == null || mc == null || mc.level == null) { + return null; + } + + if (packet instanceof ClientboundMoveEntityPacket entityPacket + && BacktrackEngine.isTargetEntityPacket(entityPacket, target, mc) + && entityPacket.hasPosition()) { + Vec3 pos = c.delayedPosition.decode( + entityPacket.getXa(), + entityPacket.getYa(), + entityPacket.getZa() + ); + c.delayedPosition.setBase(pos); + return pos; + } + if (packet instanceof ClientboundTeleportEntityPacket positionPacket && positionPacket.id() == target.getId()) { + PositionMoveRotation current = new PositionMoveRotation( + c.delayedPosition.getBase(), + target.getDeltaMovement(), + target.getYRot(), + target.getXRot() + ); + Vec3 pos = PositionMoveRotation.calculateAbsolute(current, positionPacket.change(), positionPacket.relatives()).position(); + c.delayedPosition.setBase(pos); + return pos; + } + if (packet instanceof ClientboundEntityPositionSyncPacket syncPacket && syncPacket.id() == target.getId()) { + Vec3 pos = syncPacket.values().position(); + c.delayedPosition.setBase(pos); + return pos; + } + return null; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackTargeting.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackTargeting.java new file mode 100644 index 0000000..fbe5eba --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/network/control/impl/BacktrackTargeting.java @@ -0,0 +1,171 @@ +package dev.loki.lovisual.util.network.control; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.events.impl.entity.AttackEntityEvent; +import dev.loki.lovisual.events.impl.entity.EventTargetChanged; +import dev.loki.lovisual.events.impl.render.GameTickEvent; +import dev.loki.lovisual.util.network.control.BacktrackModel.TargetMode; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Stateful target acquisition and eligibility logic for {@link BacktrackController}. + * All methods are static and mutate the controller's package-private fields through + * the passed-in instance. Pure packet classification and geometry live in + * {@link BacktrackEngine}. Package-private helper (impl subfolder, {@code control} package). + */ +final class BacktrackTargeting { + + private BacktrackTargeting() { + } + + static void onAttack(BacktrackController c, AttackEntityEvent event) { + if (!c.enabled || c.config.targetMode() != TargetMode.ATTACK || event == null) { + return; + } + + c.lastAttackMs = System.currentTimeMillis(); + c.currentChance = ThreadLocalRandom.current().nextInt(0, 100); + processTarget(c, event.getTarget()); + } + + static void onTargetChanged(BacktrackController c, EventTargetChanged event) { + if (!c.enabled) { + return; + } + if (c.config.targetMode() != TargetMode.ATTACK) { + return; + } + if (event.currentSource == dev.loki.lovisual.util.target.TargetManager.Source.ATTACK && event.current != null) { + c.lastAttackMs = System.currentTimeMillis(); + c.currentChance = ThreadLocalRandom.current().nextInt(0, 100); + processTarget(c, event.current); + } + } + + static void onTick(BacktrackController c, GameTickEvent event) { + if (!c.enabled) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) { + clear(c, true); + return; + } + + if (c.config.targetMode() == TargetMode.RANGE) { + LivingEntity enemy = BacktrackEngine.findEnemy(mc, c.config.minRange(), c.config.maxRange()); + if (enemy != null) { + processTarget(c, enemy); + } else if (c.target != null || BacktrackEngine.hasQueuedIncoming()) { + clear(c, true); + } + } + + boolean hadQueuedIncoming = BacktrackEngine.hasQueuedIncoming(); + boolean shouldCancel = shouldCancelPackets(c); + if (shouldCancel) { + long now = System.currentTimeMillis(); + BlinkManager.INSTANCE.flush(snapshot -> + snapshot.origin() == dev.loki.lovisual.util.network.packet.TransferOrigin.INCOMING + && snapshot.timestamp() <= now - c.currentDelayMs + ); + } else if (hadQueuedIncoming) { + BlinkManager.INSTANCE.flush(dev.loki.lovisual.util.network.packet.TransferOrigin.INCOMING); + } + + if (!BacktrackEngine.hasQueuedIncoming()) { + c.currentDelayMs = BacktrackEngine.randomBetween(c.config.minDelayMs(), c.config.maxDelayMs()); + } + } + + static void processTarget(BacktrackController c, Entity enemy) { + if (!(enemy instanceof LivingEntity living)) { + return; + } + + c.shouldPause = living.hurtTime >= c.config.pauseHurtTime(); + if (!shouldBacktrack(c, living)) { + return; + } + + if (enemy != c.target) { + clear(c, true, false, false); + c.delayedPosition.setBase(enemy.getPositionCodec().getBase()); + } + + c.target = enemy; + } + + static boolean shouldCancelPackets(BacktrackController c) { + return c.target instanceof LivingEntity living && living.isAlive() && shouldBacktrack(c, living); + } + + static boolean shouldBacktrack(BacktrackController c, LivingEntity entity) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || !dev.loki.lovisual.util.target.TargetingUtil.isValidCombatTarget(entity)) { + return false; + } + + long now = System.currentTimeMillis(); + boolean inRange = BacktrackEngine.boxedDistanceSq(entity, entity.position()) >= c.config.minRange() * c.config.minRange() + && BacktrackEngine.boxedDistanceSq(entity, entity.position()) <= c.config.maxRange() * c.config.maxRange(); + if (inRange) { + c.trackingBufferUntilMs = now + c.config.trackingBufferMs(); + } + + boolean attackWindow = c.config.targetMode() != TargetMode.ATTACK + || now - c.lastAttackMs <= c.config.lastAttackTimeToWorkMs(); + + return (inRange || now < c.trackingBufferUntilMs) + && mc.player.tickCount > 10 + && c.currentChance < c.config.chancePercent() + && now >= c.nextAllowedBacktrackMs + && !shouldPause(c) + && attackWindow; + } + + static boolean shouldPause(BacktrackController c) { + return c.config.pauseOnHurtTime() && c.shouldPause; + } + + static void clear(BacktrackController c, boolean handlePackets) { + clear(c, handlePackets, false, true); + } + + static void clear(BacktrackController c, boolean handlePackets, boolean clearOnly, boolean resetNextBacktrackDelay) { + if (handlePackets && !clearOnly) { + BlinkManager.INSTANCE.flush(dev.loki.lovisual.util.network.packet.TransferOrigin.INCOMING); + } else if (clearOnly) { + BlinkManager.INSTANCE.getPacketQueue().removeIf(snapshot -> + snapshot.origin() == dev.loki.lovisual.util.network.packet.TransferOrigin.INCOMING); + } + + if (c.target != null && resetNextBacktrackDelay) { + c.nextAllowedBacktrackMs = System.currentTimeMillis() + + BacktrackEngine.randomBetween(c.config.minNextBacktrackDelayMs(), c.config.maxNextBacktrackDelayMs()); + } + + c.target = null; + c.delayedPosition.setBase(Vec3.ZERO); + c.shouldPause = false; + } + + static boolean shouldRenderTrackedBody(BacktrackController c) { + Entity currentTarget = c.target; + if (!c.enabled + || !(currentTarget instanceof LivingEntity living) + || !living.isAlive() + || c.delayedPosition.getBase().equals(Vec3.ZERO)) { + return false; + } + + return c.config.targetMode() == TargetMode.RANGE + || BacktrackEngine.hasQueuedIncoming() + || System.currentTimeMillis() - c.lastAttackMs <= c.config.lastAttackTimeToWorkMs(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java index 28a9ee1..56b297b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java @@ -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.util.network.packet; import net.minecraft.network.protocol.Packet; diff --git a/mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java b/mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java index d2dc6f1..721ec0a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java +++ b/mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java @@ -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.util.network.packet; public enum TransferOrigin { diff --git a/mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java b/mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java index 22469a5..fa4ef4d 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java +++ b/mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java @@ -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.util.physics; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticleColors.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticleColors.java new file mode 100644 index 0000000..440ff3c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticleColors.java @@ -0,0 +1,91 @@ +package dev.loki.lovisual.util.player.effect.heuristics; + +import dev.loki.lovisual.mixins.accessors.state.EffectParticleEffectAccessor; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ColorParticleOption; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffects; + +import java.util.List; + +/** + * Palette-independent particle color utilities: pulling an RGB color (or a directly-tagged effect) + * out of a {@link ParticleOptions}, brightness normalization, HSV hue and the palette color arrays + * derived from a set of mob effects. Used by {@link ParticlePalette} during inference. + */ +final class ParticleColors { + private ParticleColors() { + } + + static Holder directEffectFromParticle(ParticleOptions particle) { + if (particle == ParticleTypes.RAID_OMEN) return MobEffects.RAID_OMEN; + if (particle == ParticleTypes.TRIAL_OMEN) return MobEffects.TRIAL_OMEN; + return null; + } + + static int extractColor(ParticleOptions particle) { + if (particle instanceof EffectParticleEffectAccessor effect) { + return effect.lovisual$getColor() & 0xFFFFFF; + } + if (particle instanceof ColorParticleOption tinted) { + int r = Math.min(255, Math.max(0, Math.round(tinted.getRed() * 255f))); + int g = Math.min(255, Math.max(0, Math.round(tinted.getGreen() * 255f))); + int b = Math.min(255, Math.max(0, Math.round(tinted.getBlue() * 255f))); + return (r << 16) | (g << 8) | b; + } + return 0; + } + + static int[] computeColors(List> palette) { + int[] colors = new int[palette.size()]; + for (int i = 0; i < palette.size(); i++) { + colors[i] = palette.get(i).value().getColor(); + } + return colors; + } + + static int[] computeNormalizedColors(int[] colors) { + int[] normalized = new int[colors.length]; + for (int i = 0; i < colors.length; i++) { + normalized[i] = normalizeColor(colors[i]); + } + return normalized; + } + + static int normalizeColor(int color) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + int max = Math.max(r, Math.max(g, b)); + if (max <= 0) return 0; + float scale = 255f / (float) max; + int nr = Math.min(255, Math.round(r * scale)); + int ng = Math.min(255, Math.round(g * scale)); + int nb = Math.min(255, Math.round(b * scale)); + return (nr << 16) | (ng << 8) | nb; + } + + // Basic HSV hue calculation (degrees 0-360) + static float hueDegrees(int r, int g, int b) { + float rf = r / 255f; + float gf = g / 255f; + float bf = b / 255f; + float max = Math.max(rf, Math.max(gf, bf)); + float min = Math.min(rf, Math.min(gf, bf)); + float delta = max - min; + if (delta <= 0f) return 0f; + float hue; + if (max == rf) { + hue = (gf - bf) / delta; + } else if (max == gf) { + hue = 2f + (bf - rf) / delta; + } else { + hue = 4f + (rf - gf) / delta; + } + hue *= 60f; + if (hue < 0f) hue += 360f; + return hue; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticlePalette.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticlePalette.java new file mode 100644 index 0000000..05cef9f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/ParticlePalette.java @@ -0,0 +1,221 @@ +package dev.loki.lovisual.util.player.effect.heuristics; + +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffects; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Registry-backed effect/palette and nearest-color inference used by {@link StatusEffectHeuristics} + * to guess a mob effect from a colored entity-effect particle. Holds the dynamic palette and its + * precomputed/normalized colors plus the color-matching inference; palette-independent color math + * and particle extraction live in {@link ParticleColors}. + */ +final class ParticlePalette { + static final int COLOR_THRESHOLD = 42000; + static final int PRIORITY_THRESHOLD = 120000; // allow rougher matches for hard-to-distinguish colors + static final int AMBIGUITY_DELTA = 2000; + static final double AMBIGUITY_RATIO = 1.05; // accept best if second-best is clearly worse + + // Dynamic palette of effects + colors from the registry to survive version bumps. + private static final List> PALETTE = buildPalette(); + private static final int[] COLORS = ParticleColors.computeColors(PALETTE); + private static final int[] NORMALIZED_COLORS = ParticleColors.computeNormalizedColors(COLORS); + private static final Set> PRIORITY_EFFECTS = Set.of( + MobEffects.STRENGTH, + MobEffects.RESISTANCE + ); + private static final Set> FORCE_PICK_EFFECTS = Set.of( + MobEffects.STRENGTH, + MobEffects.RESISTANCE + ); + + private ParticlePalette() { + } + + /** + * Guesses the mob effect owning the nearest palette color to {@code color}, or {@code null} when + * the color is empty, ambiguous beyond the acceptance ratio, out of the allowed threshold, or a + * night-vision effect that must not be inferred heuristically. + */ + static Holder guess(int color) { + if (color == 0) return null; + // Normalize brightness so darker/lighter particles still map to the same hue bucket. + int normalizedColor = ParticleColors.normalizeColor(color); + Nearest nearest = nearest(normalizedColor, NORMALIZED_COLORS); + boolean priority = isPriority(nearest.idx); // e.g. strength/resistance + boolean ambiguous = nearest.secondDiff - nearest.diff < AMBIGUITY_DELTA; + + // Prefer fire resistance when it's one of the ambiguous pair so we don't mislabel it as strength. + int chosenIdx = nearest.idx; + int chosenDiff = nearest.diff; + // Strength vs fire-resistance: both share orange/red; bias using channel dominance. + if ((isStrength(nearest.idx) && isFire(nearest.secondIdx)) || (isStrength(nearest.secondIdx) && isFire(nearest.idx))) { + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + // Strength tint is deep red; fire resistance skews orange (more green). Bias toward fire only when clearly orange. + float hue = ParticleColors.hueDegrees(r, g, b); + boolean looksFire = (g >= r * 0.8f) && hue >= 25f; + if (looksFire) { + chosenIdx = isFire(nearest.idx) ? nearest.idx : nearest.secondIdx; + chosenDiff = isFire(nearest.idx) ? nearest.diff : nearest.secondDiff; + } else { + chosenIdx = isStrength(nearest.idx) ? nearest.idx : nearest.secondIdx; + chosenDiff = isStrength(nearest.idx) ? nearest.diff : nearest.secondDiff; + } + priority = isPriority(chosenIdx); + ambiguous = false; // resolved manually + } else if (ambiguous && (isFire(nearest.idx) ^ isFire(nearest.secondIdx))) { + chosenIdx = isFire(nearest.idx) ? nearest.idx : nearest.secondIdx; + chosenDiff = isFire(nearest.idx) ? nearest.diff : nearest.secondDiff; + priority = isPriority(chosenIdx); + ambiguous = false; + } else if (ambiguous && !priority && isPriority(nearest.secondIdx)) { + chosenIdx = nearest.secondIdx; + chosenDiff = nearest.secondDiff; + priority = true; + ambiguous = false; // force pick priority + } + + // For priority effects (strength/resistance) allow even if ambiguous, with wide threshold. + // Treat ambiguous matches as acceptable when the best is clearly closer than the runner-up (ratio check). + if (ambiguous && !priority) { + double ratio = nearest.secondDiff / (double) Math.max(1, nearest.diff); + if (ratio < AMBIGUITY_RATIO) { + return null; + } + } + + boolean allowed = priority + ? chosenDiff < PRIORITY_THRESHOLD + : chosenDiff < COLOR_THRESHOLD; + + if (chosenIdx < 0 || chosenIdx >= PALETTE.size() || !allowed) { + return null; + } + Holder guessed = PALETTE.get(chosenIdx); + if (isNightVision(chosenIdx)) return null; // do not infer night vision heuristically + + return guessed; + } + + private static boolean isPriority(int idx) { + Holder e = at(idx); + if (e == null) return false; + return PRIORITY_EFFECTS.contains(e); + } + + private static boolean isFire(int idx) { + Holder e = at(idx); + if (e == null) return false; + return e.equals(MobEffects.FIRE_RESISTANCE); + } + + private static boolean isStrength(int idx) { + Holder e = at(idx); + if (e == null) return false; + return e.equals(MobEffects.STRENGTH); + } + + // If ambiguous, force-pick these instead of skipping + private static boolean isForcePick(int idx) { + Holder e = at(idx); + if (e == null) return false; + return FORCE_PICK_EFFECTS.contains(e); + } + + private static boolean isNightVision(int idx) { + Holder e = at(idx); + if (e == null) return false; + return e.equals(MobEffects.NIGHT_VISION); + } + + private static Nearest nearest(int color, int[] palette) { + int best = -1, second = -1; + int bestDiff = Integer.MAX_VALUE; + int secondDiff = Integer.MAX_VALUE; + int r = (color >> 16) & 0xFF; + int g = (color >> 8) & 0xFF; + int b = color & 0xFF; + for (int i = 0; i < palette.length; i++) { + int c = palette[i]; + int dr = ((c >> 16) & 0xFF) - r; + int dg = ((c >> 8) & 0xFF) - g; + int db = (c & 0xFF) - b; + int diff = dr * dr + dg * dg + db * db; + if (diff < bestDiff) { + secondDiff = bestDiff; + second = best; + bestDiff = diff; + best = i; + } else if (diff < secondDiff) { + secondDiff = diff; + second = i; + } + } + // require reasonably close match + return new Nearest(best, bestDiff, second, secondDiff); + } + + private static Holder at(int idx) { + if (idx < 0 || idx >= PALETTE.size()) return null; + return PALETTE.get(idx); + } + + private static List> buildPalette() { + try { + return BuiltInRegistries.MOB_EFFECT.listElements().collect(Collectors.toCollection(ArrayList::new)); + } catch (Exception e) { + // Fallback to static list to avoid nulls during very early load + return Arrays.asList( + MobEffects.SPEED, + MobEffects.SLOWNESS, + MobEffects.HASTE, + MobEffects.MINING_FATIGUE, + MobEffects.STRENGTH, + MobEffects.JUMP_BOOST, + MobEffects.NAUSEA, + MobEffects.REGENERATION, + MobEffects.RESISTANCE, + MobEffects.FIRE_RESISTANCE, + MobEffects.WATER_BREATHING, + MobEffects.INVISIBILITY, + MobEffects.BLINDNESS, + MobEffects.NIGHT_VISION, + MobEffects.HUNGER, + MobEffects.WEAKNESS, + MobEffects.POISON, + MobEffects.SLOW_FALLING, + MobEffects.DOLPHINS_GRACE, + MobEffects.CONDUIT_POWER, + MobEffects.HERO_OF_THE_VILLAGE, + MobEffects.WITHER, + MobEffects.HEALTH_BOOST, + MobEffects.SATURATION, + MobEffects.LUCK, + MobEffects.UNLUCK, + MobEffects.GLOWING, + MobEffects.LEVITATION, + MobEffects.BAD_OMEN, + MobEffects.DARKNESS, + MobEffects.TRIAL_OMEN, + MobEffects.RAID_OMEN, + MobEffects.WIND_CHARGED, + MobEffects.WEAVING, + MobEffects.OOZING, + MobEffects.INFESTED, + MobEffects.BREATH_OF_THE_NAUTILUS + ); + } + } + + private record Nearest(int idx, int diff, int secondIdx, int secondDiff) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java index dd82f55..c19a229 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java @@ -1,32 +1,16 @@ -/* - * 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.util.player.effect.heuristics; import net.minecraft.client.Minecraft; import net.minecraft.core.Holder; -import net.minecraft.core.particles.ColorParticleOption; import net.minecraft.core.particles.ParticleOptions; -import net.minecraft.core.particles.ParticleTypes; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.effect.MobEffect; -import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.mixins.accessors.state.EffectParticleEffectAccessor; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import dev.loki.lovisual.util.player.effect.heuristics.StatusEffectInference; import dev.loki.lovisual.util.player.effect.tracker.StatusEffectTracker; +import java.util.List; + /** * Heuristics to guess effects from colored entity-effect particles. */ @@ -36,91 +20,15 @@ public enum StatusEffectHeuristics { private static final java.util.Map, Long>> LAST_SEEN = new java.util.HashMap<>(); private static final long HEURISTIC_TIMEOUT_MS = 10000L; - // Dynamic palette of effects + colors from the registry to survive version bumps. - private static final List> PALETTE = buildPalette(); - private static final int[] COLORS = computeColors(PALETTE); - private static final int[] NORMALIZED_COLORS = computeNormalizedColors(COLORS); - private static final Set> PRIORITY_EFFECTS = Set.of( - MobEffects.STRENGTH, - MobEffects.RESISTANCE - ); - private static final Set> FORCE_PICK_EFFECTS = Set.of( - MobEffects.STRENGTH, - MobEffects.RESISTANCE - ); - - // Loose thresholds: particle tint varies a lot across versions; keep wide but still reject wild mismatches. - private static final int COLOR_THRESHOLD = 42000; - private static final int PRIORITY_THRESHOLD = 120000; // allow rougher matches for hard-to-distinguish colors - private static final int AMBIGUITY_DELTA = 2000; - private static final double AMBIGUITY_RATIO = 1.05; // accept best if second-best is clearly worse - public static void handle(ParticleOptions particle, double px, double py, double pz) { - Holder direct = directEffectFromParticle(particle); + Holder direct = ParticleColors.directEffectFromParticle(particle); if (direct != null) { registerHeuristic(direct, px, py, pz); return; } - int color = extractColor(particle); - if (color == 0) return; - // Normalize brightness so darker/lighter particles still map to the same hue bucket. - int normalizedColor = normalizeColor(color); - Nearest nearest = nearest(normalizedColor, NORMALIZED_COLORS); - boolean priority = isPriority(nearest.idx); // e.g. strength/resistance - boolean ambiguous = nearest.secondDiff - nearest.diff < AMBIGUITY_DELTA; - - // Prefer fire resistance when it's one of the ambiguous pair so we don't mislabel it as strength. - int chosenIdx = nearest.idx; - int chosenDiff = nearest.diff; - // Strength vs fire-resistance: both share orange/red; bias using channel dominance. - if ((isStrength(nearest.idx) && isFire(nearest.secondIdx)) || (isStrength(nearest.secondIdx) && isFire(nearest.idx))) { - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - // Strength tint is deep red; fire resistance skews orange (more green). Bias toward fire only when clearly orange. - float hue = hueDegrees(r, g, b); - boolean looksFire = (g >= r * 0.8f) && hue >= 25f; - if (looksFire) { - chosenIdx = isFire(nearest.idx) ? nearest.idx : nearest.secondIdx; - chosenDiff = isFire(nearest.idx) ? nearest.diff : nearest.secondDiff; - } else { - chosenIdx = isStrength(nearest.idx) ? nearest.idx : nearest.secondIdx; - chosenDiff = isStrength(nearest.idx) ? nearest.diff : nearest.secondDiff; - } - priority = isPriority(chosenIdx); - ambiguous = false; // resolved manually - } else if (ambiguous && (isFire(nearest.idx) ^ isFire(nearest.secondIdx))) { - chosenIdx = isFire(nearest.idx) ? nearest.idx : nearest.secondIdx; - chosenDiff = isFire(nearest.idx) ? nearest.diff : nearest.secondDiff; - priority = isPriority(chosenIdx); - ambiguous = false; - } else if (ambiguous && !priority && isPriority(nearest.secondIdx)) { - chosenIdx = nearest.secondIdx; - chosenDiff = nearest.secondDiff; - priority = true; - ambiguous = false; // force pick priority - } - - // For priority effects (strength/resistance) allow even if ambiguous, with wide threshold. - // Treat ambiguous matches as acceptable when the best is clearly closer than the runner-up (ratio check). - if (ambiguous && !priority) { - double ratio = nearest.secondDiff / (double) Math.max(1, nearest.diff); - if (ratio < AMBIGUITY_RATIO) { - return; - } - } - - boolean allowed = priority - ? chosenDiff < PRIORITY_THRESHOLD - : chosenDiff < COLOR_THRESHOLD; - - if (chosenIdx < 0 || chosenIdx >= PALETTE.size() || !allowed) { - return; - } - Holder guessed = PALETTE.get(chosenIdx); - if (isNightVision(chosenIdx)) return; // do not infer night vision heuristically - + Holder guessed = ParticlePalette.guess(ParticleColors.extractColor(particle)); + if (guessed == null) return; registerHeuristic(guessed, px, py, pz); } @@ -174,37 +82,6 @@ public enum StatusEffectHeuristics { StatusEffectTracker.apply(target.getId(), guessed, amplifier, heuristicDurationTicks, false, true, true, true, hideDuration); } - private static boolean isPriority(int idx) { - Holder e = at(idx); - if (e == null) return false; - return PRIORITY_EFFECTS.contains(e); - } - - private static boolean isFire(int idx) { - Holder e = at(idx); - if (e == null) return false; - return e.equals(MobEffects.FIRE_RESISTANCE); - } - - private static boolean isStrength(int idx) { - Holder e = at(idx); - if (e == null) return false; - return e.equals(MobEffects.STRENGTH); - } - - // If ambiguous, force-pick these instead of skipping - private static boolean isForcePick(int idx) { - Holder e = at(idx); - if (e == null) return false; - return FORCE_PICK_EFFECTS.contains(e); - } - - private static boolean isNightVision(int idx) { - Holder e = at(idx); - if (e == null) return false; - return e.equals(MobEffects.NIGHT_VISION); - } - private static void markSeen(int entityId, Holder effect, long time) { var map = LAST_SEEN.computeIfAbsent(entityId, k -> new java.util.HashMap<>()); map.put(effect, time); @@ -231,157 +108,4 @@ public enum StatusEffectHeuristics { } removeEntities.forEach(LAST_SEEN::remove); } - - private static int[] computeColors(List> palette) { - int[] colors = new int[palette.size()]; - for (int i = 0; i < palette.size(); i++) { - colors[i] = palette.get(i).value().getColor(); - } - return colors; - } - - private static int[] computeNormalizedColors(int[] colors) { - int[] normalized = new int[colors.length]; - for (int i = 0; i < colors.length; i++) { - normalized[i] = normalizeColor(colors[i]); - } - return normalized; - } - - private static int normalizeColor(int color) { - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - int max = Math.max(r, Math.max(g, b)); - if (max <= 0) return 0; - float scale = 255f / (float) max; - int nr = Math.min(255, Math.round(r * scale)); - int ng = Math.min(255, Math.round(g * scale)); - int nb = Math.min(255, Math.round(b * scale)); - return (nr << 16) | (ng << 8) | nb; - } - - // Basic HSV hue calculation (degrees 0-360) - private static float hueDegrees(int r, int g, int b) { - float rf = r / 255f; - float gf = g / 255f; - float bf = b / 255f; - float max = Math.max(rf, Math.max(gf, bf)); - float min = Math.min(rf, Math.min(gf, bf)); - float delta = max - min; - if (delta <= 0f) return 0f; - float hue; - if (max == rf) { - hue = (gf - bf) / delta; - } else if (max == gf) { - hue = 2f + (bf - rf) / delta; - } else { - hue = 4f + (rf - gf) / delta; - } - hue *= 60f; - if (hue < 0f) hue += 360f; - return hue; - } - - private static Nearest nearest(int color, int[] palette) { - int best = -1, second = -1; - int bestDiff = Integer.MAX_VALUE; - int secondDiff = Integer.MAX_VALUE; - int r = (color >> 16) & 0xFF; - int g = (color >> 8) & 0xFF; - int b = color & 0xFF; - for (int i = 0; i < palette.length; i++) { - int c = palette[i]; - int dr = ((c >> 16) & 0xFF) - r; - int dg = ((c >> 8) & 0xFF) - g; - int db = (c & 0xFF) - b; - int diff = dr * dr + dg * dg + db * db; - if (diff < bestDiff) { - secondDiff = bestDiff; - second = best; - bestDiff = diff; - best = i; - } else if (diff < secondDiff) { - secondDiff = diff; - second = i; - } - } - // require reasonably close match - return new Nearest(best, bestDiff, second, secondDiff); - } - - private static Holder at(int idx) { - if (idx < 0 || idx >= PALETTE.size()) return null; - return PALETTE.get(idx); - } - - private static List> buildPalette() { - try { - return BuiltInRegistries.MOB_EFFECT.listElements().collect(Collectors.toCollection(ArrayList::new)); - } catch (Exception e) { - // Fallback to static list to avoid nulls during very early load - return Arrays.asList( - MobEffects.SPEED, - MobEffects.SLOWNESS, - MobEffects.HASTE, - MobEffects.MINING_FATIGUE, - MobEffects.STRENGTH, - MobEffects.JUMP_BOOST, - MobEffects.NAUSEA, - MobEffects.REGENERATION, - MobEffects.RESISTANCE, - MobEffects.FIRE_RESISTANCE, - MobEffects.WATER_BREATHING, - MobEffects.INVISIBILITY, - MobEffects.BLINDNESS, - MobEffects.NIGHT_VISION, - MobEffects.HUNGER, - MobEffects.WEAKNESS, - MobEffects.POISON, - MobEffects.SLOW_FALLING, - MobEffects.DOLPHINS_GRACE, - MobEffects.CONDUIT_POWER, - MobEffects.HERO_OF_THE_VILLAGE, - MobEffects.WITHER, - MobEffects.HEALTH_BOOST, - MobEffects.SATURATION, - MobEffects.LUCK, - MobEffects.UNLUCK, - MobEffects.GLOWING, - MobEffects.LEVITATION, - MobEffects.BAD_OMEN, - MobEffects.DARKNESS, - MobEffects.TRIAL_OMEN, - MobEffects.RAID_OMEN, - MobEffects.WIND_CHARGED, - MobEffects.WEAVING, - MobEffects.OOZING, - MobEffects.INFESTED, - MobEffects.BREATH_OF_THE_NAUTILUS - ); - } - } - - private static Holder directEffectFromParticle(ParticleOptions particle) { - if (particle == ParticleTypes.RAID_OMEN) return MobEffects.RAID_OMEN; - if (particle == ParticleTypes.TRIAL_OMEN) return MobEffects.TRIAL_OMEN; - return null; - } - - private static int extractColor(ParticleOptions particle) { - if (particle instanceof EffectParticleEffectAccessor effect) { - return effect.lovisual$getColor() & 0xFFFFFF; - } - if (particle instanceof ColorParticleOption tinted) { - int r = Math.min(255, Math.max(0, Math.round(tinted.getRed() * 255f))); - int g = Math.min(255, Math.max(0, Math.round(tinted.getGreen() * 255f))); - int b = Math.min(255, Math.max(0, Math.round(tinted.getBlue() * 255f))); - return (r << 16) | (g << 8) | b; - } - return 0; - } - - private record Nearest(int idx, int diff, int secondIdx, int secondDiff) { - } } - diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java index 6c89413..0b219a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java @@ -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.util.player.effect.heuristics; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java index f123510..ae7c2fa 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java @@ -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.util.player.effect.tracker; import net.minecraft.core.Holder; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java index 1b60d94..d75affa 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java @@ -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.util.player.effect.tracker; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java index f0ffbee..f06a03b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java @@ -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.util.player.effect.tracker; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java index b1309e9..d83ed5a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java @@ -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.util.player.health; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java index 6838dcd..059c0a4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java @@ -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.util.player.health; import com.mojang.authlib.GameProfile; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java index b391a1f..a1a03d8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java @@ -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.util.player.interaction; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java index b48c3e2..4c57a3f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java @@ -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.util.player.interaction; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java index b0d1708..e10e312 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java @@ -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.util.player.interaction; import net.minecraft.world.entity.player.Player; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java index ee02be1..91acf3a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java @@ -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.util.player.interaction; import net.minecraft.client.player.LocalPlayer; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java b/mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java index 34fc3c6..e0b2ba4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java @@ -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.util.player.inventory.firework; import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java index 4b4a29a..dde1364 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java @@ -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.util.player.movement; import net.minecraft.client.KeyMapping; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java index 3e3c099..8a49b47 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java @@ -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.util.player.movement; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java index 3f933a3..029a3c9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java @@ -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.util.player.movement; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java index 2046b2c..6cbf47f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java @@ -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.util.player.movement; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java index 1ba14a5..2a7aad4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java @@ -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.util.player.simulation; import net.minecraft.world.entity.player.Player; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java index cd65ddb..0f7d539 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java @@ -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.util.player.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java index 5f72c46..b458b41 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java @@ -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.util.player.simulation; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java index f96f9c1..97271f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java @@ -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.util.player.simulation; import net.minecraft.client.player.LocalPlayer; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java index 525ea0a..91df643 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java @@ -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.util.player.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java index 810c41a..38a0d5c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java @@ -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.util.player.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java index 26f105c..cafb3f5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java @@ -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.util.player.simulation.remote; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java index a653892..b83490a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java @@ -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.util.player.simulation.remote; import net.minecraft.world.entity.player.Player; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java index 077db6e..b1f1a2d 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java @@ -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.util.player.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java index 48d44a2..bab7990 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java @@ -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.util.player.simulation; import net.minecraft.core.BlockPos; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java index a47bc49..062a0ac 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java @@ -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.util.projectile.calc; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java index 2f43e22..04e6cec 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java @@ -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.util.projectile.calc; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java index 73b2494..42c9b9d 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java @@ -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.util.projectile.calc; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java index 9f55ddc..d667798 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java @@ -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.util.projectile.calc; import net.minecraft.world.phys.Vec3; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java index 25b7f00..a2f3ff0 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java @@ -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.util.projectile.math; public enum ProjectileMath { diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java index 2fc31f3..1de1877 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java @@ -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.util.projectile.math; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java index 4aab31c..eacb170 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java @@ -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.util.projectile.targeting; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java index a9a444b..2f23562 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java @@ -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.util.projectile.targeting; import net.minecraft.world.entity.Entity; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java index d3efff8..ba699eb 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java @@ -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.util.projectile.targeting; import net.minecraft.world.phys.AABB; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java index 4af4e2d..291b138 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java @@ -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.util.projectile.trajectory; import net.minecraft.client.multiplayer.ClientLevel; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java index 95fe559..deca729 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java @@ -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.util.projectile.trajectory; import net.minecraft.core.component.DataComponents; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java index aa26d17..e11996d 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java @@ -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.util.projectile.trajectory; import net.minecraft.world.entity.player.Player; diff --git a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java index d97dab0..b163e78 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java +++ b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java @@ -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.util.projectile.trajectory; public enum TrajectoryType { diff --git a/mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java b/mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java index a5f7db8..072bc64 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java +++ b/mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java @@ -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.util.protection; /** diff --git a/mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java b/mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java index 3c8e90a..7c83feb 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java +++ b/mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java @@ -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.util.protection; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java index d01ae45..7e3c51b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java @@ -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.util.proxy; import dev.loki.lovisual.util.proxy.model.ProxyConfig; import dev.loki.lovisual.util.proxy.model.ProxyEntry; diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java index 45dd80d..3cbd631 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java @@ -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.util.proxy; import dev.loki.lovisual.util.proxy.model.ProxyEntry; @@ -15,12 +8,8 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.ReferenceCountUtil; -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.nio.charset.StandardCharsets; public final class ProxyHandshakeHandler extends ChannelDuplexHandler { @@ -105,11 +94,11 @@ public final class ProxyHandshakeHandler extends ChannelDuplexHandler { switch (proxy.type()) { case SOCKS4 -> { state = State.SOCKS4_RESPONSE; - ctx.writeAndFlush(createSocks4ConnectRequest(ctx)); + ctx.writeAndFlush(ProxyProtocolMessages.socks4ConnectRequest(ctx, proxy, targetAddress)); } case SOCKS5 -> { state = State.SOCKS5_METHOD; - ctx.writeAndFlush(createSocks5Greeting(ctx)); + ctx.writeAndFlush(ProxyProtocolMessages.greeting(ctx, proxy)); } } } catch (Throwable t) { @@ -145,10 +134,10 @@ public final class ProxyHandshakeHandler extends ChannelDuplexHandler { } if (method == 0x02) { state = State.SOCKS5_AUTH; - ctx.writeAndFlush(createSocks5AuthRequest(ctx)); + ctx.writeAndFlush(ProxyProtocolMessages.authRequest(ctx, proxy)); } else if (method == 0x00) { state = State.SOCKS5_CONNECT; - ctx.writeAndFlush(createSocks5ConnectRequest(ctx)); + ctx.writeAndFlush(ProxyProtocolMessages.socks5ConnectRequest(ctx, targetAddress)); } else { throw new IllegalStateException("Unsupported SOCKS5 auth method: " + method); } @@ -162,7 +151,7 @@ public final class ProxyHandshakeHandler extends ChannelDuplexHandler { throw new IllegalStateException("SOCKS5 authentication failed: " + status); } state = State.SOCKS5_CONNECT; - ctx.writeAndFlush(createSocks5ConnectRequest(ctx)); + ctx.writeAndFlush(ProxyProtocolMessages.socks5ConnectRequest(ctx, targetAddress)); again = inbound.isReadable(); } case SOCKS5_CONNECT -> { @@ -217,91 +206,6 @@ public final class ProxyHandshakeHandler extends ChannelDuplexHandler { return true; } - private ByteBuf createSocks5Greeting(ChannelHandlerContext ctx) { - ByteBuf out = ctx.alloc().buffer(3); - out.writeByte(5); - out.writeByte(1); - out.writeByte(proxy.hasCredentials() ? 0x02 : 0x00); - return out; - } - - private ByteBuf createSocks5AuthRequest(ChannelHandlerContext ctx) { - byte[] username = ascii(proxy.username()); - byte[] password = ascii(proxy.password()); - if (username.length > 255 || password.length > 255) { - throw new IllegalArgumentException("SOCKS5 username/password is too long"); - } - - ByteBuf out = ctx.alloc().buffer(3 + username.length + password.length); - out.writeByte(1); - out.writeByte(username.length); - out.writeBytes(username); - out.writeByte(password.length); - out.writeBytes(password); - return out; - } - - private ByteBuf createSocks5ConnectRequest(ChannelHandlerContext ctx) { - ByteBuf out = ctx.alloc().buffer(32); - out.writeByte(5); - out.writeByte(1); - out.writeByte(0); - writeSocks5Address(out, targetAddress); - out.writeShort(targetAddress.getPort()); - return out; - } - - private ByteBuf createSocks4ConnectRequest(ChannelHandlerContext ctx) { - ByteBuf out = ctx.alloc().buffer(64); - out.writeByte(4); - out.writeByte(1); - out.writeShort(targetAddress.getPort()); - - InetAddress address = targetAddress.getAddress(); - boolean ipv4 = address instanceof Inet4Address; - if (ipv4) { - out.writeBytes(address.getAddress()); - } else { - out.writeByte(0); - out.writeByte(0); - out.writeByte(0); - out.writeByte(1); - } - - if (proxy.hasUsername()) { - out.writeBytes(ascii(proxy.username())); - } - out.writeByte(0); - - if (!ipv4) { - out.writeBytes(ascii(targetAddress.getHostString())); - out.writeByte(0); - } - return out; - } - - private static void writeSocks5Address(ByteBuf out, InetSocketAddress address) { - InetAddress inetAddress = address.getAddress(); - if (inetAddress instanceof Inet4Address) { - out.writeByte(1); - out.writeBytes(inetAddress.getAddress()); - return; - } - if (inetAddress instanceof Inet6Address) { - out.writeByte(4); - out.writeBytes(inetAddress.getAddress()); - return; - } - - byte[] host = ascii(address.getHostString()); - if (host.length > 255) { - throw new IllegalArgumentException("SOCKS5 host is too long: " + address.getHostString()); - } - out.writeByte(3); - out.writeByte(host.length); - out.writeBytes(host); - } - private void finish(ChannelHandlerContext ctx) { state = State.DONE; @@ -343,10 +247,6 @@ public final class ProxyHandshakeHandler extends ChannelDuplexHandler { } } - private static byte[] ascii(String value) { - return (value == null ? "" : value).getBytes(StandardCharsets.US_ASCII); - } - private enum State { INIT, SOCKS4_RESPONSE, diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java index d24afdf..5b65335 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java @@ -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.util.proxy; import dev.loki.lovisual.util.proxy.model.ProxyEntry; diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyProtocolMessages.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyProtocolMessages.java new file mode 100644 index 0000000..30968a5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyProtocolMessages.java @@ -0,0 +1,111 @@ +package dev.loki.lovisual.util.proxy; + +import dev.loki.lovisual.util.proxy.model.ProxyEntry; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +/** + * Outbound SOCKS4/SOCKS5 handshake message encoders used by {@link ProxyHandshakeHandler}. These are + * pure {@link ByteBuf} builders that read only the resolved {@link ProxyEntry} credentials and the + * target address handed in by the handler, keeping behaviour identical to the original inlined + * request-creation methods. + */ +final class ProxyProtocolMessages { + private ProxyProtocolMessages() { + } + + static ByteBuf greeting(ChannelHandlerContext ctx, ProxyEntry proxy) { + ByteBuf out = ctx.alloc().buffer(3); + out.writeByte(5); + out.writeByte(1); + out.writeByte(proxy.hasCredentials() ? 0x02 : 0x00); + return out; + } + + static ByteBuf authRequest(ChannelHandlerContext ctx, ProxyEntry proxy) { + byte[] username = ascii(proxy.username()); + byte[] password = ascii(proxy.password()); + if (username.length > 255 || password.length > 255) { + throw new IllegalArgumentException("SOCKS5 username/password is too long"); + } + + ByteBuf out = ctx.alloc().buffer(3 + username.length + password.length); + out.writeByte(1); + out.writeByte(username.length); + out.writeBytes(username); + out.writeByte(password.length); + out.writeBytes(password); + return out; + } + + static ByteBuf socks5ConnectRequest(ChannelHandlerContext ctx, InetSocketAddress target) { + ByteBuf out = ctx.alloc().buffer(32); + out.writeByte(5); + out.writeByte(1); + out.writeByte(0); + writeSocks5Address(out, target); + out.writeShort(target.getPort()); + return out; + } + + static ByteBuf socks4ConnectRequest(ChannelHandlerContext ctx, ProxyEntry proxy, InetSocketAddress target) { + ByteBuf out = ctx.alloc().buffer(64); + out.writeByte(4); + out.writeByte(1); + out.writeShort(target.getPort()); + + InetAddress address = target.getAddress(); + boolean ipv4 = address instanceof Inet4Address; + if (ipv4) { + out.writeBytes(address.getAddress()); + } else { + out.writeByte(0); + out.writeByte(0); + out.writeByte(0); + out.writeByte(1); + } + + if (proxy.hasUsername()) { + out.writeBytes(ascii(proxy.username())); + } + out.writeByte(0); + + if (!ipv4) { + out.writeBytes(ascii(target.getHostString())); + out.writeByte(0); + } + return out; + } + + private static void writeSocks5Address(ByteBuf out, InetSocketAddress address) { + InetAddress inetAddress = address.getAddress(); + if (inetAddress instanceof Inet4Address) { + out.writeByte(1); + out.writeBytes(inetAddress.getAddress()); + return; + } + if (inetAddress instanceof Inet6Address) { + out.writeByte(4); + out.writeBytes(inetAddress.getAddress()); + return; + } + + byte[] host = ascii(address.getHostString()); + if (host.length > 255) { + throw new IllegalArgumentException("SOCKS5 host is too long: " + address.getHostString()); + } + out.writeByte(3); + out.writeByte(host.length); + out.writeBytes(host); + } + + private static byte[] ascii(String value) { + return (value == null ? "" : value).getBytes(StandardCharsets.US_ASCII); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java index b391c08..ad3cde9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java @@ -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.util.proxy.model; import com.google.gson.Gson; diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java index 9a7ddc8..b51fc2b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java @@ -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.util.proxy.model; import java.net.InetSocketAddress; diff --git a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java index 0e231bf..c330c79 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java +++ b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java @@ -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.util.proxy.model; import com.google.gson.annotations.SerializedName; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java index 60e498a..318b088 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java @@ -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.util.pvp.client; import net.minecraft.util.Util; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java index 13c8361..f3eb6dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java @@ -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.util.pvp.client; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java index 52439ce..5f23b1a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java @@ -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.util.pvp.client; import net.minecraft.world.item.Item; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java index ddfc9f4..e1497b1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java @@ -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.util.pvp.cooldown; import net.minecraft.world.item.Item; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java index ac602c6..da2abc0 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java @@ -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.util.pvp.cooldown; import net.minecraft.world.item.Item; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java index d1846d2..918a362 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java @@ -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.util.pvp.cooldown; import net.minecraft.world.item.Item; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java index a591dfb..8df7cb6 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java @@ -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.util.pvp.cooldown; import net.minecraft.util.Util; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java index a78243c..fe47c20 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java @@ -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.util.pvp.opponents; import net.minecraft.world.item.Item; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java index 10826e5..a26d123 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java @@ -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.util.pvp.opponents; import net.minecraft.util.Util; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java index 71b3a4b..8368889 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java @@ -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.util.pvp.opponents; import java.util.UUID; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java index 591694f..1397f14 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java @@ -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.util.pvp.parser; import java.util.Set; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java index 55d0a67..7eeb5cd 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java @@ -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.util.pvp.parser; import java.util.Set; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java index 994a414..581d3e3 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java @@ -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.util.pvp.parser; import dev.loki.lovisual.util.pvp.parser.PvpOverlayParser; import dev.loki.lovisual.util.pvp.parser.PvpTextPatternUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java index 0fa7d1e..8d375e8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java @@ -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.util.pvp.parser; import dev.loki.lovisual.util.text.format.LegacyTextUtil; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java index d1283dd..f0cbe36 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java @@ -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.util.pvp.state; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java index 630ee18..f068263 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java @@ -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.util.pvp.state; public enum PvpTagSource { diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java index 116a949..80d7f83 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java @@ -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.util.pvp.state; import net.minecraft.util.Util; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java index d20fd59..3fd11f9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java @@ -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.util.pvp.tracker; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java index 737d6cb..35713fe 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java @@ -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.util.pvp.tracker; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java b/mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java index fcc9e08..4d53fb8 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java @@ -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.util.raycast; import dev.loki.lovisual.util.aiming.data.Rotation; diff --git a/mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java b/mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java index b0cdff6..0b16413 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java +++ b/mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java @@ -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.util.resources; import dev.loki.lovisual.util.logging.DebugLog; diff --git a/mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java b/mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java index 4d3cba5..00d212e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java +++ b/mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java @@ -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.util.resources; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java b/mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java index bbff7b2..7f9c574 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java +++ b/mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java @@ -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.util.screen; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java b/mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java index 8e3a476..f887bcb 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java +++ b/mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java @@ -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.util.screen; import net.minecraft.client.gui.screens.ChatScreen; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java b/mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java index 8b05981..9493c3f 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java @@ -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.util.session; import java.util.UUID; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java b/mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java index d946805..2513a2c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java @@ -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.util.session; import net.minecraft.client.main.GameConfig; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java b/mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java index d6f5830..96c39c1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java @@ -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.util.session; import com.mojang.authlib.minecraft.UserApiService; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java b/mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java index e4c927b..fd1e9b3 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java @@ -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.util.session; import java.nio.charset.StandardCharsets; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java index d81a03b..3fe484c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java @@ -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.util.session.microsoft; import com.google.gson.Gson; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java index 7c977b7..33de4f1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java @@ -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.util.session.microsoft.auth; public final class DeviceAuthPendingException extends RuntimeException { diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java index 9666425..9e96355 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java @@ -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.util.session.microsoft.auth; public class MicrosoftAuthException extends RuntimeException { diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java index c9ec987..6892212 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java @@ -1,423 +1,92 @@ -/* - * 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.util.session.microsoft.auth; -import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; -import dev.loki.lovisual.util.session.microsoft.model.MicrosoftTokens; -import dev.loki.lovisual.util.session.microsoft.model.MinecraftProfile; -import dev.loki.lovisual.util.session.microsoft.model.XboxToken; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import dev.loki.lovisual.util.session.MicrosoftSessionResult; -import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.session.microsoft.model.MicrosoftTokens; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.time.Instant; -import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; +/** + * Microsoft OAuth / Xbox Live / Minecraft authentication entry points. + * + *

    Facade holding the public login flows; HTTP plumbing lives in {@link MicrosoftHttp}, the + * device-code polling loop in {@link MicrosoftDeviceFlow} and the Xbox/XSTS/Minecraft exchange + * chain in {@link MicrosoftXboxFlow}. + */ public final class MicrosoftAuthService { - private static final String CLIENT_ID = "54fd49e4-2103-4044-9603-2b028c814ec3"; - private static final String USER_AGENT = "LoVisual AltManager"; - private static final Duration TIMEOUT = Duration.ofSeconds(Long.getLong("lovisual.microsoftAuth.timeout", 15L)); - - private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() { - private int index; - - @Override - public synchronized Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable, "LoVisualMicrosoftAuth-" + (++index)); - thread.setDaemon(true); - return thread; - } - }); - - private static final HttpClient CLIENT = HttpClient.newBuilder() - .connectTimeout(TIMEOUT) - .version(HttpClient.Version.HTTP_2) - .followRedirects(HttpClient.Redirect.NEVER) - .executor(EXECUTOR) - .build(); - private MicrosoftAuthService() { } public static CompletableFuture requestDeviceCode() { - String payload = "client_id=" + CLIENT_ID + "&scope=XboxLive.signin%20XboxLive.offline_access"; - HttpRequest request = formPost("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", payload); - return logFailures("request Microsoft device code", - sendJson(request, "request Microsoft device code") + String payload = "client_id=" + MicrosoftHttp.CLIENT_ID + "&scope=XboxLive.signin%20XboxLive.offline_access"; + HttpRequest request = MicrosoftHttp.formPost("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", payload); + return MicrosoftHttp.logFailures("request Microsoft device code", + MicrosoftHttp.sendJson(request, "request Microsoft device code") .thenApply(MicrosoftDeviceCode::fromJson)); } public static CompletableFuture loginWithDeviceCode(MicrosoftDeviceCode code) { if (code == null) { - return failedLoggedFuture("start Microsoft device-code login", new NullPointerException("code")); + return MicrosoftHttp.failedLoggedFuture("start Microsoft device-code login", new NullPointerException("code")); } Instant deadline = Instant.now().plus(code.expiresIn()); - return logFailures("poll Microsoft device-code login", - CompletableFuture.supplyAsync(() -> pollUntilAuthorized(code, deadline), EXECUTOR)) - .thenCompose(MicrosoftAuthService::loginWithMicrosoftTokens); + return MicrosoftHttp.logFailures("poll Microsoft device-code login", + CompletableFuture.supplyAsync(() -> MicrosoftDeviceFlow.pollUntilAuthorized(code, deadline), MicrosoftHttp.EXECUTOR)) + .thenCompose(MicrosoftXboxFlow::login); } public static CompletableFuture loginWithRefreshToken(String refreshToken) { String clean; try { - clean = cleanToken(refreshToken, "refreshToken"); + clean = MicrosoftHttp.cleanToken(refreshToken, "refreshToken"); } catch (Throwable t) { - return failedLoggedFuture("start Microsoft refresh-token login", t); + return MicrosoftHttp.failedLoggedFuture("start Microsoft refresh-token login", t); } - return refreshToMicrosoftTokens(clean).thenCompose(MicrosoftAuthService::loginWithMicrosoftTokens); + return refreshToMicrosoftTokens(clean).thenCompose(MicrosoftXboxFlow::login); } public static CompletableFuture loginWithAuthorizationCode(String code, String redirectUri) { String cleanCode; String cleanRedirect; try { - cleanCode = cleanToken(code, "code"); - cleanRedirect = cleanToken(redirectUri, "redirectUri"); + cleanCode = MicrosoftHttp.cleanToken(code, "code"); + cleanRedirect = MicrosoftHttp.cleanToken(redirectUri, "redirectUri"); } catch (Throwable t) { - return failedLoggedFuture("start Microsoft authorization-code login", t); + return MicrosoftHttp.failedLoggedFuture("start Microsoft authorization-code login", t); } - String payload = "client_id=" + CLIENT_ID + - "&code=" + encode(cleanCode) + + String payload = "client_id=" + MicrosoftHttp.CLIENT_ID + + "&code=" + MicrosoftHttp.encode(cleanCode) + "&grant_type=authorization_code" + - "&redirect_uri=" + encode(cleanRedirect) + + "&redirect_uri=" + MicrosoftHttp.encode(cleanRedirect) + "&scope=XboxLive.signin%20XboxLive.offline_access"; - return logFailures("exchange Microsoft authorization code", - sendJson(formPost("https://login.live.com/oauth20_token.srf", payload), "exchange Microsoft authorization code") + return MicrosoftHttp.logFailures("exchange Microsoft authorization code", + MicrosoftHttp.sendJson(MicrosoftHttp.formPost("https://login.live.com/oauth20_token.srf", payload), "exchange Microsoft authorization code") .thenApply(MicrosoftTokens::fromJson)) - .thenCompose(MicrosoftAuthService::loginWithMicrosoftTokens); + .thenCompose(MicrosoftXboxFlow::login); } private static CompletableFuture refreshToMicrosoftTokens(String refreshToken) { String clean; try { - clean = cleanToken(refreshToken, "refreshToken"); + clean = MicrosoftHttp.cleanToken(refreshToken, "refreshToken"); } catch (Throwable t) { - return failedLoggedFuture("refresh Microsoft OAuth token", t); + return MicrosoftHttp.failedLoggedFuture("refresh Microsoft OAuth token", t); } - String payload = "client_id=" + CLIENT_ID + - "&refresh_token=" + encode(clean) + + String payload = "client_id=" + MicrosoftHttp.CLIENT_ID + + "&refresh_token=" + MicrosoftHttp.encode(clean) + "&grant_type=refresh_token" + "&scope=XboxLive.signin%20XboxLive.offline_access"; - return logFailures("refresh Microsoft OAuth token", - sendJson(formPost("https://login.live.com/oauth20_token.srf", payload), "refresh Microsoft OAuth token") + return MicrosoftHttp.logFailures("refresh Microsoft OAuth token", + MicrosoftHttp.sendJson(MicrosoftHttp.formPost("https://login.live.com/oauth20_token.srf", payload), "refresh Microsoft OAuth token") .thenApply(MicrosoftTokens::fromJson)); } static CompletableFuture loginWithMicrosoftTokens(MicrosoftTokens tokens) { - if (tokens == null) { - return failedLoggedFuture("build Microsoft session", new NullPointerException("tokens")); - } - - return microsoftAccessToXbox(tokens.accessToken()) - .thenCompose(xbl -> xboxToXsts(xbl.token(), xbl.userHash())) - .thenCompose(xsts -> xstsToMinecraftAccess(xsts.token(), xsts.userHash())) - .thenCompose(minecraftAccess -> minecraftAccessToProfile(minecraftAccess) - .thenApply(profile -> new MicrosoftSessionResult(profile.name(), profile.uuid(), minecraftAccess, tokens.refreshToken()))); - } - - private static MicrosoftTokens pollUntilAuthorized(MicrosoftDeviceCode code, Instant deadline) { - DeviceAuthPendingException pending = null; - while (Instant.now().isBefore(deadline)) { - try { - return deviceCodeToMicrosoftTokens(code.deviceCode()); - } catch (DeviceAuthPendingException e) { - pending = e; - try { - Thread.sleep(Math.max(1000L, code.interval().toMillis())); - } catch (InterruptedException interrupted) { - Thread.currentThread().interrupt(); - throw new MicrosoftAuthException("Microsoft device-code login interrupted", interrupted); - } - } - } - throw new MicrosoftAuthException("Microsoft device-code login expired", pending, "lovisual.auth.microsoft.expired"); - } - - private static MicrosoftTokens deviceCodeToMicrosoftTokens(String deviceCode) { - String clean = cleanToken(deviceCode, "deviceCode"); - String payload = "grant_type=urn:ietf:params:oauth:grant-type:device_code" + - "&client_id=" + CLIENT_ID + - "&device_code=" + encode(clean); - HttpRequest request = formPost("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", payload); - HttpResponse response; - try { - response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (Throwable t) { - throw new MicrosoftAuthException("Unable to poll Microsoft device-code login", t); - } - int status = response.statusCode(); - if (status == HttpURLConnection.HTTP_OK) { - return MicrosoftTokens.fromJson(parseJson(response.body(), "parse Microsoft device-code token response")); - } - try { - JsonObject json = parseJson(response.body(), "parse Microsoft device-code error"); - String error = MicrosoftJson.string(json, "error"); - if ("authorization_pending".equals(error)) { - throw new DeviceAuthPendingException("Microsoft device-code login is still pending"); - } - if ("authorization_declined".equals(error)) { - throw new MicrosoftAuthException("Microsoft device-code login was cancelled by the user", "lovisual.auth.microsoft.cancelled"); - } - if ("expired_token".equals(error)) { - throw new MicrosoftAuthException("Microsoft device-code login expired", "lovisual.auth.microsoft.expired"); - } - } catch (DeviceAuthPendingException | MicrosoftAuthException e) { - throw e; - } catch (Throwable ignored) { - } - throw badStatus("poll Microsoft device-code login", response); - } - - private static CompletableFuture microsoftAccessToXbox(String microsoftAccess) { - String clean; - try { - clean = cleanToken(microsoftAccess, "microsoftAccess"); - } catch (Throwable t) { - return failedLoggedFuture("exchange Microsoft access token to Xbox Live token", t); - } - - JsonObject request = new JsonObject(); - JsonObject properties = new JsonObject(); - properties.addProperty("AuthMethod", "RPS"); - properties.addProperty("SiteName", "user.auth.xboxlive.com"); - properties.addProperty("RpsTicket", "d=" + clean); - request.add("Properties", properties); - request.addProperty("RelyingParty", "http://auth.xboxlive.com"); - request.addProperty("TokenType", "JWT"); - return logFailures("exchange Microsoft access token to Xbox Live token", - sendJson(jsonPost("https://user.auth.xboxlive.com/user/authenticate", request), "exchange Microsoft access token to Xbox Live token") - .thenApply(XboxToken::fromJson)); - } - - private static CompletableFuture xboxToXsts(String xboxToken, String expectedHash) { - String cleanToken; - try { - cleanToken = cleanToken(xboxToken, "xboxToken"); - } catch (Throwable t) { - return failedLoggedFuture("exchange Xbox Live token to XSTS", t); - } - - JsonObject request = new JsonObject(); - JsonObject properties = new JsonObject(); - JsonArray userTokens = new JsonArray(); - userTokens.add(cleanToken); - properties.add("UserTokens", userTokens); - properties.addProperty("SandboxId", "RETAIL"); - request.add("Properties", properties); - request.addProperty("RelyingParty", "rp://api.minecraftservices.com/"); - request.addProperty("TokenType", "JWT"); - return logFailures("exchange Xbox Live token to XSTS", - sendRaw(jsonPost("https://xsts.auth.xboxlive.com/xsts/authorize", request), "exchange Xbox Live token to XSTS") - .thenApply(response -> { - int status = response.statusCode(); - if (status == HttpURLConnection.HTTP_UNAUTHORIZED) { - throw xstsUnauthorized(response); - } - if (status != HttpURLConnection.HTTP_OK) { - throw badStatus("exchange Xbox Live token to XSTS", response); - } - XboxToken token = XboxToken.fromJson(parseJson(response.body(), "parse XSTS response")); - if (expectedHash != null && !expectedHash.isBlank() && !expectedHash.equals(token.userHash())) { - throw new MicrosoftAuthException("Mismatching Xbox user hash between XBL and XSTS"); - } - return token; - })); - } - - private static CompletableFuture xstsToMinecraftAccess(String xstsToken, String userHash) { - String cleanToken; - String cleanHash; - try { - cleanToken = cleanToken(xstsToken, "xstsToken"); - cleanHash = cleanToken(userHash, "userHash"); - } catch (Throwable t) { - return failedLoggedFuture("exchange XSTS token to Minecraft access token", t); - } - - JsonObject request = new JsonObject(); - request.addProperty("identityToken", "XBL3.0 x=" + cleanHash + ";" + cleanToken); - return logFailures("exchange XSTS token to Minecraft access token", - sendJson(jsonPost("https://api.minecraftservices.com/authentication/login_with_xbox", request), "exchange XSTS token to Minecraft access token") - .thenApply(json -> MicrosoftJson.string(json, "access_token"))); - } - - private static CompletableFuture minecraftAccessToProfile(String minecraftAccess) { - String clean; - try { - clean = cleanToken(minecraftAccess, "minecraftAccess"); - } catch (Throwable t) { - return failedLoggedFuture("fetch Minecraft profile", t); - } - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("https://api.minecraftservices.com/minecraft/profile")) - .header("User-Agent", USER_AGENT) - .header("Authorization", "Bearer " + clean) - .timeout(TIMEOUT) - .GET() - .build(); - return logFailures("fetch Minecraft profile", - sendRaw(request, "fetch Minecraft profile") - .thenApply(response -> { - int status = response.statusCode(); - if (status == HttpURLConnection.HTTP_NOT_FOUND) { - throw new MicrosoftAuthException("Microsoft account has no Minecraft profile", "lovisual.auth.microsoft.no_profile"); - } - if (status != HttpURLConnection.HTTP_OK) { - throw badStatus("fetch Minecraft profile", response); - } - return MinecraftProfile.fromJson(parseJson(response.body(), "parse Minecraft profile")); - })); - } - - private static CompletableFuture logFailures(String action, CompletableFuture future) { - return future.whenComplete((ignored, throwable) -> { - if (throwable != null) { - logAuthFailure(action, throwable); - } - }); - } - - private static CompletableFuture failedLoggedFuture(String action, Throwable throwable) { - logAuthFailure(action, throwable); - CompletableFuture future = new CompletableFuture<>(); - future.completeExceptionally(throwable); - return future; - } - - private static void logAuthFailure(String action, Throwable throwable) { - Throwable cause = unwrap(throwable); - if (cause instanceof DeviceAuthPendingException) { - return; - } - - String message = cause.getMessage(); - DebugLog.error("[AltManager][MicrosoftAuth] Failed to %s%s", cause, action, - message == null || message.isBlank() ? "" : ": " + message); - } - - private static Throwable unwrap(Throwable throwable) { - Throwable current = throwable; - while (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - return current == null ? new MicrosoftAuthException("Unknown Microsoft authentication failure") : current; - } - - private static HttpRequest formPost(String uri, String payload) { - return HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("User-Agent", USER_AGENT) - .header("Accept", "application/json") - .header("Content-Type", "application/x-www-form-urlencoded") - .timeout(TIMEOUT) - .POST(HttpRequest.BodyPublishers.ofString(payload)) - .build(); - } - - private static HttpRequest jsonPost(String uri, JsonObject payload) { - return HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("User-Agent", USER_AGENT) - .header("Accept", "application/json") - .header("Content-Type", "application/json") - .timeout(TIMEOUT) - .POST(HttpRequest.BodyPublishers.ofString(MicrosoftJson.GSON.toJson(payload))) - .build(); - } - - private static CompletableFuture sendJson(HttpRequest request, String action) { - return sendRaw(request, action).thenApply(response -> { - if (response.statusCode() != HttpURLConnection.HTTP_OK) { - throw badStatus(action, response); - } - return parseJson(response.body(), action); - }); - } - - private static CompletableFuture> sendRaw(HttpRequest request, String action) { - return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .exceptionally(t -> { - throw new MicrosoftAuthException("Unable to " + action, t); - }); - } - - private static JsonObject parseJson(String body, String action) { - try { - JsonObject json = MicrosoftJson.GSON.fromJson(body, JsonObject.class); - return Objects.requireNonNull(json, "response body is null"); - } catch (Throwable t) { - throw new MicrosoftAuthException("Unable to " + action + ": invalid JSON response", t); - } - } - - private static MicrosoftAuthException xstsUnauthorized(HttpResponse response) { - try { - JsonObject json = parseJson(response.body(), "parse XSTS unauthorized response"); - long error = MicrosoftJson.number(json, "XErr"); - if (error == 2148916233L) { - return new MicrosoftAuthException("Microsoft account has no linked Xbox account", "lovisual.auth.microsoft.no_xbox"); - } - if (error == 2148916235L) { - return new MicrosoftAuthException("Xbox Live is not available for this account region", "lovisual.auth.microsoft.xbox_unavailable"); - } - if (error == 2148916236L || error == 2148916237L || error == 2148916238L) { - return new MicrosoftAuthException("Microsoft account is not allowed to use Xbox Live for Minecraft", "lovisual.auth.microsoft.xbox_adult_required"); - } - return new MicrosoftAuthException("Unknown XSTS authorization error: " + error); - } catch (MicrosoftAuthException e) { - return e; - } catch (Throwable t) { - return new MicrosoftAuthException("XSTS authorization failed with HTTP 401", t); - } - } - - private static MicrosoftAuthException badStatus(String action, HttpResponse response) { - return new MicrosoftAuthException("Unable to " + action + ": HTTP " + response.statusCode() + " body=" + redact(response.body())); - } - - private static String encode(String raw) { - return URLEncoder.encode(raw, StandardCharsets.UTF_8); - } - - private static String cleanToken(String token, String name) { - if (token == null || token.isBlank()) { - throw new IllegalArgumentException(name + " is blank"); - } - return token.trim(); - } - - private static String redact(String body) { - if (body == null) { - return ""; - } - if (body.length() > 512) { - return body.substring(0, 512) + "..."; - } - return body; + return MicrosoftXboxFlow.login(tokens); } } diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java index 0a7d1a4..42d7b9b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java @@ -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.util.session.microsoft.auth; import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftDeviceFlow.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftDeviceFlow.java new file mode 100644 index 0000000..eaea980 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftDeviceFlow.java @@ -0,0 +1,72 @@ +package dev.loki.lovisual.util.session.microsoft.auth; + +import com.google.gson.JsonObject; +import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; +import dev.loki.lovisual.util.session.microsoft.model.MicrosoftTokens; + +import java.net.HttpURLConnection; +import java.net.http.HttpResponse; +import java.time.Instant; + +/** + * Device-code polling loop for {@link MicrosoftAuthService}: repeatedly exchanges the device code + * for Microsoft OAuth tokens until the user authorizes, the flow is declined or the deadline passes. + */ +final class MicrosoftDeviceFlow { + private MicrosoftDeviceFlow() { + } + + static MicrosoftTokens pollUntilAuthorized(MicrosoftDeviceCode code, Instant deadline) { + DeviceAuthPendingException pending = null; + while (Instant.now().isBefore(deadline)) { + try { + return deviceCodeToMicrosoftTokens(code.deviceCode()); + } catch (DeviceAuthPendingException e) { + pending = e; + try { + Thread.sleep(Math.max(1000L, code.interval().toMillis())); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new MicrosoftAuthException("Microsoft device-code login interrupted", interrupted); + } + } + } + throw new MicrosoftAuthException("Microsoft device-code login expired", pending, "lovisual.auth.microsoft.expired"); + } + + private static MicrosoftTokens deviceCodeToMicrosoftTokens(String deviceCode) { + String clean = MicrosoftHttp.cleanToken(deviceCode, "deviceCode"); + String payload = "grant_type=urn:ietf:params:oauth:grant-type:device_code" + + "&client_id=" + MicrosoftHttp.CLIENT_ID + + "&device_code=" + MicrosoftHttp.encode(clean); + HttpResponse response; + try { + response = MicrosoftHttp.CLIENT.send( + MicrosoftHttp.formPost("https://login.microsoftonline.com/consumers/oauth2/v2.0/token", payload), + HttpResponse.BodyHandlers.ofString()); + } catch (Throwable t) { + throw new MicrosoftAuthException("Unable to poll Microsoft device-code login", t); + } + int status = response.statusCode(); + if (status == HttpURLConnection.HTTP_OK) { + return MicrosoftTokens.fromJson(MicrosoftHttp.parseJson(response.body(), "parse Microsoft device-code token response")); + } + try { + JsonObject json = MicrosoftHttp.parseJson(response.body(), "parse Microsoft device-code error"); + String error = MicrosoftJson.string(json, "error"); + if ("authorization_pending".equals(error)) { + throw new DeviceAuthPendingException("Microsoft device-code login is still pending"); + } + if ("authorization_declined".equals(error)) { + throw new MicrosoftAuthException("Microsoft device-code login was cancelled by the user", "lovisual.auth.microsoft.cancelled"); + } + if ("expired_token".equals(error)) { + throw new MicrosoftAuthException("Microsoft device-code login expired", "lovisual.auth.microsoft.expired"); + } + } catch (DeviceAuthPendingException | MicrosoftAuthException e) { + throw e; + } catch (Throwable ignored) { + } + throw MicrosoftHttp.badStatus("poll Microsoft device-code login", response); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftHttp.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftHttp.java new file mode 100644 index 0000000..63c8b86 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftHttp.java @@ -0,0 +1,179 @@ +package dev.loki.lovisual.util.session.microsoft.auth; + +import com.google.gson.JsonObject; +import dev.loki.lovisual.util.logging.DebugLog; +import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Shared HTTP transport, request builders, JSON parsing and error-mapping plumbing for + * {@link MicrosoftAuthService} and its flow helpers. Owns the singleton {@link HttpClient} and its + * daemon executor; exposes only package-static utilities. + */ +final class MicrosoftHttp { + static final String CLIENT_ID = "54fd49e4-2103-4044-9603-2b028c814ec3"; + static final String USER_AGENT = "LoVisual AltManager"; + static final Duration TIMEOUT = Duration.ofSeconds(Long.getLong("lovisual.microsoftAuth.timeout", 15L)); + + static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() { + private int index; + + @Override + public synchronized Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "LoVisualMicrosoftAuth-" + (++index)); + thread.setDaemon(true); + return thread; + } + }); + + static final HttpClient CLIENT = HttpClient.newBuilder() + .connectTimeout(TIMEOUT) + .version(HttpClient.Version.HTTP_2) + .followRedirects(HttpClient.Redirect.NEVER) + .executor(EXECUTOR) + .build(); + + private MicrosoftHttp() { + } + + static HttpRequest formPost(String uri, String payload) { + return HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("User-Agent", USER_AGENT) + .header("Accept", "application/json") + .header("Content-Type", "application/x-www-form-urlencoded") + .timeout(TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofString(payload)) + .build(); + } + + static HttpRequest jsonPost(String uri, JsonObject payload) { + return HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("User-Agent", USER_AGENT) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .timeout(TIMEOUT) + .POST(HttpRequest.BodyPublishers.ofString(MicrosoftJson.GSON.toJson(payload))) + .build(); + } + + static CompletableFuture sendJson(HttpRequest request, String action) { + return sendRaw(request, action).thenApply(response -> { + if (response.statusCode() != HttpURLConnection.HTTP_OK) { + throw badStatus(action, response); + } + return parseJson(response.body(), action); + }); + } + + static CompletableFuture> sendRaw(HttpRequest request, String action) { + return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .exceptionally(t -> { + throw new MicrosoftAuthException("Unable to " + action, t); + }); + } + + static JsonObject parseJson(String body, String action) { + try { + JsonObject json = MicrosoftJson.GSON.fromJson(body, JsonObject.class); + return Objects.requireNonNull(json, "response body is null"); + } catch (Throwable t) { + throw new MicrosoftAuthException("Unable to " + action + ": invalid JSON response", t); + } + } + + static String encode(String raw) { + return URLEncoder.encode(raw, StandardCharsets.UTF_8); + } + + static String cleanToken(String token, String name) { + if (token == null || token.isBlank()) { + throw new IllegalArgumentException(name + " is blank"); + } + return token.trim(); + } + + static CompletableFuture logFailures(String action, CompletableFuture future) { + return future.whenComplete((ignored, throwable) -> { + if (throwable != null) { + logAuthFailure(action, throwable); + } + }); + } + + static CompletableFuture failedLoggedFuture(String action, Throwable throwable) { + logAuthFailure(action, throwable); + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(throwable); + return future; + } + + static MicrosoftAuthException xstsUnauthorized(HttpResponse response) { + try { + JsonObject json = parseJson(response.body(), "parse XSTS unauthorized response"); + long error = MicrosoftJson.number(json, "XErr"); + if (error == 2148916233L) { + return new MicrosoftAuthException("Microsoft account has no linked Xbox account", "lovisual.auth.microsoft.no_xbox"); + } + if (error == 2148916235L) { + return new MicrosoftAuthException("Xbox Live is not available for this account region", "lovisual.auth.microsoft.xbox_unavailable"); + } + if (error == 2148916236L || error == 2148916237L || error == 2148916238L) { + return new MicrosoftAuthException("Microsoft account is not allowed to use Xbox Live for Minecraft", "lovisual.auth.microsoft.xbox_adult_required"); + } + return new MicrosoftAuthException("Unknown XSTS authorization error: " + error); + } catch (MicrosoftAuthException e) { + return e; + } catch (Throwable t) { + return new MicrosoftAuthException("XSTS authorization failed with HTTP 401", t); + } + } + + static MicrosoftAuthException badStatus(String action, HttpResponse response) { + return new MicrosoftAuthException("Unable to " + action + ": HTTP " + response.statusCode() + " body=" + redact(response.body())); + } + + private static void logAuthFailure(String action, Throwable throwable) { + Throwable cause = unwrap(throwable); + if (cause instanceof DeviceAuthPendingException) { + return; + } + + String message = cause.getMessage(); + DebugLog.error("[AltManager][MicrosoftAuth] Failed to %s%s", cause, action, + message == null || message.isBlank() ? "" : ": " + message); + } + + private static Throwable unwrap(Throwable throwable) { + Throwable current = throwable; + while (current instanceof CompletionException && current.getCause() != null) { + current = current.getCause(); + } + return current == null ? new MicrosoftAuthException("Unknown Microsoft authentication failure") : current; + } + + private static String redact(String body) { + if (body == null) { + return ""; + } + if (body.length() > 512) { + return body.substring(0, 512) + "..."; + } + return body; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftXboxFlow.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftXboxFlow.java new file mode 100644 index 0000000..1149975 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/impl/MicrosoftXboxFlow.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.util.session.microsoft.auth; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import dev.loki.lovisual.util.session.MicrosoftSessionResult; +import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; +import dev.loki.lovisual.util.session.microsoft.model.MicrosoftTokens; +import dev.loki.lovisual.util.session.microsoft.model.MinecraftProfile; +import dev.loki.lovisual.util.session.microsoft.model.XboxToken; + +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +/** + * Xbox Live → XSTS → Minecraft access → profile token-exchange chain for + * {@link MicrosoftAuthService}. Given Microsoft OAuth tokens, walks the four-endpoint pipeline and + * assembles the resulting {@link MicrosoftSessionResult}. + */ +final class MicrosoftXboxFlow { + private MicrosoftXboxFlow() { + } + + static CompletableFuture login(MicrosoftTokens tokens) { + if (tokens == null) { + return MicrosoftHttp.failedLoggedFuture("build Microsoft session", new NullPointerException("tokens")); + } + + return microsoftAccessToXbox(tokens.accessToken()) + .thenCompose(xbl -> xboxToXsts(xbl.token(), xbl.userHash())) + .thenCompose(xsts -> xstsToMinecraftAccess(xsts.token(), xsts.userHash())) + .thenCompose(minecraftAccess -> minecraftAccessToProfile(minecraftAccess) + .thenApply(profile -> new MicrosoftSessionResult(profile.name(), profile.uuid(), minecraftAccess, tokens.refreshToken()))); + } + + private static CompletableFuture microsoftAccessToXbox(String microsoftAccess) { + String clean; + try { + clean = MicrosoftHttp.cleanToken(microsoftAccess, "microsoftAccess"); + } catch (Throwable t) { + return MicrosoftHttp.failedLoggedFuture("exchange Microsoft access token to Xbox Live token", t); + } + + JsonObject request = new JsonObject(); + JsonObject properties = new JsonObject(); + properties.addProperty("AuthMethod", "RPS"); + properties.addProperty("SiteName", "user.auth.xboxlive.com"); + properties.addProperty("RpsTicket", "d=" + clean); + request.add("Properties", properties); + request.addProperty("RelyingParty", "http://auth.xboxlive.com"); + request.addProperty("TokenType", "JWT"); + return MicrosoftHttp.logFailures("exchange Microsoft access token to Xbox Live token", + MicrosoftHttp.sendJson(MicrosoftHttp.jsonPost("https://user.auth.xboxlive.com/user/authenticate", request), "exchange Microsoft access token to Xbox Live token") + .thenApply(XboxToken::fromJson)); + } + + private static CompletableFuture xboxToXsts(String xboxToken, String expectedHash) { + String cleanToken; + try { + cleanToken = MicrosoftHttp.cleanToken(xboxToken, "xboxToken"); + } catch (Throwable t) { + return MicrosoftHttp.failedLoggedFuture("exchange Xbox Live token to XSTS", t); + } + + JsonObject request = new JsonObject(); + JsonObject properties = new JsonObject(); + JsonArray userTokens = new JsonArray(); + userTokens.add(cleanToken); + properties.add("UserTokens", userTokens); + properties.addProperty("SandboxId", "RETAIL"); + request.add("Properties", properties); + request.addProperty("RelyingParty", "rp://api.minecraftservices.com/"); + request.addProperty("TokenType", "JWT"); + return MicrosoftHttp.logFailures("exchange Xbox Live token to XSTS", + MicrosoftHttp.sendRaw(MicrosoftHttp.jsonPost("https://xsts.auth.xboxlive.com/xsts/authorize", request), "exchange Xbox Live token to XSTS") + .thenApply(response -> { + int status = response.statusCode(); + if (status == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw MicrosoftHttp.xstsUnauthorized(response); + } + if (status != HttpURLConnection.HTTP_OK) { + throw MicrosoftHttp.badStatus("exchange Xbox Live token to XSTS", response); + } + XboxToken token = XboxToken.fromJson(MicrosoftHttp.parseJson(response.body(), "parse XSTS response")); + if (expectedHash != null && !expectedHash.isBlank() && !expectedHash.equals(token.userHash())) { + throw new MicrosoftAuthException("Mismatching Xbox user hash between XBL and XSTS"); + } + return token; + })); + } + + private static CompletableFuture xstsToMinecraftAccess(String xstsToken, String userHash) { + String cleanToken; + String cleanHash; + try { + cleanToken = MicrosoftHttp.cleanToken(xstsToken, "xstsToken"); + cleanHash = MicrosoftHttp.cleanToken(userHash, "userHash"); + } catch (Throwable t) { + return MicrosoftHttp.failedLoggedFuture("exchange XSTS token to Minecraft access token", t); + } + + JsonObject request = new JsonObject(); + request.addProperty("identityToken", "XBL3.0 x=" + cleanHash + ";" + cleanToken); + return MicrosoftHttp.logFailures("exchange XSTS token to Minecraft access token", + MicrosoftHttp.sendJson(MicrosoftHttp.jsonPost("https://api.minecraftservices.com/authentication/login_with_xbox", request), "exchange XSTS token to Minecraft access token") + .thenApply(json -> MicrosoftJson.string(json, "access_token"))); + } + + private static CompletableFuture minecraftAccessToProfile(String minecraftAccess) { + String clean; + try { + clean = MicrosoftHttp.cleanToken(minecraftAccess, "minecraftAccess"); + } catch (Throwable t) { + return MicrosoftHttp.failedLoggedFuture("fetch Minecraft profile", t); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://api.minecraftservices.com/minecraft/profile")) + .header("User-Agent", MicrosoftHttp.USER_AGENT) + .header("Authorization", "Bearer " + clean) + .timeout(MicrosoftHttp.TIMEOUT) + .GET() + .build(); + return MicrosoftHttp.logFailures("fetch Minecraft profile", + MicrosoftHttp.sendRaw(request, "fetch Minecraft profile") + .thenApply(response -> { + int status = response.statusCode(); + if (status == HttpURLConnection.HTTP_NOT_FOUND) { + throw new MicrosoftAuthException("Microsoft account has no Minecraft profile", "lovisual.auth.microsoft.no_profile"); + } + if (status != HttpURLConnection.HTTP_OK) { + throw MicrosoftHttp.badStatus("fetch Minecraft profile", response); + } + return MinecraftProfile.fromJson(MicrosoftHttp.parseJson(response.body(), "parse Minecraft profile")); + })); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java index 0f561aa..72f002b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java @@ -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.util.session.microsoft.model; import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; import dev.loki.lovisual.util.session.microsoft.auth.MicrosoftAuthException; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java index 99e9ad5..eb82d33 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java @@ -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.util.session.microsoft.model; import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; import dev.loki.lovisual.util.session.microsoft.auth.MicrosoftAuthException; diff --git a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java index b2fda68..e520981 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java +++ b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java @@ -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.util.session.microsoft.model; import dev.loki.lovisual.util.session.microsoft.MicrosoftJson; import dev.loki.lovisual.util.session.microsoft.auth.MicrosoftAuthException; diff --git a/mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java b/mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java index d13825d..86b6b4e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java +++ b/mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java @@ -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.util.target; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java b/mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java index 4b010ae..63a029e 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java @@ -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.util.target; import dev.loki.lovisual.config.values.mode.EnumValue; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java index a3eeeb8..a3702e0 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java @@ -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.util.text.edit; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java b/mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java index 5afa7df..6db7207 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java @@ -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.util.text.edit; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java index e4aa19a..f8b01fc 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java @@ -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.util.text.format; import net.minecraft.ChatFormatting; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java index 77e76dc..fcf995b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java @@ -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.util.text.format; import com.google.gson.JsonElement; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java index b9f6a70..f19ff46 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java @@ -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.util.text.format; import net.minecraft.network.chat.Component; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java index 07aca1c..f4e359b 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java @@ -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.util.text.mask; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java index 2978698..edfe980 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java @@ -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.util.text.mask; import java.util.LinkedHashMap; diff --git a/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java index 9aa324d..d1c1448 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java +++ b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java @@ -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.util.text.mask; import java.util.ArrayList; diff --git a/mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java b/mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java index 890b44e..0180304 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java +++ b/mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java @@ -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.util.time; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/time/Timer.java b/mod/src/main/java/dev/loki/lovisual/util/time/Timer.java index f28c61f..0e9e269 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/time/Timer.java +++ b/mod/src/main/java/dev/loki/lovisual/util/time/Timer.java @@ -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.util.time; public class Timer { diff --git a/mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java b/mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java index d5fd353..b2c780c 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java +++ b/mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java @@ -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.util.time; import net.minecraft.util.Mth; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java index 9728af7..b9cc07a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java @@ -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.util.wav.engine; import net.minecraft.client.Minecraft; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java index 55a9799..cb92ea5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java @@ -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.util.wav.engine; import org.lwjgl.openal.AL10; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java index 3ad5436..d23f8c4 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java @@ -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.util.wav.engine; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java index e00e0fc..a3d23f7 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java @@ -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.util.wav.loader; import org.lwjgl.BufferUtils; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java index bc321a9..d6d79c5 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java @@ -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.util.wav.loader; import org.lwjgl.openal.AL10; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java index 249d604..195dbe1 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java @@ -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.util.wav.loader; import java.io.IOException; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java index 6642b9c..8fb8502 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java @@ -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.util.wav.util; import dev.loki.lovisual.features.module.lifecycle.Modules; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java index c994964..c6ff0e9 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java @@ -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.util.wav.util; import java.util.concurrent.atomic.AtomicInteger; diff --git a/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java index 572d7d7..14e1135 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java +++ b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java @@ -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.util.wav.util; import net.minecraft.resources.Identifier; diff --git a/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java index 7da426a..10170ab 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java +++ b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java @@ -1,210 +1,95 @@ -/* - * 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.util.world; import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.Holder; -import net.minecraft.core.Registry; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.resources.ResourceKey; import net.minecraft.util.Mth; -import net.minecraft.world.damagesource.CombatRules; -import net.minecraft.world.damagesource.DamageSource; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.EntityDimensions; -import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.enchantment.Enchantment; -import net.minecraft.world.item.enchantment.EnchantmentHelper; -import net.minecraft.world.item.enchantment.Enchantments; -import net.minecraft.world.level.BlockGetter; -import net.minecraft.world.level.ClipContext; -import net.minecraft.world.level.Explosion; -import net.minecraft.world.level.material.Fluids; import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import dev.loki.lovisual.util.entity.simulation.PositionExtrapolation; -import dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess; import dev.loki.lovisual.util.player.simulation.PlayerSimulationCache; +import dev.loki.lovisual.util.world.ExplosionExposure.ExposureDebug; +/** + * Public entry point for crystal / TNT damage prediction. Simulates the vanilla + * explosion pipeline (exposure sampling, distance decay, armor, resistance, protection) + * against a target's bounding box, optionally extrapolated {@code predictedTicks} into + * the future. Also exposes the intermediate values as a {@link DamageDebug} record for + * overlays and diagnostics. + */ public enum ExplosionDamageUtil { ; - private static final float CRYSTAL_POWER = 6.0f; - private static final int MAX_EXPOSURE_TOTAL_SAMPLES = 384; - private static final int MIN_CAPPED_SAMPLES_PER_AXIS = 2; - private static final EquipmentSlot[] ARMOR_SLOTS = { - EquipmentSlot.HEAD, - EquipmentSlot.CHEST, - EquipmentSlot.LEGS, - EquipmentSlot.FEET - }; - - public static float getCrystalDamage(LivingEntity entity, - Vec3 explosionPos, - int predictedTicks, - boolean ignoreTerrain) { + /** Convenience overload: end-crystal power, no ghost-obsidian override. */ + public static float getCrystalDamage(LivingEntity entity, Vec3 explosionPos, int predictedTicks, boolean ignoreTerrain) { return getExplosionDamage(entity, explosionPos, CRYSTAL_POWER, predictedTicks, ignoreTerrain); } - - public static float getCrystalDamage(LivingEntity entity, - Vec3 explosionPos, - int predictedTicks, - boolean ignoreTerrain, - BlockPos ghostObsidianPos) { + /** Same as above but treats {@code ghostObsidianPos} as an obsidian block for ray-blocking purposes. */ + public static float getCrystalDamage(LivingEntity entity, Vec3 explosionPos, int predictedTicks, boolean ignoreTerrain, BlockPos ghostObsidianPos) { return getExplosionDamage(entity, explosionPos, CRYSTAL_POWER, predictedTicks, ignoreTerrain, ghostObsidianPos); } - - public static DamageDebug debugCrystalDamage(LivingEntity entity, - Vec3 explosionPos, - int predictedTicks, - boolean ignoreTerrain) { + public static DamageDebug debugCrystalDamage(LivingEntity entity, Vec3 explosionPos, int predictedTicks, boolean ignoreTerrain) { return calculateExplosionDamage(entity, explosionPos, CRYSTAL_POWER, predictedTicks, ignoreTerrain, null); } - - public static DamageDebug debugCrystalDamage(LivingEntity entity, - Vec3 explosionPos, - int predictedTicks, - boolean ignoreTerrain, - BlockPos ghostObsidianPos) { + public static DamageDebug debugCrystalDamage(LivingEntity entity, Vec3 explosionPos, int predictedTicks, boolean ignoreTerrain, BlockPos ghostObsidianPos) { return calculateExplosionDamage(entity, explosionPos, CRYSTAL_POWER, predictedTicks, ignoreTerrain, ghostObsidianPos); } - - public static float getExplosionDamage(LivingEntity entity, - Vec3 explosionPos, - float power, - int predictedTicks, - boolean ignoreTerrain) { + public static float getExplosionDamage(LivingEntity entity, Vec3 explosionPos, float power, int predictedTicks, boolean ignoreTerrain) { return calculateExplosionDamage(entity, explosionPos, power, predictedTicks, ignoreTerrain, null).finalDamage(); } - - public static float getExplosionDamage(LivingEntity entity, - Vec3 explosionPos, - float power, - int predictedTicks, - boolean ignoreTerrain, - BlockPos ghostObsidianPos) { + public static float getExplosionDamage(LivingEntity entity, Vec3 explosionPos, float power, int predictedTicks, boolean ignoreTerrain, BlockPos ghostObsidianPos) { return calculateExplosionDamage(entity, explosionPos, power, predictedTicks, ignoreTerrain, ghostObsidianPos).finalDamage(); } - - private static DamageDebug calculateExplosionDamage(LivingEntity entity, - Vec3 explosionPos, - float power, - int predictedTicks, - boolean ignoreTerrain, - BlockPos ghostObsidianPos) { + /** Full pipeline: null-guard → invulnerable → range box → distance → exposure → reductions. */ + private static DamageDebug calculateExplosionDamage(LivingEntity entity, Vec3 explosionPos, float power, int predictedTicks, boolean ignoreTerrain, BlockPos ghostObsidianPos) { Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || entity == null || explosionPos == null) - return emptyDebug("null", null, 0.0); + if (mc == null || mc.level == null || entity == null || explosionPos == null) return emptyDebug("null", null, 0.0); if (entity.isInvulnerable()) return emptyDebug("invulnerable", entity.getBoundingBox(), 0.0); - AABB box = getPredictedBox(entity, predictedTicks); - if (!intersectsExplosionRange(explosionPos, box, power)) { return emptyDebug("range_box", box, entity.distanceToSqr(explosionPos)); } - double explosionDiameter = power * 2.0f; double distanceToExplosionSq = getExposureDistanceSq(entity, box, explosionPos, predictedTicks); if (distanceToExplosionSq > explosionDiameter * explosionDiameter) { return new DamageDebug("distance", 0.0f, 1.0, 0.0, 0.0f, 0.0f, 0.0f, 0.0f, distanceToExplosionSq, box, 0, 0, null, null, null, false, false); } - - ExposureDebug exposureDebug = getExposure(entity, explosionPos, box, ignoreTerrain, ghostObsidianPos); + ExposureDebug exposureDebug = ExplosionExposure.of(entity, explosionPos, box, ignoreTerrain, ghostObsidianPos); float exposure = exposureDebug.exposure(); double distanceDecay = 1.0 - (Math.sqrt(distanceToExplosionSq) / explosionDiameter); double impact = exposure * distanceDecay; if (impact <= 0.0) { - return new DamageDebug( - "impact", - exposure, - distanceDecay, - impact, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - distanceToExplosionSq, - box, - exposureDebug.clearRays(), - exposureDebug.totalRays(), - exposureDebug.blockerSample(), - exposureDebug.blockerPos(), - exposureDebug.blockerBlock(), - exposureDebug.blockerAtSampleCell(), - exposureDebug.blockerAtExplosionCell() - ); + return new DamageDebug("impact", exposure, distanceDecay, impact, 0.0f, 0.0f, 0.0f, 0.0f, distanceToExplosionSq, box, exposureDebug.clearRays(), exposureDebug.totalRays(), exposureDebug.blockerSample(), exposureDebug.blockerPos(), exposureDebug.blockerBlock(), exposureDebug.blockerAtSampleCell(), exposureDebug.blockerAtExplosionCell()); } - float preDamage = (float) (((impact * impact + impact) / 2.0) * 7.0 * explosionDiameter + 1.0); - float difficultyDamage = applyDifficulty(preDamage); - float armorDamage = applyArmorReduction(entity, difficultyDamage); - float resistanceDamage = applyResistanceReduction(entity, armorDamage); - float finalDamage = Math.max(applyProtectionReduction(entity, resistanceDamage), 0.0f); - return new DamageDebug( - "ok", - exposure, - distanceDecay, - impact, - preDamage, - armorDamage, - resistanceDamage, - finalDamage, - distanceToExplosionSq, - box, - exposureDebug.clearRays(), - exposureDebug.totalRays(), - exposureDebug.blockerSample(), - exposureDebug.blockerPos(), - exposureDebug.blockerBlock(), - exposureDebug.blockerAtSampleCell(), - exposureDebug.blockerAtExplosionCell() - ); + float difficultyDamage = ExplosionReductions.applyDifficulty(preDamage); + float armorDamage = ExplosionReductions.applyArmor(entity, difficultyDamage); + float resistanceDamage = ExplosionReductions.applyResistance(entity, armorDamage); + float finalDamage = Math.max(ExplosionReductions.applyProtection(entity, resistanceDamage), 0.0f); + return new DamageDebug("ok", exposure, distanceDecay, impact, preDamage, armorDamage, resistanceDamage, finalDamage, distanceToExplosionSq, box, exposureDebug.clearRays(), exposureDebug.totalRays(), exposureDebug.blockerSample(), exposureDebug.blockerPos(), exposureDebug.blockerBlock(), exposureDebug.blockerAtSampleCell(), exposureDebug.blockerAtExplosionCell()); } - + private static DamageDebug emptyDebug(String reason, AABB box, double distanceSq) { + return new DamageDebug(reason, 0.0f, 0.0, 0.0, 0.0f, 0.0f, 0.0f, 0.0f, distanceSq, box, 0, 0, null, null, null, false, false); + } + /** Cheap AABB-vs-AABB rejection before doing any expensive sampling. */ private static boolean intersectsExplosionRange(Vec3 explosionPos, AABB box, float power) { double maxDist = power * 2.0; return new AABB( - Mth.floor(explosionPos.x - maxDist - 1.0), - Mth.floor(explosionPos.y - maxDist - 1.0), - Mth.floor(explosionPos.z - maxDist - 1.0), - Mth.floor(explosionPos.x + maxDist + 1.0), - Mth.floor(explosionPos.y + maxDist + 1.0), - Mth.floor(explosionPos.z + maxDist + 1.0) + Mth.floor(explosionPos.x - maxDist - 1.0), Mth.floor(explosionPos.y - maxDist - 1.0), Mth.floor(explosionPos.z - maxDist - 1.0), + Mth.floor(explosionPos.x + maxDist + 1.0), Mth.floor(explosionPos.y + maxDist + 1.0), Mth.floor(explosionPos.z + maxDist + 1.0) ).intersects(box); } - + /** Squared distance from the (predicted) entity centre-of-mass to the explosion origin. */ private static double getExposureDistanceSq(LivingEntity entity, AABB box, Vec3 explosionPos, int predictedTicks) { - if (predictedTicks <= 0) { - return entity.distanceToSqr(explosionPos); - } - + if (predictedTicks <= 0) return entity.distanceToSqr(explosionPos); return box.getCenter().add(0.0, -0.9, 0.0).distanceToSqr(explosionPos); } - + /** Predicted bounding box: local-player simulation cache first, generic extrapolation as fallback. */ private static AABB getPredictedBox(LivingEntity entity, int predictedTicks) { - if (entity == null) { - return AABB.unitCubeFromLowerCorner(Vec3.ZERO); - } - - if (predictedTicks <= 0) { - return entity.getBoundingBox(); - } - + if (entity == null) return AABB.unitCubeFromLowerCorner(Vec3.ZERO); + if (predictedTicks <= 0) return entity.getBoundingBox(); if (entity instanceof Player player) { PlayerSimulationCache.SimulatedPlayerCache simulation = resolveSimulation(player); if (simulation != null) { @@ -213,346 +98,19 @@ public enum ExplosionDamageUtil { return dimensions.makeBoundingBox(snapshot.pos()); } } - return PositionExtrapolation.getBestForEntity(entity).getBoxInTicks(predictedTicks); } - private static PlayerSimulationCache.SimulatedPlayerCache resolveSimulation(Player player) { Minecraft mc = Minecraft.getInstance(); - if (mc != null && player == mc.player) { - return PlayerSimulationCache.getSimulationForLocalPlayer(); - } + if (mc != null && player == mc.player) return PlayerSimulationCache.getSimulationForLocalPlayer(); return PlayerSimulationCache.getSimulationForOtherPlayers(player); } - - private static DamageDebug emptyDebug(String reason, AABB box, double distanceSq) { - return new DamageDebug(reason, 0.0f, 0.0, 0.0, 0.0f, 0.0f, 0.0f, 0.0f, distanceSq, box, 0, 0, null, null, null, false, false); - } - - private static ExposureDebug getExposure(LivingEntity entity, Vec3 source, AABB box, boolean ignoreTerrain, BlockPos ghostObsidianPos) { - return getExposureToExplosion(entity, source, box, ignoreTerrain ? 600.0f : null, ghostObsidianPos); - } - - private static ExposureDebug getExposureToExplosion(LivingEntity entity, Vec3 source, AABB box, Float maxBlastResistance, BlockPos ghostObsidianPos) { - double sizeX = box.maxX - box.minX; - double sizeY = box.maxY - box.minY; - double sizeZ = box.maxZ - box.minZ; - double stepX = 1.0 / (sizeX * 2.0 + 1.0); - double stepY = 1.0 / (sizeY * 2.0 + 1.0); - double stepZ = 1.0 / (sizeZ * 2.0 + 1.0); - double offsetX = (1.0 - Math.floor(1.0 / stepX) * stepX) / 2.0; - double offsetZ = (1.0 - Math.floor(1.0 / stepZ) * stepZ) / 2.0; - - if (stepX < 0.0 || stepY < 0.0 || stepZ < 0.0) { - return new ExposureDebug(0.0f, 0, 0, null, null, null, false, false); - } - - int sampleCountX = estimateSampleCount(sizeX); - int sampleCountY = estimateSampleCount(sizeY); - int sampleCountZ = estimateSampleCount(sizeZ); - long estimatedTotalSamples = (long) sampleCountX * sampleCountY * sampleCountZ; - - if (estimatedTotalSamples > MAX_EXPOSURE_TOTAL_SAMPLES) { - return getExposureToExplosionCapped( - entity, - source, - box, - maxBlastResistance, - sampleCountX, - sampleCountY, - sampleCountZ, - estimatedTotalSamples, - ghostObsidianPos - ); - } - - net.minecraft.world.phys.shapes.CollisionContext shapeContext = net.minecraft.world.phys.shapes.CollisionContext.withPosition(entity, box.minY); - int hits = 0; - int total = 0; - Vec3 firstBlockedSample = null; - BlockPos firstBlockedPos = null; - String firstBlockedBlock = null; - boolean blockedAtSampleCell = false; - boolean blockedAtExplosionCell = false; - - for (double x = 0.0; x <= 1.0; x += stepX) { - for (double y = 0.0; y <= 1.0; y += stepY) { - for (double z = 0.0; z <= 1.0; z += stepZ) { - double sampleX = Mth.lerp(x, box.minX, box.maxX); - double sampleY = Mth.lerp(y, box.minY, box.maxY); - double sampleZ = Mth.lerp(z, box.minZ, box.maxZ); - Vec3 sample = new Vec3(sampleX + offsetX, sampleY, sampleZ + offsetZ); - BlockHitResult hitResult = raycast(new ClipContext( - sample, - source, - ClipContext.Block.COLLIDER, - ClipContext.Fluid.NONE, - shapeContext - ), maxBlastResistance, ghostObsidianPos); - if (hitResult.getType() == HitResult.Type.MISS) { - hits++; - } else if (firstBlockedPos == null) { - firstBlockedSample = sample; - firstBlockedPos = hitResult.getBlockPos(); - firstBlockedBlock = getBlockDebugName(hitResult.getBlockPos(), ghostObsidianPos); - blockedAtSampleCell = hitResult.getBlockPos().equals(BlockPos.containing(sample)); - blockedAtExplosionCell = hitResult.getBlockPos().equals(BlockPos.containing(source)); - } - total++; - } - } - } - - return new ExposureDebug( - total <= 0 ? 0.0f : (float) hits / (float) total, - hits, - total, - firstBlockedSample, - firstBlockedPos, - firstBlockedBlock, - blockedAtSampleCell, - blockedAtExplosionCell - ); - } - - private static ExposureDebug getExposureToExplosionCapped(LivingEntity entity, - Vec3 source, - AABB box, - Float maxBlastResistance, - int baseCountX, - int baseCountY, - int baseCountZ, - long estimatedTotalSamples, - BlockPos ghostObsidianPos) { - int sampleCountX = baseCountX; - int sampleCountY = baseCountY; - int sampleCountZ = baseCountZ; - - double scale = Math.cbrt((double) MAX_EXPOSURE_TOTAL_SAMPLES / (double) estimatedTotalSamples); - sampleCountX = clampSampleCount((int) Math.round(sampleCountX * scale), baseCountX); - sampleCountY = clampSampleCount((int) Math.round(sampleCountY * scale), baseCountY); - sampleCountZ = clampSampleCount((int) Math.round(sampleCountZ * scale), baseCountZ); - - while ((long) sampleCountX * sampleCountY * sampleCountZ > MAX_EXPOSURE_TOTAL_SAMPLES) { - if (sampleCountX >= sampleCountY && sampleCountX >= sampleCountZ && sampleCountX > MIN_CAPPED_SAMPLES_PER_AXIS) { - sampleCountX--; - } else if (sampleCountY >= sampleCountZ && sampleCountY > MIN_CAPPED_SAMPLES_PER_AXIS) { - sampleCountY--; - } else if (sampleCountZ > MIN_CAPPED_SAMPLES_PER_AXIS) { - sampleCountZ--; - } else { - break; - } - } - - net.minecraft.world.phys.shapes.CollisionContext shapeContext = net.minecraft.world.phys.shapes.CollisionContext.withPosition(entity, box.minY); - int hits = 0; - int total = 0; - Vec3 firstBlockedSample = null; - BlockPos firstBlockedPos = null; - String firstBlockedBlock = null; - boolean blockedAtSampleCell = false; - boolean blockedAtExplosionCell = false; - - for (int x = 0; x < sampleCountX; x++) { - for (int y = 0; y < sampleCountY; y++) { - for (int z = 0; z < sampleCountZ; z++) { - Vec3 sample = new Vec3( - sampleCoordinate(box.minX, box.maxX, x, sampleCountX), - sampleCoordinate(box.minY, box.maxY, y, sampleCountY), - sampleCoordinate(box.minZ, box.maxZ, z, sampleCountZ) - ); - - BlockHitResult hitResult = raycast(new ClipContext( - sample, - source, - ClipContext.Block.COLLIDER, - ClipContext.Fluid.NONE, - shapeContext - ), maxBlastResistance, ghostObsidianPos); - if (hitResult.getType() == HitResult.Type.MISS) { - hits++; - } else if (firstBlockedPos == null) { - firstBlockedSample = sample; - firstBlockedPos = hitResult.getBlockPos(); - firstBlockedBlock = getBlockDebugName(hitResult.getBlockPos(), ghostObsidianPos); - blockedAtSampleCell = hitResult.getBlockPos().equals(BlockPos.containing(sample)); - blockedAtExplosionCell = hitResult.getBlockPos().equals(BlockPos.containing(source)); - } - total++; - } - } - } - - return new ExposureDebug( - total <= 0 ? 0.0f : (float) hits / (float) total, - hits, - total, - firstBlockedSample, - firstBlockedPos, - firstBlockedBlock, - blockedAtSampleCell, - blockedAtExplosionCell - ); - } - - private static int estimateSampleCount(double size) { - return Math.max(2, (int) Math.floor(size * 2.0 + 1.0) + 1); - } - - private static int clampSampleCount(int proposed, int original) { - return Math.max(MIN_CAPPED_SAMPLES_PER_AXIS, Math.min(original, proposed)); - } - - private static double sampleCoordinate(double min, double max, int index, int total) { - if (total <= 1) { - return (min + max) * 0.5; - } - double t = (index + 0.5) / total; - return Mth.lerp(t, min, max); - } - - private static BlockHitResult raycast(ClipContext context, Float maxBlastResistance, BlockPos ghostObsidianPos) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null) { - return BlockHitResult.miss(context.getTo(), Direction.getApproximateNearest(0.0, 0.0, 1.0), BlockPos.containing(context.getTo())); - } - - return BlockGetter.traverseBlocks(context.getFrom(), context.getTo(), context, (innerContext, blockPos) -> { - boolean ghostBlock = ghostObsidianPos != null && blockPos.equals(ghostObsidianPos); - var state = ghostBlock - ? net.minecraft.world.level.block.Blocks.OBSIDIAN.defaultBlockState() - : mc.level.getBlockState(blockPos); - var fluidState = ghostBlock - ? Fluids.EMPTY.defaultFluidState() - : mc.level.getFluidState(blockPos); - if (maxBlastResistance != null && state.getBlock().getExplosionResistance() < maxBlastResistance) { - state = net.minecraft.world.level.block.Blocks.VOID_AIR.defaultBlockState(); - } - if (maxBlastResistance != null && fluidState.getExplosionResistance() < maxBlastResistance) { - fluidState = Fluids.EMPTY.defaultFluidState(); - } - - Vec3 start = innerContext.getFrom(); - Vec3 end = innerContext.getTo(); - var blockShape = innerContext.getBlockShape(state, mc.level, blockPos); - var blockHit = mc.level.clipWithInteractionOverride(start, end, blockPos, blockShape, state); - var fluidShape = innerContext.getFluidShape(fluidState, mc.level, blockPos); - var fluidHit = fluidShape.clip(start, end, blockPos); - double blockDistance = blockHit == null ? Double.MAX_VALUE : start.distanceToSqr(blockHit.getLocation()); - double fluidDistance = fluidHit == null ? Double.MAX_VALUE : start.distanceToSqr(fluidHit.getLocation()); - return blockDistance <= fluidDistance ? blockHit : fluidHit; - }, innerContext -> { - Vec3 diff = innerContext.getFrom().subtract(innerContext.getTo()); - return BlockHitResult.miss(innerContext.getTo(), Direction.getApproximateNearest(diff.x, diff.y, diff.z), BlockPos.containing(innerContext.getTo())); - }); - } - - private static String getBlockDebugName(BlockPos pos, BlockPos ghostObsidianPos) { - Minecraft mc = Minecraft.getInstance(); - if (ghostObsidianPos != null && ghostObsidianPos.equals(pos)) { - return BuiltInRegistries.BLOCK.getKey(net.minecraft.world.level.block.Blocks.OBSIDIAN).toString(); - } - if (mc == null || mc.level == null || pos == null) { - return null; - } - return BuiltInRegistries.BLOCK.getKey(mc.level.getBlockState(pos).getBlock()).toString(); - } - - private static float applyDifficulty(float damage) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null) return damage; - - return switch (mc.level.getDifficulty()) { - case EASY -> Math.min(damage / 2.0f + 1.0f, damage); - case HARD -> damage * 1.5f; - default -> damage; - }; - } - - private static float applyArmorReduction(LivingEntity entity, float damage) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null) return damage; - - DamageSource source = Explosion.getDefaultDamageSource(mc.level, mc.player); - float armor = entity.getArmorValue(); - float toughness = (float) entity.getAttributeValue(Attributes.ARMOR_TOUGHNESS); - return CombatRules.getDamageAfterAbsorb(entity, damage, source, armor, toughness); - } - - private static float applyResistanceReduction(LivingEntity entity, float damage) { - MobEffectInstance resistance = StatusEffectAccess.get(entity, MobEffects.RESISTANCE); - if (resistance == null) return damage; - int reduced = 25 - (resistance.getAmplifier() + 1) * 5; - return Math.max(damage * reduced / 25.0f, 0.0f); - } - - private static float applyProtectionReduction(LivingEntity entity, float damage) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || damage <= 0.0f) return Math.max(damage, 0.0f); - float protection = getProtectionAmount(entity); - return protection > 0.0f ? CombatRules.getDamageAfterMagicAbsorb(damage, protection) : damage; - } - - private static float getProtectionAmount(LivingEntity entity) { - float total = 0.0f; - for (EquipmentSlot slot : ARMOR_SLOTS) { - total += getProtectionAmount(entity.getItemBySlot(slot)); - } - return total; - } - - private static float getProtectionAmount(ItemStack stack) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || stack == null || stack.isEmpty()) return 0.0f; - return getEnchantmentLevel(Enchantments.BLAST_PROTECTION, stack) * 2.0f - + getEnchantmentLevel(Enchantments.PROTECTION, stack); - } - - private static int getEnchantmentLevel(ResourceKey key, ItemStack stack) { - Holder entry = getEnchantmentEntry(key); - return entry != null ? EnchantmentHelper.getItemEnchantmentLevel(entry, stack) : 0; - } - - private static Holder getEnchantmentEntry(ResourceKey key) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || key == null) return null; - - Registry registry = mc.level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); - Enchantment enchantment = registry.getValue(key); - return enchantment != null ? registry.wrapAsHolder(enchantment) : null; - } - + /** Full damage-pipeline breakdown for overlays; {@code reason} explains why {@code finalDamage} is 0. */ public record DamageDebug( - String reason, - float exposure, - double distanceFactor, - double impact, - float preDamage, - float postArmorDamage, - float postResistanceDamage, - float finalDamage, - double distanceSq, - AABB box, - int clearRays, - int totalRays, - Vec3 blockerSample, - BlockPos blockerPos, - String blockerBlock, - boolean blockerAtSampleCell, - boolean blockerAtExplosionCell - ) { - } - - private record ExposureDebug( - float exposure, - int clearRays, - int totalRays, - Vec3 blockerSample, - BlockPos blockerPos, - String blockerBlock, - boolean blockerAtSampleCell, - boolean blockerAtExplosionCell - ) { - } + String reason, float exposure, double distanceFactor, double impact, + float preDamage, float postArmorDamage, float postResistanceDamage, float finalDamage, + double distanceSq, AABB box, int clearRays, int totalRays, + Vec3 blockerSample, BlockPos blockerPos, String blockerBlock, + boolean blockerAtSampleCell, boolean blockerAtExplosionCell + ) {} } diff --git a/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionExposure.java b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionExposure.java new file mode 100644 index 0000000..c506466 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionExposure.java @@ -0,0 +1,176 @@ +package dev.loki.lovisual.util.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.client.Minecraft; + +/** + * Samples the target's bounding box with rays back to the explosion origin to compute + * how exposed the entity is (0..1). Handles both a step-fraction path and a capped + * index path used when the entity is large enough that the naive loop would be too costly. + * Package-private: called only from {@link ExplosionDamageUtil}. + */ +final class ExplosionExposure { + private static final int MAX_EXPOSURE_TOTAL_SAMPLES = 384; + private static final int MIN_CAPPED_SAMPLES_PER_AXIS = 2; + private ExplosionExposure() {} + /** Entry point: choose the raw or capped sampling path based on estimated sample count. */ + static ExposureDebug of(LivingEntity entity, Vec3 source, AABB box, boolean ignoreTerrain, BlockPos ghostObsidianPos) { + return compute(entity, source, box, ignoreTerrain ? 600.0f : null, ghostObsidianPos); + } + private static ExposureDebug compute(LivingEntity entity, Vec3 source, AABB box, Float maxBlastResistance, BlockPos ghostObsidianPos) { + double sizeX = box.maxX - box.minX; + double sizeY = box.maxY - box.minY; + double sizeZ = box.maxZ - box.minZ; + double stepX = 1.0 / (sizeX * 2.0 + 1.0); + double stepY = 1.0 / (sizeY * 2.0 + 1.0); + double stepZ = 1.0 / (sizeZ * 2.0 + 1.0); + double offsetX = (1.0 - Math.floor(1.0 / stepX) * stepX) / 2.0; + double offsetZ = (1.0 - Math.floor(1.0 / stepZ) * stepZ) / 2.0; + if (stepX < 0.0 || stepY < 0.0 || stepZ < 0.0) { + return new ExposureDebug(0.0f, 0, 0, null, null, null, false, false); + } + int sampleCountX = estimateSampleCount(sizeX); + int sampleCountY = estimateSampleCount(sizeY); + int sampleCountZ = estimateSampleCount(sizeZ); + long estimated = (long) sampleCountX * sampleCountY * sampleCountZ; + if (estimated > MAX_EXPOSURE_TOTAL_SAMPLES) { + return computeCapped(entity, source, box, maxBlastResistance, sampleCountX, sampleCountY, sampleCountZ, estimated, ghostObsidianPos); + } + CollisionContext shapeContext = CollisionContext.withPosition(entity, box.minY); + Accumulator acc = new Accumulator(); + for (double x = 0.0; x <= 1.0; x += stepX) { + for (double y = 0.0; y <= 1.0; y += stepY) { + for (double z = 0.0; z <= 1.0; z += stepZ) { + Vec3 sample = new Vec3(Mth.lerp(x, box.minX, box.maxX) + offsetX, Mth.lerp(y, box.minY, box.maxY), Mth.lerp(z, box.minZ, box.maxZ) + offsetZ); + acc.record(sample, source, shootRay(sample, source, shapeContext, maxBlastResistance, ghostObsidianPos), ghostObsidianPos); + } + } + } + return acc.toDebug(); + } + /** Down-samples each axis (never below {@link #MIN_CAPPED_SAMPLES_PER_AXIS}) until the total fits the budget. */ + private static ExposureDebug computeCapped(LivingEntity entity, Vec3 source, AABB box, Float maxBlastResistance, + int baseX, int baseY, int baseZ, long estimated, BlockPos ghostObsidianPos) { + double scale = Math.cbrt((double) MAX_EXPOSURE_TOTAL_SAMPLES / (double) estimated); + int sampleCountX = clampSampleCount((int) Math.round(baseX * scale), baseX); + int sampleCountY = clampSampleCount((int) Math.round(baseY * scale), baseY); + int sampleCountZ = clampSampleCount((int) Math.round(baseZ * scale), baseZ); + while ((long) sampleCountX * sampleCountY * sampleCountZ > MAX_EXPOSURE_TOTAL_SAMPLES) { + if (sampleCountX >= sampleCountY && sampleCountX >= sampleCountZ && sampleCountX > MIN_CAPPED_SAMPLES_PER_AXIS) { + sampleCountX--; + } else if (sampleCountY >= sampleCountZ && sampleCountY > MIN_CAPPED_SAMPLES_PER_AXIS) { + sampleCountY--; + } else if (sampleCountZ > MIN_CAPPED_SAMPLES_PER_AXIS) { + sampleCountZ--; + } else { + break; + } + } + CollisionContext shapeContext = CollisionContext.withPosition(entity, box.minY); + Accumulator acc = new Accumulator(); + for (int x = 0; x < sampleCountX; x++) { + for (int y = 0; y < sampleCountY; y++) { + for (int z = 0; z < sampleCountZ; z++) { + Vec3 sample = new Vec3( + sampleCoordinate(box.minX, box.maxX, x, sampleCountX), + sampleCoordinate(box.minY, box.maxY, y, sampleCountY), + sampleCoordinate(box.minZ, box.maxZ, z, sampleCountZ) + ); + acc.record(sample, source, shootRay(sample, source, shapeContext, maxBlastResistance, ghostObsidianPos), ghostObsidianPos); + } + } + } + return acc.toDebug(); + } + private static int estimateSampleCount(double size) { + return Math.max(2, (int) Math.floor(size * 2.0 + 1.0) + 1); + } + private static int clampSampleCount(int proposed, int original) { + return Math.max(MIN_CAPPED_SAMPLES_PER_AXIS, Math.min(original, proposed)); + } + private static double sampleCoordinate(double min, double max, int index, int total) { + if (total <= 1) return (min + max) * 0.5; + return Mth.lerp((index + 0.5) / total, min, max); + } + private static BlockHitResult shootRay(Vec3 from, Vec3 to, CollisionContext ctx, Float maxBlastResistance, BlockPos ghostObsidianPos) { + return raycast(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, ctx), maxBlastResistance, ghostObsidianPos); + } + /** + * Same traversal as vanilla {@code Level.clip} but honours two overlays: + * a ghost obsidian block (for crystal predictions) and a max-blast-resistance filter + * (so {@code ignoreTerrain} mode treats weak blocks as air). + */ + private static BlockHitResult raycast(ClipContext context, Float maxBlastResistance, BlockPos ghostObsidianPos) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) { + return BlockHitResult.miss(context.getTo(), Direction.getApproximateNearest(0.0, 0.0, 1.0), BlockPos.containing(context.getTo())); + } + return BlockGetter.traverseBlocks(context.getFrom(), context.getTo(), context, (innerContext, blockPos) -> { + boolean ghostBlock = ghostObsidianPos != null && blockPos.equals(ghostObsidianPos); + var state = ghostBlock ? Blocks.OBSIDIAN.defaultBlockState() : mc.level.getBlockState(blockPos); + var fluidState = ghostBlock ? Fluids.EMPTY.defaultFluidState() : mc.level.getFluidState(blockPos); + if (maxBlastResistance != null && state.getBlock().getExplosionResistance() < maxBlastResistance) { + state = Blocks.VOID_AIR.defaultBlockState(); + } + if (maxBlastResistance != null && fluidState.getExplosionResistance() < maxBlastResistance) { + fluidState = Fluids.EMPTY.defaultFluidState(); + } + Vec3 start = innerContext.getFrom(); + Vec3 end = innerContext.getTo(); + var blockShape = innerContext.getBlockShape(state, mc.level, blockPos); + var blockHit = mc.level.clipWithInteractionOverride(start, end, blockPos, blockShape, state); + var fluidShape = innerContext.getFluidShape(fluidState, mc.level, blockPos); + var fluidHit = fluidShape.clip(start, end, blockPos); + double blockDistance = blockHit == null ? Double.MAX_VALUE : start.distanceToSqr(blockHit.getLocation()); + double fluidDistance = fluidHit == null ? Double.MAX_VALUE : start.distanceToSqr(fluidHit.getLocation()); + return blockDistance <= fluidDistance ? blockHit : fluidHit; + }, innerContext -> { + Vec3 diff = innerContext.getFrom().subtract(innerContext.getTo()); + return BlockHitResult.miss(innerContext.getTo(), Direction.getApproximateNearest(diff.x, diff.y, diff.z), BlockPos.containing(innerContext.getTo())); + }); + } + private static String getBlockDebugName(BlockPos pos, BlockPos ghostObsidianPos) { + Minecraft mc = Minecraft.getInstance(); + if (ghostObsidianPos != null && ghostObsidianPos.equals(pos)) { + return BuiltInRegistries.BLOCK.getKey(Blocks.OBSIDIAN).toString(); + } + if (mc == null || mc.level == null || pos == null) return null; + return BuiltInRegistries.BLOCK.getKey(mc.level.getBlockState(pos).getBlock()).toString(); + } + /** Mutable hit counter shared by both sampling loops; records the first blocker for debug output. */ + private static final class Accumulator { + int hits, total; + Vec3 firstBlockedSample; BlockPos firstBlockedPos; String firstBlockedBlock; + boolean atSampleCell, atExplosionCell; + void record(Vec3 sample, Vec3 source, BlockHitResult hit, BlockPos ghost) { + if (hit.getType() == HitResult.Type.MISS) { + hits++; + } else if (firstBlockedPos == null) { + firstBlockedSample = sample; + firstBlockedPos = hit.getBlockPos(); + firstBlockedBlock = getBlockDebugName(hit.getBlockPos(), ghost); + atSampleCell = hit.getBlockPos().equals(BlockPos.containing(sample)); + atExplosionCell = hit.getBlockPos().equals(BlockPos.containing(source)); + } + total++; + } + ExposureDebug toDebug() { + return new ExposureDebug(total <= 0 ? 0.0f : (float) hits / (float) total, hits, total, firstBlockedSample, firstBlockedPos, firstBlockedBlock, atSampleCell, atExplosionCell); + } + } + record ExposureDebug(float exposure, int clearRays, int totalRays, Vec3 blockerSample, BlockPos blockerPos, + String blockerBlock, boolean blockerAtSampleCell, boolean blockerAtExplosionCell) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionReductions.java b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionReductions.java new file mode 100644 index 0000000..5e4c717 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionReductions.java @@ -0,0 +1,89 @@ +package dev.loki.lovisual.util.world; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.damagesource.CombatRules; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.level.Explosion; +import dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess; + +/** + * Applies Minecraft's post-impact damage reductions (difficulty, armor, fire-resistance + * potion, blast / general protection enchantments) to the raw explosion impact. + * Package-private: called only from {@link ExplosionDamageUtil}. + */ +final class ExplosionReductions { + private static final EquipmentSlot[] ARMOR_SLOTS = { + EquipmentSlot.HEAD, EquipmentSlot.CHEST, EquipmentSlot.LEGS, EquipmentSlot.FEET + }; + private ExplosionReductions() {} + /** Scales raw damage by world difficulty: half-rounded on Easy, +50% on Hard. */ + static float applyDifficulty(float damage) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) return damage; + return switch (mc.level.getDifficulty()) { + case EASY -> Math.min(damage / 2.0f + 1.0f, damage); + case HARD -> damage * 1.5f; + default -> damage; + }; + } + /** Vanilla armor + toughness formula, evaluated against a synthetic explosion damage source. */ + static float applyArmor(LivingEntity entity, float damage) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) return damage; + DamageSource source = Explosion.getDefaultDamageSource(mc.level, mc.player); + float armor = entity.getArmorValue(); + float toughness = (float) entity.getAttributeValue(Attributes.ARMOR_TOUGHNESS); + return CombatRules.getDamageAfterAbsorb(entity, damage, source, armor, toughness); + } + /** Fire-resistance effect (level 1 = 20% reduction, +5% per amplifier). */ + static float applyResistance(LivingEntity entity, float damage) { + MobEffectInstance resistance = StatusEffectAccess.get(entity, MobEffects.RESISTANCE); + if (resistance == null) return damage; + int reduced = 25 - (resistance.getAmplifier() + 1) * 5; + return Math.max(damage * reduced / 25.0f, 0.0f); + } + /** Blast Protection + Protection enchantment reduction, applied last. Never returns a negative value. */ + static float applyProtection(LivingEntity entity, float damage) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || damage <= 0.0f) return Math.max(damage, 0.0f); + float protection = totalProtection(entity); + return protection > 0.0f ? CombatRules.getDamageAfterMagicAbsorb(damage, protection) : damage; + } + private static float totalProtection(LivingEntity entity) { + float total = 0.0f; + for (EquipmentSlot slot : ARMOR_SLOTS) { + total += protectionOf(entity.getItemBySlot(slot)); + } + return total; + } + private static float protectionOf(ItemStack stack) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || stack == null || stack.isEmpty()) return 0.0f; + return enchantmentLevel(Enchantments.BLAST_PROTECTION, stack) * 2.0f + + enchantmentLevel(Enchantments.PROTECTION, stack); + } + private static int enchantmentLevel(ResourceKey key, ItemStack stack) { + Holder entry = enchantmentEntry(key); + return entry != null ? EnchantmentHelper.getItemEnchantmentLevel(entry, stack) : 0; + } + private static Holder enchantmentEntry(ResourceKey key) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || key == null) return null; + Registry registry = mc.level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); + Enchantment enchantment = registry.getValue(key); + return enchantment != null ? registry.wrapAsHolder(enchantment) : null; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java b/mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java index 113f16e..0471e9a 100644 --- a/mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java +++ b/mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java @@ -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.util.world; /** diff --git a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java index 1e1acf0..121792a 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java index e0e6fba..35870d0 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java index 2f9970e..55b3d97 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java index 4037f69..945fb50 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java index 7ce1ef9..d788692 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java index fb3d57e..d3a1a96 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java index bbbda85..c77b96d 100644 --- a/mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java +++ b/mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java b/mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java index 96e398d..550fe15 100644 --- a/mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java +++ b/mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java @@ -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.events; import dev.loki.lovisual.Lifecycle; diff --git a/mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java index f5ca366..8a1d4bb 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java index 4889741..927e2d5 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java index 4ac7209..22947d3 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java @@ -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; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java index badf023..6bbaa28 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java @@ -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 org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java b/mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java index 09fed99..0a84ec5 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java @@ -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.gui.hud.draggable.impl.triangulator; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java index 8971c34..c92d2a6 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java @@ -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.module.modules.combat.combatlog; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java index 5303851..6fff54e 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java @@ -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.module.modules.misc.items; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java index 5ef1681..59d12d4 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java @@ -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.module.modules.misc.protect; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java index 5fed5a7..c5eeb8e 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java @@ -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.module.modules.misc.sound; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java index 4736e67..cfb6afb 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java @@ -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.module.modules.player; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java index 2ab0abd..9c56215 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java @@ -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.module.modules.player; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java index 442b185..5f23f65 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java @@ -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.module.modules.player.auto; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java index a1abb1b..80a2a89 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java @@ -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.module.modules.player.auto; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java index 60df7c2..87f1bf8 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java @@ -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.module.modules.player.auto; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java index e916d6d..f8a5346 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java @@ -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.module.modules.player.interact; import org.junit.jupiter.params.ParameterizedTest; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java index 2490c24..4d767b8 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java @@ -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.module.modules.player.inventory; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java index 0f6c307..8f9b934 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java @@ -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.module.modules.visuals; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java index 643569e..2ff0750 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java @@ -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.module.modules.visuals; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java index f5640e4..bfb7e96 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java @@ -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.module.modules.visuals; import dev.loki.lovisual.render.helpers.player.HeadAnchor; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java index e0b32f0..a5764ed 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java @@ -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.module.modules.visuals; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java index 9da1811..28c69b6 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java @@ -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.module.modules.visuals; import org.joml.Vector3f; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java index 0f6043a..62f7543 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java @@ -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.module.modules.visuals; import dev.loki.lovisual.render.engine.uniform.impl.post.PostFXUniforms; diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java index c53da13..07d5277 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java @@ -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.module.modules.visuals.wings; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java b/mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java index c29513a..82b388d 100644 --- a/mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java +++ b/mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java @@ -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.security; import net.minecraft.network.chat.Component; diff --git a/mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java b/mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java index 95f61e9..9851422 100644 --- a/mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java +++ b/mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java @@ -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.render.engine.renderer.ui.runtime.script; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java index 082e491..a0d86ce 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java @@ -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.util.animation; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java index 508aed5..15017c7 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java @@ -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.util.animation; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java index 71900e6..c3c63d9 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java @@ -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.util.animation; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java index 2ded365..fcd1859 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java @@ -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.util.animation; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java index 01c0bfe..afd5122 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java @@ -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.util.animation.hand; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java index 1f83d7f..cfb426a 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java @@ -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.util.animation.hand; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java index 842d56f..1c20707 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java @@ -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.util.animation.hand.state; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java index 5982078..76d58b6 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java @@ -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.util.animation.hand.state; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java b/mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java index cb1dc57..f0e5be1 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java @@ -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.util.combat; import net.minecraft.world.phys.AABB; diff --git a/mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java b/mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java index 3ad1cd1..a4ce9d2 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java @@ -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.util.mctiers; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java b/mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java index caf54f1..b6ece91 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java @@ -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.util.physics; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java b/mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java index bc15e86..43e6e2c 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java @@ -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.util.protection; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java b/mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java index d7f89ad..b7ed806 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java @@ -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.util.protection; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java index 6592096..9efd471 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java @@ -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.util.text; import net.minecraft.network.chat.Component; diff --git a/mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java index 8516f8b..2f0a963 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java @@ -1,9 +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.util.text; diff --git a/mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java index 9f6f0e4..cc23fe5 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java @@ -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.util.text; import org.junit.jupiter.api.Test; diff --git a/mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java b/mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java index c2face8..59235e3 100644 --- a/mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java +++ b/mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java @@ -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.util.world; import org.junit.jupiter.api.Test; diff --git a/mod/tools/strip_license_header.py b/mod/tools/strip_license_header.py new file mode 100755 index 0000000..1700dfe --- /dev/null +++ b/mod/tools/strip_license_header.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Strip the LoVisual GNU GPL file header from every Java source file. + +The block being removed looks like one of: + + /* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + + + /* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ + + +Files that do not start with either variant are skipped; a single blank line +is preserved before the next `package` / `import` for readability. +""" +from __future__ import annotations + +import pathlib +import re +import sys + +HEADER = re.compile( + r"^/\*\n" + r" \* This file is part of the LoVisual Client distribution\.\n" + r" \* Copyright \(c\) 2026 loki5512344\.\n" + r"(?: \*\n)?" + r" \* Licensed under the GNU General Public License v3\.0\.\n" + r" \*/\n" +) + + +def main(roots: list[str]) -> int: + stripped = 0 + for root_arg in roots: + root = pathlib.Path(root_arg) + for path in root.rglob("*.java"): + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + m = HEADER.match(text) + if not m: + continue + rest = text[m.end():] + if rest.startswith("package ") or rest.startswith("import "): + rest = "\n" + rest + path.write_text(rest, encoding="utf-8") + stripped += 1 + print(f"stripped {stripped} file(s)") + return 0 + + +if __name__ == "__main__": + args = sys.argv[1:] or ["."] + sys.exit(main(args))