diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index 95e2da94d..26bb4d450 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,166 @@ 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? + ) + + 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, notShown) = ModuleRegistry.modules .filterIsInstance() - .filter { it.isEnabled } - .forEach { hud -> - window("##${hud.name}", flags = DEFAULT_HUD_FLAGS) { - with(hud) { buildLayout() } + .partition { it.isEnabled } + + notShown.forEach { SnapManager.unregisterElement(it.name) } + + 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() } + if (ClickGui.isEnabled) { + drawHudOutline(foregroundDrawList, windowPos.x, windowPos.y, windowSize.x, windowSize.y) } + val rect = RectF(windowPos.x, windowPos.y, windowSize.x, windowSize.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..7d9d2c8fa --- /dev/null +++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt @@ -0,0 +1,169 @@ +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 + } + + 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) + 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 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 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) + } + } + + 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 points = floatArrayOf(proposed.left, proposed.cx, proposed.right) + processAxis(g, points, tier, bestElemX, bestScreenX, bestGridX) + } + Guide.Orientation.Horizontal -> { + val points = floatArrayOf(proposed.top, proposed.cy, proposed.bottom) + processAxis(g, points, tier, bestElemY, bestScreenY, 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..1e8e95837 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 = "cur ${current.string}${format.unit} | avg ${avg.string}${format.unit}" - @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/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) } } 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..721f3c30c 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,20 @@ 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, + override val description: String, + val unit: String = "" + ) : 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","%") + } }