From 872814a6b2c5ebcd92c833324d4a7640264c3762 Mon Sep 17 00:00:00 2001 From: Constructor Date: Tue, 19 Aug 2025 08:23:03 +0200 Subject: [PATCH 01/12] Initial draft for menu bar --- .../com/lambda/mixin/CrashReportMixin.java | 3 +- src/main/kotlin/com/lambda/Lambda.kt | 1 + src/main/kotlin/com/lambda/gui/DearImGui.kt | 10 +- .../kotlin/com/lambda/gui/LambdaScreen.kt | 3 +- src/main/kotlin/com/lambda/gui/MenuBar.kt | 595 ++++++++++++++++++ .../lambda/gui/components/ClickGuiLayout.kt | 45 +- .../kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt | 56 +- .../com/lambda/module/hud/Coordinates.kt | 21 +- .../com/lambda/module/hud/ModuleList.kt | 9 +- src/main/kotlin/com/lambda/module/hud/TPS.kt | 4 +- .../com/lambda/module/hud/TaskFlowHUD.kt | 4 +- .../kotlin/com/lambda/module/hud/Watermark.kt | 4 +- .../kotlin/com/lambda/module/tag/ModuleTag.kt | 1 + .../kotlin/com/lambda/util/Diagnostics.kt | 35 ++ .../assets/lambda/textures/github_logo.png | Bin 0 -> 4837 bytes .../lambda/textures/lambda_text_color.png | Bin 0 -> 57230 bytes src/main/resources/lambda.accesswidener | 10 + 17 files changed, 741 insertions(+), 60 deletions(-) create mode 100644 src/main/kotlin/com/lambda/gui/MenuBar.kt create mode 100644 src/main/kotlin/com/lambda/util/Diagnostics.kt create mode 100644 src/main/resources/assets/lambda/textures/github_logo.png create mode 100644 src/main/resources/assets/lambda/textures/lambda_text_color.png diff --git a/src/main/java/com/lambda/mixin/CrashReportMixin.java b/src/main/java/com/lambda/mixin/CrashReportMixin.java index 48cd6260b..d39338bae 100644 --- a/src/main/java/com/lambda/mixin/CrashReportMixin.java +++ b/src/main/java/com/lambda/mixin/CrashReportMixin.java @@ -54,8 +54,7 @@ void injectConstructor(String message, Throwable cause, CallbackInfo ci) { @WrapMethod(method = "asString(Lnet/minecraft/util/crash/ReportType;Ljava/util/List;)Ljava/lang/String;") String injectString(ReportType type, List extraInfo, Operation original) { var list = new ArrayList<>(extraInfo); - - list.add("If this issue is related to Lambda, check if other users have experienced this too, or create a new issue at https://github.com/lambda-client/lambda/issues.\n\n"); + list.add("If this issue is related to Lambda, check if other users have experienced this too, or create a new issue at " + Lambda.REPO_URL + "/issues.\n\n"); if (MinecraftClient.getInstance() != null) { list.add("Enabled modules:"); diff --git a/src/main/kotlin/com/lambda/Lambda.kt b/src/main/kotlin/com/lambda/Lambda.kt index e258dc4f6..2954fd92e 100644 --- a/src/main/kotlin/com/lambda/Lambda.kt +++ b/src/main/kotlin/com/lambda/Lambda.kt @@ -51,6 +51,7 @@ object Lambda : ClientModInitializer { const val MOD_ID = "lambda" const val SYMBOL = "λ" const val APP_ID = "1221289599427416127" + const val REPO_URL = "https://github.com/lambda-client/lambda" val VERSION: String = FabricLoader.getInstance() .getModContainer("lambda").orElseThrow() .metadata.version.friendlyString diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index f1f48f944..d981af298 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -26,6 +26,8 @@ import com.lambda.module.modules.client.GuiSettings import com.lambda.util.path import com.mojang.blaze3d.opengl.GlStateManager import com.mojang.blaze3d.systems.RenderSystem +import imgui.ImFontConfig +import imgui.ImFontGlyphRangesBuilder import imgui.ImGui import imgui.ImGuiIO import imgui.flag.ImGuiConfigFlags @@ -54,9 +56,13 @@ object DearImGui : Loadable { private fun updateScale(scale: Float) { io.fonts.clear() val baseFontSize = 13f - io.fonts.addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, baseFontSize * scale) + val glyphRanges = ImFontGlyphRangesBuilder().apply { + addRanges(io.fonts.glyphRangesDefault) + addRanges(io.fonts.glyphRangesGreek) + addChar('⤴') // U+2934 for external links + }.buildRanges() + io.fonts.addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, baseFontSize * scale, ImFontConfig(), glyphRanges) io.fonts.build() - implGl3.createFontsTexture() } diff --git a/src/main/kotlin/com/lambda/gui/LambdaScreen.kt b/src/main/kotlin/com/lambda/gui/LambdaScreen.kt index 873ed82d9..0e00e237a 100644 --- a/src/main/kotlin/com/lambda/gui/LambdaScreen.kt +++ b/src/main/kotlin/com/lambda/gui/LambdaScreen.kt @@ -22,8 +22,7 @@ import net.minecraft.client.gui.DrawContext import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text - -object LambdaScreen : Screen(Text.of("")) { +object LambdaScreen : Screen(Text.of("Lambda")) { override fun shouldPause() = false override fun removed() = ClickGui.disable() override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, deltaTicks: Float) {} diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt new file mode 100644 index 000000000..539204de2 --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -0,0 +1,595 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui + +import com.lambda.Lambda +import com.lambda.Lambda.REPO_URL +import com.lambda.Lambda.mc +import com.lambda.command.CommandRegistry +import com.lambda.config.Configuration +import com.lambda.core.Loader +import com.lambda.event.EventFlow +import com.lambda.graphics.texture.TextureOwner.upload +import com.lambda.gui.dsl.ImGuiBuilder +import com.lambda.module.ModuleRegistry +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.Communication.info +import com.lambda.util.Diagnostics.gatherDiagnostics +import com.lambda.util.FolderRegister +import com.mojang.blaze3d.platform.TextureUtil +import imgui.ImGui +import imgui.ImGui.closeCurrentPopup +import imgui.ImGui.image +import imgui.flag.ImGuiCol +import imgui.flag.ImGuiKey +import imgui.flag.ImGuiStyleVar +import imgui.flag.ImGuiWindowFlags +import imgui.type.ImString +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.util.Util +import net.minecraft.world.GameMode +import java.util.Locale + +object MenuBar { + private var aboutRequested = false + val headerLogo = upload("textures/lambda_text_color.png") + val lambdaLogo = upload("textures/lambda.png") + val githubLogo = upload("textures/github_logo.png") + + // ToDo: On pressing shift (or something else) open a quick search bar popup. + // - Search for modules, hud elements, and commands using levenshtein distance. + private val moduleSearch = ImString(64) + + fun ImGuiBuilder.buildMenuBar() { + mainMenuBar { + lambdaMenu() + menu("View") { buildViewMenu() } + menu("HUD") { buildHudMenu() } + menu("Modules") { buildModulesMenu() } + menu("Config") { buildConfigMenu() } + menu("Minecraft") { buildMinecraftMenu() } + menu("Window") { buildWindowMenu() } + buildGitHubReference() + } + + if (aboutRequested) { + ImGui.openPopup("About Lambda") + aboutRequested = false + } + + aboutPopup() + } + + private fun ImGuiBuilder.lambdaMenu() { + ImGui.pushStyleColor(ImGuiCol.Text, 0) + val opened = ImGui.beginMenu("Lam") + + val headerW = itemRectMaxX - itemRectMinX + val headerH = itemRectMaxY - itemRectMinY + + val pad = 2f + val lambdaIconSize = (headerH - pad * 2f).coerceAtLeast(1f) + val iconX = itemRectMinX + (headerW - lambdaIconSize) * 0.5f + val iconY = itemRectMinY + (headerH - lambdaIconSize) * 0.5f + + foregroundDrawList.addImage( + lambdaLogo.id.toLong(), + iconX, iconY, + iconX + lambdaIconSize, iconY + lambdaIconSize + ) + ImGui.popStyleColor() + + if (opened) { + buildLambdaMenu() + ImGui.endMenu() + } + } + + private fun ImGuiBuilder.buildLambdaMenu() { + menuItem("Documentation ⤴") { + Util.getOperatingSystem().open("$REPO_URL/wiki") + } + menuItem("Report Issue ⤴") { + mc.keyboard.clipboard = gatherDiagnostics() + info("Copied diagnostics to clipboard. Please paste it in a new issue on GitHub and click “Submit new issue”. Thank you!") + Util.getOperatingSystem().open("$REPO_URL/issues") + } + menuItem("Check for Updates ⤴") { + // ToDo: + // - Check for a newer version, show availability & changelog, and allow opening release page. + // - Needs UpdateManager + Util.getOperatingSystem().open("$REPO_URL/releases") + } + menuItem("About...") { + aboutRequested = true + } + separator() + menuItem("Close GUI", "Esc") { LambdaScreen.close() } + menuItem("Exit Client") { mc.scheduleStop() } + } + + private fun ImGuiBuilder.buildViewMenu() { + menu("Module Tag Panels") { + // ToDo: + // - For each tag, add a checkbox that toggles visibility of that tag's window (default: on). + ModuleTag.defaults.forEach { tag -> + menuItem(tag.name, selected = true) { + // toggle tag window (requires a boolean per tag) + } + } + } + menuItem("Show Status Bar", selected = true) { + // ToDo: + // - Toggle a bottom status bar window showing TPS/FPS/Ping/Active Profile/Enabled Modules/Unsaved marker. + } + separator() + menu("UI Scale") { + // ToDo: + // - Apply selected scale (100/125/150/175/200%), update fonts via DearImGui.updateScale-like method. + listOf("100%", "125%", "150%", "175%", "200%").forEach { label -> + menuItem(label, selected = (label == "125%")) { /* set scale & rebuild fonts */ } + } + } + menu("Theme") { + // ToDo: + // - Apply preset color palettes. "Custom..." opens the Style Editor panel. + listOf("Light", "Dark", "High Contrast", "Custom...").forEach { label -> + menuItem(label, selected = (label == "Dark")) { + if (label.startsWith("Custom")) ImGui.showStyleEditor() + else { + // apply preset palette/colors here + } + } + } + } + menu("Debug Overlays") { + // ToDo: + // - FPS graph, Tick time, TPS, Packet counters; draw via foreground draw list; per-overlay opacity slider. + menuItem("FPS Graph", selected = false) {} + menuItem("Tick Time", selected = false) {} + menuItem("Server TPS", selected = false) {} + menuItem("Packet Counters", selected = false) {} + } + } + + private fun ImGuiBuilder.buildHudMenu() { + menuItem("Copy HUD Layout") { + // ToDo: + // - Serialize current HUD widget tree with positions/anchors/safe-margins to memory clipboard. + } + menuItem("Paste HUD Layout") { + // ToDo: + // - Deserialize from clipboard and apply; if incompatible, show a non-blocking warning. + } + menuItem("Reset to Defaults") { + // ToDo: + // - Reset the currently focused panel’s settings to defaults (confirmation modal). + } + separator() + menuItem("Keybind Manager...") { + // ToDo (Keybind Manager Window): + // - Panel with search/filter; table columns: Action/Module | Current Key | Conflict | Change | Clear + // - Conflict detector with "Auto-resolve" suggestions. + } + menuItem("Open Editor", "Ctrl+Alt+C") { + // ToDo (HUD Editor Window): + // - Full-screen canvas with grid; left "Elements" list; right "Properties" inspector. + // - Drag & drop, snap grid, lock/unlock, safe margins, anchors, multi-select & alignment tools. + } + menu("Add Widget") { + // ToDo: + // - Populate from available HUD widgets. On click, add centered and select for property editing. + menuItem("Stats") {} + menuItem("Clock") {} + menuItem("Ping") {} + menuItem("Coordinates") {} + menuItem("Module List") {} + } + menu("Layouts") { + // ToDo: + // - New/Save/Save As/Load/Import/Export layout actions; Toggle "Autosave on change". + menuItem("New...") {} + menuItem("Save") {} + menuItem("Save As...") {} + menuItem("Load...") {} + menuItem("Import...") {} + menuItem("Export...") {} + separator() + menuItem("Autosave on change", selected = true) {} + } + menuItem("Reset Layout") { + // ToDo: + // - Confirm and restore the default HUD layout. + } + menuItem("Toggle Edit Handles", selected = true) { + // ToDo: + // - Show/hide bounds, anchors, labels while in edit mode. + } + } + + private fun ImGuiBuilder.buildModulesMenu() { + menuItem("Enable All") { + // ToDo: + // - Confirmation modal. Then enable all modules. + } + menuItem("Disable All") { + // ToDo: + // - Confirmation modal. Then disable all modules. + } + separator() + menuItem("Search Modules...", "Ctrl+K") { + // ToDo (Modules Search Window): + // - Search input, tag filter dropdown, "enabled only" toggle. + // - List rows: [Enable] Module | Tag | "Settings..." button to jump to module panel. + } + // By Tag → quick enable/disable per module + ModuleTag.defaults.forEach { tag -> + menu(tag.name) { + ModuleRegistry.modules + .filter { it.tag == tag } + .sortedBy { it.name.lowercase() } + .forEach { module -> + menuItem(module.name, selected = module.isEnabled) { + if (module.isEnabled) module.disable() else module.enable() + } + // Optionally, offer a "Settings..." item to focus this module’s details UI. + } + } + } + separator() + menuItem("Manage Module Presets...") { + // ToDo (Module Presets Window): + // - Save/Load named sets of module states (and optionally settings) independent of profiles. + // - Offer Import/Export and Delete. Provide "Apply (merge)" and "Apply (replace)" options. + } + } + + private fun ImGuiBuilder.buildConfigMenu() { + menuItem("New Profile...") { + // ToDo (New Profile): + // - Open a modal "New Profile" with: + // [Profile Name] text input + // [Template] combo: Empty / Recommended Defaults / Copy from Current + // [Include HUD Layout] checkbox + // - On Create: instantiate and activate the profile, optionally copying values from current. + // - On Cancel: close modal with no changes. + } + menuItem("Open Config Folder") { + Util.getOperatingSystem().open(FolderRegister.config) + } + separator() + menuItem("Save All Configs", "Ctrl+S") { + // Save every configuration file and show a toast with the total count. + Configuration.configurations.forEach { it.trySave(true) } + runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } + } + menuItem("Load All Configs", "Ctrl+L") { + // Load every configuration file and show a toast with the total count. + Configuration.configurations.forEach { it.tryLoad() } + runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + } + separator() + menuItem("Import Profile...") { + // ToDo (Import Profile): + // - Show a file picker for profile file(s). + // - Preview dialog: profile name, version, module count, settings count, includes HUD? + // - Provide options: Merge into Current / Replace Current. + // - Apply with progress/rollback on failure; toast result. + } + menuItem("Export Current Profile...") { + // ToDo (Export Profile): + // - File save modal with checkboxes: + // [Include HUD Layout] [Include Keybinds] [Include Backups Metadata] + // - Create the export and toast result. + } + menu("Recent Profiles") { + // ToDo (MRU Profiles): + // - Populate from a most-recently-used (MRU) list persisted in preferences. + // - On click: switch active profile (confirm if unsaved changes). + menuItem("Example Profile") {} + } + menuItem("Save All", "Ctrl+S") { + Configuration.configurations.forEach { it.trySave(true) } + runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } + } + menuItem("Load All", "Ctrl+L") { + Configuration.configurations.forEach { it.tryLoad() } + runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + } + separator() + menu("Autosave Settings") { + // ToDo: + // - Toggle autosave, set interval (1..60s), backup rotation count (0..20). + menuItem("Autosave on changes", selected = true) {} + menuItem("Autosave Interval: 10s") {} + menuItem("Rotate Backups: 5") {} + } + menu("Backup & Restore") { + // ToDo: + // - “Create Backup Now” and “Manage/Restore Backups” UIs; list with timestamps/comments. + menuItem("Create Backup Now") {} + menuItem("Restore From Backup...") {} + menuItem("Manage Backups...") {} + } + menuItem("Profiles & Scopes...") { + // ToDo (Profiles & Scopes Window): + // - Active Profile dropdown. + // - Scopes: Global / Per-Server / Per-World with enable overrides. + // - Show overridden-only list, origin badges, and precedence explanation. + } + menuItem("Module Settings Inspector...") { + // ToDo (Settings Inspector Window): + // - Left: Tree (Tag → Module → Group). + // - Right: Settings editor with search; filters (Changed-only, Overridden-only, Advanced). + // - Reset group/module actions. + } + menuItem("Placement/Build Settings...") { + // ToDo (Placement Panel): + // - Rotate For Place, Air Place Mode, Axis Rotate (conditional), + // - Place Stage Mask (multi-select), Place Confirmation Mode, + // - Max Pending Placements, Places Per Tick, + // - Swing On Place + Swing Type, Place Sounds. + // - Provide concise tooltips for trade-offs. + } + menuItem("Inventory Settings...") { + // ToDo (Inventory Panel): + // - Container group: Disposables editor (list add/remove + defaults), Swap with Disposables, + // Provider/Store Priorities. + // - Access group: Access Shulkers/Ender/Chests/Stashes toggles. + // - “Test Access” helper to simulate lookups. + } + } + + private fun ImGuiBuilder.buildMinecraftMenu() { + menu("Open Folder") { + menuItem("Open Minecraft Folder") { + Util.getOperatingSystem().open(FolderRegister.minecraft) + } + menuItem("Open Lambda Folder") { + Util.getOperatingSystem().open(FolderRegister.lambda) + } + menuItem("Open Config Folder") { + Util.getOperatingSystem().open(FolderRegister.config) + } + menuItem("Open Cache Folder") { + Util.getOperatingSystem().open(FolderRegister.cache) + } + menuItem("Open Capes Folder") { + Util.getOperatingSystem().open(FolderRegister.capes) + } + menuItem("Open Structures Folder") { + Util.getOperatingSystem().open(FolderRegister.structure) + } + menuItem("Open Maps Folder") { + Util.getOperatingSystem().open(FolderRegister.maps) + } + } + separator() + runSafe { + menu("Gamemode", enabled = player.hasPermissionLevel(2)) { + menuItem("Survival", selected = interaction.gameMode == GameMode.SURVIVAL) { + connection.sendCommand("gamemode survival") + } + menuItem("Creative", selected = interaction.gameMode == GameMode.CREATIVE) { + connection.sendCommand("gamemode creative") + } + menuItem("Adventure", selected = interaction.gameMode == GameMode.ADVENTURE) { + connection.sendCommand("gamemode adventure") + } + menuItem("Spectator", selected = interaction.gameMode == GameMode.SPECTATOR) { + connection.sendCommand("gamemode spectator") + } + } + menu("Debug Menu") { + menuItem( + "Show Debug Menu", "F3", + mc.debugHud.showDebugHud + ) { mc.debugHud.toggleDebugHud() } + menuItem( + "Rendering Chart", "F3+1", + mc.debugHud.renderingChartVisible + ) { mc.debugHud.toggleRenderingChart() } + menuItem( + "Rendering & Tick Charts", "F3+2", + mc.debugHud.renderingAndTickChartsVisible + ) { mc.debugHud.toggleRenderingAndTickCharts() } + menuItem( + "Packet Size & Ping Charts", "F3+3", + mc.debugHud.packetSizeAndPingChartsVisible + ) { mc.debugHud.togglePacketSizeAndPingCharts() } + + separator() + + menuItem("Reload Chunks", "F3+A") { + mc.worldRenderer.reload() + } + menuItem( + "Show Chunk Borders", "F3+G", + mc.debugRenderer.showChunkBorder + ) { mc.debugRenderer.toggleShowChunkBorder() } + menuItem("Show Octree", selected = mc.debugRenderer.showOctree) { + mc.debugRenderer.toggleShowOctree() + } + menuItem( + label = "Show Hitboxes", + shortcut = "F3+B", + selected = mc.entityRenderDispatcher.shouldRenderHitboxes() + ) { + val now = !mc.entityRenderDispatcher.shouldRenderHitboxes() + mc.entityRenderDispatcher.setRenderHitboxes(now) + } + menuItem("Copy Location (as command)", "F3+C") { + val cmd = String.format( + Locale.ROOT, + "/execute in %s run tp @s %.2f %.2f %.2f %.2f %.2f", + world.registryKey.value, + player.x, player.y, player.z, player.yaw, player.pitch + ) + ImGui.setClipboardText(cmd) + info("Copied location command to clipboard.") + } + menuItem("Clear Chat", "F3+D") { + mc.inGameHud?.chatHud?.clear(false) + } + + separator() + + menuItem( + label = "Advanced Tooltips", + shortcut = "F3+H", + selected = mc.options.advancedItemTooltips + ) { + mc.options.advancedItemTooltips = !mc.options.advancedItemTooltips + mc.options.write() + } + menuItem("Inspect (Copy Look At)", "F3+I") { + // TODO: Implement precise copyLookAt(hasOp = player.hasPermissionLevel(2), raycastBlocksIfNotShift = !Screen.hasShiftDown()) + info("Inspect: Not yet implemented.") + } + + separator() + + menuItem("Start/Stop Profiler", "F3+L") { + // TODO: Wire mc.toggleDebugProfiler with callback logging + info("Profiler control: Not yet implemented.") + } + + separator() + + menuItem( + label = "Pause On Lost Focus", + shortcut = "F3+Esc", + selected = mc.options.pauseOnLostFocus + ) { + mc.options.pauseOnLostFocus = !mc.options.pauseOnLostFocus + mc.options.write() + info("Pause on lost focus ${if (mc.options.pauseOnLostFocus) "enabled" else "disabled"}.") + } + + separator() + + menuItem("Dump Dynamic Textures", "F3+S") { + val root = mc.runDirectory.toPath().toAbsolutePath() + val output = TextureUtil.getDebugTexturePath(root) + mc.textureManager.dumpDynamicTextures(output) + info("Dumped dynamic textures to: ${root.relativize(output)}") + } + menuItem("Reload Resource Packs", "F3+T") { + info("Reloading resource packs...") + mc.reloadResources() + } + } + } ?: menuItem("Debug (only available ingame)", enabled = false) + } + + private fun ImGuiBuilder.buildWindowMenu() { + menu("Layouts") { + // ToDo: + // - Save/Load/Restore default for the docking layout and window positions. + menuItem("Save Window Layout") {} + menuItem("Load Window Layout") {} + menuItem("Restore Default Window Layout") {} + } + menu("Panels") { + // ToDo: + // - Provide toggles for common panels to quickly show/hide them. + menuItem("Modules Search", selected = false) {} + menuItem("Settings Inspector", selected = false) {} + menuItem("Logs/Console", selected = false) {} + menuItem("Network Inspector", selected = false) {} + menuItem("Task Flow Monitor", selected = false) {} + menuItem("Status Bar", selected = true) {} + } + menuItem("Close All Floating Windows") { + // ToDo: + // - Iterate and turn off visibility booleans for transient windows (search/inspector/etc.). + } + } + + private fun ImGuiBuilder.aboutPopup() { + popupModal("About Lambda", ImGuiWindowFlags.AlwaysAutoResize or ImGuiWindowFlags.NoTitleBar) { + imageHorizontallyCentered(headerLogo.id.toLong(), 553f, 200f) + group { + text("Version: ${Lambda.VERSION}") + if (Lambda.isDebug) text("Development Environment") + text("Runtime: ${Loader.runtime}") + text("Modules: ${ModuleRegistry.modules.size}") + text("Commands: ${CommandRegistry.commands.size}") + val totalSettings = Configuration.configurations.sumOf { cfg -> + cfg.configurables.sumOf { it.settings.size } + } + text("Settings: $totalSettings") + text("Synchronous listeners: ${EventFlow.syncListeners.size}") + text("Concurrent listeners: ${EventFlow.concurrentListeners.size}") + } + separator() + text("Authors") + FabricLoader.getInstance().getModContainer("lambda") + .orElseThrow { IllegalStateException("Could not find Lambda mod container!") } + .metadata + .authors + .forEach { author -> + if (author.name.isEmpty()) return@forEach + author.name.split(",").forEach { name -> + bulletText(name.trim()) + } + } + text("Thanks to all community members") + + separator() + group { + button("Copy Diagnostics") { + ImGui.setClipboardText(gatherDiagnostics()) + } + sameLine() + button("View License ⤴") { + Util.getOperatingSystem().open("$REPO_URL/blob/master/LICENSE.md") + } + sameLine() + button("Close") { + aboutRequested = false + closeCurrentPopup() + } + } + } + } + + private fun ImGuiBuilder.buildGitHubReference() { + val frameH = frameHeight - 2f + val iconSize = (frameH - 6f).coerceAtLeast(14f) + val spacingPx = 8f + + sameLine() + cursorPosX = windowContentRegionMaxX - iconSize - spacingPx + + withStyleVar(ImGuiStyleVar.FramePadding, 2f, 2f) { + withStyleColor(ImGuiCol.Button, 0x00000000) { + withStyleColor(ImGuiCol.ButtonHovered, 0x22FFFFFF) { + withStyleColor(ImGuiCol.ButtonActive, 0x44FFFFFF) { + val clicked = ImGui.imageButton("##github", githubLogo.id.toLong(), iconSize, iconSize) + lambdaTooltip("Open GitHub Repository ⤴") + if (clicked) { + Util.getOperatingSystem().open(REPO_URL) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt index 5a8ccc987..3c888fc97 100644 --- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt @@ -17,55 +17,46 @@ package com.lambda.gui.components -import com.lambda.config.Configuration import com.lambda.core.Loadable import com.lambda.event.events.GuiEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.gui.MenuBar.buildMenuBar import com.lambda.gui.dsl.ImGuiBuilder.buildLayout import com.lambda.module.ModuleRegistry import com.lambda.module.modules.client.ClickGui import com.lambda.module.tag.ModuleTag -import com.lambda.threading.runSafe -import com.lambda.util.Communication.info import imgui.ImGui import imgui.flag.ImGuiWindowFlags.AlwaysAutoResize object ClickGuiLayout : Loadable { + private val shownTags = ModuleTag.defaults.toMutableList() + init { listen { if (!ClickGui.isEnabled) return@listen buildLayout { - ModuleTag.defaults - .forEach { tag -> - window(tag.name, flags = AlwaysAutoResize) { - ModuleRegistry.modules - .filter { it.tag == tag } - .forEach { with(ModuleEntry(it)) { buildLayout() } } - } - } + shownTags.forEach { tag -> + window(tag.name, flags = AlwaysAutoResize) { + ModuleRegistry.modules + .filter { it.tag == tag } + .forEach { with(ModuleEntry(it)) { buildLayout() } } - mainMenuBar { - menu("File") { - menuItem("Save Configs", "Ctrl+S") { - Configuration.configurations.forEach { config -> - config.trySave(true) - } - runSafe { - info("Saved ${Configuration.configurations.size} configuration files.") - } - } - menuItem("Load Configs", "Ctrl+L") { - Configuration.configurations.forEach { config -> - config.tryLoad() - } - runSafe { - info("Loaded ${Configuration.configurations.size} configuration files.") + // ToDo: Add a proper context menu to the window + popupContextWindow("lambda_window_ctx") { + text("Window Menu") + separator() + menuItem("Close This Window") { + // ToDo (Close under-cursor window): + // - Requires a mapping from ImGui window to your visibility flag. + // - Can be implemented if you track per-window IDs & vis flags. } } } } + buildMenuBar() + ImGui.showDemoWindow() } } diff --git a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt index a00e84185..caa84d9de 100644 --- a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt +++ b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt @@ -1496,10 +1496,10 @@ object ImGuiBuilder { inline fun popupModal( title: String, value: KMutableProperty0, - flags: Int = ImGuiPopupFlags.None, + windowFlags: Int = ImGuiWindowFlags.None, block: ProcedureBlock, ) { - if (withBool(value) { beginPopupModal(title, it, flags) }) { + if (withBool(value) { beginPopupModal(title, it, windowFlags) }) { block() endPopup() } @@ -1508,10 +1508,10 @@ object ImGuiBuilder { @ImGuiDsl inline fun popupModal( title: String, - flags: Int = ImGuiPopupFlags.None, + windowFlags: Int = ImGuiWindowFlags.None, block: ProcedureBlock, ) { - if (beginPopupModal(title, flags)) { + if (beginPopupModal(title, windowFlags)) { block() endPopup() } @@ -1912,6 +1912,36 @@ object ImGuiBuilder { @ImGuiDsl val foregroundDrawList: ImDrawList get() = getForegroundDrawList() + /** + * Represents the minimum X-coordinate of the current item's rectangle in the UI. + * + * This value is typically used to calculate the dimensions or positioning + * of graphical elements relative to the current UI item. + */ + @ImGuiDsl + val itemRectMinX: Float get() = getItemRectMinX() + + @ImGuiDsl + val itemRectMinY: Float get() = getItemRectMinY() + + @ImGuiDsl + val itemRectMaxX: Float get() = getItemRectMaxX() + + @ImGuiDsl + val itemRectMaxY: Float get() = getItemRectMaxY() + + @ImGuiDsl + val frameHeight: Float get() = getFrameHeight() + + @ImGuiDsl + val frameHeightWithSpacing: Float get() = getFrameHeightWithSpacing() + + @ImGuiDsl + val windowContentRegionMaxX: Float get() = getWindowContentRegionMaxX() + + @ImGuiDsl + val windowContentRegionMaxY: Float get() = getWindowContentRegionMaxY() + /** * Creates a frame with optional border. */ @@ -1942,6 +1972,24 @@ object ImGuiBuilder { } } + @ImGuiDsl + var cursorPosX: Float get() = getCursorPosX(); set(value) { + setCursorPosX(value) + } + + @ImGuiDsl + var cursorPosY: Float get() = getCursorPosY(); set(value) { + setCursorPosY(value) + } + + @ImGuiDsl + fun imageHorizontallyCentered(textureId: Long, width: Float, height: Float) { + val contentW = getContentRegionAvail().x + val offsetX = (contentW - width) * 0.5f + cursorPosX += maxOf(0f, offsetX) + image(textureId, width, height) + } + @ImGuiDsl fun buildLayout(block: ProcedureBlock) { block() diff --git a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt b/src/main/kotlin/com/lambda/module/hud/Coordinates.kt index 209831dc4..a20020500 100644 --- a/src/main/kotlin/com/lambda/module/hud/Coordinates.kt +++ b/src/main/kotlin/com/lambda/module/hud/Coordinates.kt @@ -29,24 +29,23 @@ import com.lambda.util.math.netherCoord import com.lambda.util.math.overworldCoord object Coordinates : HudModule( - name = "Coordinates", + name = "Coordinates", description = "Show your coordinates", - tag = ModuleTag.HUD, + tag = ModuleTag.HUD, ) { private val showDimension by setting("Show Dimension", true) private val decimals by setting("Decimals", 2, 0..4, 1) - //override fun getText() = runSafe { "XYZ ${if (showDimension) world.dimensionName else ""} ${positionForDimension()}" } ?: "" - override fun ImGuiBuilder.buildLayout() { runSafe { - val text = "XYZ ${if (showDimension) world.dimensionName else ""}" - - val coord = - if (world.isNether) "${player.pos.asString(decimals)} [${player.overworldCoord.x.string}; ${player.overworldCoord.z.string}]" - else "${player.pos.asString(decimals)} [${player.netherCoord.x.string}; ${player.netherCoord.z.string}]" - - textCopyable("$text $coord") + val pos = player.pos.asString(decimals) + val coord = if (world.isNether) { + "$pos [${player.overworldCoord.x.string}, ${player.overworldCoord.z.string}]" + } else { + "$pos [${player.netherCoord.x.string}, ${player.netherCoord.z.string}]" + } + val dimension = if (showDimension) " ${world.dimensionName}" else "" + textCopyable("$coord$dimension") } } } diff --git a/src/main/kotlin/com/lambda/module/hud/ModuleList.kt b/src/main/kotlin/com/lambda/module/hud/ModuleList.kt index 672888cf2..587087c50 100644 --- a/src/main/kotlin/com/lambda/module/hud/ModuleList.kt +++ b/src/main/kotlin/com/lambda/module/hud/ModuleList.kt @@ -26,8 +26,8 @@ import imgui.flag.ImGuiCol import java.awt.Color object ModuleList : HudModule( - name = "ModuleList", - tag = ModuleTag.HUD, + name = "ModuleList", + tag = ModuleTag.HUD, ) { override val isVisible: Boolean get() = false @@ -39,10 +39,7 @@ object ModuleList : HudModule( enabled.forEach { text(it.name); sameLine() - - val color = - if (it.keybind == KeyCode.UNBOUND) Color.RED - else Color.GREEN + val color = if (it.keybind == KeyCode.UNBOUND) Color.RED else Color.GREEN withStyleColor(ImGuiCol.Text, color) { text(" [${it.keybind.name}]") } } diff --git a/src/main/kotlin/com/lambda/module/hud/TPS.kt b/src/main/kotlin/com/lambda/module/hud/TPS.kt index 965a735a2..a97a94707 100644 --- a/src/main/kotlin/com/lambda/module/hud/TPS.kt +++ b/src/main/kotlin/com/lambda/module/hud/TPS.kt @@ -25,9 +25,9 @@ import com.lambda.util.NamedEnum import com.lambda.util.ServerTPS.averageMSPerTick object TPS : HudModule( - name = "TPS", + name = "TPS", description = "Display the server's tick rate", - tag = ModuleTag.HUD, + tag = ModuleTag.HUD, ) { private val format by setting("Tick format", TickFormat.TPS) diff --git a/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt b/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt index e2756c2e1..48f44b0f6 100644 --- a/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt +++ b/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt @@ -23,8 +23,8 @@ import com.lambda.module.tag.ModuleTag import com.lambda.task.RootTask object TaskFlowHUD : HudModule( - name = "TaskFlowHud", - tag = ModuleTag.HUD, + name = "TaskFlowHud", + tag = ModuleTag.HUD, ) { override fun ImGuiBuilder.buildLayout() { text(RootTask.toString()) diff --git a/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/src/main/kotlin/com/lambda/module/hud/Watermark.kt index 47311be53..90d46b41e 100644 --- a/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -23,8 +23,8 @@ import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag object Watermark : HudModule( - name = "Watermark", - tag = ModuleTag.HUD, + name = "Watermark", + tag = ModuleTag.HUD, ) { private val texture = upload("textures/lambda.png") diff --git a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt index 7bd2fbe7a..5dd360995 100644 --- a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt +++ b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt @@ -34,6 +34,7 @@ import com.lambda.util.Nameable */ data class ModuleTag(override val name: String) : Nameable { // Totally needs to be reworked + // ToDo: Add registry for tags companion object { val COMBAT = ModuleTag("Combat") val MOVEMENT = ModuleTag("Movement") diff --git a/src/main/kotlin/com/lambda/util/Diagnostics.kt b/src/main/kotlin/com/lambda/util/Diagnostics.kt new file mode 100644 index 000000000..e773d2bfb --- /dev/null +++ b/src/main/kotlin/com/lambda/util/Diagnostics.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util + +import com.lambda.module.ModuleRegistry.modules + +object Diagnostics { + // ToDo: Expand this to include more information like version, etc. + fun gatherDiagnostics() = buildString { + modules.filter { it.isEnabled } + .forEach { module -> + append("\t${module.name}") + module.settings + .filter { it.isModified } + .forEach { setting -> + append("\t\t${setting.name} -> ${setting.value}") + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/lambda/textures/github_logo.png b/src/main/resources/assets/lambda/textures/github_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..50b81752278d084ba9d449fff25f4051df162b0f GIT binary patch literal 4837 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/lambda/textures/lambda_text_color.png b/src/main/resources/assets/lambda/textures/lambda_text_color.png new file mode 100644 index 0000000000000000000000000000000000000000..b47f2f4ffa15c1338e2eafc8168d85f30ba2706a GIT binary patch literal 57230 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%0Agy&R^Ru(!h-NU~r4$0&2w` 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/lambda.accesswidener b/src/main/resources/lambda.accesswidener index 9b6ded8d2..2e9104da7 100644 --- a/src/main/resources/lambda.accesswidener +++ b/src/main/resources/lambda.accesswidener @@ -8,6 +8,7 @@ accessible field net/minecraft/client/MinecraftClient uptimeInTicks J accessible field net/minecraft/client/input/Input movementVector Lnet/minecraft/util/math/Vec2f; accessible field net/minecraft/client/MinecraftClient renderTaskQueue Ljava/util/Queue; accessible field net/minecraft/client/option/KeyBinding boundKey Lnet/minecraft/client/util/InputUtil$Key; +accessible method net/minecraft/client/MinecraftClient getWindowTitle ()Ljava/lang/String; # World accessible field net/minecraft/client/world/ClientWorld entityManager Lnet/minecraft/world/entity/ClientEntityManager; @@ -43,6 +44,7 @@ accessible field net/minecraft/entity/EntityEquipment map Ljava/util/EnumMap; accessible method net/minecraft/entity/LivingEntity getHandSwingDuration ()I accessible method net/minecraft/client/network/ClientPlayerInteractionManager syncSelectedSlot ()V accessible method net/minecraft/util/math/Direction listClosest (Lnet/minecraft/util/math/Direction;Lnet/minecraft/util/math/Direction;Lnet/minecraft/util/math/Direction;)[Lnet/minecraft/util/math/Direction; +accessible field net/minecraft/client/network/ClientPlayerInteractionManager gameMode Lnet/minecraft/world/GameMode; # Camera accessible method net/minecraft/client/render/Camera setPos (DDD)V @@ -90,6 +92,14 @@ accessible method net/minecraft/util/math/Vec3i setX (I)Lnet/minecraft/util/math accessible method net/minecraft/util/math/Vec3i setY (I)Lnet/minecraft/util/math/Vec3i; accessible method net/minecraft/util/math/Vec3i setZ (I)Lnet/minecraft/util/math/Vec3i; +# Debug +accessible field net/minecraft/client/gui/hud/DebugHud showDebugHud Z +accessible field net/minecraft/client/gui/hud/DebugHud renderingChartVisible Z +accessible field net/minecraft/client/gui/hud/DebugHud renderingAndTickChartsVisible Z +accessible field net/minecraft/client/gui/hud/DebugHud packetSizeAndPingChartsVisible Z +accessible field net/minecraft/client/render/debug/DebugRenderer showChunkBorder Z +accessible field net/minecraft/client/render/debug/DebugRenderer showOctree Z + # Other accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List; accessible method net/minecraft/item/BlockItem getPlacementState (Lnet/minecraft/item/ItemPlacementContext;)Lnet/minecraft/block/BlockState; From b630bd0d59b3f4958170397dfc7bca64fe031ad1 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 23 Aug 2025 04:58:28 +0200 Subject: [PATCH 02/12] Improved MenuBar --- .../com/lambda/config/AbstractSetting.kt | 6 +- src/main/kotlin/com/lambda/gui/DearImGui.kt | 4 +- src/main/kotlin/com/lambda/gui/MenuBar.kt | 309 +++++++----------- .../lambda/gui/components/ClickGuiLayout.kt | 15 +- .../com/lambda/gui/components/HudGuiLayout.kt | 3 +- .../com/lambda/gui/components/ModuleEntry.kt | 37 ++- .../kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt | 2 - src/main/kotlin/com/lambda/module/Module.kt | 10 +- .../module/modules/debug/SettingTest.kt | 2 +- .../module/modules/network/PacketDelay.kt | 52 ++- .../module/modules/network/PacketLogger.kt | 1 + .../lambda/module/modules/player/Freecam.kt | 5 +- .../lambda/module/modules/player/Replay.kt | 1 + .../kotlin/com/lambda/module/tag/ModuleTag.kt | 12 + 14 files changed, 227 insertions(+), 232 deletions(-) diff --git a/src/main/kotlin/com/lambda/config/AbstractSetting.kt b/src/main/kotlin/com/lambda/config/AbstractSetting.kt index 4571546db..1bc909d40 100644 --- a/src/main/kotlin/com/lambda/config/AbstractSetting.kt +++ b/src/main/kotlin/com/lambda/config/AbstractSetting.kt @@ -158,12 +158,12 @@ abstract class AbstractSetting( groups.add(path.toList()) } - fun reset() { - if (value == defaultValue) { + fun reset(silent: Boolean = false) { + if (!silent && value == defaultValue) { ConfigCommand.info(notChangedMessage()) return } - ConfigCommand.info(resetMessage(value, defaultValue)) + if (!silent) ConfigCommand.info(resetMessage(value, defaultValue)) value = defaultValue } diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index d981af298..7273b7e95 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -43,6 +43,8 @@ object DearImGui : Loadable { val implGlfw = ImGuiImplGlfw() val implGl3 = ImGuiImplGl3() + const val EXTERNAL_LINK = '↗' + val io: ImGuiIO get() = ImGui.getIO() const val DEFAULT_FLAGS = ImGuiConfigFlags.NavEnableKeyboard or // Enable Keyboard Controls ImGuiConfigFlags.NavEnableSetMousePos or // Move the cursor using the keyboard @@ -59,7 +61,7 @@ object DearImGui : Loadable { val glyphRanges = ImFontGlyphRangesBuilder().apply { addRanges(io.fonts.glyphRangesDefault) addRanges(io.fonts.glyphRangesGreek) - addChar('⤴') // U+2934 for external links + addChar(EXTERNAL_LINK) }.buildRanges() io.fonts.addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, baseFontSize * scale, ImFontConfig(), glyphRanges) io.fonts.build() diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt index 539204de2..4aea2644e 100644 --- a/src/main/kotlin/com/lambda/gui/MenuBar.kt +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -25,6 +25,7 @@ import com.lambda.config.Configuration import com.lambda.core.Loader import com.lambda.event.EventFlow import com.lambda.graphics.texture.TextureOwner.upload +import com.lambda.gui.DearImGui.EXTERNAL_LINK import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag @@ -35,9 +36,7 @@ import com.lambda.util.FolderRegister import com.mojang.blaze3d.platform.TextureUtil import imgui.ImGui import imgui.ImGui.closeCurrentPopup -import imgui.ImGui.image import imgui.flag.ImGuiCol -import imgui.flag.ImGuiKey import imgui.flag.ImGuiStyleVar import imgui.flag.ImGuiWindowFlags import imgui.type.ImString @@ -54,17 +53,15 @@ object MenuBar { // ToDo: On pressing shift (or something else) open a quick search bar popup. // - Search for modules, hud elements, and commands using levenshtein distance. - private val moduleSearch = ImString(64) + private val quickSearch = ImString(64) fun ImGuiBuilder.buildMenuBar() { mainMenuBar { lambdaMenu() - menu("View") { buildViewMenu() } menu("HUD") { buildHudMenu() } menu("Modules") { buildModulesMenu() } - menu("Config") { buildConfigMenu() } menu("Minecraft") { buildMinecraftMenu() } - menu("Window") { buildWindowMenu() } + menu("Help") { buildHelpMenu() } buildGitHubReference() } @@ -102,20 +99,100 @@ object MenuBar { } private fun ImGuiBuilder.buildLambdaMenu() { - menuItem("Documentation ⤴") { - Util.getOperatingSystem().open("$REPO_URL/wiki") + menuItem("New Profile...") { + // ToDo (New Profile): + // - Open a modal "New Profile" with: + // [Profile Name] text input + // [Template] combo: Empty / Recommended Defaults / Copy from Current + // [Include HUD Layout] checkbox + // - On Create: instantiate and activate the profile, optionally copying values from current. + // - On Cancel: close modal with no changes. } - menuItem("Report Issue ⤴") { - mc.keyboard.clipboard = gatherDiagnostics() - info("Copied diagnostics to clipboard. Please paste it in a new issue on GitHub and click “Submit new issue”. Thank you!") - Util.getOperatingSystem().open("$REPO_URL/issues") + menuItem("Open Config Folder") { + Util.getOperatingSystem().open(FolderRegister.config) } - menuItem("Check for Updates ⤴") { + separator() + menuItem("Save All Configs", "Ctrl+S") { + // Save every configuration file and show a toast with the total count. + Configuration.configurations.forEach { it.trySave(true) } + runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } + } + menuItem("Load All Configs", "Ctrl+L") { + // Load every configuration file and show a toast with the total count. + Configuration.configurations.forEach { it.tryLoad() } + runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + } + separator() + menuItem("Import Profile...") { + // ToDo (Import Profile): + // - Show a file picker for profile file(s). + // - Preview dialog: profile name, version, module count, settings count, includes HUD? + // - Provide options: Merge into Current / Replace Current. + // - Apply with progress/rollback on failure; toast result. + } + menuItem("Export Current Profile...") { + // ToDo (Export Profile): + // - File save modal with checkboxes: + // [Include HUD Layout] [Include Keybinds] [Include Backups Metadata] + // - Create the export and toast result. + } + menu("Recent Profiles") { + // ToDo (MRU Profiles): + // - Populate from a most-recently-used (MRU) list persisted in preferences. + // - On click: switch active profile (confirm if unsaved changes). + menuItem("Example Profile") {} + } + menuItem("Save All", "Ctrl+S") { + Configuration.configurations.forEach { it.trySave(true) } + runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } + } + menuItem("Load All", "Ctrl+L") { + Configuration.configurations.forEach { it.tryLoad() } + runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + } + separator() + menu("Autosave Settings") { // ToDo: - // - Check for a newer version, show availability & changelog, and allow opening release page. - // - Needs UpdateManager - Util.getOperatingSystem().open("$REPO_URL/releases") + // - Toggle autosave, set interval (1..60s), backup rotation count (0..20). + menuItem("Autosave on changes", selected = true) {} + menuItem("Autosave Interval: 10s") {} + menuItem("Rotate Backups: 5") {} + } + menu("Backup & Restore") { + // ToDo: + // - “Create Backup Now” and “Manage/Restore Backups” UIs; list with timestamps/comments. + menuItem("Create Backup Now") {} + menuItem("Restore From Backup...") {} + menuItem("Manage Backups...") {} + } + menuItem("Profiles & Scopes...") { + // ToDo (Profiles & Scopes Window): + // - Active Profile dropdown. + // - Scopes: Global / Per-Server / Per-World with enable overrides. + // - Show overridden-only list, origin badges, and precedence explanation. + } + menuItem("Module Settings Inspector...") { + // ToDo (Settings Inspector Window): + // - Left: Tree (Tag → Module → Group). + // - Right: Settings editor with search; filters (Changed-only, Overridden-only, Advanced). + // - Reset group/module actions. + } + menuItem("Placement/Build Settings...") { + // ToDo (Placement Panel): + // - Rotate For Place, Air Place Mode, Axis Rotate (conditional), + // - Place Stage Mask (multi-select), Place Confirmation Mode, + // - Max Pending Placements, Places Per Tick, + // - Swing On Place + Swing Type, Place Sounds. + // - Provide concise tooltips for trade-offs. + } + menuItem("Inventory Settings...") { + // ToDo (Inventory Panel): + // - Container group: Disposables editor (list add/remove + defaults), Swap with Disposables, + // Provider/Store Priorities. + // - Access group: Access Shulkers/Ender/Chests/Stashes toggles. + // - “Test Access” helper to simulate lookups. } + separator() menuItem("About...") { aboutRequested = true } @@ -125,19 +202,7 @@ object MenuBar { } private fun ImGuiBuilder.buildViewMenu() { - menu("Module Tag Panels") { - // ToDo: - // - For each tag, add a checkbox that toggles visibility of that tag's window (default: on). - ModuleTag.defaults.forEach { tag -> - menuItem(tag.name, selected = true) { - // toggle tag window (requires a boolean per tag) - } - } - } - menuItem("Show Status Bar", selected = true) { - // ToDo: - // - Toggle a bottom status bar window showing TPS/FPS/Ping/Active Profile/Enabled Modules/Unsaved marker. - } + separator() menu("UI Scale") { // ToDo: @@ -146,26 +211,6 @@ object MenuBar { menuItem(label, selected = (label == "125%")) { /* set scale & rebuild fonts */ } } } - menu("Theme") { - // ToDo: - // - Apply preset color palettes. "Custom..." opens the Style Editor panel. - listOf("Light", "Dark", "High Contrast", "Custom...").forEach { label -> - menuItem(label, selected = (label == "Dark")) { - if (label.startsWith("Custom")) ImGui.showStyleEditor() - else { - // apply preset palette/colors here - } - } - } - } - menu("Debug Overlays") { - // ToDo: - // - FPS graph, Tick time, TPS, Packet counters; draw via foreground draw list; per-overlay opacity slider. - menuItem("FPS Graph", selected = false) {} - menuItem("Tick Time", selected = false) {} - menuItem("Server TPS", selected = false) {} - menuItem("Packet Counters", selected = false) {} - } } private fun ImGuiBuilder.buildHudMenu() { @@ -224,20 +269,14 @@ object MenuBar { } private fun ImGuiBuilder.buildModulesMenu() { - menuItem("Enable All") { - // ToDo: - // - Confirmation modal. Then enable all modules. - } - menuItem("Disable All") { - // ToDo: - // - Confirmation modal. Then disable all modules. + menu("Module Tag") { + ModuleTag.defaults.forEach { tag -> + menuItem(tag.name, selected = ModuleTag.isTagShown(tag)) { + ModuleTag.toggleTag(tag) + } + } } separator() - menuItem("Search Modules...", "Ctrl+K") { - // ToDo (Modules Search Window): - // - Search input, tag filter dropdown, "enabled only" toggle. - // - List rows: [Enable] Module | Tag | "Settings..." button to jump to module panel. - } // By Tag → quick enable/disable per module ModuleTag.defaults.forEach { tag -> menu(tag.name) { @@ -252,108 +291,6 @@ object MenuBar { } } } - separator() - menuItem("Manage Module Presets...") { - // ToDo (Module Presets Window): - // - Save/Load named sets of module states (and optionally settings) independent of profiles. - // - Offer Import/Export and Delete. Provide "Apply (merge)" and "Apply (replace)" options. - } - } - - private fun ImGuiBuilder.buildConfigMenu() { - menuItem("New Profile...") { - // ToDo (New Profile): - // - Open a modal "New Profile" with: - // [Profile Name] text input - // [Template] combo: Empty / Recommended Defaults / Copy from Current - // [Include HUD Layout] checkbox - // - On Create: instantiate and activate the profile, optionally copying values from current. - // - On Cancel: close modal with no changes. - } - menuItem("Open Config Folder") { - Util.getOperatingSystem().open(FolderRegister.config) - } - separator() - menuItem("Save All Configs", "Ctrl+S") { - // Save every configuration file and show a toast with the total count. - Configuration.configurations.forEach { it.trySave(true) } - runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } - } - menuItem("Load All Configs", "Ctrl+L") { - // Load every configuration file and show a toast with the total count. - Configuration.configurations.forEach { it.tryLoad() } - runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } - } - separator() - menuItem("Import Profile...") { - // ToDo (Import Profile): - // - Show a file picker for profile file(s). - // - Preview dialog: profile name, version, module count, settings count, includes HUD? - // - Provide options: Merge into Current / Replace Current. - // - Apply with progress/rollback on failure; toast result. - } - menuItem("Export Current Profile...") { - // ToDo (Export Profile): - // - File save modal with checkboxes: - // [Include HUD Layout] [Include Keybinds] [Include Backups Metadata] - // - Create the export and toast result. - } - menu("Recent Profiles") { - // ToDo (MRU Profiles): - // - Populate from a most-recently-used (MRU) list persisted in preferences. - // - On click: switch active profile (confirm if unsaved changes). - menuItem("Example Profile") {} - } - menuItem("Save All", "Ctrl+S") { - Configuration.configurations.forEach { it.trySave(true) } - runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } - } - menuItem("Load All", "Ctrl+L") { - Configuration.configurations.forEach { it.tryLoad() } - runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } - } - separator() - menu("Autosave Settings") { - // ToDo: - // - Toggle autosave, set interval (1..60s), backup rotation count (0..20). - menuItem("Autosave on changes", selected = true) {} - menuItem("Autosave Interval: 10s") {} - menuItem("Rotate Backups: 5") {} - } - menu("Backup & Restore") { - // ToDo: - // - “Create Backup Now” and “Manage/Restore Backups” UIs; list with timestamps/comments. - menuItem("Create Backup Now") {} - menuItem("Restore From Backup...") {} - menuItem("Manage Backups...") {} - } - menuItem("Profiles & Scopes...") { - // ToDo (Profiles & Scopes Window): - // - Active Profile dropdown. - // - Scopes: Global / Per-Server / Per-World with enable overrides. - // - Show overridden-only list, origin badges, and precedence explanation. - } - menuItem("Module Settings Inspector...") { - // ToDo (Settings Inspector Window): - // - Left: Tree (Tag → Module → Group). - // - Right: Settings editor with search; filters (Changed-only, Overridden-only, Advanced). - // - Reset group/module actions. - } - menuItem("Placement/Build Settings...") { - // ToDo (Placement Panel): - // - Rotate For Place, Air Place Mode, Axis Rotate (conditional), - // - Place Stage Mask (multi-select), Place Confirmation Mode, - // - Max Pending Placements, Places Per Tick, - // - Swing On Place + Swing Type, Place Sounds. - // - Provide concise tooltips for trade-offs. - } - menuItem("Inventory Settings...") { - // ToDo (Inventory Panel): - // - Container group: Disposables editor (list add/remove + defaults), Swap with Disposables, - // Provider/Store Priorities. - // - Access group: Access Shulkers/Ender/Chests/Stashes toggles. - // - “Test Access” helper to simulate lookups. - } } private fun ImGuiBuilder.buildMinecraftMenu() { @@ -498,27 +435,33 @@ object MenuBar { } ?: menuItem("Debug (only available ingame)", enabled = false) } - private fun ImGuiBuilder.buildWindowMenu() { - menu("Layouts") { + private fun ImGuiBuilder.buildHelpMenu() { + menuItem("Quick Search...") { // ToDo: - // - Save/Load/Restore default for the docking layout and window positions. - menuItem("Save Window Layout") {} - menuItem("Load Window Layout") {} - menuItem("Restore Default Window Layout") {} + // - Search for modules, commands, and HUD widgets. + // - Show matches in a search panel below the GUI. + // - Support regex. + // - Support levenshtein distance. + // - Support multiple search terms. + // - Support search history. + // - Support search filters (by type, enabled/disabled, etc). + // - Support search scopes (all/enabled/disabled). + // - Support search shortcuts (Ctrl+F, Cmd+F, etc). + // - Show match count in the search panel. + } + menuItem("Documentation $EXTERNAL_LINK") { + Util.getOperatingSystem().open("$REPO_URL/wiki") } - menu("Panels") { - // ToDo: - // - Provide toggles for common panels to quickly show/hide them. - menuItem("Modules Search", selected = false) {} - menuItem("Settings Inspector", selected = false) {} - menuItem("Logs/Console", selected = false) {} - menuItem("Network Inspector", selected = false) {} - menuItem("Task Flow Monitor", selected = false) {} - menuItem("Status Bar", selected = true) {} - } - menuItem("Close All Floating Windows") { + menuItem("Report Issue $EXTERNAL_LINK") { + mc.keyboard.clipboard = gatherDiagnostics() + info("Copied diagnostics to clipboard. Please paste it in a new issue on GitHub and click “Submit new issue”. Thank you!") + Util.getOperatingSystem().open("$REPO_URL/issues") + } + menuItem("Check for Updates $EXTERNAL_LINK") { // ToDo: - // - Iterate and turn off visibility booleans for transient windows (search/inspector/etc.). + // - Check for a newer version, show availability & changelog, and allow opening release page. + // - Needs UpdateManager + Util.getOperatingSystem().open("$REPO_URL/releases") } } @@ -558,7 +501,7 @@ object MenuBar { ImGui.setClipboardText(gatherDiagnostics()) } sameLine() - button("View License ⤴") { + button("View License $EXTERNAL_LINK") { Util.getOperatingSystem().open("$REPO_URL/blob/master/LICENSE.md") } sameLine() @@ -583,7 +526,7 @@ object MenuBar { withStyleColor(ImGuiCol.ButtonHovered, 0x22FFFFFF) { withStyleColor(ImGuiCol.ButtonActive, 0x44FFFFFF) { val clicked = ImGui.imageButton("##github", githubLogo.id.toLong(), iconSize, iconSize) - lambdaTooltip("Open GitHub Repository ⤴") + lambdaTooltip("Open GitHub Repository $EXTERNAL_LINK") if (clicked) { Util.getOperatingSystem().open(REPO_URL) } diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt index 3c888fc97..dba01cfa0 100644 --- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt @@ -24,13 +24,11 @@ import com.lambda.gui.MenuBar.buildMenuBar import com.lambda.gui.dsl.ImGuiBuilder.buildLayout import com.lambda.module.ModuleRegistry import com.lambda.module.modules.client.ClickGui -import com.lambda.module.tag.ModuleTag +import com.lambda.module.tag.ModuleTag.Companion.shownTags import imgui.ImGui import imgui.flag.ImGuiWindowFlags.AlwaysAutoResize object ClickGuiLayout : Loadable { - private val shownTags = ModuleTag.defaults.toMutableList() - init { listen { if (!ClickGui.isEnabled) return@listen @@ -41,17 +39,6 @@ object ClickGuiLayout : Loadable { ModuleRegistry.modules .filter { it.tag == tag } .forEach { with(ModuleEntry(it)) { buildLayout() } } - - // ToDo: Add a proper context menu to the window - popupContextWindow("lambda_window_ctx") { - text("Window Menu") - separator() - menuItem("Close This Window") { - // ToDo (Close under-cursor window): - // - Requires a mapping from ImGui window to your visibility flag. - // - Can be implemented if you track per-window IDs & vis flags. - } - } } } diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index 87e1c1102..95e2da94d 100644 --- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt @@ -29,7 +29,8 @@ object HudGuiLayout : Loadable { const val DEFAULT_HUD_FLAGS = ImGuiWindowFlags.NoDecoration or ImGuiWindowFlags.NoBackground or - ImGuiWindowFlags.AlwaysAutoResize + ImGuiWindowFlags.AlwaysAutoResize or + ImGuiWindowFlags.NoDocking init { listen { diff --git a/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt b/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt index 49e686be2..86b4b9c26 100644 --- a/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt +++ b/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt @@ -22,21 +22,46 @@ import com.lambda.gui.Layout import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.Module import com.lambda.util.NamedEnum +import com.lambda.util.KeyCode +import imgui.ImGui import imgui.flag.ImGuiTabBarFlags +import imgui.flag.ImGuiCol class ModuleEntry(val module: Module): Layout { override fun ImGuiBuilder.buildLayout() { - checkbox("##-$module", module::isEnabled) + if (module.isEnabled) { + withStyleColor(ImGuiCol.Header, 0.20f, 0.55f, 0.25f, 0.85f) { + withStyleColor(ImGuiCol.HeaderHovered, 0.25f, 0.65f, 0.30f, 0.90f) { + withStyleColor(ImGuiCol.HeaderActive, 0.20f, 0.55f, 0.25f, 1.00f) { + selectable(module.name, selected = true) { + module.toggle() + } + } + } + } + } else { + selectable(module.name, selected = false) { + module.toggle() + } + } lambdaTooltip(module.description) - sameLine() - treeNode(module.name) { - lambdaTooltip(module.description) + + ImGui.setNextWindowSizeConstraints(0f, 0f, Float.MAX_VALUE, io.displaySize.y * 0.5f) + popupContextItem("##ctx-${module.name}") { group { - val visibleSettings = module.settings.filter { it.visibility() } + with(module.keybindSetting) { buildLayout() } + sameLine() + smallButton("Reset") { + module.settings.forEach { it.reset(silent = true) } + } + lambdaTooltip("Resets all settings for this module to their default values") + } + separator() + group { + val visibleSettings = module.settings.filter { it.visibility() } - module.keybindSetting val (grouped, ungrouped) = visibleSettings.partition { it.groups.isNotEmpty() } ungrouped.forEach { with(it) { buildLayout() } } - renderGroup(grouped, emptyList()) } } diff --git a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt index caa84d9de..ffd67d765 100644 --- a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt +++ b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt @@ -36,10 +36,8 @@ package com.lambda.gui.dsl -import com.lambda.context.SafeContext import com.lambda.gui.dsl.ImGuiBuilder.text import com.lambda.module.modules.client.ClickGui -import com.lambda.threading.runSafe import com.lambda.util.math.Vec2d import imgui.* import imgui.ImGui.* diff --git a/src/main/kotlin/com/lambda/module/Module.kt b/src/main/kotlin/com/lambda/module/Module.kt index 2038340c6..1de014b1f 100644 --- a/src/main/kotlin/com/lambda/module/Module.kt +++ b/src/main/kotlin/com/lambda/module/Module.kt @@ -24,6 +24,8 @@ import com.lambda.config.Configuration import com.lambda.config.configurations.ModuleConfig import com.lambda.context.SafeContext import com.lambda.event.Muteable +import com.lambda.event.events.ClientEvent +import com.lambda.event.events.ConnectionEvent import com.lambda.event.events.KeyboardEvent import com.lambda.event.listener.Listener import com.lambda.event.listener.SafeListener @@ -111,10 +113,10 @@ abstract class Module( private val alwaysListening: Boolean = false, enabledByDefault: Boolean = false, defaultKeybind: KeyCode = KeyCode.UNBOUND, + autoDisable: Boolean = false ) : Nameable, Muteable, Configurable(ModuleConfig) { private val isEnabledSetting = setting("Enabled", enabledByDefault) { false } - private val keybindSetting = setting("Keybind", defaultKeybind) - val reset by setting("Reset", { settings.forEach { it.reset() }; this@Module.info("Settings set to default") }, "Reset settings values to default.") + val keybindSetting = setting("Keybind", defaultKeybind) { false } open val isVisible: Boolean = true @@ -142,6 +144,10 @@ abstract class Module( onEnableUnsafe { LambdaSound.MODULE_ON.play() } onDisableUnsafe { LambdaSound.MODULE_OFF.play() } + + listen { if (autoDisable) disable() } + listen { if (autoDisable) disable() } + listen { if (autoDisable) disable() } } fun enable() { diff --git a/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt b/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt index 2f89a7571..4b97b12ca 100644 --- a/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt +++ b/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt @@ -68,7 +68,7 @@ object SettingTest : Module( private val blockPosSetting by setting("Block Position", BlockPos(0, 0, 0)).group(Group.COMPLEX) private val blockSetting by setting("Block Setting", Blocks.OBSIDIAN).group(Group.COMPLEX) private val colorSetting by setting("Color Setting", Color.GREEN).group(Group.COMPLEX) - private val keybindSetting by setting("Key Bind Setting", KeyCode.T).group(Group.COMPLEX) + private val keybindSettingTest by setting("Key Bind Setting", KeyCode.T).group(Group.COMPLEX) // Complex collections private val blockPosSet by setting("Block Position Set", setOf(BlockPos(0, 0, 0)), setOf(BlockPos(0, 0, 0))).group(Group.COMPLEX) diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt index 3cf88493f..b5d6f130e 100644 --- a/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt +++ b/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt @@ -26,6 +26,8 @@ import com.lambda.module.tag.ModuleTag import com.lambda.threading.runConcurrent import com.lambda.threading.runGameScheduled import com.lambda.util.ClientPacket +import com.lambda.util.Describable +import com.lambda.util.NamedEnum import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.ServerPacket @@ -39,11 +41,11 @@ object PacketDelay : Module( description = "Delays packets client-bound & server-bound", tag = ModuleTag.NETWORK, ) { - private val mode by setting("Mode", Mode.STATIC) - private val networkScope by setting("Network Scope", Direction.BOTH) - private val packetScope by setting("Packet Scope", PacketType.ANY) - private val inboundDelay by setting("Inbound Delay", 250L, 0L..5000L, 10L, unit = "ms") { networkScope != Direction.OUTBOUND } - private val outboundDelay by setting("Outbound Delay", 250L, 0L..5000L, 10L, unit = "ms") { networkScope != Direction.INBOUND } + private val mode by setting("Mode", Mode.Static, description = "How the delay is applied: Static queues packets until a flush; Pulse delays each packet individually.") + private val networkScope by setting("Network Scope", Direction.Both, description = "Which direction(s) to affect: inbound (server → you), outbound (you → server), or both.") + private val packetScope by setting("Packet Scope", PacketType.Any, description = "What packets to delay. Choose all packets or a specific packet type.") + private val inboundDelay by setting("Inbound Delay", 250L, 0L..5000L, 10L, unit = "ms", description = "Time to delay packets received from the server before processing.") { networkScope != Direction.Outbound } + private val outboundDelay by setting("Outbound Delay", 250L, 0L..5000L, 10L, unit = "ms", description = "Time to delay packets sent to the server before sending.") { networkScope != Direction.Inbound } private var outboundPool = ConcurrentLinkedDeque() private var inboundPool = ConcurrentLinkedDeque() @@ -52,7 +54,7 @@ object PacketDelay : Module( init { listen { - if (mode != Mode.STATIC) return@listen + if (mode != Mode.Static) return@listen flushPools(System.currentTimeMillis()) } @@ -61,12 +63,12 @@ object PacketDelay : Module( if (!packetScope.filter(event.packet)) return@listen when (mode) { - Mode.STATIC -> { + Mode.Static -> { outboundPool.add(event.packet) event.cancel() } - Mode.PULSE -> { + Mode.Pulse -> { runConcurrent { delay(outboundDelay) runGameScheduled { @@ -82,12 +84,12 @@ object PacketDelay : Module( if (!packetScope.filter(event.packet)) return@listen when (mode) { - Mode.STATIC -> { + Mode.Static -> { inboundPool.add(event.packet) event.cancel() } - Mode.PULSE -> { + Mode.Pulse -> { runConcurrent { delay(inboundDelay) runGameScheduled { @@ -128,10 +130,30 @@ object PacketDelay : Module( } } - enum class Mode { STATIC, PULSE, } - enum class Direction { BOTH, INBOUND, OUTBOUND } - enum class PacketType(val filter: (Packet<*>) -> Boolean) { - ANY({ true }), - KEEP_ALIVE({ it is KeepAliveC2SPacket }) + enum class Mode( + override val displayName: String, + override val description: String, + ) : NamedEnum, Describable { + Static("Static", "Queue packets and release them in bursts based on your delay. Useful for batching traffic."), + Pulse("Pulse", "Apply a per-packet delay before it is sent/processed. Useful for smoothing timing.") } + + enum class Direction( + override val displayName: String, + override val description: String, + ) : NamedEnum, Describable { + Both("Both", "Affects both outbound (client → server) and inbound (server → client) packets."), + Inbound("Inbound", "Affects only packets received from the server."), + Outbound("Outbound", "Affects only packets sent to the server.") + } + + enum class PacketType( + override val displayName: String, + override val description: String, + val filter: (Packet<*>) -> Boolean, + ) : NamedEnum, Describable { + Any("Any", "Delay every packet regardless of type.", { true }), + KeepAlive("Keep-Alive", "Delay only KeepAlive packets (useful for simulating higher ping).", { it is KeepAliveC2SPacket }) + } + } diff --git a/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt b/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt index af0c0901b..4ca42ad88 100644 --- a/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt +++ b/src/main/kotlin/com/lambda/module/modules/network/PacketLogger.kt @@ -49,6 +49,7 @@ object PacketLogger : Module( name = "PacketLogger", description = "Serializes network traffic and persists it for later analysis", tag = ModuleTag.NETWORK, + autoDisable = true ) { private val logToChat by setting("Log To Chat", false, "Log packets to chat") diff --git a/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt b/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt index fe83b8949..82d104d4b 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/Freecam.kt @@ -52,6 +52,7 @@ object Freecam : Module( name = "Freecam", description = "Move your camera freely", tag = ModuleTag.PLAYER, + autoDisable = true, ) { private val speed by setting("Speed", 0.5, 0.1..1.0, 0.1) private val sprint by setting("Sprint Multiplier", 3.0, 0.1..10.0, 0.1, description = "Set below 1.0 to fly slower on sprint.") @@ -145,9 +146,5 @@ object Freecam : Module( .rayCast(reach, lerpPos) .orMiss // Can't be null (otherwise mc will spam "Null returned as 'hitResult', this shouldn't happen!") } - - listen { - disable() - } } } diff --git a/src/main/kotlin/com/lambda/module/modules/player/Replay.kt b/src/main/kotlin/com/lambda/module/modules/player/Replay.kt index 04ceaba03..53be575f1 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/Replay.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/Replay.kt @@ -87,6 +87,7 @@ object Replay : Module( name = "Replay", description = "Record gameplay actions and replay them like a TAS.", tag = ModuleTag.PLAYER, + autoDisable = true ) { private val record by setting("Record", KeyCode.R) private val play by setting("Play / Stop", KeyCode.C) diff --git a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt index 5dd360995..51e253196 100644 --- a/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt +++ b/src/main/kotlin/com/lambda/module/tag/ModuleTag.kt @@ -46,5 +46,17 @@ data class ModuleTag(override val name: String) : Nameable { val HUD = ModuleTag("Hud") val defaults = setOf(COMBAT, MOVEMENT, RENDER, PLAYER, NETWORK, DEBUG, CLIENT, HUD) + + val shownTags = defaults.toMutableSet() + + fun toggleTag(tag: ModuleTag) { + if (shownTags.contains(tag)) { + shownTags.remove(tag) + } else { + shownTags.add(tag) + } + } + + fun isTagShown(tag: ModuleTag) = shownTags.contains(tag) } } From d105954d7658c28c978329d6dc7a0bcd29b0e146 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sat, 23 Aug 2025 20:47:19 +0200 Subject: [PATCH 03/12] Maybe working MacOS icon --- .../kotlin/com/lambda/util/WindowIcons.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/lambda/util/WindowIcons.kt b/src/main/kotlin/com/lambda/util/WindowIcons.kt index e1768b532..dbdd296d6 100644 --- a/src/main/kotlin/com/lambda/util/WindowIcons.kt +++ b/src/main/kotlin/com/lambda/util/WindowIcons.kt @@ -18,12 +18,16 @@ package com.lambda.util import com.lambda.Lambda.mc +import net.minecraft.client.util.MacWindowUtil import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFWImage import org.lwjgl.system.MemoryStack import org.lwjgl.system.MemoryUtil import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +import javax.imageio.ImageIO object WindowIcons { /** @@ -86,19 +90,15 @@ object WindowIcons { } GLFW.GLFW_PLATFORM_COCOA -> { - // On macOS glfwSetWindowIcon is ignored; set dock icon instead if possible. - try { - val largest = iconPaths - .mapNotNull { runCatching { it.readImage() }.getOrNull() } - .maxByOrNull { it.width * it.height } - ?: return + val largest = iconPaths + .mapNotNull { runCatching { it.readImage() }.getOrNull() } + .maxByOrNull { it.width * it.height } + ?: return - // Try Minecraft's MacWindowUtil if present - val klass = Class.forName("net.minecraft.client.util.MacWindowUtil") - val method = klass.getMethod("setApplicationIconImage", BufferedImage::class.java) - method.invoke(null, largest) - } catch (_: Throwable) { - // Silently ignore if class isn't available; no safe fallback on macOS via GLFW. + MacWindowUtil.setApplicationIconImage { + val baos = ByteArrayOutputStream() + ImageIO.write(largest, "PNG", baos) + ByteArrayInputStream(baos.toByteArray()) } } From 44fe4e811c0e326851e03f00be6db810165435a8 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 24 Aug 2025 08:55:07 +0200 Subject: [PATCH 04/12] QuickSearch --- src/main/kotlin/com/lambda/gui/DearImGui.kt | 2 + src/main/kotlin/com/lambda/gui/MenuBar.kt | 19 +- .../lambda/gui/components/ClickGuiLayout.kt | 2 + .../com/lambda/gui/components/ModuleEntry.kt | 16 +- .../com/lambda/gui/components/QuickSearch.kt | 269 ++++++++++++++++++ 5 files changed, 278 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/lambda/gui/components/QuickSearch.kt diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index 7273b7e95..7b30cfaa3 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -44,6 +44,7 @@ object DearImGui : Loadable { val implGl3 = ImGuiImplGl3() const val EXTERNAL_LINK = '↗' + const val BREADCRUMB_SEPARATOR = '»' val io: ImGuiIO get() = ImGui.getIO() const val DEFAULT_FLAGS = ImGuiConfigFlags.NavEnableKeyboard or // Enable Keyboard Controls @@ -62,6 +63,7 @@ object DearImGui : Loadable { addRanges(io.fonts.glyphRangesDefault) addRanges(io.fonts.glyphRangesGreek) addChar(EXTERNAL_LINK) + addChar(BREADCRUMB_SEPARATOR) }.buildRanges() io.fonts.addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, baseFontSize * scale, ImFontConfig(), glyphRanges) io.fonts.build() diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt index 4aea2644e..8ab49202c 100644 --- a/src/main/kotlin/com/lambda/gui/MenuBar.kt +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -26,6 +26,7 @@ import com.lambda.core.Loader import com.lambda.event.EventFlow import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.gui.DearImGui.EXTERNAL_LINK +import com.lambda.gui.components.QuickSearch import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.ModuleRegistry import com.lambda.module.tag.ModuleTag @@ -51,10 +52,6 @@ object MenuBar { val lambdaLogo = upload("textures/lambda.png") val githubLogo = upload("textures/github_logo.png") - // ToDo: On pressing shift (or something else) open a quick search bar popup. - // - Search for modules, hud elements, and commands using levenshtein distance. - private val quickSearch = ImString(64) - fun ImGuiBuilder.buildMenuBar() { mainMenuBar { lambdaMenu() @@ -436,18 +433,8 @@ object MenuBar { } private fun ImGuiBuilder.buildHelpMenu() { - menuItem("Quick Search...") { - // ToDo: - // - Search for modules, commands, and HUD widgets. - // - Show matches in a search panel below the GUI. - // - Support regex. - // - Support levenshtein distance. - // - Support multiple search terms. - // - Support search history. - // - Support search filters (by type, enabled/disabled, etc). - // - Support search scopes (all/enabled/disabled). - // - Support search shortcuts (Ctrl+F, Cmd+F, etc). - // - Show match count in the search panel. + menuItem("Quick Search...", "Shift+Shift") { + QuickSearch.open() } menuItem("Documentation $EXTERNAL_LINK") { Util.getOperatingSystem().open("$REPO_URL/wiki") diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt index dba01cfa0..82e4c3d9f 100644 --- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt @@ -21,6 +21,7 @@ import com.lambda.core.Loadable import com.lambda.event.events.GuiEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.gui.MenuBar.buildMenuBar +import com.lambda.gui.components.QuickSearch.renderQuickSearch import com.lambda.gui.dsl.ImGuiBuilder.buildLayout import com.lambda.module.ModuleRegistry import com.lambda.module.modules.client.ClickGui @@ -43,6 +44,7 @@ object ClickGuiLayout : Loadable { } buildMenuBar() + renderQuickSearch() ImGui.showDemoWindow() } diff --git a/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt b/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt index 86b4b9c26..5b154fc79 100644 --- a/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt +++ b/src/main/kotlin/com/lambda/gui/components/ModuleEntry.kt @@ -29,20 +29,8 @@ import imgui.flag.ImGuiCol class ModuleEntry(val module: Module): Layout { override fun ImGuiBuilder.buildLayout() { - if (module.isEnabled) { - withStyleColor(ImGuiCol.Header, 0.20f, 0.55f, 0.25f, 0.85f) { - withStyleColor(ImGuiCol.HeaderHovered, 0.25f, 0.65f, 0.30f, 0.90f) { - withStyleColor(ImGuiCol.HeaderActive, 0.20f, 0.55f, 0.25f, 1.00f) { - selectable(module.name, selected = true) { - module.toggle() - } - } - } - } - } else { - selectable(module.name, selected = false) { - module.toggle() - } + selectable(module.name, selected = module.isEnabled) { + module.toggle() } lambdaTooltip(module.description) diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt new file mode 100644 index 000000000..233e1a947 --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.components + +import com.lambda.Lambda.mc +import com.lambda.command.CommandRegistry +import com.lambda.command.LambdaCommand +import com.lambda.config.AbstractSetting +import com.lambda.config.Configurable +import com.lambda.config.Configuration +import com.lambda.event.events.KeyboardEvent +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.gui.Layout +import com.lambda.gui.dsl.ImGuiBuilder +import com.lambda.module.Module +import com.lambda.module.ModuleRegistry +import com.lambda.util.KeyCode +import com.lambda.util.StringUtils.capitalize +import com.lambda.util.StringUtils.findSimilarStrings +import imgui.ImGui +import imgui.flag.ImGuiHoveredFlags +import imgui.flag.ImGuiInputTextFlags +import imgui.flag.ImGuiStyleVar +import imgui.flag.ImGuiWindowFlags +import imgui.type.ImString +import net.minecraft.client.gui.screen.ChatScreen + +object QuickSearch { + private val searchInput = ImString(256) + private var isOpen = false + private var shouldFocus = false + + private var lastShiftPressTime = 0L + private var lastShiftKeyCode = -1 + private const val DOUBLE_SHIFT_WINDOW_MS = 500L + + private const val MAX_RESULTS = 50 + private const val SIMILARITY_THRESHOLD = 3 + + init { + listenUnsafe { event -> + handleKeyPress(event) + } + } + + interface SearchResult : Layout { + val breadcrumb: String + } + + private class ModuleResult(val module: Module) : SearchResult { + override val breadcrumb = "Module" + + override fun ImGuiBuilder.buildLayout() { + with(ModuleEntry(module)) { buildLayout() } + } + + companion object { + fun search(query: String): List { + val modules = ModuleRegistry.modules + val direct = modules.filter { + it.name.lowercase().let { name -> name.startsWith(query) || name.contains(query) } + } + + if (direct.isNotEmpty()) return direct.map(::ModuleResult) + + val names = modules.map { it.name }.toSet() + val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD) + return similar.mapNotNull { name -> modules.find { it.name == name } } + .map(::ModuleResult) + } + } + } + + private class CommandResult(val command: LambdaCommand) : SearchResult { + override val breadcrumb = "Command" + override fun ImGuiBuilder.buildLayout() { + text(command.name.capitalize()) + sameLine() + smallButton("Insert") { mc.setScreen(ChatScreen("${CommandRegistry.prefix}${command.name} ")) } + if (command.description.isNotBlank()) { + sameLine() + textDisabled(command.description) + } + } + + companion object { + fun search(query: String): List { + val commands = CommandRegistry.commands + val direct = commands.filter { + val name = it.name.lowercase() + name.startsWith(query) || name.contains(query) || it.aliases.any { alias -> alias.lowercase().contains(query) } + } + + if (direct.isNotEmpty()) return direct.map(::CommandResult) + + val names = commands.map { it.name }.toSet() + val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD) + return similar.mapNotNull { name -> commands.find { it.name == name } } + .map(::CommandResult) + } + } + } + + private class SettingResult(val setting: AbstractSetting<*>, val configurable: Configurable) : SearchResult { + override val breadcrumb: String by lazy { buildSettingBreadcrumb(configurable.name, setting) } + + override fun ImGuiBuilder.buildLayout() { + with(setting) { buildLayout() } + } + + companion object { + fun search(query: String) = + Configuration.configurations.flatMap { config -> + config.configurables.flatMap { configurable -> + val confNameL = configurable.name.lowercase() + configurable.settings.filter { setting -> + setting.visibility() && (setting.name.lowercase().contains(query) || confNameL.contains(query)) + }.map { setting -> + SettingResult(setting, configurable) + } + } + }.take(15) + } + } + + fun open() { + isOpen = true + shouldFocus = true + searchInput.clear() + } + + fun close() { + isOpen = false + shouldFocus = false + } + + fun toggle() { + if (isOpen) close() else open() + } + + fun ImGuiBuilder.renderQuickSearch() { + if (!isOpen) return + ImGui.openPopup("QuickSearch") + + val windowFlags = ImGuiWindowFlags.AlwaysAutoResize or + ImGuiWindowFlags.NoTitleBar or + ImGuiWindowFlags.NoMove or + ImGuiWindowFlags.NoResize or + ImGuiWindowFlags.NoScrollbar or + ImGuiWindowFlags.NoScrollWithMouse + + ImGui.setNextFrameWantCaptureKeyboard(true) + + val display = ImGui.getIO().displaySize + val maxW = display.x * 0.5f + val maxH = display.y * 0.5f + + val popupX = (display.x - maxW) * 0.5f + val popupY = display.y * 0.3f + ImGui.setNextWindowPos(popupX, popupY) + ImGui.setNextWindowSize(maxW, 0f) + ImGui.setNextWindowSizeConstraints(0f, 0f, maxW, maxH) + + popupModal("QuickSearch", windowFlags) { + // ToDo: Fix close with background click and escape + if (ImGui.isKeyPressed(256)) { // ESC key + close() + ImGui.closeCurrentPopup() + return@popupModal + } + +// val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) && +// !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow) +// if (bgClick) { +// close() +// ImGui.closeCurrentPopup() +// return@popupModal +// } + + if (shouldFocus) { + ImGui.setKeyboardFocusHere() + shouldFocus = false + } + + withItemWidth(ImGui.getContentRegionAvailX() * 0.98f) { + withStyleVar(ImGuiStyleVar.FramePadding, style.framePadding.x, style.framePadding.y * 1.35f) { + ImGui.inputTextWithHint( + "##qs-input", + "Type to search modules, settings, and commands...", + searchInput, + ImGuiInputTextFlags.AutoSelectAll + ) + } + } + + val query = searchInput.get().trim() + if (query.isEmpty()) return@popupModal + + val results = performSearch(query) + if (results.isEmpty()) { + textDisabled("Nothing found.") + return@popupModal + } + + val rowH = frameHeightWithSpacing + val topArea = ImGui.getCursorPosY() + style.windowPadding.y + val listH = (results.size * rowH).coerceAtMost(maxH - topArea).coerceAtLeast(rowH) + + child("qs_rows", 0f, listH, false) { + results.forEachIndexed { idx, result -> + withId(idx) { + with(result) { + if (breadcrumb.isNotBlank()) { + textDisabled(breadcrumb) + sameLine() + } + buildLayout() + } + } + } + } + } + } + + + private fun performSearch(query: String) = + listOf(ModuleResult::search, CommandResult::search, SettingResult::search) + .flatMap { it(query.lowercase()) }.take(MAX_RESULTS) + + private fun buildSettingBreadcrumb(configurableName: String, setting: AbstractSetting<*>): String { + val group = setting.groups + .minByOrNull { it.size } + ?.joinToString(" » ") { it.displayName } + ?: return configurableName + return "$configurableName » $group" + } + + private fun handleKeyPress(event: KeyboardEvent.Press) { + if (!event.isPressed || !(event.keyCode == KeyCode.LEFT_SHIFT.code || event.keyCode == KeyCode.RIGHT_SHIFT.code)) return + + val currentTime = System.currentTimeMillis() + if (lastShiftKeyCode == event.keyCode && + currentTime - lastShiftPressTime <= DOUBLE_SHIFT_WINDOW_MS + ) { + open() + lastShiftPressTime = 0L + lastShiftKeyCode = -1 + } else { + lastShiftPressTime = currentTime + lastShiftKeyCode = event.keyCode + } + } +} \ No newline at end of file From 3ce6dc6082343be622882cf31c12aa4cfbf60699 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 24 Aug 2025 09:03:03 +0200 Subject: [PATCH 05/12] Cleanup --- .../com/lambda/gui/components/QuickSearch.kt | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 233e1a947..2f29b32f3 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -34,7 +34,6 @@ import com.lambda.util.KeyCode import com.lambda.util.StringUtils.capitalize import com.lambda.util.StringUtils.findSimilarStrings import imgui.ImGui -import imgui.flag.ImGuiHoveredFlags import imgui.flag.ImGuiInputTextFlags import imgui.flag.ImGuiStyleVar import imgui.flag.ImGuiWindowFlags @@ -53,6 +52,13 @@ object QuickSearch { private const val MAX_RESULTS = 50 private const val SIMILARITY_THRESHOLD = 3 + private const val WINDOW_FLAGS = ImGuiWindowFlags.AlwaysAutoResize or + ImGuiWindowFlags.NoTitleBar or + ImGuiWindowFlags.NoMove or + ImGuiWindowFlags.NoResize or + ImGuiWindowFlags.NoScrollbar or + ImGuiWindowFlags.NoScrollWithMouse + init { listenUnsafe { event -> handleKeyPress(event) @@ -158,26 +164,18 @@ object QuickSearch { if (!isOpen) return ImGui.openPopup("QuickSearch") - val windowFlags = ImGuiWindowFlags.AlwaysAutoResize or - ImGuiWindowFlags.NoTitleBar or - ImGuiWindowFlags.NoMove or - ImGuiWindowFlags.NoResize or - ImGuiWindowFlags.NoScrollbar or - ImGuiWindowFlags.NoScrollWithMouse - ImGui.setNextFrameWantCaptureKeyboard(true) - val display = ImGui.getIO().displaySize - val maxW = display.x * 0.5f - val maxH = display.y * 0.5f + val maxW = io.displaySize.x * 0.5f + val maxH = io.displaySize.y * 0.5f - val popupX = (display.x - maxW) * 0.5f - val popupY = display.y * 0.3f + val popupX = (io.displaySize.x - maxW) * 0.5f + val popupY = io.displaySize.y * 0.3f ImGui.setNextWindowPos(popupX, popupY) ImGui.setNextWindowSize(maxW, 0f) ImGui.setNextWindowSizeConstraints(0f, 0f, maxW, maxH) - popupModal("QuickSearch", windowFlags) { + popupModal("QuickSearch", WINDOW_FLAGS) { // ToDo: Fix close with background click and escape if (ImGui.isKeyPressed(256)) { // ESC key close() @@ -198,8 +196,8 @@ object QuickSearch { shouldFocus = false } - withItemWidth(ImGui.getContentRegionAvailX() * 0.98f) { - withStyleVar(ImGuiStyleVar.FramePadding, style.framePadding.x, style.framePadding.y * 1.35f) { + withItemWidth(ImGui.getContentRegionAvailX()) { + withStyleVar(ImGuiStyleVar.FramePadding, style.framePadding.x, style.framePadding.y) { ImGui.inputTextWithHint( "##qs-input", "Type to search modules, settings, and commands...", @@ -219,7 +217,7 @@ object QuickSearch { } val rowH = frameHeightWithSpacing - val topArea = ImGui.getCursorPosY() + style.windowPadding.y + val topArea = cursorPosY + style.windowPadding.y val listH = (results.size * rowH).coerceAtMost(maxH - topArea).coerceAtLeast(rowH) child("qs_rows", 0f, listH, false) { From 97a2feb0f88d5f95f800c5f0c05644d44323aed3 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 03:48:45 +0200 Subject: [PATCH 06/12] Add MC Font --- src/main/kotlin/com/lambda/gui/DearImGui.kt | 14 +++++++++----- .../com/lambda/gui/components/QuickSearch.kt | 7 +++---- .../lambda/fonts/MinecraftDefault-Regular.ttf | Bin 0 -> 235228 bytes 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 src/main/resources/assets/lambda/fonts/MinecraftDefault-Regular.ttf diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index 7b30cfaa3..eac593a3d 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -35,7 +35,6 @@ import imgui.gl3.ImGuiImplGl3 import imgui.glfw.ImGuiImplGlfw import net.minecraft.client.gl.GlBackend import net.minecraft.client.texture.GlTexture -import org.lwjgl.opengl.GL11.glViewport import org.lwjgl.opengl.GL30.GL_FRAMEBUFFER import kotlin.math.abs @@ -45,6 +44,7 @@ object DearImGui : Loadable { const val EXTERNAL_LINK = '↗' const val BREADCRUMB_SEPARATOR = '»' + const val BASE_FONT_SCALE = 13f val io: ImGuiIO get() = ImGui.getIO() const val DEFAULT_FLAGS = ImGuiConfigFlags.NavEnableKeyboard or // Enable Keyboard Controls @@ -57,16 +57,20 @@ object DearImGui : Loadable { private var targetScale = 0f private fun updateScale(scale: Float) { - io.fonts.clear() - val baseFontSize = 13f val glyphRanges = ImFontGlyphRangesBuilder().apply { addRanges(io.fonts.glyphRangesDefault) addRanges(io.fonts.glyphRangesGreek) addChar(EXTERNAL_LINK) addChar(BREADCRUMB_SEPARATOR) }.buildRanges() - io.fonts.addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, baseFontSize * scale, ImFontConfig(), glyphRanges) - io.fonts.build() + val fontConfig = ImFontConfig() + val size = BASE_FONT_SCALE * scale + with(io.fonts) { + clear() + addFontFromFileTTF("fonts/FiraSans-Regular.ttf".path, size, fontConfig, glyphRanges) + addFontFromFileTTF("fonts/MinecraftDefault-Regular.ttf".path, size, fontConfig, glyphRanges) + build() + } implGl3.createFontsTexture() } diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 2f29b32f3..5c45bb256 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -47,11 +47,10 @@ object QuickSearch { private var lastShiftPressTime = 0L private var lastShiftKeyCode = -1 - private const val DOUBLE_SHIFT_WINDOW_MS = 500L + private const val DOUBLE_SHIFT_WINDOW_MS = 500L private const val MAX_RESULTS = 50 private const val SIMILARITY_THRESHOLD = 3 - private const val WINDOW_FLAGS = ImGuiWindowFlags.AlwaysAutoResize or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoMove or @@ -136,12 +135,12 @@ object QuickSearch { config.configurables.flatMap { configurable -> val confNameL = configurable.name.lowercase() configurable.settings.filter { setting -> - setting.visibility() && (setting.name.lowercase().contains(query) || confNameL.contains(query)) + setting.visibility() && (setting.name.lowercase().contains(query)) }.map { setting -> SettingResult(setting, configurable) } } - }.take(15) + } } } diff --git a/src/main/resources/assets/lambda/fonts/MinecraftDefault-Regular.ttf b/src/main/resources/assets/lambda/fonts/MinecraftDefault-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7f3d0dcf6ea279c0f5b6d6e7bcfd6428fdd0d2a2 GIT binary patch literal 235228 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

`KH>^m2cEhT|XQC!==4q=XUwRoQ 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�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 E0D3>oNB{r; literal 0 HcmV?d00001 From 7dc60c0261a112278be474fc261416d699798d9d Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 04:11:55 +0200 Subject: [PATCH 07/12] Smart search result relevance scoring with lenient fuzzy search --- .../com/lambda/gui/components/QuickSearch.kt | 170 +++++++++++------- .../kotlin/com/lambda/util/StringUtils.kt | 2 +- 2 files changed, 110 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 5c45bb256..5386285b9 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -3,7 +3,6 @@ * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * @@ -32,13 +31,14 @@ import com.lambda.module.Module import com.lambda.module.ModuleRegistry import com.lambda.util.KeyCode import com.lambda.util.StringUtils.capitalize -import com.lambda.util.StringUtils.findSimilarStrings +import com.lambda.util.StringUtils.levenshteinDistance import imgui.ImGui import imgui.flag.ImGuiInputTextFlags import imgui.flag.ImGuiStyleVar import imgui.flag.ImGuiWindowFlags import imgui.type.ImString import net.minecraft.client.gui.screen.ChatScreen +import kotlin.math.max object QuickSearch { private val searchInput = ImString(256) @@ -50,7 +50,6 @@ object QuickSearch { private const val DOUBLE_SHIFT_WINDOW_MS = 500L private const val MAX_RESULTS = 50 - private const val SIMILARITY_THRESHOLD = 3 private const val WINDOW_FLAGS = ImGuiWindowFlags.AlwaysAutoResize or ImGuiWindowFlags.NoTitleBar or ImGuiWindowFlags.NoMove or @@ -74,22 +73,6 @@ object QuickSearch { override fun ImGuiBuilder.buildLayout() { with(ModuleEntry(module)) { buildLayout() } } - - companion object { - fun search(query: String): List { - val modules = ModuleRegistry.modules - val direct = modules.filter { - it.name.lowercase().let { name -> name.startsWith(query) || name.contains(query) } - } - - if (direct.isNotEmpty()) return direct.map(::ModuleResult) - - val names = modules.map { it.name }.toSet() - val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD) - return similar.mapNotNull { name -> modules.find { it.name == name } } - .map(::ModuleResult) - } - } } private class CommandResult(val command: LambdaCommand) : SearchResult { @@ -103,23 +86,6 @@ object QuickSearch { textDisabled(command.description) } } - - companion object { - fun search(query: String): List { - val commands = CommandRegistry.commands - val direct = commands.filter { - val name = it.name.lowercase() - name.startsWith(query) || name.contains(query) || it.aliases.any { alias -> alias.lowercase().contains(query) } - } - - if (direct.isNotEmpty()) return direct.map(::CommandResult) - - val names = commands.map { it.name }.toSet() - val similar = findSimilarStrings(query, names, SIMILARITY_THRESHOLD) - return similar.mapNotNull { name -> commands.find { it.name == name } } - .map(::CommandResult) - } - } } private class SettingResult(val setting: AbstractSetting<*>, val configurable: Configurable) : SearchResult { @@ -128,20 +94,6 @@ object QuickSearch { override fun ImGuiBuilder.buildLayout() { with(setting) { buildLayout() } } - - companion object { - fun search(query: String) = - Configuration.configurations.flatMap { config -> - config.configurables.flatMap { configurable -> - val confNameL = configurable.name.lowercase() - configurable.settings.filter { setting -> - setting.visibility() && (setting.name.lowercase().contains(query)) - }.map { setting -> - SettingResult(setting, configurable) - } - } - } - } } fun open() { @@ -182,13 +134,13 @@ object QuickSearch { return@popupModal } -// val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) && -// !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow) -// if (bgClick) { -// close() -// ImGui.closeCurrentPopup() -// return@popupModal -// } + // val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) && + // !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow) + // if (bgClick) { + // close() + // ImGui.closeCurrentPopup() + // return@popupModal + // } if (shouldFocus) { ImGui.setKeyboardFocusHere() @@ -209,7 +161,7 @@ object QuickSearch { val query = searchInput.get().trim() if (query.isEmpty()) return@popupModal - val results = performSearch(query) + val results = SearchService.performSearch(query) if (results.isEmpty()) { textDisabled("Nothing found.") return@popupModal @@ -235,10 +187,106 @@ object QuickSearch { } } + private object SearchService { + private data class RankedSearchResult(val result: SearchResult, val score: Int) + + private const val MODULE_PRIORITY_BONUS = 300 + private const val COMMAND_PRIORITY_BONUS = 200 + + /** + * Calculates a relevance score for a query against a target string. + * Returns 0 for no match. Higher scores are better. + * The `lenient` flag adjusts the threshold for fuzzy matching. + */ + private fun calculateScore(query: String, target: String, lenient: Boolean = false): Int { + if (query.isEmpty() || target.isEmpty()) return 0 + + // 1. Strong Matches (Exact, Prefix, Substring) + if (target == query) return 200 + if (target.startsWith(query)) { + val completeness = (query.length * 50) / target.length + return 100 + completeness // Score: 101 - 150 + } + if (target.contains(query)) { + val completeness = (query.length * 40) / target.length + return 50 + completeness // Score: 51 - 90 + } - private fun performSearch(query: String) = - listOf(ModuleResult::search, CommandResult::search, SettingResult::search) - .flatMap { it(query.lowercase()) }.take(MAX_RESULTS) + // 2. Weak Match (Fuzzy) + val distance = query.levenshteinDistance(target) + val strictThreshold = (query.length / 3).coerceAtLeast(1).coerceAtMost(4) + val lenientThreshold = (query.length / 2).coerceAtLeast(2).coerceAtMost(6) + val threshold = if (lenient) lenientThreshold else strictThreshold + + return if (distance <= threshold) { + (50 - (distance * 10)).coerceAtLeast(1) // Score: 1-40 + } else { + 0 + } + } + + /** + * Performs a search and returns a list of ranked results. This is the internal + * implementation that can be run in strict or lenient mode. + */ + private fun searchInternal(query: String, lenient: Boolean): List { + val lowerCaseQuery = query.lowercase() + + val moduleResults = ModuleRegistry.modules.mapNotNull { module -> + val nameScore = calculateScore(lowerCaseQuery, module.name.lowercase(), lenient) + val tagScore = calculateScore(lowerCaseQuery, module.tag.name.lowercase(), lenient) + val bestScore = max(nameScore, tagScore) + + if (bestScore > 0) { + RankedSearchResult(ModuleResult(module), bestScore + MODULE_PRIORITY_BONUS) + } else null + } + + val commandResults = CommandRegistry.commands.mapNotNull { command -> + val nameScore = calculateScore(lowerCaseQuery, command.name.lowercase(), lenient) + val aliasScore = command.aliases.maxOfOrNull { calculateScore(lowerCaseQuery, it.lowercase(), lenient) } ?: 0 + val bestScore = max(nameScore, aliasScore) + + if (bestScore > 0) { + RankedSearchResult(CommandResult(command), bestScore + COMMAND_PRIORITY_BONUS) + } else null + } + + val settingResults = Configuration.configurations.flatMap { + it.configurables.flatMap { configurable -> + configurable.settings + .filter { setting -> setting.visibility() } + .mapNotNull { setting -> + val score = calculateScore(lowerCaseQuery, setting.name.lowercase(), lenient) + if (score > 0) RankedSearchResult(SettingResult(setting, configurable), score) else null + } + } + } + + return moduleResults + commandResults + settingResults + } + + /** + * Main search entry point. It first attempts a strict search. If no results + * are found, it falls back to a more lenient fuzzy search. + */ + fun performSearch(query: String): List { + // First pass: strict search for high-quality matches. + val strictResults = searchInternal(query, lenient = false) + if (strictResults.isNotEmpty()) { + return strictResults + .sortedByDescending { it.score } + .map { it.result } + .take(MAX_RESULTS) + } + + // Second pass: if nothing was found, perform a more generous fuzzy search. + return searchInternal(query, lenient = true) + .sortedByDescending { it.score } + .map { it.result } + .take(MAX_RESULTS) + } + } private fun buildSettingBreadcrumb(configurableName: String, setting: AbstractSetting<*>): String { val group = setting.groups diff --git a/src/main/kotlin/com/lambda/util/StringUtils.kt b/src/main/kotlin/com/lambda/util/StringUtils.kt index 7b1b4f845..27c8826b7 100644 --- a/src/main/kotlin/com/lambda/util/StringUtils.kt +++ b/src/main/kotlin/com/lambda/util/StringUtils.kt @@ -69,7 +69,7 @@ object StringUtils { * @receiver The string to compare. * @param rhs The string to compare against. */ - private fun CharSequence.levenshteinDistance(rhs: CharSequence): Int { + fun CharSequence.levenshteinDistance(rhs: CharSequence): Int { if (this == rhs) { return 0 } From 3eb0a8273200e033dcc806abb7963dc31d8958b0 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 04:35:06 +0200 Subject: [PATCH 08/12] Rework menu bar entries layout --- src/main/kotlin/com/lambda/gui/MenuBar.kt | 246 +++++++----------- .../com/lambda/gui/components/QuickSearch.kt | 1 + 2 files changed, 88 insertions(+), 159 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt index 8ab49202c..c66ed343d 100644 --- a/src/main/kotlin/com/lambda/gui/MenuBar.kt +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -34,6 +34,7 @@ import com.lambda.threading.runSafe import com.lambda.util.Communication.info import com.lambda.util.Diagnostics.gatherDiagnostics import com.lambda.util.FolderRegister +import com.lambda.util.FolderRegister.minecraft import com.mojang.blaze3d.platform.TextureUtil import imgui.ImGui import imgui.ImGui.closeCurrentPopup @@ -42,8 +43,10 @@ import imgui.flag.ImGuiStyleVar import imgui.flag.ImGuiWindowFlags import imgui.type.ImString import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.Keyboard import net.minecraft.util.Util import net.minecraft.world.GameMode +import java.nio.file.Path import java.util.Locale object MenuBar { @@ -96,7 +99,7 @@ object MenuBar { } private fun ImGuiBuilder.buildLambdaMenu() { - menuItem("New Profile...") { + menuItem("New Profile...", enabled = false) { // ToDo (New Profile): // - Open a modal "New Profile" with: // [Profile Name] text input @@ -105,29 +108,50 @@ object MenuBar { // - On Create: instantiate and activate the profile, optionally copying values from current. // - On Cancel: close modal with no changes. } - menuItem("Open Config Folder") { - Util.getOperatingSystem().open(FolderRegister.config) + menu("Open Folder") { + menuItem("Open Lambda Folder") { + Util.getOperatingSystem().open(FolderRegister.lambda) + } + menuItem("Open Config Folder") { + Util.getOperatingSystem().open(FolderRegister.config) + } + menuItem("Open Packet Logs Folder") { + Util.getOperatingSystem().open(FolderRegister.packetLogs) + } + menuItem("Open Replay Folder") { + Util.getOperatingSystem().open(FolderRegister.replay) + } + menuItem("Open Cache Folder") { + Util.getOperatingSystem().open(FolderRegister.cache) + } + menuItem("Open Capes Folder") { + Util.getOperatingSystem().open(FolderRegister.capes) + } + menuItem("Open Structures Folder") { + Util.getOperatingSystem().open(FolderRegister.structure) + } + menuItem("Open Maps Folder") { + Util.getOperatingSystem().open(FolderRegister.maps) + } } separator() - menuItem("Save All Configs", "Ctrl+S") { - // Save every configuration file and show a toast with the total count. + menuItem("Save Configs") { Configuration.configurations.forEach { it.trySave(true) } - runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } + info("Saved ${Configuration.configurations.size} configuration files.") } - menuItem("Load All Configs", "Ctrl+L") { - // Load every configuration file and show a toast with the total count. + menuItem("Load Configs") { Configuration.configurations.forEach { it.tryLoad() } - runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + info("Loaded ${Configuration.configurations.size} configuration files.") } separator() - menuItem("Import Profile...") { + menuItem("Import Profile...", enabled = false) { // ToDo (Import Profile): // - Show a file picker for profile file(s). // - Preview dialog: profile name, version, module count, settings count, includes HUD? // - Provide options: Merge into Current / Replace Current. // - Apply with progress/rollback on failure; toast result. } - menuItem("Export Current Profile...") { + menuItem("Export Current Profile...", enabled = false) { // ToDo (Export Profile): // - File save modal with checkboxes: // [Include HUD Layout] [Include Keybinds] [Include Backups Metadata] @@ -137,58 +161,29 @@ object MenuBar { // ToDo (MRU Profiles): // - Populate from a most-recently-used (MRU) list persisted in preferences. // - On click: switch active profile (confirm if unsaved changes). - menuItem("Example Profile") {} - } - menuItem("Save All", "Ctrl+S") { - Configuration.configurations.forEach { it.trySave(true) } - runSafe { info("Saved ${Configuration.configurations.size} configuration files.") } - } - menuItem("Load All", "Ctrl+L") { - Configuration.configurations.forEach { it.tryLoad() } - runSafe { info("Loaded ${Configuration.configurations.size} configuration files.") } + menuItem("Example Profile", enabled = false) {} } separator() menu("Autosave Settings") { // ToDo: // - Toggle autosave, set interval (1..60s), backup rotation count (0..20). - menuItem("Autosave on changes", selected = true) {} - menuItem("Autosave Interval: 10s") {} - menuItem("Rotate Backups: 5") {} + menuItem("Autosave on changes", selected = true, enabled = false) {} + menuItem("Autosave Interval: 10s", enabled = false) {} + menuItem("Rotate Backups: 5", enabled = false) {} } menu("Backup & Restore") { // ToDo: // - “Create Backup Now” and “Manage/Restore Backups” UIs; list with timestamps/comments. - menuItem("Create Backup Now") {} - menuItem("Restore From Backup...") {} - menuItem("Manage Backups...") {} + menuItem("Create Backup Now", enabled = false) {} + menuItem("Restore From Backup...", enabled = false) {} + menuItem("Manage Backups...", enabled = false) {} } - menuItem("Profiles & Scopes...") { + menuItem("Profiles & Scopes...", enabled = false) { // ToDo (Profiles & Scopes Window): // - Active Profile dropdown. // - Scopes: Global / Per-Server / Per-World with enable overrides. // - Show overridden-only list, origin badges, and precedence explanation. } - menuItem("Module Settings Inspector...") { - // ToDo (Settings Inspector Window): - // - Left: Tree (Tag → Module → Group). - // - Right: Settings editor with search; filters (Changed-only, Overridden-only, Advanced). - // - Reset group/module actions. - } - menuItem("Placement/Build Settings...") { - // ToDo (Placement Panel): - // - Rotate For Place, Air Place Mode, Axis Rotate (conditional), - // - Place Stage Mask (multi-select), Place Confirmation Mode, - // - Max Pending Placements, Places Per Tick, - // - Swing On Place + Swing Type, Place Sounds. - // - Provide concise tooltips for trade-offs. - } - menuItem("Inventory Settings...") { - // ToDo (Inventory Panel): - // - Container group: Disposables editor (list add/remove + defaults), Swap with Disposables, - // Provider/Store Priorities. - // - Access group: Access Shulkers/Ender/Chests/Stashes toggles. - // - “Test Access” helper to simulate lookups. - } separator() menuItem("About...") { aboutRequested = true @@ -198,51 +193,12 @@ object MenuBar { menuItem("Exit Client") { mc.scheduleStop() } } - private fun ImGuiBuilder.buildViewMenu() { - - separator() - menu("UI Scale") { - // ToDo: - // - Apply selected scale (100/125/150/175/200%), update fonts via DearImGui.updateScale-like method. - listOf("100%", "125%", "150%", "175%", "200%").forEach { label -> - menuItem(label, selected = (label == "125%")) { /* set scale & rebuild fonts */ } - } - } - } - private fun ImGuiBuilder.buildHudMenu() { - menuItem("Copy HUD Layout") { - // ToDo: - // - Serialize current HUD widget tree with positions/anchors/safe-margins to memory clipboard. - } - menuItem("Paste HUD Layout") { - // ToDo: - // - Deserialize from clipboard and apply; if incompatible, show a non-blocking warning. - } - menuItem("Reset to Defaults") { - // ToDo: - // - Reset the currently focused panel’s settings to defaults (confirmation modal). - } - separator() - menuItem("Keybind Manager...") { - // ToDo (Keybind Manager Window): - // - Panel with search/filter; table columns: Action/Module | Current Key | Conflict | Change | Clear - // - Conflict detector with "Auto-resolve" suggestions. - } - menuItem("Open Editor", "Ctrl+Alt+C") { + menuItem("Open Editor") { // ToDo (HUD Editor Window): // - Full-screen canvas with grid; left "Elements" list; right "Properties" inspector. // - Drag & drop, snap grid, lock/unlock, safe margins, anchors, multi-select & alignment tools. } - menu("Add Widget") { - // ToDo: - // - Populate from available HUD widgets. On click, add centered and select for property editing. - menuItem("Stats") {} - menuItem("Clock") {} - menuItem("Ping") {} - menuItem("Coordinates") {} - menuItem("Module List") {} - } menu("Layouts") { // ToDo: // - New/Save/Save As/Load/Import/Export layout actions; Toggle "Autosave on change". @@ -255,10 +211,6 @@ object MenuBar { separator() menuItem("Autosave on change", selected = true) {} } - menuItem("Reset Layout") { - // ToDo: - // - Confirm and restore the default HUD layout. - } menuItem("Toggle Edit Handles", selected = true) { // ToDo: // - Show/hide bounds, anchors, labels while in edit mode. @@ -295,23 +247,17 @@ object MenuBar { menuItem("Open Minecraft Folder") { Util.getOperatingSystem().open(FolderRegister.minecraft) } - menuItem("Open Lambda Folder") { - Util.getOperatingSystem().open(FolderRegister.lambda) + menuItem("Open Saves Folder") { + Util.getOperatingSystem().open(mc.runDirectory.toPath().toAbsolutePath().resolve("saves").toFile()) } - menuItem("Open Config Folder") { - Util.getOperatingSystem().open(FolderRegister.config) + menuItem("Open Screenshots Folder") { + Util.getOperatingSystem().open(mc.runDirectory.toPath().toAbsolutePath().resolve("screenshots").toFile()) } - menuItem("Open Cache Folder") { - Util.getOperatingSystem().open(FolderRegister.cache) + menuItem("Open Resource Packs Folder") { + Util.getOperatingSystem().open(mc.runDirectory.toPath().toAbsolutePath().resolve("resourcepacks").toFile()) } - menuItem("Open Capes Folder") { - Util.getOperatingSystem().open(FolderRegister.capes) - } - menuItem("Open Structures Folder") { - Util.getOperatingSystem().open(FolderRegister.structure) - } - menuItem("Open Maps Folder") { - Util.getOperatingSystem().open(FolderRegister.maps) + menuItem("Open Mods Folder") { + Util.getOperatingSystem().open(mc.runDirectory.toPath().toAbsolutePath().resolve("mods").toFile()) } } separator() @@ -331,40 +277,17 @@ object MenuBar { } } menu("Debug Menu") { - menuItem( - "Show Debug Menu", "F3", - mc.debugHud.showDebugHud - ) { mc.debugHud.toggleDebugHud() } - menuItem( - "Rendering Chart", "F3+1", - mc.debugHud.renderingChartVisible - ) { mc.debugHud.toggleRenderingChart() } - menuItem( - "Rendering & Tick Charts", "F3+2", - mc.debugHud.renderingAndTickChartsVisible - ) { mc.debugHud.toggleRenderingAndTickCharts() } - menuItem( - "Packet Size & Ping Charts", "F3+3", - mc.debugHud.packetSizeAndPingChartsVisible - ) { mc.debugHud.togglePacketSizeAndPingCharts() } - - separator() - - menuItem("Reload Chunks", "F3+A") { - mc.worldRenderer.reload() + menuItem("Show Advanced Tooltips", "F3+H", mc.options.advancedItemTooltips) { + mc.options.advancedItemTooltips = !mc.options.advancedItemTooltips + mc.options.write() + } + menuItem("Show Chunk Borders", "F3+G", mc.debugRenderer.showChunkBorder) { + mc.debugRenderer.toggleShowChunkBorder() } - menuItem( - "Show Chunk Borders", "F3+G", - mc.debugRenderer.showChunkBorder - ) { mc.debugRenderer.toggleShowChunkBorder() } menuItem("Show Octree", selected = mc.debugRenderer.showOctree) { mc.debugRenderer.toggleShowOctree() } - menuItem( - label = "Show Hitboxes", - shortcut = "F3+B", - selected = mc.entityRenderDispatcher.shouldRenderHitboxes() - ) { + menuItem("Show Hitboxes", "F3+B", mc.entityRenderDispatcher.shouldRenderHitboxes()) { val now = !mc.entityRenderDispatcher.shouldRenderHitboxes() mc.entityRenderDispatcher.setRenderHitboxes(now) } @@ -385,49 +308,54 @@ object MenuBar { separator() menuItem( - label = "Advanced Tooltips", - shortcut = "F3+H", - selected = mc.options.advancedItemTooltips + label = "Pause On Lost Focus", + shortcut = "F3+Esc", + selected = mc.options.pauseOnLostFocus ) { - mc.options.advancedItemTooltips = !mc.options.advancedItemTooltips + mc.options.pauseOnLostFocus = !mc.options.pauseOnLostFocus mc.options.write() - } - menuItem("Inspect (Copy Look At)", "F3+I") { - // TODO: Implement precise copyLookAt(hasOp = player.hasPermissionLevel(2), raycastBlocksIfNotShift = !Screen.hasShiftDown()) - info("Inspect: Not yet implemented.") + info("Pause on lost focus ${if (mc.options.pauseOnLostFocus) "enabled" else "disabled"}.") } separator() - menuItem("Start/Stop Profiler", "F3+L") { - // TODO: Wire mc.toggleDebugProfiler with callback logging - info("Profiler control: Not yet implemented.") + menuItem("Reload Resource Packs", "F3+T") { + info("Reloading resource packs...") + mc.reloadResources() + } + + menuItem("Reload Chunks", "F3+A") { + mc.worldRenderer.reload() } separator() - menuItem( - label = "Pause On Lost Focus", - shortcut = "F3+Esc", - selected = mc.options.pauseOnLostFocus - ) { - mc.options.pauseOnLostFocus = !mc.options.pauseOnLostFocus - mc.options.write() - info("Pause on lost focus ${if (mc.options.pauseOnLostFocus) "enabled" else "disabled"}.") + menuItem("Show Debug Menu", "F3", mc.debugHud.showDebugHud) { + mc.debugHud.toggleDebugHud() + } + menuItem("Rendering Chart", "F3+1", mc.debugHud.renderingChartVisible) { + mc.debugHud.toggleRenderingChart() + } + menuItem("Rendering & Tick Charts", "F3+2", mc.debugHud.renderingAndTickChartsVisible) { + mc.debugHud.toggleRenderingAndTickCharts() + } + menuItem("Packet Size & Ping Charts", "F3+3", mc.debugHud.packetSizeAndPingChartsVisible) { + mc.debugHud.togglePacketSizeAndPingCharts() } separator() + menuItem("Start/Stop Profiler", "F3+L") { + mc.toggleDebugProfiler { message -> + info(message) + } + } menuItem("Dump Dynamic Textures", "F3+S") { val root = mc.runDirectory.toPath().toAbsolutePath() val output = TextureUtil.getDebugTexturePath(root) mc.textureManager.dumpDynamicTextures(output) info("Dumped dynamic textures to: ${root.relativize(output)}") } - menuItem("Reload Resource Packs", "F3+T") { - info("Reloading resource packs...") - mc.reloadResources() - } } } ?: menuItem("Debug (only available ingame)", enabled = false) } diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 5386285b9..914251b91 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -40,6 +40,7 @@ import imgui.type.ImString import net.minecraft.client.gui.screen.ChatScreen import kotlin.math.max +// ToDo: Add support for searching of menu bar entries object QuickSearch { private val searchInput = ImString(256) private var isOpen = false From ab62d154e8935e622b62e39f432f0c03da9bb0ad Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 04:36:57 +0200 Subject: [PATCH 09/12] Disabled not implemented features --- src/main/kotlin/com/lambda/gui/MenuBar.kt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/MenuBar.kt b/src/main/kotlin/com/lambda/gui/MenuBar.kt index c66ed343d..21888de84 100644 --- a/src/main/kotlin/com/lambda/gui/MenuBar.kt +++ b/src/main/kotlin/com/lambda/gui/MenuBar.kt @@ -194,7 +194,7 @@ object MenuBar { } private fun ImGuiBuilder.buildHudMenu() { - menuItem("Open Editor") { + menuItem("Open Editor", enabled = false) { // ToDo (HUD Editor Window): // - Full-screen canvas with grid; left "Elements" list; right "Properties" inspector. // - Drag & drop, snap grid, lock/unlock, safe margins, anchors, multi-select & alignment tools. @@ -202,16 +202,16 @@ object MenuBar { menu("Layouts") { // ToDo: // - New/Save/Save As/Load/Import/Export layout actions; Toggle "Autosave on change". - menuItem("New...") {} - menuItem("Save") {} - menuItem("Save As...") {} - menuItem("Load...") {} - menuItem("Import...") {} - menuItem("Export...") {} + menuItem("New...", enabled = false) {} + menuItem("Save", enabled = false) {} + menuItem("Save As...", enabled = false) {} + menuItem("Load...", enabled = false) {} + menuItem("Import...", enabled = false) {} + menuItem("Export...", enabled = false) {} separator() - menuItem("Autosave on change", selected = true) {} + menuItem("Autosave on change", selected = true, enabled = false) {} } - menuItem("Toggle Edit Handles", selected = true) { + menuItem("Toggle Edit Handles", selected = true, enabled = false) { // ToDo: // - Show/hide bounds, anchors, labels while in edit mode. } @@ -245,7 +245,7 @@ object MenuBar { private fun ImGuiBuilder.buildMinecraftMenu() { menu("Open Folder") { menuItem("Open Minecraft Folder") { - Util.getOperatingSystem().open(FolderRegister.minecraft) + Util.getOperatingSystem().open(minecraft) } menuItem("Open Saves Folder") { Util.getOperatingSystem().open(mc.runDirectory.toPath().toAbsolutePath().resolve("saves").toFile()) From c5b05c80fda496f1fe9b719623745ce66ff5a40b Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 04:57:13 +0200 Subject: [PATCH 10/12] Fix quick search close on ESC --- .../com/lambda/mixin/render/ScreenMixin.java | 38 +++++++++++++++++++ .../com/lambda/gui/components/QuickSearch.kt | 18 +-------- src/main/resources/lambda.mixins.common.json | 1 + 3 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/lambda/mixin/render/ScreenMixin.java diff --git a/src/main/java/com/lambda/mixin/render/ScreenMixin.java b/src/main/java/com/lambda/mixin/render/ScreenMixin.java new file mode 100644 index 000000000..2acb68348 --- /dev/null +++ b/src/main/java/com/lambda/mixin/render/ScreenMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.mixin.render; + +import com.lambda.gui.components.QuickSearch; +import net.minecraft.client.gui.screen.Screen; +import org.lwjgl.glfw.GLFW; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Screen.class) +public class ScreenMixin { + + @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) + private void onKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE && QuickSearch.INSTANCE.isOpen()) { + QuickSearch.INSTANCE.close(); + cir.setReturnValue(true); + } + } +} diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 914251b91..09777600b 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -43,7 +43,8 @@ import kotlin.math.max // ToDo: Add support for searching of menu bar entries object QuickSearch { private val searchInput = ImString(256) - private var isOpen = false + var isOpen = false + private set private var shouldFocus = false private var lastShiftPressTime = 0L @@ -128,21 +129,6 @@ object QuickSearch { ImGui.setNextWindowSizeConstraints(0f, 0f, maxW, maxH) popupModal("QuickSearch", WINDOW_FLAGS) { - // ToDo: Fix close with background click and escape - if (ImGui.isKeyPressed(256)) { // ESC key - close() - ImGui.closeCurrentPopup() - return@popupModal - } - - // val bgClick = (ImGui.isMouseClicked(0) || ImGui.isMouseClicked(1)) && - // !ImGui.isWindowHovered(ImGuiHoveredFlags.AnyWindow) - // if (bgClick) { - // close() - // ImGui.closeCurrentPopup() - // return@popupModal - // } - if (shouldFocus) { ImGui.setKeyboardFocusHere() shouldFocus = false diff --git a/src/main/resources/lambda.mixins.common.json b/src/main/resources/lambda.mixins.common.json index 72a4d2a99..d306b5035 100644 --- a/src/main/resources/lambda.mixins.common.json +++ b/src/main/resources/lambda.mixins.common.json @@ -47,6 +47,7 @@ "render.PlayerListHudMixin", "render.RenderLayersMixin", "render.ScreenHandlerMixin", + "render.ScreenMixin", "render.SplashOverlayMixin", "render.SplashOverlayMixin$LogoTextureMixin", "render.TooltipComponentMixin", From 976a0de437ad46ead03e73a33fc10fd30324ab69 Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 06:15:09 +0200 Subject: [PATCH 11/12] Enable using ClickGui keybind to close GUI --- .../kotlin/com/lambda/gui/components/QuickSearch.kt | 2 ++ src/main/kotlin/com/lambda/module/Module.kt | 11 ++++++++++- .../com/lambda/module/modules/client/ClickGui.kt | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index 09777600b..c80164b88 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -25,6 +25,7 @@ import com.lambda.config.Configurable import com.lambda.config.Configuration import com.lambda.event.events.KeyboardEvent import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.gui.LambdaScreen import com.lambda.gui.Layout import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.Module @@ -61,6 +62,7 @@ object QuickSearch { init { listenUnsafe { event -> + if (mc.currentScreen !is LambdaScreen) return@listenUnsafe handleKeyPress(event) } } diff --git a/src/main/kotlin/com/lambda/module/Module.kt b/src/main/kotlin/com/lambda/module/Module.kt index 1de014b1f..9fb8d927d 100644 --- a/src/main/kotlin/com/lambda/module/Module.kt +++ b/src/main/kotlin/com/lambda/module/Module.kt @@ -31,12 +31,16 @@ import com.lambda.event.listener.Listener import com.lambda.event.listener.SafeListener import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.event.listener.UnsafeListener +import com.lambda.gui.DearImGui +import com.lambda.gui.LambdaScreen +import com.lambda.module.modules.client.ClickGui import com.lambda.module.tag.ModuleTag import com.lambda.sound.LambdaSound import com.lambda.sound.SoundManager.play import com.lambda.util.Communication.info import com.lambda.util.KeyCode import com.lambda.util.Nameable +import imgui.ImGui /** * A [Module] is a feature or tool for the utility mod. @@ -134,7 +138,12 @@ abstract class Module( if (!event.isPressed) return@listen if (keybind == KeyCode.UNBOUND) return@listen if (event.translated != keybind) return@listen - if (mc.currentScreen != null) return@listen + if (mc.currentScreen != null) { + if (ClickGui.isEnabled && mc.currentScreen == LambdaScreen && !DearImGui.io.wantTextInput) { + LambdaScreen.close() + } + return@listen + } toggle() } diff --git a/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt index eaf5d6117..f7aba1700 100644 --- a/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt +++ b/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt @@ -41,6 +41,7 @@ object ClickGui : Module( description = "ImGui", tag = ModuleTag.CLIENT, defaultKeybind = KeyCode.Y, + autoDisable = true ) { private enum class Group(override val displayName: String) : NamedEnum { General("General"), From c9665a4df6363a50567c9e7a44192453ab35334a Mon Sep 17 00:00:00 2001 From: Constructor Date: Mon, 25 Aug 2025 07:27:06 +0200 Subject: [PATCH 12/12] Better alignment --- .../com/lambda/gui/components/QuickSearch.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt index c80164b88..0789d7830 100644 --- a/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt +++ b/src/main/kotlin/com/lambda/gui/components/QuickSearch.kt @@ -75,7 +75,13 @@ object QuickSearch { override val breadcrumb = "Module" override fun ImGuiBuilder.buildLayout() { - with(ModuleEntry(module)) { buildLayout() } + with(ModuleEntry(module)) { + buildLayout { + withItemWidth(ImGui.getContentRegionAvailX()) { + buildLayout() + } + } + } } } @@ -96,7 +102,13 @@ object QuickSearch { override val breadcrumb: String by lazy { buildSettingBreadcrumb(configurable.name, setting) } override fun ImGuiBuilder.buildLayout() { - with(setting) { buildLayout() } + with(setting) { + buildLayout { + withItemWidth(ImGui.getContentRegionAvailX()) { + buildLayout() + } + } + } } }