From 3a309f69f3a4accb2aa1347c52a917308f3e9ff3 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 7 Sep 2025 21:33:01 +0200 Subject: [PATCH 1/5] Fixed plotting and added snapping guides --- .../com/lambda/gui/components/HudGuiLayout.kt | 173 +++++++++++++++++- .../kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt | 76 +++++++- src/main/kotlin/com/lambda/gui/snap/Guide.kt | 28 +++ src/main/kotlin/com/lambda/gui/snap/RectF.kt | 27 +++ .../kotlin/com/lambda/gui/snap/SnapManager.kt | 165 +++++++++++++++++ src/main/kotlin/com/lambda/module/hud/TPS.kt | 42 +++-- .../module/modules/client/GuiSettings.kt | 24 ++- src/main/kotlin/com/lambda/util/ServerTPS.kt | 18 +- 8 files changed, 523 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/lambda/gui/snap/Guide.kt create mode 100644 src/main/kotlin/com/lambda/gui/snap/RectF.kt create mode 100644 src/main/kotlin/com/lambda/gui/snap/SnapManager.kt diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index 95e2da94d..8e56e0990 100644 --- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt @@ -20,10 +20,20 @@ package com.lambda.gui.components import com.lambda.core.Loadable import com.lambda.event.events.GuiEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.gui.dsl.ImGuiBuilder.buildLayout +import com.lambda.gui.snap.Guide +import com.lambda.gui.snap.RectF +import com.lambda.gui.snap.SnapManager import com.lambda.module.HudModule import com.lambda.module.ModuleRegistry +import com.lambda.module.modules.client.GuiSettings +import com.lambda.module.modules.client.ClickGui +import imgui.ImGui +import imgui.ImDrawList +import imgui.flag.ImDrawListFlags import imgui.flag.ImGuiWindowFlags +import kotlin.math.PI object HudGuiLayout : Loadable { const val DEFAULT_HUD_FLAGS = @@ -31,19 +41,174 @@ object HudGuiLayout : Loadable { ImGuiWindowFlags.NoBackground or ImGuiWindowFlags.AlwaysAutoResize or ImGuiWindowFlags.NoDocking + private var activeDragHudName: String? = null + private var mouseWasDown = false + private var dragOffsetX = 0f + private var dragOffsetY = 0f + private val lastBounds = mutableMapOf() + private val pendingPositions = mutableMapOf>() + private val snapOverlays = mutableMapOf() + + private data class SnapVisual( + val snapX: Float?, + val snapY: Float?, + val kindX: Guide.Kind?, + val kindY: Guide.Kind? + ) + + // Precomputed Float PI values to avoid repeated conversions + private const val PI_F = PI.toFloat() + private const val HALF_PI_F = (0.5f * PI).toFloat() + private const val THREE_HALVES_PI_F = (1.5f * PI).toFloat() + private const val TWO_PI_F = (2f * PI).toFloat() init { listen { buildLayout { - ModuleRegistry.modules + val vp = ImGui.getMainViewport() + SnapManager.beginFrame(vp.sizeX, vp.sizeY, io.fontGlobalScale) + + val mouseDown = io.mouseDown[0] + val mousePressedThisFrame = mouseDown && !mouseWasDown + val mouseReleasedThisFrame = !mouseDown && mouseWasDown + mouseWasDown = mouseDown + if (mouseReleasedThisFrame) { + activeDragHudName = null + } + + pendingPositions.clear() + snapOverlays.clear() + + val huds = ModuleRegistry.modules .filterIsInstance() .filter { it.isEnabled } - .forEach { hud -> - window("##${hud.name}", flags = DEFAULT_HUD_FLAGS) { - with(hud) { buildLayout() } + + if (ClickGui.isEnabled && activeDragHudName == null && mousePressedThisFrame) { + tryBeginDrag(huds) + } + + if (ClickGui.isEnabled && activeDragHudName != null && mouseDown) { + updateDragAndSnapping() + } + + huds.forEach { hud -> + val override = pendingPositions[hud.name] + if (override != null) { + ImGui.setNextWindowPos(override.first, override.second) + } + window("##${hud.name}", flags = DEFAULT_HUD_FLAGS) { + val vis = snapOverlays[hud.name] + if (vis != null) { + SnapManager.drawSnapLines( + foregroundDrawList, + vis.snapX, vis.kindX, + vis.snapY, vis.kindY + ) } + with(hud) { buildLayout() } + // Rounded-corner only outline; pull parameters from settings + if (ClickGui.isEnabled) { + drawHudOutline( + draw = foregroundDrawList, + x = windowPos.x, + y = windowPos.y, + w = windowSize.x, + h = windowSize.y + ) + } + val p = windowPos + val s = windowSize + val rect = RectF(p.x, p.y, s.x, s.y) + SnapManager.registerElement(hud.name, rect) + lastBounds[hud.name] = rect } + } } } } + + private fun ImGuiBuilder.tryBeginDrag(huds: List) { + val mx = io.mousePos.x + val my = io.mousePos.y + huds.forEach { hud -> + val r = lastBounds[hud.name] ?: return@forEach + val inside = mx >= r.x && mx <= r.x + r.w && my >= r.y && my <= r.y + r.h + if (inside) { + activeDragHudName = hud.name + dragOffsetX = mx - r.x + dragOffsetY = my - r.y + return + } + } + } + + private fun ImGuiBuilder.updateDragAndSnapping() { + val id = activeDragHudName ?: return + val last = lastBounds[id] ?: return + val mx = io.mousePos.x + val my = io.mousePos.y + val targetX = mx - dragOffsetX + val targetY = my - dragOffsetY + val proposed = RectF(targetX, targetY, last.w, last.h) + val snap = SnapManager.computeSnap(proposed, id) + val finalX = targetX + snap.dx + val finalY = targetY + snap.dy + pendingPositions[id] = finalX to finalY + snapOverlays[id] = SnapVisual(snap.snapX, snap.snapY, snap.kindX, snap.kindY) + } + + private fun ImGuiBuilder.drawHudOutline(draw: ImDrawList, x: Float, y: Float, w: Float, h: Float) { + val baseRadius = GuiSettings.hudOutlineCornerRadius + val rounding = if (baseRadius > 0f) baseRadius else style.windowRounding + val inflate = GuiSettings.hudOutlineCornerInflate + // Soft halo corners (gray, slightly smaller) + drawCornerArcs( + draw, + x, y, w, h, + (rounding + inflate).coerceAtLeast(0f), + GuiSettings.hudOutlineHaloColor.rgb, + GuiSettings.hudOutlineHaloThickness + ) + // Crisp inner corner arcs + drawCornerArcs( + draw, + x, y, w, h, + rounding.coerceAtLeast(0f), + GuiSettings.hudOutlineBorderColor.rgb, + GuiSettings.hudOutlineBorderThickness + ) + } + + private fun drawCornerArcs( + draw: ImDrawList, + x: Float, y: Float, w: Float, h: Float, + radius: Float, + color: Int, + thickness: Float + ) { + if (radius <= 0f || thickness <= 0f) return + val tlCx = x + radius + val tlCy = y + radius + val trCx = x + w - radius + val trCy = y + radius + val brCx = x + w - radius + val brCy = y + h - radius + val blCx = x + radius + val blCy = y + h - radius + + fun strokeArc(cx: Float, cy: Float, start: Float, end: Float) { + draw.pathClear() + draw.pathArcTo(cx, cy, radius, start, end, 0) + draw.pathStroke(color, ImDrawListFlags.None, thickness) + } + + // TL: pi -> 1.5pi + strokeArc(tlCx, tlCy, PI_F, THREE_HALVES_PI_F) + // TR: 1.5pi -> 2pi + strokeArc(trCx, trCy, THREE_HALVES_PI_F, TWO_PI_F) + // BR: 0 -> 0.5pi + strokeArc(brCx, brCy, 0f, HALF_PI_F) + // BL: 0.5pi -> pi + strokeArc(blCx, blCy, HALF_PI_F, PI_F) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt index ffd67d765..9afb286d0 100644 --- a/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt +++ b/src/main/kotlin/com/lambda/gui/dsl/ImGuiBuilder.kt @@ -1272,7 +1272,7 @@ object ImGuiBuilder { * @param scaleMin Minimum scale value * @param scaleMax Maximum scale value * @param graphSize Size of the graph - * @param stride Stride between values + * @param stride Sample decimation step (>= 1). Use 0/1 for contiguous data. */ @ImGuiDsl fun plotLines( @@ -1283,8 +1283,17 @@ object ImGuiBuilder { scaleMin: Float = Float.MAX_VALUE, scaleMax: Float = Float.MAX_VALUE, graphSize: ImVec2 = ImVec2(), - stride: Int = 1, - ) = ImGui.plotLines(label, values, valuesOffset, overlayText, scaleMin, scaleMax, graphSize, stride) + stride: Int = 0, + ) { + val (src, sMin, sMax) = preparePlotSeries(values, stride, scaleMin, scaleMax) + val count = src.size + val offset = if (count == 0) 0 else valuesOffset.coerceIn(0, count - 1) + if (count < 2) { + plotLines(label, floatArrayOf(), 0, 0, overlayText, sMin, sMax, graphSize.x, graphSize.y) + return + } + plotLines(label, src, count, offset, overlayText, sMin, sMax, graphSize.x, graphSize.y) + } /** * Creates a plot of histogram values. @@ -1296,7 +1305,7 @@ object ImGuiBuilder { * @param scaleMin Minimum scale value * @param scaleMax Maximum scale value * @param graphSize Size of the graph - * @param stride Stride between values + * @param stride Sample decimation step (>= 1). Use 0/1 for contiguous data. */ @ImGuiDsl fun plotHistogram( @@ -1307,8 +1316,61 @@ object ImGuiBuilder { scaleMin: Float = Float.MAX_VALUE, scaleMax: Float = Float.MAX_VALUE, graphSize: ImVec2 = ImVec2(), - stride: Int = 1, - ) = ImGui.plotHistogram(label, values, valuesOffset, overlayText, scaleMin, scaleMax, graphSize, stride) + stride: Int = 0, + ) { + val (src, sMin, sMax) = preparePlotSeries(values, stride, scaleMin, scaleMax) + val count = src.size + val offset = if (count == 0) 0 else valuesOffset.coerceIn(0, count - 1) + if (count < 1) { + plotHistogram(label, floatArrayOf(), 0, 0, overlayText, sMin, sMax, graphSize.x, graphSize.y) + return + } + plotHistogram(label, src, count, offset, overlayText, sMin, sMax, graphSize.x, graphSize.y) + } + + private fun preparePlotSeries( + values: FloatArray, + stride: Int, + scaleMin: Float, + scaleMax: Float + ): Triple { + val contiguous = (stride <= 1) + val src = if (contiguous) { + values + } else { + val outSize = (values.size + stride - 1) / stride + val out = FloatArray(outSize) + var i = 0 + var j = 0 + while (i < values.size) { + out[j++] = values[i] + i += stride + } + out + } + var sMin = scaleMin + var sMax = scaleMax + if (sMin == Float.MAX_VALUE && sMax == Float.MAX_VALUE) { + var minV = Float.POSITIVE_INFINITY + var maxV = Float.NEGATIVE_INFINITY + for (v in src) if (v.isFinite()) { + if (v < minV) minV = v + if (v > maxV) maxV = v + } + if (!minV.isFinite() || !maxV.isFinite()) { + minV = 0f; maxV = 1f + } + if (minV == maxV) { + val base = if (minV == 0f) 1f else kotlin.math.abs(minV) + val pad = base * 0.01f + minV -= pad + maxV += pad + } + sMin = minV + sMax = maxV + } + return Triple(src, sMin, sMax) + } /** * Creates a main menu bar. @@ -1485,7 +1547,7 @@ object ImGuiBuilder { * * @param title Title of the modal * @param value Boolean reference to control visibility - * @param flags Additional window flags + * @param windowFlags Additional window flags * @param block Content of the modal * * @see ImGuiPopupFlags diff --git a/src/main/kotlin/com/lambda/gui/snap/Guide.kt b/src/main/kotlin/com/lambda/gui/snap/Guide.kt new file mode 100644 index 000000000..4ad9d90b8 --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/snap/Guide.kt @@ -0,0 +1,28 @@ +/* + * 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.snap + +data class Guide( + val orientation: Orientation, + val pos: Float, + val strength: Int, + val kind: Kind +) { + enum class Orientation { Vertical, Horizontal } + enum class Kind { ElementEdge, ElementCenter, ScreenCenter, Grid } +} diff --git a/src/main/kotlin/com/lambda/gui/snap/RectF.kt b/src/main/kotlin/com/lambda/gui/snap/RectF.kt new file mode 100644 index 000000000..28a43652e --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/snap/RectF.kt @@ -0,0 +1,27 @@ +/* + * 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.snap + +data class RectF(val x: Float, val y: Float, val w: Float, val h: Float) { + val left get() = x + val right get() = x + w + val top get() = y + val bottom get() = y + h + val cx get() = x + w * 0.5f + val cy get() = y + h * 0.5f +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt new file mode 100644 index 000000000..5a1fc45a0 --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt @@ -0,0 +1,165 @@ +package com.lambda.gui.snap + +import com.lambda.module.modules.client.GuiSettings +import com.lambda.module.modules.client.GuiSettings.gridSize +import com.lambda.module.modules.client.GuiSettings.snapEnabled +import com.lambda.module.modules.client.GuiSettings.snapToCenters +import com.lambda.module.modules.client.GuiSettings.snapToEdges +import com.lambda.module.modules.client.GuiSettings.snapToGrid +import com.lambda.module.modules.client.GuiSettings.snapToScreenCenter +import imgui.ImDrawList +import kotlin.math.abs +import kotlin.math.max + +object SnapManager { + private data class SnapGuide(val guide: Guide, val sourceId: String?) + private val frameGuides = ArrayList(512) + private val elementRects = LinkedHashMap() + private var viewW = 0f + private var viewH = 0f + private var scale = 1f + + fun beginFrame(viewWidth: Float, viewHeight: Float, uiScale: Float) { + viewW = max(1f, viewWidth) + viewH = max(1f, viewHeight) + scale = max(0.5f, uiScale) + frameGuides.clear() + + if (snapEnabled && snapToScreenCenter) { + frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, viewW * 0.5f, 30, Guide.Kind.ScreenCenter), null) + frameGuides += SnapGuide(Guide(Guide.Orientation.Horizontal, viewH * 0.5f, 30, Guide.Kind.ScreenCenter), null) + } + + if (snapEnabled && snapToGrid && gridSize > 0f) { + val step = max(4f, gridSize * scale) + var x = 0f + while (x <= viewW) { + frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, x, 10, Guide.Kind.Grid), null) + x += step + } + var y = 0f + while (y <= viewH) { + frameGuides += SnapGuide(Guide(Guide.Orientation.Horizontal, y, 10, Guide.Kind.Grid), null) + y += step + } + } + + elementRects.forEach { (id, r) -> addElementGuides(id, r) } + } + + fun registerElement(id: String, rect: RectF) { + elementRects[id] = rect + } + + private fun addElementGuides(sourceId: String, r: RectF) { + if (snapEnabled && snapToEdges) { + frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, r.left, 100, Guide.Kind.ElementEdge), sourceId) + frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, r.right, 100, Guide.Kind.ElementEdge), sourceId) + frameGuides += SnapGuide(Guide(Guide.Orientation.Horizontal, r.top, 100, Guide.Kind.ElementEdge), sourceId) + frameGuides += SnapGuide(Guide(Guide.Orientation.Horizontal, r.bottom, 100, Guide.Kind.ElementEdge), sourceId) + } + if (snapEnabled && snapToCenters) { + frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, r.cx, 80, Guide.Kind.ElementCenter), sourceId) + frameGuides += SnapGuide(Guide(Guide.Orientation.Horizontal, r.cy, 80, Guide.Kind.ElementCenter), sourceId) + } + } + + data class SnapResult( + val dx: Float, + val dy: Float, + val snapX: Float?, + val snapY: Float?, + val kindX: Guide.Kind?, + val kindY: Guide.Kind? + ) + + private fun thresholdFor(kind: Guide.Kind): Float = when (kind) { + Guide.Kind.ElementEdge, Guide.Kind.ElementCenter -> GuiSettings.snapDistanceElement * scale + Guide.Kind.ScreenCenter -> GuiSettings.snapDistanceScreen * scale + Guide.Kind.Grid -> GuiSettings.snapDistanceGrid * scale + } + + private fun score(dist: Float, strength: Int): Float = dist - strength * 0.08f + + fun computeSnap(proposed: RectF, currentId: String?): SnapResult { + data class Best(var s: Float = Float.POSITIVE_INFINITY, var d: Float = 0f, var p: Float? = null, var k: Guide.Kind? = null) + val bestElemX = Best(); val bestElemY = Best() + val bestScreenX = Best(); val bestScreenY = Best() + val bestGridX = Best(); val bestGridY = Best() + + fun considerX(g: Guide, point: Float, out: Best) { + val dist = abs(point - g.pos) + if (dist <= max(1f, thresholdFor(g.kind))) { + val sc = score(dist, g.strength) + if (sc < out.s) { out.s = sc; out.d = g.pos - point; out.p = g.pos; out.k = g.kind } + } + } + fun considerY(g: Guide, point: Float, out: Best) { + val dist = abs(point - g.pos) + if (dist <= max(1f, thresholdFor(g.kind))) { + val sc = score(dist, g.strength) + if (sc < out.s) { out.s = sc; out.d = g.pos - point; out.p = g.pos; out.k = g.kind } + } + } + + frameGuides.forEach { sg -> + val g = sg.guide + val isSelf = (currentId != null && sg.sourceId == currentId) + val tier = when (g.kind) { + Guide.Kind.ElementEdge, Guide.Kind.ElementCenter -> if (isSelf) null else "elem" + Guide.Kind.ScreenCenter -> "screen" + Guide.Kind.Grid -> "grid" + } ?: return@forEach + + when (g.orientation) { + Guide.Orientation.Vertical -> { + val pts = floatArrayOf(proposed.left, proposed.cx, proposed.right) + when (tier) { + "elem" -> for (p in pts) considerX(g, p, bestElemX) + "screen" -> for (p in pts) considerX(g, p, bestScreenX) + "grid" -> for (p in pts) considerX(g, p, bestGridX) + } + } + Guide.Orientation.Horizontal -> { + val pts = floatArrayOf(proposed.top, proposed.cy, proposed.bottom) + when (tier) { + "elem" -> for (p in pts) considerY(g, p, bestElemY) + "screen" -> for (p in pts) considerY(g, p, bestScreenY) + "grid" -> for (p in pts) considerY(g, p, bestGridY) + } + } + } + } + + val choiceX = when { + bestElemX.s.isFinite() -> bestElemX + bestScreenX.s.isFinite() -> bestScreenX + bestGridX.s.isFinite() -> bestGridX + else -> Best() + } + val choiceY = when { + bestElemY.s.isFinite() -> bestElemY + bestScreenY.s.isFinite() -> bestScreenY + bestGridY.s.isFinite() -> bestGridY + else -> Best() + } + + return SnapResult( + dx = if (choiceX.s.isFinite()) choiceX.d else 0f, + dy = if (choiceY.s.isFinite()) choiceY.d else 0f, + snapX = choiceX.p, snapY = choiceY.p, + kindX = choiceX.k, kindY = choiceY.k + ) + } + + fun drawSnapLines(draw: ImDrawList, snapX: Float?, kindX: Guide.Kind?, snapY: Float?, kindY: Guide.Kind?) { + val showX = kindX == Guide.Kind.ElementEdge || kindX == Guide.Kind.ElementCenter + val showY = kindY == Guide.Kind.ElementEdge || kindY == Guide.Kind.ElementCenter + if (!showX && !showY) return + + val col = GuiSettings.snapLineColor.rgb + val thick = 2f + if (showX && snapX != null) draw.addLine(snapX, 0f, snapX, viewH, col, thick) + if (showY && snapY != null) draw.addLine(0f, snapY, viewW, snapY, col, thick) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/hud/TPS.kt b/src/main/kotlin/com/lambda/module/hud/TPS.kt index a97a94707..95b852284 100644 --- a/src/main/kotlin/com/lambda/module/hud/TPS.kt +++ b/src/main/kotlin/com/lambda/module/hud/TPS.kt @@ -21,29 +21,41 @@ import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag import com.lambda.util.Formatting.string -import com.lambda.util.NamedEnum -import com.lambda.util.ServerTPS.averageMSPerTick +import com.lambda.util.ServerTPS +import com.lambda.util.ServerTPS.recentData +import imgui.ImVec2 object TPS : HudModule( name = "TPS", description = "Display the server's tick rate", tag = ModuleTag.HUD, ) { - private val format by setting("Tick format", TickFormat.TPS) + private val format by setting("Tick format", ServerTPS.TickFormat.TPS) + private val showGraph by setting("Show TPS Graph", false) + private val graphHeight by setting("Graph Height", 40f, 10f..200f, 1f) + private val graphWidth by setting("Graph Width", 200f, 10f..500f, 1f) + private val graphStride by setting("Graph Stride", 1, 1..20, 1) override fun ImGuiBuilder.buildLayout() { - text("${format.displayName}: ${format.output().string}${format.unit}") - } + val data = recentData(format) + if (data.isEmpty()) { + text("No ${format.displayName} data yet") + return + } + val current = data.last() + val avg = data.average().toFloat() + if (!showGraph) { + text("${format.displayName}: ${avg.string}${format.unit}") + return + } + val overlay = "${format.displayName}: cur ${current.string} | avg ${avg.string}" - @Suppress("unused") - private enum class TickFormat( - val output: () -> Double, - override val displayName: String, - val unit: String = "" - ) : NamedEnum { - TPS({ 1000 / averageMSPerTick }, "TPS"), - MSPT({ averageMSPerTick }, "MSPT", " ms"), - Normalized({ 50 / averageMSPerTick }, "TPS"), - Percentage({ 5000 / averageMSPerTick }, "TPS", "%") + plotLines( + label = "##TPSPlot", + values = data, + overlayText = overlay, + graphSize = ImVec2(graphWidth, graphHeight), + stride = graphStride + ) } } diff --git a/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt index 79c265ae0..e6ab5240b 100644 --- a/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt +++ b/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt @@ -38,8 +38,30 @@ object GuiSettings : Module( val colorHeight by setting("Shade Height", 200.0, 10.0..1000.0, 10.0).group(Group.Colors) val colorSpeed by setting("Color Speed", 1.0, 0.1..5.0, 0.1).group(Group.Colors) + // Snapping + val snapEnabled by setting("Enable Snapping", true, "Master toggle for HUD snapping").group(Group.Snapping) + val gridSize by setting("Grid Size", 16f, 2f..128f, 1f, "Grid step in pixels") { snapEnabled }.group(Group.Snapping) + val snapToEdges by setting("Snap To Element Edges", true) { snapEnabled }.group(Group.Snapping) + val snapToCenters by setting("Snap To Element Centers", true) { snapEnabled }.group(Group.Snapping) + val snapToScreenCenter by setting("Snap To Screen Center", true) { snapEnabled }.group(Group.Snapping) + val snapToGrid by setting("Snap To Grid", true) { snapEnabled }.group(Group.Snapping) + val snapDistanceElement by setting("Snap Distance (Elements)", 20f, 1f..48f, 1f, "Distance threshold in px") { snapEnabled }.group(Group.Snapping) + val snapDistanceScreen by setting("Snap Distance (Screen Center)", 14f, 1f..48f, 1f) { snapEnabled }.group(Group.Snapping) + val snapDistanceGrid by setting("Snap Distance (Grid)", 12f, 1f..48f, 1f) { snapEnabled }.group(Group.Snapping) + val snapLineColor by setting("Snap Line Color", Color(255, 160, 0, 220)) { snapEnabled }.group(Group.Snapping) + + // HUD Outline + val hudOutlineCornerRadius by setting("HUD Corner Radius", 6.0f, 0.0f..24.0f, 0.5f).group(Group.HudOutline) + val hudOutlineHaloColor by setting("HUD Corner Halo Color", Color(140, 140, 140, 90)).group(Group.HudOutline) + val hudOutlineBorderColor by setting("HUD Corner Border Color", Color(190, 190, 190, 200)).group(Group.HudOutline) + val hudOutlineHaloThickness by setting("HUD Corner Halo Thickness", 3.0f, 1.0f..6.0f, 0.5f).group(Group.HudOutline) + val hudOutlineBorderThickness by setting("HUD Corner Border Thickness", 1.5f, 1.0f..4.0f, 0.5f).group(Group.HudOutline) + val hudOutlineCornerInflate by setting("HUD Corner Inflate", 1.0f, 0.0f..4.0f, 0.5f, "Extra radius for the halo arc").group(Group.HudOutline) + enum class Group(override val displayName: String) : NamedEnum { General("General"), - Colors("Colors") + Colors("Colors"), + Snapping("Snapping"), + HudOutline("HUD Outline") } } diff --git a/src/main/kotlin/com/lambda/util/ServerTPS.kt b/src/main/kotlin/com/lambda/util/ServerTPS.kt index b29903082..503e504f4 100644 --- a/src/main/kotlin/com/lambda/util/ServerTPS.kt +++ b/src/main/kotlin/com/lambda/util/ServerTPS.kt @@ -28,9 +28,6 @@ object ServerTPS { private val updateHistory = LimitedDecayQueue(61, 60000) private var lastUpdate = 0L - val averageMSPerTick: Double - get() = (if (updateHistory.isEmpty()) 1000.0 else updateHistory.average()) / 20 - init { listen(priority = 10000) { if (it.packet !is WorldTimeUpdateS2CPacket) return@listen @@ -48,4 +45,19 @@ object ServerTPS { lastUpdate = 0 } } + + fun recentData(tickFormat: TickFormat = TickFormat.MSPT) = + updateHistory.map { tickFormat.value(it).toFloat() }.toFloatArray() + + @Suppress("unused") + enum class TickFormat( + val value: (Long) -> Double, + override val displayName: String, + val unit: String = "" + ) : NamedEnum { + TPS({ it / 50.0 }, "TPS"), + MSPT({ it / 20.0 }, "MSPT", " ms"), + Normalized({ 50.0 / it }, "TPS"), + Percentage({ it / 100.0 }, "TPS", "%") + } } From 3e8d215487a36b4caaf01182280d4b9095acea2e Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 7 Sep 2025 21:37:58 +0200 Subject: [PATCH 2/5] Reduce code clones --- .../com/lambda/gui/components/HudGuiLayout.kt | 2 -- .../kotlin/com/lambda/gui/snap/SnapManager.kt | 36 +++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index 8e56e0990..dcb0db9ba 100644 --- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt @@ -56,7 +56,6 @@ object HudGuiLayout : Loadable { val kindY: Guide.Kind? ) - // Precomputed Float PI values to avoid repeated conversions private const val PI_F = PI.toFloat() private const val HALF_PI_F = (0.5f * PI).toFloat() private const val THREE_HALVES_PI_F = (1.5f * PI).toFloat() @@ -106,7 +105,6 @@ object HudGuiLayout : Loadable { ) } with(hud) { buildLayout() } - // Rounded-corner only outline; pull parameters from settings if (ClickGui.isEnabled) { drawHudOutline( draw = foregroundDrawList, diff --git a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt index 5a1fc45a0..1b151aabf 100644 --- a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt +++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt @@ -87,18 +87,26 @@ object SnapManager { val bestScreenX = Best(); val bestScreenY = Best() val bestGridX = Best(); val bestGridY = Best() - fun considerX(g: Guide, point: Float, out: Best) { + fun consider(g: Guide, point: Float, out: Best) { val dist = abs(point - g.pos) if (dist <= max(1f, thresholdFor(g.kind))) { val sc = score(dist, g.strength) if (sc < out.s) { out.s = sc; out.d = g.pos - point; out.p = g.pos; out.k = g.kind } } } - fun considerY(g: Guide, point: Float, out: Best) { - val dist = abs(point - g.pos) - if (dist <= max(1f, thresholdFor(g.kind))) { - val sc = score(dist, g.strength) - if (sc < out.s) { out.s = sc; out.d = g.pos - point; out.p = g.pos; out.k = g.kind } + + fun processAxis( + g: Guide, + points: FloatArray, + tier: String, + elem: Best, + screen: Best, + grid: Best + ) { + when (tier) { + "elem" -> for (p in points) consider(g, p, elem) + "screen" -> for (p in points) consider(g, p, screen) + "grid" -> for (p in points) consider(g, p, grid) } } @@ -113,20 +121,12 @@ object SnapManager { when (g.orientation) { Guide.Orientation.Vertical -> { - val pts = floatArrayOf(proposed.left, proposed.cx, proposed.right) - when (tier) { - "elem" -> for (p in pts) considerX(g, p, bestElemX) - "screen" -> for (p in pts) considerX(g, p, bestScreenX) - "grid" -> for (p in pts) considerX(g, p, bestGridX) - } + val points = floatArrayOf(proposed.left, proposed.cx, proposed.right) + processAxis(g, points, tier, bestElemX, bestScreenX, bestGridX) } Guide.Orientation.Horizontal -> { - val pts = floatArrayOf(proposed.top, proposed.cy, proposed.bottom) - when (tier) { - "elem" -> for (p in pts) considerY(g, p, bestElemY) - "screen" -> for (p in pts) considerY(g, p, bestScreenY) - "grid" -> for (p in pts) considerY(g, p, bestGridY) - } + val points = floatArrayOf(proposed.top, proposed.cy, proposed.bottom) + processAxis(g, points, tier, bestElemY, bestScreenY, bestGridY) } } } From cbe18f47da10486c9baa14ed2654103997440f25 Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 7 Sep 2025 21:43:52 +0200 Subject: [PATCH 3/5] Remove snap guides when hud is disabled --- .../com/lambda/gui/components/HudGuiLayout.kt | 18 ++++++------------ .../kotlin/com/lambda/gui/snap/SnapManager.kt | 4 ++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index dcb0db9ba..26bb4d450 100644 --- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt @@ -78,9 +78,11 @@ object HudGuiLayout : Loadable { pendingPositions.clear() snapOverlays.clear() - val huds = ModuleRegistry.modules + val (huds, notShown) = ModuleRegistry.modules .filterIsInstance() - .filter { it.isEnabled } + .partition { it.isEnabled } + + notShown.forEach { SnapManager.unregisterElement(it.name) } if (ClickGui.isEnabled && activeDragHudName == null && mousePressedThisFrame) { tryBeginDrag(huds) @@ -106,17 +108,9 @@ object HudGuiLayout : Loadable { } with(hud) { buildLayout() } if (ClickGui.isEnabled) { - drawHudOutline( - draw = foregroundDrawList, - x = windowPos.x, - y = windowPos.y, - w = windowSize.x, - h = windowSize.y - ) + drawHudOutline(foregroundDrawList, windowPos.x, windowPos.y, windowSize.x, windowSize.y) } - val p = windowPos - val s = windowSize - val rect = RectF(p.x, p.y, s.x, s.y) + val rect = RectF(windowPos.x, windowPos.y, windowSize.x, windowSize.y) SnapManager.registerElement(hud.name, rect) lastBounds[hud.name] = rect } diff --git a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt index 1b151aabf..7d9d2c8fa 100644 --- a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt +++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt @@ -51,6 +51,10 @@ object SnapManager { elementRects[id] = rect } + fun unregisterElement(id: String) { + elementRects.remove(id) + } + private fun addElementGuides(sourceId: String, r: RectF) { if (snapEnabled && snapToEdges) { frameGuides += SnapGuide(Guide(Guide.Orientation.Vertical, r.left, 100, Guide.Kind.ElementEdge), sourceId) From e9bb4240686642541c203fa4e4cb7210d7c02b6b Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 7 Sep 2025 22:10:54 +0200 Subject: [PATCH 4/5] Fix TPS formats --- src/main/kotlin/com/lambda/module/hud/TPS.kt | 2 +- src/main/kotlin/com/lambda/util/ServerTPS.kt | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/hud/TPS.kt b/src/main/kotlin/com/lambda/module/hud/TPS.kt index 95b852284..1e8e95837 100644 --- a/src/main/kotlin/com/lambda/module/hud/TPS.kt +++ b/src/main/kotlin/com/lambda/module/hud/TPS.kt @@ -48,7 +48,7 @@ object TPS : HudModule( text("${format.displayName}: ${avg.string}${format.unit}") return } - val overlay = "${format.displayName}: cur ${current.string} | avg ${avg.string}" + val overlay = "cur ${current.string}${format.unit} | avg ${avg.string}${format.unit}" plotLines( label = "##TPSPlot", diff --git a/src/main/kotlin/com/lambda/util/ServerTPS.kt b/src/main/kotlin/com/lambda/util/ServerTPS.kt index 503e504f4..721f3c30c 100644 --- a/src/main/kotlin/com/lambda/util/ServerTPS.kt +++ b/src/main/kotlin/com/lambda/util/ServerTPS.kt @@ -53,11 +53,12 @@ object ServerTPS { enum class TickFormat( val value: (Long) -> Double, override val displayName: String, + override val description: String, val unit: String = "" - ) : NamedEnum { - TPS({ it / 50.0 }, "TPS"), - MSPT({ it / 20.0 }, "MSPT", " ms"), - Normalized({ 50.0 / it }, "TPS"), - Percentage({ it / 100.0 }, "TPS", "%") + ) : NamedEnum, Describable { + TPS({ it / 50.0 }, "TPS", "Ticks Per Second", " t/s"), + MSPT({ it / 20.0 }, "MSPT", "Milliseconds Per Tick", " ms/t"), + Normalized({ it / 1000.0 }, "nTPS", "Normalized Ticks Per Second"), + Percentage({ it / 10.0 }, "TPS%", "Deviation from 20 TPS","%") } } From 26b28fb36f8c026c259c1ca9431e8fd0a3eed7fe Mon Sep 17 00:00:00 2001 From: Constructor Date: Sun, 7 Sep 2025 22:17:17 +0200 Subject: [PATCH 5/5] Fix watermark --- src/main/kotlin/com/lambda/module/hud/Watermark.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/src/main/kotlin/com/lambda/module/hud/Watermark.kt index 90d46b41e..b1ad8af2e 100644 --- a/src/main/kotlin/com/lambda/module/hud/Watermark.kt +++ b/src/main/kotlin/com/lambda/module/hud/Watermark.kt @@ -21,19 +21,19 @@ import com.lambda.graphics.texture.TextureOwner.upload import com.lambda.gui.dsl.ImGuiBuilder import com.lambda.module.HudModule import com.lambda.module.tag.ModuleTag +import imgui.ImGui object Watermark : HudModule( name = "Watermark", tag = ModuleTag.HUD, + enabledByDefault = true, ) { private val texture = upload("textures/lambda.png") + private val scale by setting("Scale", 0.15f, 0.01f..1f, 0.01f) override fun ImGuiBuilder.buildLayout() { - windowDrawList.addImage( - texture.id.toLong(), - texture.width.toFloat(), - texture.height.toFloat(), - 1f, 0f, 0f, 1f - ) + val width = texture.width * scale + val height = texture.height * scale + ImGui.image(texture.id.toLong(), width, height) } }