Skip to content
Merged

Hud #50

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/main/kotlin/org/cobalt/Cobalt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package org.cobalt
import net.fabricmc.api.ClientModInitializer
import org.cobalt.api.command.CommandManager
import org.cobalt.api.event.EventBus
import org.cobalt.api.hud.HudModuleManager
import org.cobalt.api.hud.modules.WatermarkModule
import org.cobalt.api.module.ModuleManager
import org.cobalt.api.notification.NotificationManager
import org.cobalt.api.rotation.RotationExecutor
Expand All @@ -16,6 +18,8 @@ object Cobalt : ClientModInitializer {


override fun onInitializeClient() {
ModuleManager.addModules(listOf(WatermarkModule(), WatermarkModule()))

AddonLoader.getAddons().map { it.second }.forEach {
it.onLoad()
ModuleManager.addModules(it.getModules())
Expand All @@ -26,7 +30,7 @@ object Cobalt : ClientModInitializer {

listOf(
TickScheduler, MainCommand, NotificationManager,
RotationExecutor,
RotationExecutor, HudModuleManager,
).forEach { EventBus.register(it) }
Config.loadModulesConfig()
EventBus.register(this)
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/org/cobalt/api/addon/Addon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ package org.cobalt.api.addon

import org.cobalt.api.module.Module

/**
* Base class for Cobalt addons. Implement this to provide modules, HUD elements, and commands.
*
* Define your addon entry in `cobalt.addon.json` and return your modules from [getModules].
*/
abstract class Addon {

/** Called when the addon is loaded during game startup. */
abstract fun onLoad()

/** Called when the addon is unloaded. */
abstract fun onUnload()

/** Returns the list of [Module]s this addon provides. Override to register your modules. */
open fun getModules(): List<Module> =
emptyList()

Expand Down
46 changes: 46 additions & 0 deletions src/main/kotlin/org/cobalt/api/hud/HudAnchor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.cobalt.api.hud

/**
* Screen anchor point for positioning a [HudElement].
*
* Offsets push the element **inward** from the anchor edge:
* - LEFT anchors: `offsetX` moves right from the left edge
* - RIGHT anchors: `offsetX` moves left from the right edge
* - TOP anchors: `offsetY` moves down from the top edge
* - BOTTOM anchors: `offsetY` moves up from the bottom edge
* - CENTER anchors: offsets adjust from the screen center
*/
enum class HudAnchor {
TOP_LEFT,
TOP_CENTER,
TOP_RIGHT,
CENTER_LEFT,
CENTER,
CENTER_RIGHT,
BOTTOM_LEFT,
BOTTOM_CENTER,
BOTTOM_RIGHT;

fun computeScreenPosition(
offsetX: Float,
offsetY: Float,
moduleWidth: Float,
moduleHeight: Float,
screenWidth: Float,
screenHeight: Float,
): Pair<Float, Float> {
val x = when (this) {
TOP_LEFT, CENTER_LEFT, BOTTOM_LEFT -> offsetX
TOP_CENTER, CENTER, BOTTOM_CENTER -> screenWidth / 2f - moduleWidth / 2f + offsetX
TOP_RIGHT, CENTER_RIGHT, BOTTOM_RIGHT -> screenWidth - moduleWidth - offsetX
}

val y = when (this) {
TOP_LEFT, TOP_CENTER, TOP_RIGHT -> offsetY
CENTER_LEFT, CENTER, CENTER_RIGHT -> screenHeight / 2f - moduleHeight / 2f + offsetY
BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT -> screenHeight - moduleHeight - offsetY
}

return Pair(x, y)
}
}
98 changes: 98 additions & 0 deletions src/main/kotlin/org/cobalt/api/hud/HudElement.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.cobalt.api.hud

import org.cobalt.api.module.setting.Setting
import org.cobalt.api.module.setting.SettingsContainer

/**
* A HUD overlay element rendered on the in-game screen.
*
* Created via the [hudElement][org.cobalt.api.hud.hudElement] DSL inside a [Module][org.cobalt.api.module.Module].
* Each element is independently draggable, scalable, and toggleable through the HUD editor.
* Position, scale, enabled state, and settings are automatically persisted.
*
* @property id Unique identifier used for serialization. Must be stable across versions.
* @property name Display name shown in the HUD editor and settings popup.
* @property description Optional description shown in the UI.
*/
abstract class HudElement(
val id: String,
val name: String,
val description: String = "",
) : SettingsContainer {

/** Whether this element is rendered. Toggled by the user in the HUD editor. */
var enabled: Boolean = true

/** Screen anchor point. Determines which edge/corner offsets are relative to. */
var anchor: HudAnchor = HudAnchor.TOP_LEFT

/** Horizontal offset from the [anchor] edge, in pixels. */
var offsetX: Float = 10f

/** Vertical offset from the [anchor] edge, in pixels. */
var offsetY: Float = 10f

/** Render scale factor, clamped to 0.5-3.0 on load. */
var scale: Float = 1.0f

protected open val defaultAnchor: HudAnchor = HudAnchor.TOP_LEFT
protected open val defaultOffsetX: Float = 10f
protected open val defaultOffsetY: Float = 10f
protected open val defaultScale: Float = 1.0f

private val settingsList = mutableListOf<Setting<*>>()

override fun addSetting(vararg settings: Setting<*>) {
settingsList.addAll(listOf(*settings))
}

override fun getSettings(): List<Setting<*>> {
return settingsList
}

/** Returns the unscaled width of this element in pixels. */
abstract fun getBaseWidth(): Float

/** Returns the unscaled height of this element in pixels. */
abstract fun getBaseHeight(): Float

/**
* Called every frame when this element is [enabled].
* Draw using [NVGRenderer][org.cobalt.api.util.ui.NVGRenderer] — coordinates are pre-translated,
* so draw relative to (0, 0).
*/
abstract fun render(screenX: Float, screenY: Float, scale: Float)

fun getScaledWidth(): Float = getBaseWidth() * scale
fun getScaledHeight(): Float = getBaseHeight() * scale

fun getScreenPosition(screenWidth: Float, screenHeight: Float): Pair<Float, Float> =
anchor.computeScreenPosition(
offsetX, offsetY,
getScaledWidth(), getScaledHeight(),
screenWidth, screenHeight
)

/** Resets position, anchor, and scale to the defaults set in the DSL builder. */
fun resetPosition() {
anchor = defaultAnchor
offsetX = defaultOffsetX
offsetY = defaultOffsetY
scale = defaultScale
}

/** Resets all settings to their default values. */
fun resetSettings() {
for (setting in getSettings()) {
@Suppress("UNCHECKED_CAST")
val typedSetting = setting as Setting<Any?>
typedSetting.value = typedSetting.defaultValue
}
}

fun containsPoint(px: Float, py: Float, screenWidth: Float, screenHeight: Float): Boolean {
val (sx, sy) = getScreenPosition(screenWidth, screenHeight)
return px >= sx && px <= sx + getScaledWidth() &&
py >= sy && py <= sy + getScaledHeight()
}
}
141 changes: 141 additions & 0 deletions src/main/kotlin/org/cobalt/api/hud/HudModuleDSL.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.cobalt.api.hud

import org.cobalt.api.module.Module
import org.cobalt.api.module.setting.Setting
import org.cobalt.api.module.setting.SettingsContainer

/**
* Creates and registers a [HudElement] on this module using a DSL builder.
*
* Usage:
* ```
* class MyModule : Module("My Module") {
* val hud = hudElement("my-hud", "My HUD", "Shows something") {
* anchor = HudAnchor.TOP_RIGHT
* offsetX = 10f
* offsetY = 10f
*
* val showDecimals = setting(CheckboxSetting("Decimals", "", false))
*
* width { 80f }
* height { 20f }
* render { screenX, screenY, scale -> /* use showDecimals.value */ }
* }
* }
* ```
*
* @param id Stable unique identifier used for serialization.
* @param name Display name shown in the HUD editor.
* @param description Optional description for the settings popup.
* @param init Builder block — configure position, settings, size, and rendering.
* @return The constructed [HudElement] instance.
*/
fun Module.hudElement(
id: String,
name: String,
description: String = "",
init: HudElementBuilder.() -> Unit
): HudElement {
val builder = HudElementBuilder(id, name, description)
builder.init()
val element = builder.build()
addHudElement(element)
return element
}

/**
* Builder for configuring a [HudElement] inside the [hudElement] DSL block.
*
* Register settings with [setting] and read their values via `.value`.
* Do **not** use `by` delegation for settings inside this builder — it won't compile
* because Kotlin local delegates require a different type signature.
*/
class HudElementBuilder(
private val id: String,
private val name: String,
private val description: String = ""
) : SettingsContainer {

private var widthProvider: () -> Float = { 100f }
private var heightProvider: () -> Float = { 20f }

/** Screen anchor point. Determines which edge/corner offsets are relative to. */
var anchor: HudAnchor = HudAnchor.TOP_LEFT

/** Horizontal offset from the [anchor] edge, in pixels. */
var offsetX: Float = 10f

/** Vertical offset from the [anchor] edge, in pixels. */
var offsetY: Float = 10f

/** Default render scale (clamped to 0.5-3.0 on load). */
var scale: Float = 1.0f
private var renderLambda: ((Float, Float, Float) -> Unit)? = null

private val settingsList = mutableListOf<Setting<*>>()

override fun addSetting(vararg settings: Setting<*>) {
settingsList.addAll(listOf(*settings))
}

override fun getSettings(): List<Setting<*>> {
return settingsList
}

/** Sets the dynamic width provider. Called every frame — can return values based on setting state. */
fun width(provider: () -> Float) {
widthProvider = provider
}

/** Sets the dynamic height provider. Called every frame — can return values based on setting state. */
fun height(provider: () -> Float) {
heightProvider = provider
}

/**
* Registers a setting on this HUD element and returns it.
* Access the current value via `.value`:
* ```
* val speed = setting(SliderSetting("Speed", "Movement speed", 1.0, 0.1, 5.0))
* render { _, _, _ -> /* use speed.value */ }
* ```
*/
fun <T, S : Setting<T>> setting(setting: S): S {
addSetting(setting)
return setting
}

/** Sets the render callback, called every frame when this element is enabled. */
fun render(block: (screenX: Float, screenY: Float, scale: Float) -> Unit) {
renderLambda = block
}

fun build(): HudElement {
val capturedRender = renderLambda ?: { _, _, _ -> }
val capturedWidth = widthProvider
val capturedHeight = heightProvider
val capturedSettings = settingsList.toList()
val capturedAnchor = anchor
val capturedOffsetX = offsetX
val capturedOffsetY = offsetY
val capturedScale = scale

return object : HudElement(id, name, description) {
override val defaultAnchor = capturedAnchor
override val defaultOffsetX = capturedOffsetX
override val defaultOffsetY = capturedOffsetY
override val defaultScale = capturedScale

init {
capturedSettings.forEach { addSetting(it) }
resetPosition()
}

override fun getBaseWidth(): Float = capturedWidth()
override fun getBaseHeight(): Float = capturedHeight()
override fun render(screenX: Float, screenY: Float, scale: Float) {
capturedRender(screenX, screenY, scale)
}
}
}
}
46 changes: 46 additions & 0 deletions src/main/kotlin/org/cobalt/api/hud/HudModuleManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.cobalt.api.hud

import net.minecraft.client.Minecraft
import org.cobalt.api.event.annotation.SubscribeEvent
import org.cobalt.api.event.impl.render.NvgEvent
import org.cobalt.api.module.ModuleManager
import org.cobalt.api.util.ui.NVGRenderer

object HudModuleManager {

private val mc: Minecraft = Minecraft.getInstance()

@Volatile
var isEditorOpen: Boolean = false

fun getElements(): List<HudElement> =
ModuleManager.getModules().flatMap { it.getHudElements() }

fun resetAllPositions() {
getElements().forEach { it.resetPosition() }
}

@Suppress("unused")
@SubscribeEvent
fun onRender(event: NvgEvent) {
if (mc.screen != null && !isEditorOpen) return

val window = mc.window
val screenWidth = window.screenWidth.toFloat()
val screenHeight = window.screenHeight.toFloat()

NVGRenderer.beginFrame(screenWidth, screenHeight)

getElements().filter { it.enabled }.forEach { element ->
val (screenX, screenY) = element.getScreenPosition(screenWidth, screenHeight)

NVGRenderer.push()
NVGRenderer.translate(screenX, screenY)
NVGRenderer.scale(element.scale, element.scale)
element.render(0f, 0f, element.scale)
NVGRenderer.pop()
}

NVGRenderer.endFrame()
}
}
Loading