From 011c36b9fb5a3f6ca55b6b6091d73909e45ddf2e Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 12 Apr 2026 10:01:53 +0200 Subject: [PATCH 01/18] feat: copilot generated KDoc on all public functions and properties --- .../client/AbstractClientPlayerAccessor.java | 5 +++ src/main/kotlin/org/cobalt/Cobalt.kt | 6 +++ src/main/kotlin/org/cobalt/command/Command.kt | 7 ++++ .../org/cobalt/command/CommandManager.kt | 5 +++ src/main/kotlin/org/cobalt/dsl/render.kt | 5 +++ src/main/kotlin/org/cobalt/dsl/utils.kt | 7 ++++ src/main/kotlin/org/cobalt/event/Event.kt | 15 +++++++ src/main/kotlin/org/cobalt/event/EventBus.kt | 4 ++ .../cobalt/event/annotation/SubscribeEvent.kt | 6 +++ .../org/cobalt/event/impl/ChatSendEvent.kt | 6 ++- .../org/cobalt/event/impl/PacketEvent.kt | 5 +++ .../org/cobalt/event/impl/SkiaDrawEvent.kt | 4 ++ .../kotlin/org/cobalt/event/impl/TickEvent.kt | 6 +++ .../org/cobalt/event/impl/WorldRenderEvent.kt | 2 + src/main/kotlin/org/cobalt/module/Module.kt | 24 ++++++++++- .../kotlin/org/cobalt/module/ModuleManager.kt | 6 +++ src/main/kotlin/org/cobalt/ui/UIComponent.kt | 7 ++++ .../org/cobalt/ui/animation/Animation.kt | 10 +++++ src/main/kotlin/org/cobalt/util/ChatUtils.kt | 13 ++++++ src/main/kotlin/org/cobalt/util/ColorUtils.kt | 13 ++++++ .../kotlin/org/cobalt/util/FrustumUtils.kt | 6 +++ src/main/kotlin/org/cobalt/util/MouseUtils.kt | 3 ++ .../kotlin/org/cobalt/util/RenderUtils.kt | 42 +++++++++++++++++++ src/main/kotlin/org/cobalt/util/WebUtils.kt | 8 ++++ .../kotlin/org/cobalt/util/helper/Layers.kt | 3 ++ .../org/cobalt/util/helper/TickScheduler.kt | 4 ++ .../kotlin/org/cobalt/util/skia/SkiaImage.kt | 6 +++ .../org/cobalt/util/skia/SkiaRenderer.kt | 29 +++++++++++++ 28 files changed, 255 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java b/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java index bcafb250..62f2fd38 100644 --- a/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java +++ b/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java @@ -8,6 +8,11 @@ @Mixin(AbstractClientPlayer.class) public interface AbstractClientPlayerAccessor { + /** + * Returns the backing {@link PlayerInfo} instance from {@link AbstractClientPlayer}. + * + * @return the current player info for this client player + */ @Accessor("playerInfo") PlayerInfo getClientPlayerInfo(); diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index af43eb68..82636a35 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -11,20 +11,26 @@ import org.cobalt.event.EventBus import org.cobalt.event.impl.WorldRenderEvent import org.cobalt.module.ModuleManager +/** Main mod entrypoint and shared constants for the Cobalt client mod. */ object Cobalt : ClientModInitializer { + /** Cached Minecraft client instance. */ @JvmField val minecraft: Minecraft = Minecraft.getInstance() + /** The Fabric ModContainer for this mod. */ @JvmField val MOD_CONTAINER: ModContainer = FabricLoader.getInstance().getModContainer("cobalt").orElseThrow() + /** Human-readable mod name. */ @JvmField val MOD_NAME: String = MOD_CONTAINER.metadata.name + /** Friendly mod version string. */ @JvmField val MOD_VERSION: String = MOD_CONTAINER.metadata.version.friendlyString + /** Called when the client initializes; registers modules and commands and wires render events. */ override fun onInitializeClient() { ModuleManager.registerModules() CommandManager.register(MainCommand) diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index ae105a45..e097751f 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -11,8 +11,13 @@ import net.minecraft.client.multiplayer.ClientSuggestionProvider import org.cobalt.command.annotation.DefaultHandler import org.cobalt.command.annotation.SubCommand +/** Base class for defining chat commands; reflection is used to discover handlers and subcommands. + * + * @property name the primary literal name of this command + */ abstract class Command(val name: String) { + /** Build a Brigadier LiteralArgumentBuilder for this command, wiring discovered handlers and subcommands. */ fun build(): LiteralArgumentBuilder { val root = LiteralArgumentBuilder.literal(name) val functions = this::class.declaredFunctions @@ -36,6 +41,7 @@ abstract class Command(val name: String) { return root } + /** Construct a subcommand literal from a handler function and its parameters. */ private fun buildSubCommand(function: KFunction<*>): LiteralArgumentBuilder { val literal = LiteralArgumentBuilder.literal(function.name) val params = function.parameters.drop(1) @@ -78,6 +84,7 @@ abstract class Command(val name: String) { return literal.then(argumentTree) } + /** Create a Brigadier RequiredArgumentBuilder for a supported parameter type. */ private fun createArgument( name: String, type: Any?, diff --git a/src/main/kotlin/org/cobalt/command/CommandManager.kt b/src/main/kotlin/org/cobalt/command/CommandManager.kt index 1b947010..c98da456 100644 --- a/src/main/kotlin/org/cobalt/command/CommandManager.kt +++ b/src/main/kotlin/org/cobalt/command/CommandManager.kt @@ -8,21 +8,26 @@ import org.cobalt.util.ChatUtils import org.slf4j.LoggerFactory +/** Central command dispatcher and helpers for registering and executing chat commands. */ object CommandManager { private val logger = LoggerFactory.getLogger(this::class.java) + /** Brigadier dispatcher used to register command trees. */ @JvmStatic val dispatcher = CommandDispatcher() + /** Character prefix used to identify chat commands. */ @JvmStatic val prefix: Char = '.' + /** Register a top-level command into the Brigadier dispatcher. */ @JvmStatic fun register(command: Command) { dispatcher.register(command.build()) } + /** Execute a command line string as if entered by the player; logs and notifies on failure. */ @JvmStatic fun handleCommandExecution(content: String) { val player = minecraft.player ?: return diff --git a/src/main/kotlin/org/cobalt/dsl/render.kt b/src/main/kotlin/org/cobalt/dsl/render.kt index c90290a9..4b8a92d7 100644 --- a/src/main/kotlin/org/cobalt/dsl/render.kt +++ b/src/main/kotlin/org/cobalt/dsl/render.kt @@ -8,17 +8,22 @@ import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.cobalt.util.RenderUtils +/** Draw a wireframe outline of a block at the given world block position. */ fun LevelRenderContext.drawBlockPos(pos: BlockPos, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawBlockPos(this, pos, color, esp, lineWidth) +/** Draw an outline around an entity using the provided color and line width. */ fun LevelRenderContext.drawEntityOutline(entity: Entity, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawEntityOutline(this, entity, color, esp, lineWidth) +/** Draw a tracer (line) from the camera/renderer position to the given world vector. */ fun LevelRenderContext.drawTracer(to: Vec3, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawTracer(this, to, color, esp, lineWidth) +/** Draw a colored axis-aligned bounding box (AABB) in world space. */ fun LevelRenderContext.drawBox(box: AABB, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawBox(this, box, color, esp, lineWidth) +/** Draw a colored line between two world-space points using the renderer context. */ fun LevelRenderContext.drawLine(from: Vec3, to: Vec3, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawLine(this, from, to, color, esp, lineWidth) diff --git a/src/main/kotlin/org/cobalt/dsl/utils.kt b/src/main/kotlin/org/cobalt/dsl/utils.kt index e2d5555e..5c6807cd 100644 --- a/src/main/kotlin/org/cobalt/dsl/utils.kt +++ b/src/main/kotlin/org/cobalt/dsl/utils.kt @@ -2,20 +2,27 @@ package org.cobalt.dsl import org.cobalt.Cobalt.minecraft + +/** Extract the red component (0-255) from an ARGB integer. */ inline val Int.red get() = this shr 16 and 0xFF +/** Extract the green component (0-255) from an ARGB integer. */ inline val Int.green get() = this shr 8 and 0xFF +/** Extract the blue component (0-255) from an ARGB integer. */ inline val Int.blue get() = this and 0xFF +/** Extract the alpha component (0-255) from an ARGB integer. */ inline val Int.alpha get() = this shr 24 and 0xFF +/** Current mouse X position in screen coordinates as a Float. */ inline val mouseX: Float get() = minecraft.mouseHandler.xpos().toFloat() +/** Current mouse Y position in screen coordinates as a Float. */ inline val mouseY: Float get() = minecraft.mouseHandler.ypos().toFloat() diff --git a/src/main/kotlin/org/cobalt/event/Event.kt b/src/main/kotlin/org/cobalt/event/Event.kt index 8fa132b8..627a174d 100644 --- a/src/main/kotlin/org/cobalt/event/Event.kt +++ b/src/main/kotlin/org/cobalt/event/Event.kt @@ -1,29 +1,44 @@ package org.cobalt.event +/** Base event type used by the module event system. */ abstract class Event { + /** Base class for cancellable events which can be prevented from propagating. */ abstract class Cancellable : Event() { private var cancelled = false + /** Return true when this event has been cancelled and should not be processed further. */ fun isCancelled(): Boolean { return cancelled } + /** Mark this event as cancelled or not. */ fun setCancelled(cancelled: Boolean) { this.cancelled = cancelled } } + /** Event delivery priority used by subscribers to control ordering. */ enum class Priority { + /** Highest delivery priority; handlers with this priority run before others. */ HIGHEST, + + /** High delivery priority; runs after HIGHEST but before MEDIUM. */ HIGH, + + /** Default delivery priority for handlers. */ MEDIUM, + + /** Low delivery priority; runs after MEDIUM. */ LOW, + + /** Lowest delivery priority; runs last. */ LOWEST; + /** Numeric weight corresponding to the priority's ordinal. */ fun weight(): Int { return ordinal } diff --git a/src/main/kotlin/org/cobalt/event/EventBus.kt b/src/main/kotlin/org/cobalt/event/EventBus.kt index 0e25cd37..2e04f372 100644 --- a/src/main/kotlin/org/cobalt/event/EventBus.kt +++ b/src/main/kotlin/org/cobalt/event/EventBus.kt @@ -6,6 +6,7 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import org.slf4j.LoggerFactory +/** Lightweight event bus used to register listeners and post events to handlers with priorities. */ object EventBus { private data class Handler( @@ -21,6 +22,7 @@ object EventBus { private val cache = ConcurrentHashMap, Array>() private val logger = LoggerFactory.getLogger(this::class.java) + /** Register all methods annotated with [SubscribeEvent] from the given listener instance. */ @JvmStatic fun register(listener: Any) { if (handlers.any { it.listener === listener }) { @@ -67,12 +69,14 @@ object EventBus { cache.clear() } + /** Unregister all handlers for the given listener instance. */ @JvmStatic fun unregister(listener: Any) { handlers.removeIf { it.listener === listener } cache.clear() } + /** Post an event to all matching handlers and return the event. */ @JvmStatic fun post(event: Event): Event { val eventClass = event.javaClass diff --git a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt index 1796345d..25a289b8 100644 --- a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt +++ b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt @@ -4,6 +4,12 @@ import org.cobalt.event.Event @Target(AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) +/** Annotation to mark methods as event subscribers. + * + * @param ignoreCancelled if true the subscriber will still receive events that were cancelled + * @param priority delivery priority for ordering subscribers + * @param once when true the subscriber will be removed after the first invocation + */ annotation class SubscribeEvent( val ignoreCancelled: Boolean = false, val priority: Event.Priority = Event.Priority.MEDIUM, diff --git a/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt b/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt index 20b7716e..a769e53d 100644 --- a/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt @@ -2,4 +2,8 @@ package org.cobalt.event.impl import org.cobalt.event.Event -class ChatSendEvent(val message: String) : Event.Cancellable() +/** Event fired when the player sends a chat message; cancellable. */ +class ChatSendEvent( + /** The raw chat message content that is being sent. */ + val message: String +) : Event.Cancellable() diff --git a/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt b/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt index 496a9885..ec2e2c34 100644 --- a/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt @@ -3,11 +3,16 @@ package org.cobalt.event.impl import net.minecraft.network.protocol.Packet import org.cobalt.event.Event +/** Base event for network packet send/receive operations; cancellable. */ abstract class PacketEvent( + /** The network packet associated with the event. */ val packet: Packet<*>, ) : Event.Cancellable() { + /** Event fired when a packet is sent from the client. */ class Send(packet: Packet<*>) : PacketEvent(packet) + + /** Event fired when a packet is received by the client. */ class Receive(packet: Packet<*>) : PacketEvent(packet) } diff --git a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt index 783c984d..d407a403 100644 --- a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt @@ -5,8 +5,12 @@ import io.github.humbleui.skija.DirectContext import org.cobalt.event.Event import org.cobalt.util.skia.WrappedBackendRenderTarget +/** Event fired when Skia drawing is performed for a render pass. */ class SkiaDrawEvent( + /** The Skia DirectContext used for GPU operations. */ val context: DirectContext, + /** The wrapped backend render target representing the surface being rendered to. */ val renderTarget: WrappedBackendRenderTarget, + /** The Skia Canvas used to issue draw commands. */ val canvas: Canvas ) : Event() diff --git a/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt b/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt index 087a48ed..4ece4c75 100644 --- a/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt @@ -2,7 +2,13 @@ package org.cobalt.event.impl import org.cobalt.event.Event +/** Events that represent client tick boundaries. */ abstract class TickEvent : Event() { + + /** Event fired at the start of the client tick. */ class Start : TickEvent() + + /** Event fired at the end of the client tick. */ class End : TickEvent() + } diff --git a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt index 10305422..544402bf 100644 --- a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt @@ -7,6 +7,8 @@ import net.minecraft.client.renderer.MultiBufferSource import net.minecraft.client.renderer.culling.Frustum import org.cobalt.event.Event +/** Event emitted during the world render pass providing the rendering context. */ class WorldRenderEvent( + /** The LevelRenderContext for the current render pass. */ val context: LevelRenderContext ) : Event() diff --git a/src/main/kotlin/org/cobalt/module/Module.kt b/src/main/kotlin/org/cobalt/module/Module.kt index 33ea5b44..957b934f 100644 --- a/src/main/kotlin/org/cobalt/module/Module.kt +++ b/src/main/kotlin/org/cobalt/module/Module.kt @@ -1,49 +1,71 @@ package org.cobalt.module +/** Base class for all client modules/features. */ abstract class Module( + /** Human-readable module name. */ val name: String, + /** Category used to group modules in UI. */ val category: ModuleCategory, ) { + /** Whether this module is currently enabled. */ var enabled: Boolean = false + /** Called when the module is registered with the module manager. */ open fun onRegistration() {} + /** Return true when this module supports render callbacks. */ fun isRenderable(): Boolean { return this is RenderableModule } } +/** Module variant that exposes screen-space rendering hooks and layout properties. */ abstract class RenderableModule( name: String, category: ModuleCategory, + /** UI X position for rendering the module. */ var xPos: Float, + /** UI Y position for rendering the module. */ var yPos: Float, ) : Module(name, category) { + /** UI scale factor for rendering the module. */ var scale: Float = 1.0f + /** Return the current width of the module's rendered area. */ abstract fun getWidth(): Float + + /** Return the current height of the module's rendered area. */ abstract fun getHeight(): Float + + /** Render the module's UI onto the current canvas/context. */ abstract fun renderModule() } -class ModuleCategory private constructor(val displayName: String) { +/** Logical grouping for modules used by UI and registration. */ +class ModuleCategory private constructor( + /** Human-friendly display name for this category. */ + val displayName: String +) { companion object { private val entries = mutableMapOf() + /** Pre-registered render category used for renderable modules. */ @JvmField val RENDER = register(displayName = "Render") + /** Register or return an existing ModuleCategory for the given display name. */ fun register(displayName: String): ModuleCategory { return entries.getOrPut(displayName.lowercase()) { ModuleCategory(displayName) } } + /** Return all registered module categories. */ fun getCategories(): Collection { return entries.values } diff --git a/src/main/kotlin/org/cobalt/module/ModuleManager.kt b/src/main/kotlin/org/cobalt/module/ModuleManager.kt index 5c6bd63f..4a203d0e 100644 --- a/src/main/kotlin/org/cobalt/module/ModuleManager.kt +++ b/src/main/kotlin/org/cobalt/module/ModuleManager.kt @@ -7,6 +7,7 @@ import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.module.impl.render.PerformanceHUD import org.cobalt.util.skia.SkiaRenderer +/** Manager responsible for registering, storing and dispatching modules. */ object ModuleManager { private val modules = mutableSetOf() @@ -15,6 +16,7 @@ object ModuleManager { EventBus.register(this) } + /** Register built-in modules and perform their onRegistration lifecycle call. */ fun registerModules() { val builtIn = arrayOf( PerformanceHUD @@ -25,6 +27,7 @@ object ModuleManager { } } + /** Add a module to the manager; will throw if a module with the same name is already registered. */ fun addModule(module: Module) { if (!modules.add(module)) { error("'${module.name}' is already registered") @@ -33,16 +36,19 @@ object ModuleManager { module.onRegistration() } + /** Lookup a module by its name (case-insensitive). */ fun getModule(moduleName: String): Module? { return modules.find { module -> module.name.equals(moduleName, true) } } + /** Return the set of registered modules. */ fun getModules(): Set { return modules } + /** Draw all enabled modules that implement renderable behavior during the Skia render pass. */ @SubscribeEvent fun drawRenderableModules(event: SkiaDrawEvent) { if (minecraft.level == null) { diff --git a/src/main/kotlin/org/cobalt/ui/UIComponent.kt b/src/main/kotlin/org/cobalt/ui/UIComponent.kt index 65f8155f..9554fa62 100644 --- a/src/main/kotlin/org/cobalt/ui/UIComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/UIComponent.kt @@ -1,14 +1,21 @@ package org.cobalt.ui +/** Base UI component with position and size used by HUD/editor screens. */ abstract class UIComponent( + /** Screen-space X coordinate. */ var xPos: Float, + /** Screen-space Y coordinate. */ var yPos: Float, + /** Component width in pixels. */ open val width: Float = 0.0f, + /** Component height in pixels. */ open val height: Float = 0.0f, ) { + /** Render the component's contents onto the current canvas/context. */ abstract fun renderComponent() + /** Update the component's screen-space position and return this instance. */ fun updateBounds(xPos: Float, yPos: Float): UIComponent { this.xPos = xPos this.yPos = yPos diff --git a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt index 0add9c0c..1af77bb9 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt @@ -5,14 +5,22 @@ package org.cobalt.ui.animation +/** Generic animation base for interpolating values over a duration in milliseconds. */ abstract class Animation(private val duration: Long) { private var startTime: Long = 0L private var animating = false private var reversed = false + /** Compute the interpolated value between start and end for the current animation progress. + * + * @param start starting value + * @param end ending value + * @param reverse whether the animation is reversed + */ abstract fun get(start: T, end: T, reverse: Boolean = false): T + /** Start or toggle the animation; handles reversal when already animating. */ fun start() { val currentTime = System.currentTimeMillis() @@ -29,6 +37,7 @@ abstract class Animation(private val duration: Long) { return } + /** Return animation progress as a percentage between 0 and 100. */ fun getPercent(): Float { if (!animating) return 100f val percent = ((System.currentTimeMillis() - startTime) / duration.toFloat() * 100f) @@ -41,6 +50,7 @@ abstract class Animation(private val duration: Long) { return percent.coerceAtMost(100f) } + /** Whether the animation is currently running. */ fun isAnimating(): Boolean { return animating } diff --git a/src/main/kotlin/org/cobalt/util/ChatUtils.kt b/src/main/kotlin/org/cobalt/util/ChatUtils.kt index 26835ee4..fb780b6a 100644 --- a/src/main/kotlin/org/cobalt/util/ChatUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ChatUtils.kt @@ -7,6 +7,7 @@ import org.cobalt.Cobalt import org.cobalt.Cobalt.minecraft import org.slf4j.LoggerFactory + /** Utilities for sending chat/system messages and building message components. */ object ChatUtils { private val defaultPrefix = Component.literal("") @@ -21,6 +22,11 @@ object ChatUtils { private val logger = LoggerFactory.getLogger(this::class.java) + /** Send a system chat message to the player using the configured message prefix. + * + * @param message the plain text message to send + * @param type the MessageType that controls prefixing/formatting + */ @JvmStatic fun sendMessage(message: String, type: MessageType = MessageType.DEFAULT) { val player = minecraft.player @@ -39,11 +45,13 @@ object ChatUtils { player.sendSystemMessage(component) } + /** Convert a plain string into a MutableComponent. */ @JvmStatic fun stringToComponent(string: String): MutableComponent { return Component.literal(string) } + /** Send a raw chat message as though typed by the player. */ @JvmStatic fun sendChatMessage(message: String) { val player = minecraft.player @@ -56,6 +64,7 @@ object ChatUtils { player.connection.sendChat(message) } + /** Send a client-side command string to the server. */ @JvmStatic fun sendCommand(command: String) { val player = minecraft.player @@ -70,8 +79,12 @@ object ChatUtils { } +/** Type of message to send via ChatUtils. */ enum class MessageType { + /** Default message includes the mod prefix. */ DEFAULT, + /** Debug messages include the debug prefix. */ DEBUG, + /** Raw messages are sent without any prefix. */ RAW } diff --git a/src/main/kotlin/org/cobalt/util/ColorUtils.kt b/src/main/kotlin/org/cobalt/util/ColorUtils.kt index 3e115037..445ebbd3 100644 --- a/src/main/kotlin/org/cobalt/util/ColorUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ColorUtils.kt @@ -10,8 +10,17 @@ import org.cobalt.dsl.blue import org.cobalt.dsl.green import org.cobalt.dsl.red +/** Color-related utility helpers for building text gradients and extracting ARGB components. */ object ColorUtils { + /** Build a MutableComponent where each character of the input text is colored with an interpolated + * color between startColor and endColor. + * + * @param text the text to colorize + * @param startColor ARGB integer color used at the start of the text + * @param endColor ARGB integer color used at the end of the text + * @return a MutableComponent with per-character gradient coloring + */ @JvmStatic fun buildTextGradient(text: String, startColor: Int, endColor: Int): MutableComponent { val result = Component.empty() @@ -38,15 +47,19 @@ object ColorUtils { return result } + /** Return the red component (0-255) of the supplied ARGB color integer. */ @JvmStatic fun getRed(color: Int) = color.red + /** Return the green component (0-255) of the supplied ARGB color integer. */ @JvmStatic fun getGreen(color: Int) = color.green + /** Return the blue component (0-255) of the supplied ARGB color integer. */ @JvmStatic fun getBlue(color: Int) = color.blue + /** Return the alpha component (0-255) of the supplied ARGB color integer. */ @JvmStatic fun getAlpha(color: Int) = color.alpha diff --git a/src/main/kotlin/org/cobalt/util/FrustumUtils.kt b/src/main/kotlin/org/cobalt/util/FrustumUtils.kt index 4afafaa7..fc0deebe 100644 --- a/src/main/kotlin/org/cobalt/util/FrustumUtils.kt +++ b/src/main/kotlin/org/cobalt/util/FrustumUtils.kt @@ -5,13 +5,19 @@ import net.minecraft.world.phys.AABB import org.cobalt.mixin.render.FrustumInvoker import org.joml.FrustumIntersection +/** Utilities for frustum visibility testing used by rendering helpers. */ object FrustumUtils { + /** Check whether an AABB is inside or intersects the provided frustum. */ @JvmStatic fun isVisible(frustum: Frustum, box: AABB): Boolean { return isVisible(frustum, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ) } + /** Check whether the specified axis-aligned cube bounds are visible in the frustum. + * + * @return true when the bounds are inside or intersect the frustum + */ @JvmStatic fun isVisible( frustum: Frustum, diff --git a/src/main/kotlin/org/cobalt/util/MouseUtils.kt b/src/main/kotlin/org/cobalt/util/MouseUtils.kt index 4e642b8c..51ba81c0 100644 --- a/src/main/kotlin/org/cobalt/util/MouseUtils.kt +++ b/src/main/kotlin/org/cobalt/util/MouseUtils.kt @@ -3,13 +3,16 @@ package org.cobalt.util import org.cobalt.dsl.mouseX import org.cobalt.dsl.mouseY +/** Convenience accessors for the current mouse position in screen coordinates. */ object MouseUtils { + /** Return the current mouse X position as a Float. */ @JvmStatic fun getMouseX(): Float { return mouseX } + /** Return the current mouse Y position as a Float. */ @JvmStatic fun getMouseY(): Float { return mouseY diff --git a/src/main/kotlin/org/cobalt/util/RenderUtils.kt b/src/main/kotlin/org/cobalt/util/RenderUtils.kt index f0a583bb..364e9bbd 100644 --- a/src/main/kotlin/org/cobalt/util/RenderUtils.kt +++ b/src/main/kotlin/org/cobalt/util/RenderUtils.kt @@ -12,8 +12,17 @@ import net.minecraft.world.phys.Vec3 import org.cobalt.Cobalt.minecraft import org.cobalt.util.helper.Layers +/** Utility rendering helpers for drawing boxes, outlines, tracers and lines in world space. */ object RenderUtils { + /** Draw a unit cube wireframe and optional translucent fill at the given block position. + * + * @param context the level render context to draw with + * @param pos the block position to draw + * @param color the color to use for outline/fill + * @param esp when true uses ESP render type variants + * @param lineWidth line thickness for outlines + */ @JvmStatic fun drawBlockPos( context: LevelRenderContext, @@ -34,6 +43,14 @@ object RenderUtils { drawBox(context, box, color, esp, lineWidth) } + /** Draw an outline around the provided entity's bounding box, interpolated for rendering. + * + * @param context the level render context to draw with + * @param entity the entity whose bounding box will be outlined + * @param color the outline color + * @param esp when true uses ESP render type variants + * @param lineWidth outline thickness + */ fun drawEntityOutline( context: LevelRenderContext, entity: Entity, @@ -54,6 +71,14 @@ object RenderUtils { drawBox(context, entity.boundingBox.move(dx, dy, dz), color, esp, lineWidth) } + /** Draw a line from the camera position toward the supplied world-space target point. + * + * @param context the level render context to draw with + * @param to world-space target coordinate for the tracer + * @param color tracer color + * @param esp when true uses ESP render type variants + * @param lineWidth tracer thickness + */ @JvmStatic fun drawTracer( context: LevelRenderContext, @@ -69,6 +94,14 @@ object RenderUtils { drawLine(context, from, to, color, esp, lineWidth) } + /** Draw a colored axis-aligned bounding box (AABB) with optional translucent fill and outline. + * + * @param context the level render context to draw with + * @param box the world-space axis-aligned bounding box + * @param color color used for fill/outline + * @param esp when true uses ESP render type variants + * @param lineWidth outline thickness + */ @JvmStatic fun drawBox( context: LevelRenderContext, @@ -138,6 +171,15 @@ object RenderUtils { bufferSource.endBatch(Layers.getLines(esp)) } + /** Draw a colored line between two world-space points. + * + * @param context the level render context to draw with + * @param from start point in world coordinates + * @param to end point in world coordinates + * @param color the color to use for the line + * @param esp when true uses ESP render type variants + * @param lineWidth line thickness + */ @JvmStatic fun drawLine( context: LevelRenderContext, diff --git a/src/main/kotlin/org/cobalt/util/WebUtils.kt b/src/main/kotlin/org/cobalt/util/WebUtils.kt index 64d2b584..e0729d1e 100644 --- a/src/main/kotlin/org/cobalt/util/WebUtils.kt +++ b/src/main/kotlin/org/cobalt/util/WebUtils.kt @@ -5,8 +5,16 @@ import java.net.HttpURLConnection import java.net.URI import org.cobalt.Cobalt +/** Small HTTP helpers for fetching resources. */ object WebUtils { + /** Open an InputStream for the given URL using a simple GET request. + * + * @param url the URL to fetch + * @param timeout connect/read timeout in milliseconds + * @param cache whether to allow URLConnection caching + * @return an InputStream for reading the response body; caller is responsible for closing it + */ @JvmStatic fun getInputStream(url: String, timeout: Int = 5000, cache: Boolean = true): InputStream { val connection = (URI(url).toURL().openConnection() as HttpURLConnection).apply { diff --git a/src/main/kotlin/org/cobalt/util/helper/Layers.kt b/src/main/kotlin/org/cobalt/util/helper/Layers.kt index d287282b..ff6783df 100644 --- a/src/main/kotlin/org/cobalt/util/helper/Layers.kt +++ b/src/main/kotlin/org/cobalt/util/helper/Layers.kt @@ -6,6 +6,7 @@ import net.minecraft.client.renderer.rendertype.OutputTarget import net.minecraft.client.renderer.rendertype.RenderSetup import net.minecraft.client.renderer.rendertype.RenderType +/** Pre-configured RenderType providers used by RenderUtils for lines and quads (ESP variants). */ object Layers { private val LINES: RenderType = RenderType.create( @@ -40,10 +41,12 @@ object Layers { .createRenderSetup() ) + /** Return a RenderType for drawing quads; pass true to use the ESP variant. */ fun getQuads(esp: Boolean): RenderType { return if (esp) QUADS_ESP else QUADS } + /** Return a RenderType for drawing lines; pass true to use the ESP variant. */ fun getLines(esp: Boolean): RenderType { return if (esp) LINES_ESP else LINES } diff --git a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt index 36f92c0d..d5841ffb 100644 --- a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt +++ b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt @@ -5,6 +5,8 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent +/** Schedule Runnable tasks to run after a number of client ticks. + * Tasks are executed on TickEvent.End and are ordered by scheduled tick. */ object TickScheduler { private val taskQueue = PriorityQueue(Comparator.comparingLong(ScheduledTask::executeTick)) @@ -16,11 +18,13 @@ object TickScheduler { EventBus.register(this) } + /** Schedule an action to execute after the given number of ticks. */ @JvmStatic fun schedule(delayTicks: Long, action: Runnable) { taskQueue.offer(ScheduledTask(currentTick + delayTicks, action)) } + /** Internal event handler invoked at the end of each client tick to flush scheduled tasks. */ @SubscribeEvent fun onClientTick(event: TickEvent.End) { currentTick++ diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt index 772c7d68..d9bb6322 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt @@ -11,10 +11,14 @@ import java.nio.file.Files import kotlinx.coroutines.runBlocking import org.cobalt.util.WebUtils +/** Represents a lazily-loaded image resource; supports raster images and SVGs with caching and optional rounding/color masks. */ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: Int? = null) { + /** True when the identifier points to an SVG resource. */ val isSvg = identifier.endsWith(".svg", ignoreCase = true) + /** Deferred Skia Image for raster formats; null for SVG resources. */ val skiaImage: Image? + /** Parsed SVG DOM for SVG resources; null for raster images. */ val svgDom: SVGDOM? private var cachedRaster: Image? = null @@ -33,6 +37,7 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In } } + /** Return a raster Image sized to the requested width/height. For SVGs this will generate and cache a raster snapshot. */ fun getOrGenerateRaster(width: Int, height: Int): Image? { if (!isSvg) return skiaImage val dom = svgDom ?: return null @@ -61,6 +66,7 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In return cachedRaster } + /** Release any native image resources held by this instance. */ fun delete() { skiaImage?.close() svgDom?.close() diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt index e991acd1..26d4d0d0 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt @@ -6,6 +6,9 @@ import io.github.humbleui.types.Rect import java.io.IOException import org.cobalt.Cobalt.minecraft + /** High-level Skia drawing helpers used by UI and module renderers. + * Provides convenience functions for text, shapes, images and scissor management. + */ object SkiaRenderer { private data class ImageCacheKey( @@ -23,6 +26,7 @@ object SkiaRenderer { private val canvas: Canvas? get() = SkiaContext.canvas + /** Calculate a window scale factor relative to a 1920x1080 baseline for consistent UI sizing. */ fun getWindowScale(): Float { val baseWidth = 1920f val baseHeight = 1080f @@ -33,26 +37,34 @@ object SkiaRenderer { return minOf(windowWidth / baseWidth, windowHeight / baseHeight) } + /** Save the current Skia canvas state. */ @JvmStatic fun save() = this.canvas?.save() + /** Restore the previously saved Skia canvas state. */ @JvmStatic fun restore() = this.canvas?.restore() + /** Translate the canvas by the given x/y offset. */ @JvmStatic fun translate(x: Float, y: Float) = this.canvas?.translate(x, y) + /** Rotate the canvas by the given angle in degrees. */ @JvmStatic fun rotate(angleDeg: Float) = this.canvas?.rotate(angleDeg) + /** Scale the canvas by the specified X and Y factors. */ @JvmStatic fun scale(x: Float, y: Float) = this.canvas?.scale(x, y) + /** Push a scissor/clip rectangle onto the canvas stack. Subsequent draws will be clipped. + * Coordinates are in canvas space. + */ @JvmStatic fun pushScissor(x: Float, y: Float, width: Float, height: Float) { val canvas = this.canvas ?: return @@ -66,6 +78,7 @@ object SkiaRenderer { scissorStackDepth++ } + /** Pop the last scissor/clip rectangle and restore the previous canvas state. */ @JvmStatic fun popScissor() { if (scissorStackDepth <= 0) return @@ -73,6 +86,7 @@ object SkiaRenderer { scissorStackDepth-- } + /** Load and cache a font from the given resource path. */ @JvmStatic fun loadFont(resourcePath: String) = fonts.computeIfAbsent(resourcePath) { val bytes = javaClass.classLoader @@ -90,6 +104,7 @@ object SkiaRenderer { } } + /** Draw text using the specified font at the given position and size. */ @JvmStatic fun text(font: Font, text: String, x: Float, y: Float, fontSize: Float, color: Int) { val canvas = this.canvas ?: return @@ -104,6 +119,7 @@ object SkiaRenderer { } } + /** Measure and return the width of the given text using the font and size. */ @JvmStatic fun textWidth(font: Font, text: String, fontSize: Float): Float { font.size = fontSize @@ -113,6 +129,7 @@ object SkiaRenderer { } } + /** Load and cache an image by identifier, optionally applying rounding and color masking. */ @JvmStatic fun loadImage( identifier: String, @@ -127,6 +144,7 @@ object SkiaRenderer { } } + /** Draw a SkiaImage raster into the specified destination rectangle. */ @JvmStatic fun image(image: SkiaImage, x: Float, y: Float, width: Float, height: Float) { val canvas = this.canvas ?: return @@ -159,6 +177,7 @@ object SkiaRenderer { } } + /** Draw a straight line between two points. */ @JvmStatic fun line(x1: Float, x2: Float, y1: Float, y2: Float, color: Int, thickness: Float = 1f) { val canvas = this.canvas ?: return @@ -173,6 +192,7 @@ object SkiaRenderer { } } + /** Draw a filled rectangle. */ @JvmStatic fun rect(x: Float, y: Float, width: Float, height: Float, color: Int) { val canvas = this.canvas ?: return @@ -186,6 +206,7 @@ object SkiaRenderer { } } + /** Draw a stroked rectangle outline with the given thickness. */ @JvmStatic fun outline(x: Float, y: Float, width: Float, height: Float, color: Int, thickness: Float = 1f) { val canvas = this.canvas ?: return @@ -210,6 +231,7 @@ object SkiaRenderer { } } + /** Draw a filled rounded rectangle with the specified corner radius. */ @JvmStatic fun roundedRect(x: Float, y: Float, width: Float, height: Float, radius: Float, color: Int) { val canvas = this.canvas ?: return @@ -223,6 +245,7 @@ object SkiaRenderer { } } + /** Draw a rounded rectangle outline with the specified thickness. */ @JvmStatic fun roundedOutline( x: Float, @@ -256,6 +279,7 @@ object SkiaRenderer { } } + /** Draw a rectangle filled with a two-color linear gradient. */ @JvmStatic fun gradientRect( x: Float, y: Float, width: Float, height: Float, @@ -290,6 +314,7 @@ object SkiaRenderer { } } + /** Draw a rounded rectangle filled with a two-color linear gradient. */ @JvmStatic fun gradientRoundedRect( x: Float, @@ -330,6 +355,10 @@ object SkiaRenderer { } } + /** Draw a rectangle with rounded corners on one side only. + * + * @param side which side should have rounded corners + */ @JvmStatic fun halfRoundedRect( x: Float, y: Float, width: Float, height: Float, From 8d9c7ca9c264a4b8b77a0c26d4c36afc144d907f Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 18:31:26 +0200 Subject: [PATCH 02/18] refactor: fix magic values in PerformanceHUD --- .../module/impl/render/PerformanceHUD.kt | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt index f317f90b..8c76f556 100644 --- a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt +++ b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt @@ -8,41 +8,58 @@ import org.cobalt.ui.ColorPalette import org.cobalt.util.ServerUtils import org.cobalt.util.skia.SkiaRenderer +private const val DEFAULT_OFFSET = 5.0f + object PerformanceHUD : RenderableModule( name = "Performance HUD", category = ModuleCategory.RENDER, - xPos = 5.0f, - yPos = 5.0f, + xPos = DEFAULT_OFFSET, + yPos = DEFAULT_OFFSET, ) { private const val PADDING = 25f + private const val CORNER_RADIUS = 5f + private const val OUTLINE_THICKNESS = 2f + private const val FONT_SIZE = 16f + private const val TEXT_SPACING = 5f + private const val DIVIDER_HALF_HEIGHT = 10f + private const val PANEL_HEIGHT = 50f + private const val MID_FACTOR = 0.5f + private const val DIVIDER_GAP = PADDING / 2 + TEXT_SPACING override fun renderModule() { val width = getWidth() val height = getHeight() val centerY = yPos + height / 2 - SkiaRenderer.roundedRect(xPos, yPos, width, height, 5f, ColorPalette.PANEL) - SkiaRenderer.roundedOutline(xPos, yPos, width, height, 5f, ColorPalette.BORDER, 2f) + SkiaRenderer.roundedRect(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.PANEL) + SkiaRenderer.roundedOutline(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.BORDER, OUTLINE_THICKNESS) var currentX = xPos + PADDING - val textY = centerY - 16f / 2 + val textY = centerY - FONT_SIZE / 2 for ((index, stat) in getStats().withIndex()) { if (index > 0) { - currentX += PADDING / 2 + 5f - - val midY = yPos + height * 0.5f - SkiaRenderer.line(currentX, currentX, midY - 10f, midY + 10f, ColorPalette.BORDER, 2f) - - currentX += PADDING / 2 + 5f + currentX += DIVIDER_GAP + + val midY = yPos + height * MID_FACTOR + SkiaRenderer.line( + currentX, + currentX, + midY - DIVIDER_HALF_HEIGHT, + midY + DIVIDER_HALF_HEIGHT, + ColorPalette.BORDER, + OUTLINE_THICKNESS + ) + + currentX += DIVIDER_GAP } - SkiaRenderer.text(SkiaRenderer.primaryFont, stat.value, currentX, textY, 16f, ColorPalette.TEXT_PRIMARY) - currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, 16f) + 5f + SkiaRenderer.text(SkiaRenderer.primaryFont, stat.value, currentX, textY, FONT_SIZE, ColorPalette.TEXT_PRIMARY) + currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - SkiaRenderer.text(SkiaRenderer.primaryFont, stat.unit, currentX, textY, 16f, ColorPalette.TEXT_DISABLED) - currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, 16f) + SkiaRenderer.text(SkiaRenderer.primaryFont, stat.unit, currentX, textY, FONT_SIZE, ColorPalette.TEXT_DISABLED) + currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, FONT_SIZE) } } @@ -51,17 +68,17 @@ object PerformanceHUD : RenderableModule( for ((index, stat) in getStats().withIndex()) { if (index > 0) { - width += PADDING / 2 + PADDING / 2 + 10f + width += PADDING + 2 * TEXT_SPACING } - width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, 16f) + 5f - width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, 16f) + width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, FONT_SIZE) } return width } - override fun getHeight(): Float = 50f + override fun getHeight(): Float = PANEL_HEIGHT private fun getStats() = listOf( Stat(getFPS(), "FPS"), From 3e882dfd76059db76e012c36ce5e3e68ac710c4f Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 18:31:40 +0200 Subject: [PATCH 03/18] feat: change detekt config to max 3 returns instead of 1 --- config/detekt/detekt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index b250319a..b2b1c3b2 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -27,7 +27,7 @@ style: ReturnCount: active: true - max: 1 + max: 3 excludedFunctions: - equals From ec246b66d47d4b80472774424353d703722cb106 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 18:31:52 +0200 Subject: [PATCH 04/18] refactor: rename skiaImage to image for clarity and improve KDoc formatting --- .../kotlin/org/cobalt/util/skia/SkiaImage.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt index d9bb6322..9977b274 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt @@ -11,13 +11,15 @@ import java.nio.file.Files import kotlinx.coroutines.runBlocking import org.cobalt.util.WebUtils -/** Represents a lazily-loaded image resource; supports raster images and SVGs with caching and optional rounding/color masks. */ +/** Represents a lazily-loaded image resource; + * supports raster images and SVGs with caching and optional rounding/color masks. + */ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: Int? = null) { /** True when the identifier points to an SVG resource. */ val isSvg = identifier.endsWith(".svg", ignoreCase = true) /** Deferred Skia Image for raster formats; null for SVG resources. */ - val skiaImage: Image? + val image: Image? /** Parsed SVG DOM for SVG resources; null for raster images. */ val svgDom: SVGDOM? @@ -30,16 +32,18 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In if (isSvg) { svgDom = Data.makeFromBytes(bytes).use { data -> SVGDOM(data) } - skiaImage = null + image = null } else { - skiaImage = Image.makeDeferredFromEncodedBytes(bytes) + image = Image.makeDeferredFromEncodedBytes(bytes) svgDom = null } } - /** Return a raster Image sized to the requested width/height. For SVGs this will generate and cache a raster snapshot. */ + /** Return a raster Image sized to the requested width/height. + * For SVGs this will generate and cache a raster snapshot. + */ fun getOrGenerateRaster(width: Int, height: Int): Image? { - if (!isSvg) return skiaImage + if (!isSvg) return image val dom = svgDom ?: return null if (cachedRaster != null && width == lastWidth && height == lastHeight) { @@ -68,7 +72,7 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In /** Release any native image resources held by this instance. */ fun delete() { - skiaImage?.close() + image?.close() svgDom?.close() cachedRaster?.close() } From ff59af60434bf2b3cb9ac79dcb048d7c501980c7 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 18:39:50 +0200 Subject: [PATCH 05/18] refactor: rename message sending functions for clarity and improve logging, remove some magic numbers --- .../org/cobalt/command/CommandManager.kt | 2 +- src/main/kotlin/org/cobalt/util/ChatUtils.kt | 8 +++---- src/main/kotlin/org/cobalt/util/ColorUtils.kt | 11 +++++++--- .../kotlin/org/cobalt/util/RenderUtils.kt | 3 ++- .../cobalt/util/rotation/DefaultRotations.kt | 4 ++-- .../org/cobalt/util/skia/SkiaRenderer.kt | 22 ++++++++++++++----- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/CommandManager.kt b/src/main/kotlin/org/cobalt/command/CommandManager.kt index c98da456..7d8ffb58 100644 --- a/src/main/kotlin/org/cobalt/command/CommandManager.kt +++ b/src/main/kotlin/org/cobalt/command/CommandManager.kt @@ -41,7 +41,7 @@ object CommandManager { dispatcher.execute(commandLine, player.connection.suggestionsProvider) } catch (exception: Exception) { logger.error("Error while executing command: $commandLine", exception) - ChatUtils.sendMessage("${ChatFormatting.RED}Something went wrong when executing the command") + ChatUtils.sendSystemMessage("${ChatFormatting.RED}Something went wrong when executing the command") } } diff --git a/src/main/kotlin/org/cobalt/util/ChatUtils.kt b/src/main/kotlin/org/cobalt/util/ChatUtils.kt index fb780b6a..440a57c8 100644 --- a/src/main/kotlin/org/cobalt/util/ChatUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ChatUtils.kt @@ -28,11 +28,11 @@ object ChatUtils { * @param type the MessageType that controls prefixing/formatting */ @JvmStatic - fun sendMessage(message: String, type: MessageType = MessageType.DEFAULT) { + fun sendSystemMessage(message: String, type: MessageType = MessageType.DEFAULT) { val player = minecraft.player if (player == null) { - logger.error("Attempted to send message ($message) but mc.player is null") + logger.error("Attempted to send system message ($message) but mc.player is null") return } @@ -53,11 +53,11 @@ object ChatUtils { /** Send a raw chat message as though typed by the player. */ @JvmStatic - fun sendChatMessage(message: String) { + fun sendMessageAsPlayer(message: String) { val player = minecraft.player if (player == null) { - logger.error("Attempted to send message ($message) but mc.player is null") + logger.error("Attempted to send message as player ($message) but mc.player is null") return } diff --git a/src/main/kotlin/org/cobalt/util/ColorUtils.kt b/src/main/kotlin/org/cobalt/util/ColorUtils.kt index 445ebbd3..e408c9d2 100644 --- a/src/main/kotlin/org/cobalt/util/ColorUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ColorUtils.kt @@ -13,6 +13,10 @@ import org.cobalt.dsl.red /** Color-related utility helpers for building text gradients and extracting ARGB components. */ object ColorUtils { + private const val MIN_TEXT_LENGTH = 1 + private const val SHIFT_RED = 16 + private const val SHIFT_GREEN = 8 + /** Build a MutableComponent where each character of the input text is colored with an interpolated * color between startColor and endColor. * @@ -26,17 +30,18 @@ object ColorUtils { val result = Component.empty() val textLength = text.length - if (textLength <= 1) { + if (textLength <= MIN_TEXT_LENGTH) { return Component.literal(text) .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(startColor))) } for (index in text.indices) { - val ratio = index.toDouble() / (textLength - 1) + val denominator = (textLength - MIN_TEXT_LENGTH).toDouble() + val ratio = index.toDouble() / denominator val red = (startColor.red + ratio * (endColor.red - startColor.red)).roundToInt() val green = (startColor.green + ratio * (endColor.green - startColor.green)).roundToInt() val blue = (startColor.blue + ratio * (endColor.blue - startColor.blue)).roundToInt() - val interpolatedColor = (red shl 16) or (green shl 8) or blue + val interpolatedColor = (red shl SHIFT_RED) or (green shl SHIFT_GREEN) or blue val coloredChar = Component.literal(text[index].toString()) .setStyle(Style.EMPTY.withColor(TextColor.fromRgb(interpolatedColor))) diff --git a/src/main/kotlin/org/cobalt/util/RenderUtils.kt b/src/main/kotlin/org/cobalt/util/RenderUtils.kt index 364e9bbd..84de84f8 100644 --- a/src/main/kotlin/org/cobalt/util/RenderUtils.kt +++ b/src/main/kotlin/org/cobalt/util/RenderUtils.kt @@ -14,6 +14,7 @@ import org.cobalt.util.helper.Layers /** Utility rendering helpers for drawing boxes, outlines, tracers and lines in world space. */ object RenderUtils { + private const val ALPHA = 100; /** Draw a unit cube wireframe and optional translucent fill at the given block position. * @@ -126,7 +127,7 @@ object RenderUtils { val matrix = poseStack.last().pose() val poseEntry = poseStack.last() - val fillColor = Color(color.red, color.green, color.blue, 100) + val fillColor = Color(color.red, color.green, color.blue, ALPHA) val corners = arrayOf( Vec3(box.minX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.maxZ), Vec3(box.minX, box.minY, box.maxZ), diff --git a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt b/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt index 5ecc2563..011aebf7 100644 --- a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt +++ b/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt @@ -21,12 +21,12 @@ object DefaultRotations : IRotation { currentYaw = getPlayerYaw() currentPitch = getPlayerPitch() currentSpeed = speed - ChatUtils.sendMessage("Rotation started to $yaw, $pitch", MessageType.DEBUG) + ChatUtils.sendSystemMessage("Rotation started to $yaw, $pitch", MessageType.DEBUG) } override fun onRotationEnd() { rotating = false - ChatUtils.sendMessage("Ended rotation.", MessageType.DEBUG) + ChatUtils.sendSystemMessage("Ended rotation.", MessageType.DEBUG) } override fun onRotationWorldRender() { diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt index 26d4d0d0..de23c18f 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt @@ -1,6 +1,19 @@ package org.cobalt.util.skia -import io.github.humbleui.skija.* +import io.github.humbleui.skija.BlendMode +import io.github.humbleui.skija.Canvas +import io.github.humbleui.skija.ClipMode +import io.github.humbleui.skija.ColorFilter +import io.github.humbleui.skija.Data +import io.github.humbleui.skija.Font +import io.github.humbleui.skija.FontEdging +import io.github.humbleui.skija.FontHinting +import io.github.humbleui.skija.FontMgr +import io.github.humbleui.skija.Paint +import io.github.humbleui.skija.PaintMode +import io.github.humbleui.skija.SamplingMode +import io.github.humbleui.skija.Shader +import io.github.humbleui.skija.TextLine import io.github.humbleui.types.RRect import io.github.humbleui.types.Rect import java.io.IOException @@ -10,6 +23,8 @@ import org.cobalt.Cobalt.minecraft * Provides convenience functions for text, shapes, images and scissor management. */ object SkiaRenderer { + private const val BASE_WIDTH = 1920f + private const val BASE_HEIGHT = 1080f private data class ImageCacheKey( val identifier: String, @@ -28,13 +43,10 @@ object SkiaRenderer { /** Calculate a window scale factor relative to a 1920x1080 baseline for consistent UI sizing. */ fun getWindowScale(): Float { - val baseWidth = 1920f - val baseHeight = 1080f - val windowWidth = minecraft.window.width.toFloat() val windowHeight = minecraft.window.height.toFloat() - return minOf(windowWidth / baseWidth, windowHeight / baseHeight) + return minOf(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) } /** Save the current Skia canvas state. */ From 835a0c51f2355e31d65aca042bb33f17709346fb Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 21:42:44 +0200 Subject: [PATCH 06/18] refactor: remove even more magic numbers --- .../org/cobalt/command/impl/MainCommand.kt | 5 ++- .../org/cobalt/dsl/{render.kt => Render.kt} | 0 .../org/cobalt/dsl/{utils.kt => Utils.kt} | 0 src/main/kotlin/org/cobalt/event/EventBus.kt | 2 +- .../org/cobalt/ui/animation/Animation.kt | 23 +++++++---- .../cobalt/ui/animation/BounceAnimation.kt | 26 +++++++++---- .../cobalt/util/rotation/DefaultRotations.kt | 39 +++++++++++-------- .../kotlin/org/cobalt/util/skia/SkiaEnums.kt | 32 ++++++++++++++- 8 files changed, 91 insertions(+), 36 deletions(-) rename src/main/kotlin/org/cobalt/dsl/{render.kt => Render.kt} (100%) rename src/main/kotlin/org/cobalt/dsl/{utils.kt => Utils.kt} (100%) diff --git a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt index 2f4954ce..dcc917a0 100644 --- a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt +++ b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt @@ -11,17 +11,18 @@ import org.cobalt.util.rotation.DefaultRotations import org.cobalt.util.rotation.RotationManager internal object MainCommand : Command(name = "cobalt") { + private const val DELAY_TICKS = 1L @DefaultHandler fun main() { - TickScheduler.schedule(1) { + TickScheduler.schedule(DELAY_TICKS) { minecraft.setScreen(ConfigScreen) } } @SubCommand fun hud() { - TickScheduler.schedule(1) { + TickScheduler.schedule(DELAY_TICKS) { minecraft.setScreen(HudEditorScreen) } } diff --git a/src/main/kotlin/org/cobalt/dsl/render.kt b/src/main/kotlin/org/cobalt/dsl/Render.kt similarity index 100% rename from src/main/kotlin/org/cobalt/dsl/render.kt rename to src/main/kotlin/org/cobalt/dsl/Render.kt diff --git a/src/main/kotlin/org/cobalt/dsl/utils.kt b/src/main/kotlin/org/cobalt/dsl/Utils.kt similarity index 100% rename from src/main/kotlin/org/cobalt/dsl/utils.kt rename to src/main/kotlin/org/cobalt/dsl/Utils.kt diff --git a/src/main/kotlin/org/cobalt/event/EventBus.kt b/src/main/kotlin/org/cobalt/event/EventBus.kt index 2e04f372..1ca4e985 100644 --- a/src/main/kotlin/org/cobalt/event/EventBus.kt +++ b/src/main/kotlin/org/cobalt/event/EventBus.kt @@ -46,7 +46,7 @@ object EventBus { return@forEach } - val eventType = params[0] + val eventType = params.first() val lookup = MethodHandles.privateLookupIn(listener.javaClass, MethodHandles.lookup()) val handle = lookup.unreflect(method).bindTo(listener) diff --git a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt index 1af77bb9..c81302ee 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt @@ -8,7 +8,14 @@ package org.cobalt.ui.animation /** Generic animation base for interpolating values over a duration in milliseconds. */ abstract class Animation(private val duration: Long) { - private var startTime: Long = 0L + companion object { + private const val PERCENT_MAX: Float = 100f + private const val MIN_PROGRESS: Float = 0f + private const val MAX_PROGRESS: Float = 1f + private const val ZERO_TIME: Long = 0L + } + + private var startTime: Long = ZERO_TIME private var animating = false private var reversed = false @@ -31,23 +38,23 @@ abstract class Animation(private val duration: Long) { return } - val percent = ((currentTime - startTime) / duration.toFloat()).coerceIn(0f, 1f) + val percent = ((currentTime - startTime) / duration.toFloat()).coerceIn(MIN_PROGRESS, MAX_PROGRESS) reversed = !reversed - startTime = currentTime - ((1f - percent) * duration).toLong() + startTime = currentTime - ((MAX_PROGRESS - percent) * duration).toLong() return } /** Return animation progress as a percentage between 0 and 100. */ fun getPercent(): Float { - if (!animating) return 100f - val percent = ((System.currentTimeMillis() - startTime) / duration.toFloat() * 100f) + if (!animating) return PERCENT_MAX + val percent = ((System.currentTimeMillis() - startTime) / duration.toFloat() * PERCENT_MAX) - if (percent >= 100f) { + if (percent >= PERCENT_MAX) { animating = false - return 100f + return PERCENT_MAX } - return percent.coerceAtMost(100f) + return percent.coerceAtMost(PERCENT_MAX) } /** Whether the animation is currently running. */ diff --git a/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt b/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt index 43059f74..5260e9e7 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt @@ -4,25 +4,35 @@ import kotlin.math.pow class BounceAnimation(duration: Long) : Animation(duration) { + companion object { + private const val PERCENT_DIVISOR: Float = 100f + private const val FIRST_PHASE_THRESHOLD: Float = 0.3f + private const val SECOND_PHASE_RANGE: Float = 0.7f + private const val OVERSHOOT: Float = 1.05f + private const val OVERSHOOT_DECAY: Float = 0.05f + private const val FIRST_EASE_EXP: Float = 3f + private const val SECOND_EASE_EXP: Float = 2f + } + override fun get(start: Float, end: Float, reverse: Boolean): Float { if (!isAnimating()) return if (reverse) start else end return if (reverse) end + (start - end) * ease() else start + (end - start) * ease() } private fun ease(): Float { - val x = getPercent() / 100f + val x = getPercent() / PERCENT_DIVISOR return when { - x < 0.3f -> { - val t = x / 0.3f - val easeOut = 1f - (1f - t).pow(3) - easeOut * 1.05f + x < FIRST_PHASE_THRESHOLD -> { + val t = x / FIRST_PHASE_THRESHOLD + val easeOut = 1f - (1f - t).pow(FIRST_EASE_EXP) + easeOut * OVERSHOOT } else -> { - val t = (x - 0.3f) / 0.7f - val easeOut = 1f - (1f - t).pow(2) - 1.05f - (0.05f * easeOut) + val t = (x - FIRST_PHASE_THRESHOLD) / SECOND_PHASE_RANGE + val easeOut = 1f - (1f - t).pow(SECOND_EASE_EXP) + OVERSHOOT - (OVERSHOOT_DECAY * easeOut) } } } diff --git a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt b/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt index 011aebf7..53edb003 100644 --- a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt +++ b/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt @@ -7,12 +7,20 @@ import org.cobalt.util.MessageType object DefaultRotations : IRotation { + private const val FULL_CIRCLE = 360.0 + private const val HALF_CIRCLE = 180.0 + private const val NORMALIZE_OFFSET = FULL_CIRCLE + HALF_CIRCLE + private const val ANGLE_TOLERANCE = 0.5 + private const val ZERO_ANGLE = 0.0 + private var rotating = false - private var targetYaw = 0.0 - private var targetPitch = 0.0 - private var currentYaw = 0.0 - private var currentPitch = 0.0 - private var currentSpeed = 0.0 + private var targetYaw = ZERO_ANGLE + private var targetPitch = ZERO_ANGLE + private var currentYaw = ZERO_ANGLE + private var currentPitch = ZERO_ANGLE + private var currentSpeed = ZERO_ANGLE + + private val player = minecraft.player override fun onRotationStart(yaw: Double, pitch: Double, speed: Double) { rotating = true @@ -32,7 +40,7 @@ object DefaultRotations : IRotation { override fun onRotationWorldRender() { if (!rotating) return - val player = getPlayer() ?: return + val player = player?: return val currentYaw = player.yRot.toDouble() val currentPitch = player.xRot.toDouble() @@ -44,15 +52,15 @@ object DefaultRotations : IRotation { applyRotation(newYaw, newPitch) if ( - angleDistance(newYaw, targetYaw) < 0.5 && - abs(newPitch - targetPitch) < 0.5 + angleDistance(newYaw, targetYaw) < ANGLE_TOLERANCE && + abs(newPitch - targetPitch) < ANGLE_TOLERANCE ) { stopRotation() } } - // lerp! + private fun lerpAngle(current: Double, target: Double, alpha: Double): Double { - val delta = ((target - current + 540) % 360) - 180 + val delta = ((target - current + NORMALIZE_OFFSET) % FULL_CIRCLE) - HALF_CIRCLE return current + delta * alpha } @@ -61,9 +69,10 @@ object DefaultRotations : IRotation { } private fun angleDistance(a: Double, b: Double): Double { - val d = ((b - a + 540) % 360) - 180 + val d = ((b - a + NORMALIZE_OFFSET) % FULL_CIRCLE) - HALF_CIRCLE return abs(d) } + private fun distance(a: Double, b: Double): Double { return abs(a - b) } @@ -71,7 +80,7 @@ object DefaultRotations : IRotation { override fun isRotating(): Boolean = rotating private fun applyRotation(yaw: Double, pitch: Double) { - val player = getPlayer() ?: return + val player = player ?: return val y = yaw.toFloat() val p = pitch.toFloat() @@ -81,13 +90,11 @@ object DefaultRotations : IRotation { } private fun getPlayerYaw(): Double { - return getPlayer()?.yRot?.toDouble() ?: 0.0 + return player?.yRot?.toDouble() ?: ZERO_ANGLE } private fun getPlayerPitch(): Double { - return getPlayer()?.xRot?.toDouble() ?: 0.0 + return player?.xRot?.toDouble() ?: ZERO_ANGLE } - private fun getPlayer() = minecraft.player - } diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt index c520f26c..c57739dc 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt @@ -1,10 +1,40 @@ package org.cobalt.util.skia +/** + * Lightweight enums used by the Skia-based renderer helpers. + * + * These enums provide small, strongly-typed descriptors that make drawing code + * easier to read and less error-prone than using raw booleans or integers. + */ enum class SkiaGradient { + /** + * Indicates a gradient that interpolates colors from the top edge toward the + * bottom edge of the target rectangle (y increases). + */ TOP_TO_BOTTOM, + + /** + * Indicates a gradient that interpolates colors from the left edge toward the + * right edge of the target rectangle (x increases). + */ LEFT_TO_RIGHT } +/** + * Represents a side of a rectangle or box and is useful for APIs that need to + * specify a particular edge (for example, where to draw a border or place a + * badge). + */ enum class SkiaSide { - TOP, BOTTOM, LEFT, RIGHT + /** The top edge of a rectangle. */ + TOP, + + /** The bottom edge of a rectangle. */ + BOTTOM, + + /** The left edge of a rectangle. */ + LEFT, + + /** The right edge of a rectangle. */ + RIGHT } From 30fd3474c0662519bb4f9a4f70293d4552989826 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 21:52:10 +0200 Subject: [PATCH 07/18] refactor: fix even more detekt issues --- config/detekt/detekt.yml | 1 - src/main/kotlin/org/cobalt/command/Command.kt | 6 +++- src/main/kotlin/org/cobalt/ui/ColorPalette.kt | 30 +++++++++++++++++++ .../cobalt/ui/notification/Notification.kt | 22 +++++++++++--- .../kotlin/org/cobalt/util/ServerUtils.kt | 1 - .../org/cobalt/util/helper/Pipelines.kt | 22 ++++++++++---- .../org/cobalt/util/skia/SkiaContext.kt | 8 ++++- .../kotlin/org/cobalt/util/skia/gl/States.kt | 4 ++- 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index b2b1c3b2..2908b74d 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -44,7 +44,6 @@ style: ForbiddenComment: active: true comments: - - "TODO:" - "FIXME:" - "STOPSHIP:" diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index e097751f..d65c5318 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -1,6 +1,10 @@ package org.cobalt.command -import com.mojang.brigadier.arguments.* +import com.mojang.brigadier.arguments.BoolArgumentType +import com.mojang.brigadier.arguments.DoubleArgumentType +import com.mojang.brigadier.arguments.FloatArgumentType +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder import kotlin.reflect.KFunction diff --git a/src/main/kotlin/org/cobalt/ui/ColorPalette.kt b/src/main/kotlin/org/cobalt/ui/ColorPalette.kt index d0e7bd60..0e485e8f 100644 --- a/src/main/kotlin/org/cobalt/ui/ColorPalette.kt +++ b/src/main/kotlin/org/cobalt/ui/ColorPalette.kt @@ -5,25 +5,55 @@ import java.awt.Color // TODO: bring this over to a theme manager object ColorPalette { + /** Primary background color used for main surfaces (RGB int). */ val BACKGROUND_PRIMARY = Color(18, 18, 18).rgb + + /** Secondary background color for panels and elevated surfaces (RGB int). */ val BACKGROUND_SECONDARY = Color(24, 24, 24).rgb + + /** Panel background color for containers (RGB int). */ val PANEL = Color(30, 30, 30).rgb + + /** Hover background color for interactive elements (RGB int). */ val HOVER = Color(37, 37, 37).rgb + + /** Border color for separators and outlines (RGB int). */ val BORDER = Color(42, 42, 42).rgb + /** Primary accent color for highlights and buttons (RGB int). */ val ACCENT_PRIMARY = Color(79, 140, 255).rgb + + /** Accent color used on hover states (RGB int). */ val ACCENT_HOVER = Color(106, 162, 255).rgb + + /** Accent color used for active/pressed states (RGB int). */ val ACCENT_ACTIVE = Color(58, 116, 230).rgb + + /** Accent glow color with alpha for soft glow effects (RGBA int). */ val ACCENT_GLOW = Color(47, 95, 191, 64).rgb + /** Primary text color for high-contrast text (RGB int). */ val TEXT_PRIMARY = Color(230, 230, 230).rgb + + /** Secondary text color for less prominent text (RGB int). */ val TEXT_SECONDARY = Color(179, 179, 179).rgb + + /** Muted text color for de-emphasized labels (RGB int). */ val TEXT_MUTED = Color(122, 122, 122).rgb + + /** Disabled text color for inactive elements (RGB int). */ val TEXT_DISABLED = Color(95, 95, 95).rgb + /** Success semantic color for positive states (RGB int). */ val SUCCESS = Color(63, 191, 127).rgb + + /** Warning semantic color for cautionary states (RGB int). */ val WARNING = Color(230, 181, 102).rgb + + /** Error semantic color for negative states (RGB int). */ val ERROR = Color(224, 90, 90).rgb + + /** Informational semantic color for neutral/utility states (RGB int). */ val INFO = Color(93, 169, 233).rgb } diff --git a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt index 24a6d287..2f66e2ce 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt @@ -3,17 +3,31 @@ package org.cobalt.ui.notification import kotlin.time.Duration import org.cobalt.ui.UIComponent +/** Simple on-screen notification displayed for a given duration. + * + * @param title short headline text shown prominently + * @param description body text shown below the title + * @param duration how long the notification should be visible + */ data class Notification( val title: String, val description: String, val duration: Duration ) : UIComponent( - xPos = 0f, - yPos = 0f, - width = 100f, - height = 100f + xPos = DEFAULT_X, + yPos = DEFAULT_Y, + width = DEFAULT_WIDTH, + height = DEFAULT_HEIGHT ) { + companion object { + private const val DEFAULT_X: Float = 0f + private const val DEFAULT_Y: Float = 0f + private const val DEFAULT_WIDTH: Float = 100f + private const val DEFAULT_HEIGHT: Float = 100f + } + + /** Render the notification UI. */ override fun renderComponent() { } diff --git a/src/main/kotlin/org/cobalt/util/ServerUtils.kt b/src/main/kotlin/org/cobalt/util/ServerUtils.kt index 5967a635..86654eb4 100644 --- a/src/main/kotlin/org/cobalt/util/ServerUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ServerUtils.kt @@ -8,7 +8,6 @@ import org.cobalt.event.impl.PacketEvent import org.cobalt.mixin.client.AbstractClientPlayerAccessor object ServerUtils { - private var lastTickTime = 0L var averageTps = 20f diff --git a/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt b/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt index e1475d27..771cd2d2 100644 --- a/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt +++ b/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt @@ -6,26 +6,38 @@ import java.util.* import net.minecraft.client.renderer.RenderPipelines import net.minecraft.resources.Identifier +/** Central registry for custom render pipelines used by the client. */ object Pipelines { + private const val NAMESPACE = "cobalt" + private const val PIPELINE_DIR = "pipeline/" + private const val LINES_ESP_PATH = PIPELINE_DIR + "lines_esp" + private const val QUADS_PATH = PIPELINE_DIR + "quads" + private const val QUADS_ESP_PATH = PIPELINE_DIR + "quads_esp" + + private val NO_DEPTH_STENCIL: Optional = Optional.empty() + + /** Pipeline for rendering ESP-style lines (no depth/stencil). */ val LINES_ESP: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.LINES_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("cobalt", "pipeline/lines_esp")) - .withDepthStencilState(Optional.empty()) + .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, LINES_ESP_PATH)) + .withDepthStencilState(NO_DEPTH_STENCIL) .build() ) + /** Pipeline for rendering filled debug quads (default depth/stencil). */ val QUADS: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.DEBUG_FILLED_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("cobalt", "pipeline/quads")) + .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, QUADS_PATH)) .withDepthStencilState(DepthStencilState.DEFAULT) .build() ) + /** Pipeline for rendering filled quads used in ESP (no depth/stencil). */ val QUADS_ESP: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.DEBUG_FILLED_SNIPPET) - .withLocation(Identifier.fromNamespaceAndPath("cobalt", "pipeline/quads_esp")) - .withDepthStencilState(Optional.empty()) + .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, QUADS_ESP_PATH)) + .withDepthStencilState(NO_DEPTH_STENCIL) .build() ) diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt index 872a603e..cbb62b59 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt @@ -18,7 +18,13 @@ package org.cobalt.util.skia -import io.github.humbleui.skija.* +import io.github.humbleui.skija.Canvas +import io.github.humbleui.skija.ColorSpace +import io.github.humbleui.skija.ColorType +import io.github.humbleui.skija.DirectContext +import io.github.humbleui.skija.FramebufferFormat +import io.github.humbleui.skija.Surface +import io.github.humbleui.skija.SurfaceOrigin import org.cobalt.event.EventBus import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.util.skia.gl.States diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt index 74e4e821..783ed9ba 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt @@ -18,8 +18,10 @@ package org.cobalt.util.skia.gl -import org.lwjgl.opengl.GL30.* import java.util.* +import org.lwjgl.opengl.GL30.glGetIntegerv +import org.lwjgl.opengl.GL30.GL_MAJOR_VERSION +import org.lwjgl.opengl.GL30.GL_MINOR_VERSION object States { From 5964d2e0df79f0343bfd3f12fcb1164bfea84ccc Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 22:00:58 +0200 Subject: [PATCH 08/18] refactor: add even more kdoc --- .../cobalt/command/annotation/SubCommand.kt | 6 ++ .../cobalt/ui/notification/Notification.kt | 27 ++++++- .../ui/notification/NotificationManager.kt | 25 ++++++ .../org/cobalt/ui/screen/HudEditorScreen.kt | 11 +++ .../kotlin/org/cobalt/util/ServerUtils.kt | 25 ++++++ .../org/cobalt/util/rotation/IRotation.kt | 32 ++++++++ .../cobalt/util/rotation/RotationManager.kt | 26 +++++++ .../kotlin/org/cobalt/util/skia/SkiaImage.kt | 42 ++++++++-- .../org/cobalt/util/skia/SkiaRenderer.kt | 4 + .../util/skia/WrappedBackendRenderTarget.kt | 25 ++++++ .../org/cobalt/util/skia/gl/Properties.kt | 76 +++++++++++++++++++ .../kotlin/org/cobalt/util/skia/gl/State.kt | 30 ++++++++ .../kotlin/org/cobalt/util/skia/gl/States.kt | 20 +++++ 13 files changed, 337 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt index 9339d0d9..134d3e03 100644 --- a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt +++ b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt @@ -1,5 +1,11 @@ package org.cobalt.command.annotation +/** + * Marks a function as a named sub-command for a command handler. + * + * @param name optional sub-command label; when empty the function name is + * used by the dispatcher + */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class SubCommand(val name: String = "") diff --git a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt index 2f66e2ce..3db40956 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt @@ -5,13 +5,18 @@ import org.cobalt.ui.UIComponent /** Simple on-screen notification displayed for a given duration. * - * @param title short headline text shown prominently - * @param description body text shown below the title - * @param duration how long the notification should be visible + * @property title short headline text shown prominently + * @property description body text shown below the title + * @property duration how long the notification should be visible */ data class Notification( + /** Short headline text shown prominently. */ val title: String, + + /** Body text shown below the title. */ val description: String, + + /** How long the notification should be visible. */ val duration: Duration ) : UIComponent( xPos = DEFAULT_X, @@ -27,7 +32,13 @@ data class Notification( private const val DEFAULT_HEIGHT: Float = 100f } - /** Render the notification UI. */ + /** + * Render the notification UI. + * + * Implementations should draw the notification background, title and + * description within the component bounds. This method is called every + * frame while the notification is visible. + */ override fun renderComponent() { } @@ -35,9 +46,17 @@ data class Notification( } +/** Types of notifications used to indicate severity or purpose. */ enum class NotificationType { + /** Indicates a successful operation. */ SUCCESS, + + /** Indicates a warning or non-critical issue. */ WARNING, + + /** Indicates an error or critical problem. */ ERROR, + + /** Informational message without success/error semantics. */ INFO } diff --git a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt index c6d93917..beec50d1 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt @@ -5,18 +5,43 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.util.skia.SkiaRenderer +/** + * Manager responsible for displaying on-screen notifications. + * + * Notifications pushed to this manager will be rendered each frame via the + * Skia renderer. The manager registers itself on the global event bus in the + * initializer so it receives draw callbacks. + */ object NotificationManager { + // Internal storage for active notifications. private val notificationsList = mutableSetOf() init { EventBus.register(this) } + /** + * Enqueue a notification for rendering. + * + * The notification will be retained until it decides to remove itself or + * the manager is cleared. Notifications are rendered in an unspecified + * iteration order. + * + * @param notification the notification to display + */ fun pushNotification(notification: Notification) { notificationsList.add(notification) } + /** + * Event handler invoked during the Skia draw pass. + * + * This method iterates over all active notifications and renders each one + * using the `SkiaRenderer`. Notifications are drawn with the appropriate + * window scale and the renderer state is saved/restored around each + * notification draw call. + */ @SubscribeEvent fun onSkiaDraw(event: SkiaDrawEvent) { val windowScale = SkiaRenderer.getWindowScale() diff --git a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt index 900ce90c..06191b64 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt @@ -6,12 +6,23 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent +/** + * Screen instance used for editing HUD elements. The object registers itself + * on the global event bus to receive Skia draw callbacks while the screen + * is active. + */ object HudEditorScreen : Screen(Component.empty()) { init { EventBus.register(this) } + /** + * Handle Skia draw events to render the HUD editor overlay when this + * screen is the currently displayed Minecraft screen. + * + * @param event the Skia draw event (unused in the current implementation) + */ @SubscribeEvent fun onSkiaDraw(event: SkiaDrawEvent) { if (minecraft.screen != this) { diff --git a/src/main/kotlin/org/cobalt/util/ServerUtils.kt b/src/main/kotlin/org/cobalt/util/ServerUtils.kt index 86654eb4..bd7a56b5 100644 --- a/src/main/kotlin/org/cobalt/util/ServerUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ServerUtils.kt @@ -7,12 +7,32 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent import org.cobalt.mixin.client.AbstractClientPlayerAccessor +/** + * Utility object for retrieving and tracking server-side metrics such as + * average TPS (ticks per second) and the current player ping. + * + * The object listens for incoming packets and updates an internal smoothed + * TPS estimate whenever the server time packet is received. + */ object ServerUtils { private var lastTickTime = 0L + /** + * Smoothed average server ticks per second (TPS). + * + * This value is updated when a `ClientboundSetTimePacket` is received and is + * smoothed over time to avoid rapid fluctuations. The setter is private; the + * value should be read-only from callers. + */ var averageTps = 20f private set + /** + * The current player's network latency (ping) in milliseconds. + * + * Obtained from the Minecraft client player info via an accessor mixin. + * Returns 0 when no player info is available. + */ val currentPing get() = (minecraft.player as AbstractClientPlayerAccessor).clientPlayerInfo?.latency ?: 0 @@ -20,6 +40,11 @@ object ServerUtils { EventBus.register(this) } + /** + * Event handler for incoming packets. When a `ClientboundSetTimePacket` is + * received we use its arrival timestamp to estimate the server tick time and + * update [averageTps] with a small smoothing factor. + */ @SubscribeEvent fun onPacketReceive(event: PacketEvent.Receive) { if (event.packet is ClientboundSetTimePacket) { diff --git a/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt b/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt index 8d58686f..96747a4e 100644 --- a/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt +++ b/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt @@ -1,12 +1,44 @@ package org.cobalt.util.rotation +/** + * Contract for components that perform or manage rotation over time. + * + * Implementations should handle starting, updating and stopping rotations + * and expose whether a rotation is currently in progress. + */ interface IRotation { + /** + * Called during world rendering when rotation-related rendering or updates + * should be performed. Typically used to update camera orientation visuals + * or perform per-frame interpolation while a rotation is active. + */ fun onRotationWorldRender() + + /** + * Called when a rotation finishes or is explicitly stopped. Implementations + * should perform any cleanup required when rotation ends. + */ fun onRotationEnd() + + /** + * Start a rotation towards the given yaw and pitch. + * + * @param yaw target yaw in degrees + * @param pitch target pitch in degrees + * @param speed interpolation speed (higher = faster). Defaults to 0.15. + */ fun onRotationStart(yaw: Double, pitch: Double, speed: Double = 0.15) + + /** + * Returns true when a rotation is currently in progress. + */ fun isRotating(): Boolean + /** + * Convenience helper to stop rotation. Default implementation delegates to + * [onRotationEnd]. Implementations may override to perform additional work. + */ fun stopRotation() { onRotationEnd() } diff --git a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt b/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt index 7f605760..8034c764 100644 --- a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt +++ b/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt @@ -4,6 +4,16 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.WorldRenderEvent +/** + * Global manager for the currently active rotation controller. + * + * The manager holds an [IRotation] implementation which performs rotation + * logic. Use [setActiveRotation] to switch controllers, [getActiveRotation] + * to query the current controller and [resetRotation] to return to the + * default implementation. This object registers itself on the event bus and + * forwards per-frame world render updates to the active rotation when + * applicable. + */ object RotationManager { private var rotation: IRotation = DefaultRotations @@ -12,6 +22,11 @@ object RotationManager { EventBus.register(this) } + /** + * Make [newRotation] the active rotation controller and start a rotation + * towards the provided [yaw] and [pitch]. If a rotation is already in + * progress it will be stopped before switching controllers. + */ fun setActiveRotation(newRotation: IRotation, yaw: Double, pitch: Double) { if (rotation.isRotating()) { rotation.stopRotation() @@ -21,8 +36,15 @@ object RotationManager { rotation.onRotationStart(yaw, pitch) } + /** + * Returns the currently active [IRotation] controller. + */ fun getActiveRotation(): IRotation = rotation + /** + * Reset the active rotation controller to the default implementation and + * stop any ongoing rotation. + */ fun resetRotation() { if (rotation.isRotating()) { rotation.stopRotation() @@ -31,6 +53,10 @@ object RotationManager { rotation = DefaultRotations } + /** + * Event handler invoked during world rendering. Forwards the render event + * to the active rotation controller when a rotation is in progress. + */ @SubscribeEvent fun onWorldRender(event: WorldRenderEvent) { if (!rotation.isRotating()) return diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt index 9977b274..74b3f9f6 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt @@ -11,16 +11,28 @@ import java.nio.file.Files import kotlinx.coroutines.runBlocking import org.cobalt.util.WebUtils -/** Represents a lazily-loaded image resource; - * supports raster images and SVGs with caching and optional rounding/color masks. +/** + * Represents a lazily-loaded image resource. Supports both raster images and + * SVG documents. Raster images are loaded as deferred Skia [Image] objects, + * while SVGs are parsed into an [SVGDOM] and can be rasterized on demand. + * + * @property radius optional corner radius to apply when rendering the image + * @property colorMask optional ARGB color mask applied when drawing */ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: Int? = null) { - /** True when the identifier points to an SVG resource. */ + /** True when the identifier points to an SVG resource (case-insensitive). */ val isSvg = identifier.endsWith(".svg", ignoreCase = true) - /** Deferred Skia Image for raster formats; null for SVG resources. */ + + /** + * Deferred Skia [Image] used for raster formats (png/jpg/etc.). This will be + * null for SVG resources. + */ val image: Image? - /** Parsed SVG DOM for SVG resources; null for raster images. */ + + /** + * Parsed [SVGDOM] for SVG resources. Null for raster images. + */ val svgDom: SVGDOM? private var cachedRaster: Image? = null @@ -39,8 +51,18 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In } } - /** Return a raster Image sized to the requested width/height. - * For SVGs this will generate and cache a raster snapshot. + /** + * Return a raster [Image] sized to the requested [width]/[height]. + * + * For raster inputs this returns the deferred image (no resizing). For SVG + * inputs this will render the SVG to a raster surface, cache the generated + * snapshot and return it. Subsequent calls with the same dimensions will + * return the cached snapshot. + * + * @param width desired pixel width of the rasterized image + * @param height desired pixel height of the rasterized image + * @return a Skia [Image] at the requested dimensions, or null if the image + * could not be loaded or rendered */ fun getOrGenerateRaster(width: Int, height: Int): Image? { if (!isSvg) return image @@ -70,7 +92,11 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In return cachedRaster } - /** Release any native image resources held by this instance. */ + /** + * Release any native resources (Skia images and DOMs) held by this + * instance. After calling this method the instance should not be used to + * produce images. + */ fun delete() { image?.close() svgDom?.close() diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt index de23c18f..f06b8416 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt @@ -36,6 +36,10 @@ object SkiaRenderer { private val images = mutableMapOf() private var scissorStackDepth = 0 + /** + * Primary UI font used throughout the client. Loaded and cached at + * initialization from the bundled assets. + */ val primaryFont = loadFont("assets/cobalt/font/ProductSans-Bold.ttf") private val canvas: Canvas? diff --git a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt index 3536bbd8..73fe1bc9 100644 --- a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt +++ b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt @@ -22,6 +22,18 @@ import io.github.humbleui.skija.BackendRenderTarget import io.github.humbleui.skija.impl.Stats import org.jetbrains.annotations.Contract +/** + * A thin wrapper around Skija's [BackendRenderTarget] that carries additional + * information about the GL framebuffer used to back the render target. + * + * @property width framebuffer width in pixels + * @property height framebuffer height in pixels + * @property sampleCnt number of samples for multisampling + * @property stencilBits number of stencil bits in the framebuffer + * @property fbId OpenGL framebuffer id + * @property fbFormat OpenGL framebuffer format + * @param ptr native pointer passed to the Skija BackendRenderTarget base + */ class WrappedBackendRenderTarget( val width: Int, val height: Int, @@ -35,6 +47,19 @@ class WrappedBackendRenderTarget( companion object { @Contract("_, _, _, _, _, _ -> new") + /** + * Create a new [WrappedBackendRenderTarget] backed by an OpenGL framebuffer. + * + * The native helper [_nMakeGL] is invoked to allocate the platform-specific + * backend render target and its pointer is stored in the created wrapper. + * + * @param width framebuffer width in pixels + * @param height framebuffer height in pixels + * @param sampleCnt number of samples for multisampling + * @param stencilBits number of stencil bits in the framebuffer + * @param fbId OpenGL framebuffer id + * @param fbFormat OpenGL framebuffer format + */ fun makeGL( width: Int, height: Int, sampleCnt: Int, stencilBits: Int, fbId: Int, fbFormat: Int ): WrappedBackendRenderTarget { diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt b/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt index 30612cca..74744224 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt @@ -20,80 +20,156 @@ package org.cobalt.util.skia.gl import java.util.* +/** + * Container for cached OpenGL state used by the renderer. + * + * Each property holds the last known value for the corresponding GL state so + * the library can avoid redundant GL calls when the state is already set. + */ class Properties { + /** The last active texture unit (GL_ACTIVE_TEXTURE). Stored as a single-element IntArray. */ val lastActiveTexture = IntArray(1) + + /** The last linked/used program object (GL_CURRENT_PROGRAM). */ val lastProgram = IntArray(1) + + /** The last bound texture id. */ val lastTexture = IntArray(1) + + /** The last bound sampler id. */ val lastSampler = IntArray(1) + + /** The last bound array buffer id (GL_ARRAY_BUFFER). */ val lastArrayBuffer = IntArray(1) + + /** The last bound vertex array object id (GL_VERTEX_ARRAY). */ val lastVertexArrayObject = IntArray(1) + + /** The last polygon rasterization mode(s) (e.g., GL_POLYGON_MODE). Two elements for front and back. */ val lastPolygonMode = IntArray(2) + + /** The last viewport rectangle (x, y, width, height). */ val lastViewport = IntArray(4) + + /** The last scissor box rectangle (x, y, width, height). */ val lastScissorBox = IntArray(4) + + /** The last blend source factor for RGB (GL_BLEND_SRC_RGB). */ val lastBlendSrcRgb = IntArray(1) + + /** The last blend destination factor for RGB (GL_BLEND_DST_RGB). */ val lastBlendDstRgb = IntArray(1) + + /** The last blend source factor for alpha (GL_BLEND_SRC_ALPHA). */ val lastBlendSrcAlpha = IntArray(1) + + /** The last blend destination factor for alpha (GL_BLEND_DST_ALPHA). */ val lastBlendDstAlpha = IntArray(1) + + /** The last blend equation for RGB (GL_BLEND_EQUATION_RGB). */ val lastBlendEquationRgb = IntArray(1) + + /** The last blend equation for alpha (GL_BLEND_EQUATION_ALPHA). */ val lastBlendEquationAlpha = IntArray(1) + /** The last bound pixel unpack buffer (GL_PIXEL_UNPACK_BUFFER). */ val lastPixelUnpackBufferBinding = IntArray(1) + + /** The last pixel unpack alignment (GL_UNPACK_ALIGNMENT). */ val lastUnpackAlignment = IntArray(1) + + /** The last pixel unpack row length (GL_UNPACK_ROW_LENGTH). */ val lastUnpackRowLength = IntArray(1) + + /** The last pixel unpack skip pixels (GL_UNPACK_SKIP_PIXELS). */ val lastUnpackSkipPixels = IntArray(1) + + /** The last pixel unpack skip rows (GL_UNPACK_SKIP_ROWS). */ val lastUnpackSkipRows = IntArray(1) + + /** The last pack swap bytes flag (GL_PACK_SWAP_BYTES). */ val lastPackSwapBytes = IntArray(1) + + /** The last pack LSB first flag (GL_PACK_LSB_FIRST). */ val lastPackLsbFirst = IntArray(1) + + /** The last pack row length (GL_PACK_ROW_LENGTH). */ val lastPackRowLength = IntArray(1) + + /** The last pack image height (GL_PACK_IMAGE_HEIGHT). */ val lastPackImageHeight = IntArray(1) + + /** The last pack skip pixels (GL_PACK_SKIP_PIXELS). */ val lastPackSkipPixels = IntArray(1) + + /** The last pack skip rows (GL_PACK_SKIP_ROWS). */ val lastPackSkipRows = IntArray(1) + + /** The last pack skip images (GL_PACK_SKIP_IMAGES). */ val lastPackSkipImages = IntArray(1) + + /** The last pack alignment (GL_PACK_ALIGNMENT). */ val lastPackAlignment = IntArray(1) + + /** The last unpack swap bytes flag (GL_UNPACK_SWAP_BYTES). */ val lastUnpackSwapBytes = IntArray(1) + + /** The last unpack LSB first flag (GL_UNPACK_LSB_FIRST). */ val lastUnpackLsbFirst = IntArray(1) + + /** The last unpack image height (GL_UNPACK_IMAGE_HEIGHT). */ val lastUnpackImageHeight = IntArray(1) + + /** The last unpack skip images (GL_UNPACK_SKIP_IMAGES). */ val lastUnpackSkipImages = IntArray(1) + // Internal bitset for boolean GL state flags. private val flags = BitSet(7) + /** Whether blending was last enabled (GL_BLEND). */ var lastEnableBlend get() = flags[0] set(value) { flags[0] = value } + /** Whether face culling was last enabled (GL_CULL_FACE). */ var lastEnableCullFace get() = flags[1] set(value) { flags[1] = value } + /** Whether depth testing was last enabled (GL_DEPTH_TEST). */ var lastEnableDepthTest get() = flags[2] set(value) { flags[2] = value } + /** Whether stencil testing was last enabled (GL_STENCIL_TEST). */ var lastEnableStencilTest get() = flags[3] set(value) { flags[3] = value } + /** Whether scissor testing was last enabled (GL_SCISSOR_TEST). */ var lastEnableScissorTest get() = flags[4] set(value) { flags[4] = value } + /** Whether primitive restart was last enabled (GL_PRIMITIVE_RESTART). */ var lastEnablePrimitiveRestart get() = flags[5] set(value) { flags[5] = value } + /** The last depth mask value (whether depth writes were enabled). */ var lastDepthMask get() = flags[6] set(value) { diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt index db586c85..6568b97d 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt @@ -21,10 +21,30 @@ package org.cobalt.util.skia.gl import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45.* +/** + * Represents a snapshot of relevant OpenGL state that can be pushed and + * restored. The snapshot reads multiple GL bindings and pixel store + * parameters so rendering code can change GL state and then restore it to + * the previous values. + * + * @param glVersion computed GL version (major * 100 + minor * 10) used to + * determine which GL features are available when capturing/restoring state. + */ class State(private val glVersion: Int) { private val props = Properties() + /** + * Capture the current GL state into this [State] instance. + * + * This method queries a wide set of GL bindings (textures, buffers, + * vertex arrays), pixel store parameters and enabled/disabled flags and + * stores them inside the internal [Properties] object. It also resets a + * handful of pixel store parameters (unpack alignment/row/skip) to safe + * defaults required by the renderer. + * + * @return this [State] instance for convenience. + */ fun push(): State { with(props) { glGetIntegerv(GL_ACTIVE_TEXTURE, lastActiveTexture) @@ -97,6 +117,16 @@ class State(private val glVersion: Int) { return this } + /** + * Restore GL state previously captured by [push]. + * + * The stored values are applied back to the GL context, including bound + * program, textures, samplers, vertex arrays, pixel store parameters and + * enabled/disabled capabilities. The method returns this [State] + * instance for chaining if desired. + * + * @return this [State] instance after restoration. + */ fun pop(): State { with(props) { glUseProgram(lastProgram[0]) diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt index 783ed9ba..06eb0e29 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt @@ -23,15 +23,35 @@ import org.lwjgl.opengl.GL30.glGetIntegerv import org.lwjgl.opengl.GL30.GL_MAJOR_VERSION import org.lwjgl.opengl.GL30.GL_MINOR_VERSION +/** + * Utility that manages a stack of OpenGL state snapshots. + * + * Use [push] to capture the current GL state and [pop] to restore the most + * recently captured state. The object's initializer reads the OpenGL major + * and minor version and stores a computed integer representation for use by + * created [State] instances. + */ object States { private val glVersion: Int private val states = Stack() + /** + * Capture the current GL state and push a snapshot onto the internal stack. + * + * A new [State] is created using the GL version detected at startup and + * its [State.push] method is invoked to record the GL state. + */ fun push() { states += State(glVersion).push() } + /** + * Restore and remove the most recently pushed GL state snapshot. + * + * Throws an [IllegalArgumentException] if there is no saved state to + * restore. + */ fun pop() { require(states.isNotEmpty()) { "No state to restore." } states.pop().pop() From 46bf7cf1fa61ddeb1331d2f68147a56039648ccb Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 14 Apr 2026 22:03:49 +0200 Subject: [PATCH 09/18] refactor: remove the last magic numbers --- .../kotlin/org/cobalt/util/ServerUtils.kt | 12 +++- .../org/cobalt/util/skia/SkiaContext.kt | 19 +++++- .../kotlin/org/cobalt/util/skia/gl/State.kt | 65 +++++++++++++------ .../kotlin/org/cobalt/util/skia/gl/States.kt | 5 +- 4 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/org/cobalt/util/ServerUtils.kt b/src/main/kotlin/org/cobalt/util/ServerUtils.kt index bd7a56b5..b4f576c0 100644 --- a/src/main/kotlin/org/cobalt/util/ServerUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ServerUtils.kt @@ -7,6 +7,12 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent import org.cobalt.mixin.client.AbstractClientPlayerAccessor +private const val DEFAULT_TPS = 20f +private const val TPS_ALPHA = 0.05 +private const val TPS_BETA = 0.95 +private const val TPS_SCALE = 20000.0 +private const val TPS_MAX = 20.0 + /** * Utility object for retrieving and tracking server-side metrics such as * average TPS (ticks per second) and the current player ping. @@ -24,7 +30,7 @@ object ServerUtils { * smoothed over time to avoid rapid fluctuations. The setter is private; the * value should be read-only from callers. */ - var averageTps = 20f + var averageTps = DEFAULT_TPS private set /** @@ -60,8 +66,8 @@ object ServerUtils { if (delta <= 0) return - val tps = (20000.0 / delta).coerceIn(0.0, 20.0) - averageTps = (averageTps * 0.95 + tps * 0.05).toFloat() + val tps = (TPS_SCALE / delta).coerceIn(0.0, TPS_MAX) + averageTps = (averageTps * TPS_BETA + tps * TPS_ALPHA).toFloat() } } diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt index cbb62b59..c09661a2 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt @@ -30,6 +30,14 @@ import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.util.skia.gl.States import org.lwjgl.opengl.GL11 +private const val DEFAULT_SAMPLES = 0 +private const val DEFAULT_STENCIL_BITS = 8 +private const val DEFAULT_PREFER_SAMPLES = 0 +private const val CLEAR_R = 0f +private const val CLEAR_G = 0f +private const val CLEAR_B = 0f +private const val CLEAR_A_TRANSPARENT = 0f + internal object SkiaContext { private var context: DirectContext? = null @@ -51,7 +59,14 @@ internal object SkiaContext { surface?.close() renderTarget?.close() - renderTarget = WrappedBackendRenderTarget.makeGL(width, height, 0, 8, 0, FramebufferFormat.GR_GL_RGBA8) + renderTarget = WrappedBackendRenderTarget.makeGL( + width, + height, + DEFAULT_SAMPLES, + DEFAULT_STENCIL_BITS, + DEFAULT_PREFER_SAMPLES, + FramebufferFormat.GR_GL_RGBA8 + ) surface = Surface.wrapBackendRenderTarget( requireNotNull(context), requireNotNull(renderTarget), @@ -68,7 +83,7 @@ internal object SkiaContext { States.push() GL11.glDisable(GL11.GL_CULL_FACE) - GL11.glClearColor(0f, 0f, 0f, 0f) + GL11.glClearColor(CLEAR_R, CLEAR_G, CLEAR_B, CLEAR_A_TRANSPARENT) context?.resetGLAll() diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt index 6568b97d..5fcb3613 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt @@ -21,6 +21,28 @@ package org.cobalt.util.skia.gl import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45.* +private const val GL_VERSION_3_3 = 330 +private const val GL_VERSION_3_1 = 310 +private const val GL_VERSION_2_0 = 200 +private const val GL_VERSION_1_2 = 120 + +private const val DEFAULT_PIXEL_UNPACK_BINDING = 0 +private const val DEFAULT_SAMPLER_UNIT = 0 + +private const val DEFAULT_UNPACK_ALIGNMENT = 1 +private const val DEFAULT_UNPACK_ROW_LENGTH = 0 +private const val DEFAULT_UNPACK_SKIP_PIXELS = 0 +private const val DEFAULT_UNPACK_SKIP_ROWS = 0 +private const val VIEWPORT_X = 0 +private const val VIEWPORT_Y = 1 +private const val VIEWPORT_W = 2 +private const val VIEWPORT_H = 3 + +private const val SCISSOR_X = 0 +private const val SCISSOR_Y = 1 +private const val SCISSOR_W = 2 +private const val SCISSOR_H = 3 + /** * Represents a snapshot of relevant OpenGL state that can be pushed and * restored. The snapshot reads multiple GL bindings and pixel store @@ -52,14 +74,14 @@ class State(private val glVersion: Int) { glGetIntegerv(GL_CURRENT_PROGRAM, lastProgram) glGetIntegerv(GL_TEXTURE_BINDING_2D, lastTexture) - if (glVersion >= 330 || GL.getCapabilities().GL_ARB_sampler_objects) { + if (glVersion >= GL_VERSION_3_3 || GL.getCapabilities().GL_ARB_sampler_objects) { glGetIntegerv(GL_SAMPLER_BINDING, lastSampler) } glGetIntegerv(GL_ARRAY_BUFFER_BINDING, lastArrayBuffer) glGetIntegerv(GL_VERTEX_ARRAY_BINDING, lastVertexArrayObject) - if (glVersion >= 200) { + if (glVersion >= GL_VERSION_2_0) { glGetIntegerv(GL_POLYGON_MODE, lastPolygonMode) } @@ -78,14 +100,14 @@ class State(private val glVersion: Int) { lastEnableStencilTest = glIsEnabled(GL_STENCIL_TEST) lastEnableScissorTest = glIsEnabled(GL_SCISSOR_TEST) - if (glVersion >= 310) { + if (glVersion >= GL_VERSION_3_1) { lastEnablePrimitiveRestart = glIsEnabled(GL_PRIMITIVE_RESTART) } lastDepthMask = glGetBoolean(GL_DEPTH_WRITEMASK) glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, lastPixelUnpackBufferBinding) - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0) + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, DEFAULT_PIXEL_UNPACK_BINDING) glGetIntegerv(GL_PACK_SWAP_BYTES, lastPackSwapBytes) glGetIntegerv(GL_PACK_LSB_FIRST, lastPackLsbFirst) @@ -101,17 +123,17 @@ class State(private val glVersion: Int) { glGetIntegerv(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels) glGetIntegerv(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows) - if (glVersion >= 120) { + if (glVersion >= GL_VERSION_1_2) { glGetIntegerv(GL_PACK_IMAGE_HEIGHT, lastPackImageHeight) glGetIntegerv(GL_PACK_SKIP_IMAGES, lastPackSkipImages) glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, lastUnpackImageHeight) glGetIntegerv(GL_UNPACK_SKIP_IMAGES, lastUnpackSkipImages) } - glPixelStorei(GL_UNPACK_ALIGNMENT, 1) - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0) - glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0) - glPixelStorei(GL_UNPACK_SKIP_ROWS, 0) + glPixelStorei(GL_UNPACK_ALIGNMENT, DEFAULT_UNPACK_ALIGNMENT) + glPixelStorei(GL_UNPACK_ROW_LENGTH, DEFAULT_UNPACK_ROW_LENGTH) + glPixelStorei(GL_UNPACK_SKIP_PIXELS, DEFAULT_UNPACK_SKIP_PIXELS) + glPixelStorei(GL_UNPACK_SKIP_ROWS, DEFAULT_UNPACK_SKIP_ROWS) } return this @@ -132,8 +154,8 @@ class State(private val glVersion: Int) { glUseProgram(lastProgram[0]) glBindTexture(GL_TEXTURE_2D, lastTexture[0]) - if (glVersion >= 330 || GL.getCapabilities().GL_ARB_sampler_objects) { - glBindSampler(0, lastSampler[0]) + if (glVersion >= GL_VERSION_3_3 || GL.getCapabilities().GL_ARB_sampler_objects) { + glBindSampler(DEFAULT_SAMPLER_UNIT, lastSampler[0]) } glActiveTexture(lastActiveTexture[0]) @@ -158,21 +180,26 @@ class State(private val glVersion: Int) { if (lastEnableScissorTest) glEnable(GL_SCISSOR_TEST) else glDisable(GL_SCISSOR_TEST) - if (glVersion >= 310) { + if (glVersion >= GL_VERSION_3_1) { if (lastEnablePrimitiveRestart) glEnable(GL_PRIMITIVE_RESTART) else glDisable(GL_PRIMITIVE_RESTART) } - if (glVersion >= 200) { + if (glVersion >= GL_VERSION_2_0) { glPolygonMode(GL_FRONT_AND_BACK, lastPolygonMode[0]) } - glViewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3]) + glViewport( + lastViewport[VIEWPORT_X], + lastViewport[VIEWPORT_Y], + lastViewport[VIEWPORT_W], + lastViewport[VIEWPORT_H] + ) glScissor( - lastScissorBox[0], - lastScissorBox[1], - lastScissorBox[2], - lastScissorBox[3] + lastScissorBox[SCISSOR_X], + lastScissorBox[SCISSOR_Y], + lastScissorBox[SCISSOR_W], + lastScissorBox[SCISSOR_H] ) glPixelStorei(GL_PACK_SWAP_BYTES, lastPackSwapBytes[0]) @@ -190,7 +217,7 @@ class State(private val glVersion: Int) { glPixelStorei(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels[0]) glPixelStorei(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows[0]) - if (glVersion >= 120) { + if (glVersion >= GL_VERSION_1_2) { glPixelStorei(GL_PACK_IMAGE_HEIGHT, lastPackImageHeight[0]) glPixelStorei(GL_PACK_SKIP_IMAGES, lastPackSkipImages[0]) glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, lastUnpackImageHeight[0]) diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt index 06eb0e29..5ef1a242 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt @@ -23,6 +23,9 @@ import org.lwjgl.opengl.GL30.glGetIntegerv import org.lwjgl.opengl.GL30.GL_MAJOR_VERSION import org.lwjgl.opengl.GL30.GL_MINOR_VERSION +private const val GL_MAJOR_MULTIPLIER = 100 +private const val GL_MINOR_MULTIPLIER = 10 + /** * Utility that manages a stack of OpenGL state snapshots. * @@ -62,7 +65,7 @@ object States { val minor = IntArray(1) glGetIntegerv(GL_MAJOR_VERSION, major) glGetIntegerv(GL_MINOR_VERSION, minor) - glVersion = major[0] * 100 + minor[0] * 10 + glVersion = major[0] * GL_MAJOR_MULTIPLIER + minor[0] * GL_MINOR_MULTIPLIER } } From 1ade2efee0b6890f72b7e5ee8edcb7aa9b293149 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 18 Apr 2026 17:16:20 +0200 Subject: [PATCH 10/18] refactor: fix even more detekt issues --- config/detekt/detekt.yml | 4 +- .../cobalt/mixin/client/MinecraftMixin.java | 2 +- .../cobalt/mixin/platform/WindowMixin.java | 2 +- src/main/kotlin/org/cobalt/command/Command.kt | 23 +- .../org/cobalt/command/CommandManager.kt | 10 +- src/main/kotlin/org/cobalt/dsl/Render.kt | 4 +- .../org/cobalt/event/impl/SkiaDrawEvent.kt | 2 +- .../kotlin/org/cobalt/module/ModuleManager.kt | 4 +- .../module/impl/render/PerformanceHUD.kt | 21 +- .../cobalt/{util => render}/RenderUtils.kt | 27 +- .../{util => render}/skia/SkiaContext.kt | 4 +- .../cobalt/{util => render}/skia/SkiaEnums.kt | 2 +- .../cobalt/{util => render}/skia/SkiaImage.kt | 2 +- .../org/cobalt/render/skia/SkiaImages.kt | 89 ++++ .../org/cobalt/render/skia/SkiaRenderer.kt | 40 ++ .../org/cobalt/render/skia/SkiaShapes.kt | 284 ++++++++++++ .../kotlin/org/cobalt/render/skia/SkiaText.kt | 65 +++ .../skia/WrappedBackendRenderTarget.kt | 2 +- .../{util => render}/skia/gl/Properties.kt | 2 +- .../cobalt/{util => render}/skia/gl/State.kt | 50 ++- .../cobalt/{util => render}/skia/gl/States.kt | 2 +- .../ui/notification/NotificationManager.kt | 4 +- .../org/cobalt/ui/screen/ConfigScreen.kt | 13 +- .../org/cobalt/ui/screen/HudEditorScreen.kt | 2 +- .../org/cobalt/util/helper/TickScheduler.kt | 2 +- .../cobalt/util/rotation/RotationManager.kt | 2 +- .../org/cobalt/util/skia/SkiaRenderer.kt | 403 ------------------ 27 files changed, 602 insertions(+), 465 deletions(-) rename src/main/kotlin/org/cobalt/{util => render}/RenderUtils.kt (93%) rename src/main/kotlin/org/cobalt/{util => render}/skia/SkiaContext.kt (97%) rename src/main/kotlin/org/cobalt/{util => render}/skia/SkiaEnums.kt (96%) rename src/main/kotlin/org/cobalt/{util => render}/skia/SkiaImage.kt (99%) create mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt create mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt create mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt create mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaText.kt rename src/main/kotlin/org/cobalt/{util => render}/skia/WrappedBackendRenderTarget.kt (98%) rename src/main/kotlin/org/cobalt/{util => render}/skia/gl/Properties.kt (99%) rename src/main/kotlin/org/cobalt/{util => render}/skia/gl/State.kt (92%) rename src/main/kotlin/org/cobalt/{util => render}/skia/gl/States.kt (98%) delete mode 100644 src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 2908b74d..de06dd78 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -23,7 +23,7 @@ style: maxLineLength: 120 excludePackageStatements: true excludeImportStatements: true - excludeCommentStatements: false + excludeCommentStatements: true ReturnCount: active: true @@ -61,7 +61,7 @@ complexity: NestedBlockDepth: active: true - allowedDepth: 2 + allowedDepth: 3 LongParameterList: active: true diff --git a/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java b/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java index b5aa0814..cea354d9 100644 --- a/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java +++ b/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java @@ -23,7 +23,7 @@ import org.cobalt.Cobalt; import org.cobalt.event.EventBus; import org.cobalt.event.impl.TickEvent; -import org.cobalt.util.skia.SkiaContext; +import org.cobalt.render.skia.SkiaContext; import org.lwjgl.glfw.GLFW; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java index 7dccad38..aa1cfaf4 100644 --- a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java +++ b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java @@ -19,7 +19,7 @@ package org.cobalt.mixin.platform; import com.mojang.blaze3d.platform.Window; -import org.cobalt.util.skia.SkiaContext; +import org.cobalt.render.skia.SkiaContext; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index d65c5318..0420b708 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -8,6 +8,7 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder import kotlin.reflect.KFunction +import kotlin.reflect.KParameter import kotlin.reflect.full.declaredFunctions import kotlin.reflect.full.findAnnotation import kotlin.reflect.jvm.isAccessible @@ -48,9 +49,11 @@ abstract class Command(val name: String) { /** Construct a subcommand literal from a handler function and its parameters. */ private fun buildSubCommand(function: KFunction<*>): LiteralArgumentBuilder { val literal = LiteralArgumentBuilder.literal(function.name) - val params = function.parameters.drop(1) - if (params.isEmpty()) { + val parameters = function.parameters + val valueParams = parameters.filter { it.kind == KParameter.Kind.VALUE } + + if (valueParams.isEmpty()) { literal.executes { function.call(this) return@executes 1 @@ -59,13 +62,13 @@ abstract class Command(val name: String) { return literal } - val arguments = params.mapIndexed { index, param -> + val arguments = valueParams.mapIndexed { index, param -> val name = param.name ?: "argument$index" createArgument(name, param.type.classifier) } arguments.last().executes { ctx -> - val mappedArgs = params.mapIndexed { index, param -> + val mappedValues = valueParams.mapIndexed { index, param -> val argumentName = param.name ?: "argument$index" when (param.type.classifier) { Double::class -> DoubleArgumentType.getDouble(ctx, argumentName) @@ -77,7 +80,17 @@ abstract class Command(val name: String) { } } - function.call(this, *mappedArgs.toTypedArray()) + val argsMap = mutableMapOf() + + val instanceParam = parameters.firstOrNull { it.kind == KParameter.Kind.INSTANCE } + if (instanceParam != null) argsMap[instanceParam] = this + + for (i in valueParams.indices) { + argsMap[valueParams[i]] = mappedValues[i] + } + + function.callBy(argsMap) + return@executes 1 } diff --git a/src/main/kotlin/org/cobalt/command/CommandManager.kt b/src/main/kotlin/org/cobalt/command/CommandManager.kt index 7d8ffb58..48df41e5 100644 --- a/src/main/kotlin/org/cobalt/command/CommandManager.kt +++ b/src/main/kotlin/org/cobalt/command/CommandManager.kt @@ -5,14 +5,11 @@ import net.minecraft.ChatFormatting import net.minecraft.client.multiplayer.ClientSuggestionProvider import org.cobalt.Cobalt.minecraft import org.cobalt.util.ChatUtils -import org.slf4j.LoggerFactory - +import com.mojang.brigadier.exceptions.CommandSyntaxException /** Central command dispatcher and helpers for registering and executing chat commands. */ object CommandManager { - private val logger = LoggerFactory.getLogger(this::class.java) - /** Brigadier dispatcher used to register command trees. */ @JvmStatic val dispatcher = CommandDispatcher() @@ -39,9 +36,8 @@ object CommandManager { try { dispatcher.execute(commandLine, player.connection.suggestionsProvider) - } catch (exception: Exception) { - logger.error("Error while executing command: $commandLine", exception) - ChatUtils.sendSystemMessage("${ChatFormatting.RED}Something went wrong when executing the command") + } catch (exception: CommandSyntaxException) { + ChatUtils.sendSystemMessage("${ChatFormatting.RED}${exception.message}") } } diff --git a/src/main/kotlin/org/cobalt/dsl/Render.kt b/src/main/kotlin/org/cobalt/dsl/Render.kt index 4b8a92d7..d8a291b3 100644 --- a/src/main/kotlin/org/cobalt/dsl/Render.kt +++ b/src/main/kotlin/org/cobalt/dsl/Render.kt @@ -6,7 +6,7 @@ import net.minecraft.core.BlockPos import net.minecraft.world.entity.Entity import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 -import org.cobalt.util.RenderUtils +import org.cobalt.render.RenderUtils /** Draw a wireframe outline of a block at the given world block position. */ fun LevelRenderContext.drawBlockPos(pos: BlockPos, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = @@ -26,4 +26,4 @@ fun LevelRenderContext.drawBox(box: AABB, color: Color, esp: Boolean = false, li /** Draw a colored line between two world-space points using the renderer context. */ fun LevelRenderContext.drawLine(from: Vec3, to: Vec3, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = - RenderUtils.drawLine(this, from, to, color, esp, lineWidth) + RenderUtils.drawLine(this, from, to, color, RenderUtils.LineStyle(esp, lineWidth)) diff --git a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt index d407a403..02010af6 100644 --- a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt @@ -3,7 +3,7 @@ package org.cobalt.event.impl import io.github.humbleui.skija.Canvas import io.github.humbleui.skija.DirectContext import org.cobalt.event.Event -import org.cobalt.util.skia.WrappedBackendRenderTarget +import org.cobalt.render.skia.WrappedBackendRenderTarget /** Event fired when Skia drawing is performed for a render pass. */ class SkiaDrawEvent( diff --git a/src/main/kotlin/org/cobalt/module/ModuleManager.kt b/src/main/kotlin/org/cobalt/module/ModuleManager.kt index 4a203d0e..7360519d 100644 --- a/src/main/kotlin/org/cobalt/module/ModuleManager.kt +++ b/src/main/kotlin/org/cobalt/module/ModuleManager.kt @@ -5,7 +5,7 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.module.impl.render.PerformanceHUD -import org.cobalt.util.skia.SkiaRenderer +import org.cobalt.render.skia.SkiaRenderer /** Manager responsible for registering, storing and dispatching modules. */ object ModuleManager { @@ -50,7 +50,7 @@ object ModuleManager { /** Draw all enabled modules that implement renderable behavior during the Skia render pass. */ @SubscribeEvent - fun drawRenderableModules(event: SkiaDrawEvent) { + fun drawRenderableModules(@Suppress("UnusedParameter") event: SkiaDrawEvent) { if (minecraft.level == null) { return } diff --git a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt index 8c76f556..2de915f5 100644 --- a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt +++ b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt @@ -6,7 +6,8 @@ import org.cobalt.module.ModuleCategory import org.cobalt.module.RenderableModule import org.cobalt.ui.ColorPalette import org.cobalt.util.ServerUtils -import org.cobalt.util.skia.SkiaRenderer +import org.cobalt.render.skia.SkiaShapes +import org.cobalt.render.skia.SkiaText private const val DEFAULT_OFFSET = 5.0f @@ -32,8 +33,8 @@ object PerformanceHUD : RenderableModule( val height = getHeight() val centerY = yPos + height / 2 - SkiaRenderer.roundedRect(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.PANEL) - SkiaRenderer.roundedOutline(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.BORDER, OUTLINE_THICKNESS) + SkiaShapes.roundedRect(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.PANEL) + SkiaShapes.roundedOutline(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.BORDER, OUTLINE_THICKNESS) var currentX = xPos + PADDING val textY = centerY - FONT_SIZE / 2 @@ -43,7 +44,7 @@ object PerformanceHUD : RenderableModule( currentX += DIVIDER_GAP val midY = yPos + height * MID_FACTOR - SkiaRenderer.line( + SkiaShapes.line( currentX, currentX, midY - DIVIDER_HALF_HEIGHT, @@ -55,11 +56,11 @@ object PerformanceHUD : RenderableModule( currentX += DIVIDER_GAP } - SkiaRenderer.text(SkiaRenderer.primaryFont, stat.value, currentX, textY, FONT_SIZE, ColorPalette.TEXT_PRIMARY) - currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + SkiaText.text(SkiaText.primaryFont, stat.value, currentX, textY, SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_PRIMARY)) + currentX += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - SkiaRenderer.text(SkiaRenderer.primaryFont, stat.unit, currentX, textY, FONT_SIZE, ColorPalette.TEXT_DISABLED) - currentX += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, FONT_SIZE) + SkiaText.text(SkiaText.primaryFont, stat.unit, currentX, textY, SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_DISABLED)) + currentX += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) } } @@ -71,8 +72,8 @@ object PerformanceHUD : RenderableModule( width += PADDING + 2 * TEXT_SPACING } - width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - width += SkiaRenderer.textWidth(SkiaRenderer.primaryFont, stat.unit, FONT_SIZE) + width += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + width += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) } return width diff --git a/src/main/kotlin/org/cobalt/util/RenderUtils.kt b/src/main/kotlin/org/cobalt/render/RenderUtils.kt similarity index 93% rename from src/main/kotlin/org/cobalt/util/RenderUtils.kt rename to src/main/kotlin/org/cobalt/render/RenderUtils.kt index 84de84f8..e71bb1bc 100644 --- a/src/main/kotlin/org/cobalt/util/RenderUtils.kt +++ b/src/main/kotlin/org/cobalt/render/RenderUtils.kt @@ -1,20 +1,27 @@ -package org.cobalt.util +package org.cobalt.render import java.awt.Color import kotlin.math.max import kotlin.math.min import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext -import net.minecraft.client.Minecraft import net.minecraft.core.BlockPos import net.minecraft.world.entity.Entity import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 import org.cobalt.Cobalt.minecraft +import org.cobalt.util.FrustumUtils import org.cobalt.util.helper.Layers /** Utility rendering helpers for drawing boxes, outlines, tracers and lines in world space. */ object RenderUtils { - private const val ALPHA = 100; + private const val ALPHA = 100 + + /** Style options for drawing lines. + * + * @param esp when true uses ESP render type variants + * @param lineWidth line thickness + */ + data class LineStyle(val esp: Boolean = false, val lineWidth: Float = 1f) /** Draw a unit cube wireframe and optional translucent fill at the given block position. * @@ -92,7 +99,7 @@ object RenderUtils { val cameraPos = camera.position() val from = cameraPos.add(Vec3.directionFromRotation(camera.xRot(), camera.yRot())) - drawLine(context, from, to, color, esp, lineWidth) + drawLine(context, from, to, color, LineStyle(esp, lineWidth)) } /** Draw a colored axis-aligned bounding box (AABB) with optional translucent fill and outline. @@ -178,8 +185,7 @@ object RenderUtils { * @param from start point in world coordinates * @param to end point in world coordinates * @param color the color to use for the line - * @param esp when true uses ESP render type variants - * @param lineWidth line thickness + * @param style line drawing style (esp and lineWidth) */ @JvmStatic fun drawLine( @@ -187,8 +193,7 @@ object RenderUtils { from: Vec3, to: Vec3, color: Color, - esp: Boolean = false, - lineWidth: Float = 1f, + style: LineStyle = LineStyle(), ) { if (color.alpha == 0) { return @@ -211,7 +216,7 @@ object RenderUtils { val cameraPos = minecraft.gameRenderer.mainCamera.position() val poseEntry = poseStack.last() val matrix = poseEntry.pose() - val lineBuffer = bufferSource.getBuffer(Layers.getLines(esp)) + val lineBuffer = bufferSource.getBuffer(Layers.getLines(style.esp)) val lineNormal = to.subtract(from).normalize() for (vertex in listOf(from, to)) { @@ -222,12 +227,12 @@ object RenderUtils { (vertex.y - cameraPos.y).toFloat(), (vertex.z - cameraPos.z).toFloat() ) - .setLineWidth(lineWidth) + .setLineWidth(style.lineWidth) .setColor(color.red, color.green, color.blue, color.alpha) .setNormal(poseEntry, lineNormal.x.toFloat(), lineNormal.y.toFloat(), lineNormal.z.toFloat()) } - bufferSource.endBatch(Layers.getLines(esp)) + bufferSource.endBatch(Layers.getLines(style.esp)) } private val BOX_QUADS = intArrayOf( diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt similarity index 97% rename from src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt rename to src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt index c09661a2..eb6d5ab4 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.util.skia +package org.cobalt.render.skia import io.github.humbleui.skija.Canvas import io.github.humbleui.skija.ColorSpace @@ -27,7 +27,7 @@ import io.github.humbleui.skija.Surface import io.github.humbleui.skija.SurfaceOrigin import org.cobalt.event.EventBus import org.cobalt.event.impl.SkiaDrawEvent -import org.cobalt.util.skia.gl.States +import org.cobalt.render.skia.gl.States import org.lwjgl.opengl.GL11 private const val DEFAULT_SAMPLES = 0 diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt similarity index 96% rename from src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt rename to src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt index c57739dc..7431054e 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt @@ -1,4 +1,4 @@ -package org.cobalt.util.skia +package org.cobalt.render.skia /** * Lightweight enums used by the Skia-based renderer helpers. diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt similarity index 99% rename from src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt rename to src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt index 74b3f9f6..aa212744 100644 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt @@ -1,4 +1,4 @@ -package org.cobalt.util.skia +package org.cobalt.render.skia import io.github.humbleui.skija.Data import io.github.humbleui.skija.Image diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt new file mode 100644 index 00000000..8c64204f --- /dev/null +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt @@ -0,0 +1,89 @@ +package org.cobalt.render.skia + +import io.github.humbleui.skija.BlendMode +import io.github.humbleui.skija.ClipMode +import io.github.humbleui.skija.ColorFilter +import io.github.humbleui.skija.Paint +import io.github.humbleui.skija.SamplingMode +import io.github.humbleui.types.RRect +import io.github.humbleui.types.Rect + +/** Utilities for loading and drawing cached Skia images. + * Images may be rounded and color-masked when drawn. + */ +object SkiaImages { + private data class ImageCacheKey( + val identifier: String, + val radius: Float?, + val colorMask: Int?, + ) + + private val images = mutableMapOf() + private val canvas get() = SkiaContext.canvas + + /** + * Load or create a cached image for the given identifier. + * + * @param identifier resource identifier for the image + * @param radius optional corner radius to apply when rendering + * @param colorMask optional ARGB color mask to blend over the image + * @return a cached or newly created [SkiaImage] + */ + @JvmStatic + fun loadImage( + identifier: String, + radius: Float? = null, + colorMask: Int? = null, + ): SkiaImage { + val normalizedRadius = radius?.takeIf { it > 0f } + val key = ImageCacheKey(identifier, normalizedRadius, colorMask) + + return images.computeIfAbsent(key) { + SkiaImage(identifier, normalizedRadius, colorMask) + } + } + + /** + * Draw the provided [SkiaImage] into the destination rectangle. + * + * @param image the cached image to draw + * @param x destination x coordinate + * @param y destination y coordinate + * @param width destination width in pixels + * @param height destination height in pixels + */ + @JvmStatic + fun image(image: SkiaImage, x: Float, y: Float, width: Float, height: Float) { + val canvas = this.canvas ?: return + if (width <= 0 || height <= 0) return + + val sourceImage = image.getOrGenerateRaster(width.toInt(), height.toInt()) ?: return + + Paint().use { paint -> + image.colorMask?.let { + paint.colorFilter = ColorFilter.makeBlend(it, BlendMode.SRC_ATOP) + } + + if (image.radius != null && image.radius > 0f) { + canvas.save() + canvas.clipRRect(RRect.makeXYWH(x, y, width, height, image.radius), ClipMode.INTERSECT, true) + } + + canvas.drawImageRect( + sourceImage, + Rect.makeWH(sourceImage.width.toFloat(), sourceImage.height.toFloat()), + Rect.makeXYWH(x, y, width, height), + SamplingMode.MITCHELL, + paint, + false + ) + + if (image.radius != null && image.radius > 0f) { + canvas.restore() + } + } + } + +} + + diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt new file mode 100644 index 00000000..5f0b389d --- /dev/null +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt @@ -0,0 +1,40 @@ +package org.cobalt.render.skia + +import io.github.humbleui.skija.Canvas +import org.cobalt.Cobalt.minecraft + + /** High-level Skia drawing helpers used by UI and module renderers. + * Provides convenience functions for text, shapes, images and scissor management. + */ +object SkiaRenderer { + private const val BASE_WIDTH = 1920f + private const val BASE_HEIGHT = 1080f + + private val canvas: Canvas? + get() = SkiaContext.canvas + + /** Calculate a window scale factor relative to a 1920x1080 baseline for consistent UI sizing. */ + fun getWindowScale(): Float { + val windowWidth = minecraft.window.width.toFloat() + val windowHeight = minecraft.window.height.toFloat() + + return minOf(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) + } + + /** Save the current Skia canvas state. */ + @JvmStatic + fun save() = this.canvas?.save() + + /** Restore the previously saved Skia canvas state. */ + @JvmStatic + fun restore() = this.canvas?.restore() + + /** Translate the canvas by the given x/y offset. */ + @JvmStatic + fun translate(x: Float, y: Float) = this.canvas?.translate(x, y) + + /** Scale the canvas by the specified X and Y factors. */ + @JvmStatic + fun scale(x: Float, y: Float) = this.canvas?.scale(x, y) + +} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt new file mode 100644 index 00000000..906606ba --- /dev/null +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt @@ -0,0 +1,284 @@ +package org.cobalt.render.skia + +import io.github.humbleui.skija.ClipMode +import io.github.humbleui.skija.Paint +import io.github.humbleui.skija.PaintMode +import io.github.humbleui.skija.Shader +import io.github.humbleui.types.RRect +import io.github.humbleui.types.Rect + +/** Shape and scissor drawing helpers backed by the Skia canvas. */ +object SkiaShapes { + private var scissorStackDepth = 0 + private val canvas get() = SkiaContext.canvas + + /** + * Push a scissor/clip rectangle onto the canvas stack. Subsequent draws + * will be clipped to the rectangle. + * + * @param x clip rectangle x coordinate + * @param y clip rectangle y coordinate + * @param width clip rectangle width + * @param height clip rectangle height + */ + @JvmStatic + fun pushScissor(x: Float, y: Float, width: Float, height: Float) { + val canvas = this.canvas ?: return + + if (width <= 0 || height <= 0) return + + canvas.save() + canvas.clipRect(Rect.makeXYWH(x, y, width, height), ClipMode.INTERSECT, true) + scissorStackDepth++ + } + + /** Pop the last scissor/clip rectangle and restore the previous canvas state. */ + @JvmStatic + fun popScissor() { + if (scissorStackDepth <= 0) return + canvas?.restore() + scissorStackDepth-- + } + + /** + * Draw a straight line between two points. + * + * @param x1 start x + * @param x2 end x + * @param y1 start y + * @param y2 end y + * @param color ARGB color + * @param thickness line thickness + */ + @JvmStatic + fun line(x1: Float, x2: Float, y1: Float, y2: Float, color: Int, thickness: Float = 1f) { + val canvas = this.canvas ?: return + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = thickness.coerceAtLeast(0f) + isAntiAlias = true + }.use { paint -> + canvas.drawLine(x1, y1, x2, y2, paint) + } + } + + /** Draw a filled rectangle. */ + @JvmStatic + fun rect(x: Float, y: Float, width: Float, height: Float, color: Int) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + Paint().setColor(color).use { paint -> + canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) + } + } + + /** + * Draw a stroked rectangle outline with the given thickness. + * + * @param x rectangle x + * @param y rectangle y + * @param width rectangle width + * @param height rectangle height + * @param color ARGB color for the outline + * @param thickness outline thickness + */ + @JvmStatic + fun outline(x: Float, y: Float, width: Float, height: Float, color: Int, thickness: Float = 1f) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + val t = thickness.coerceAtLeast(0f) + val half = t / 2f + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = t + isAntiAlias = true + }.use { paint -> + canvas.drawRect(Rect.makeXYWH(x + half, y + half, width - t, height - t), paint) + } + } + + /** Draw a filled rounded rectangle with the specified corner radius. */ + @JvmStatic + fun roundedRect(x: Float, y: Float, width: Float, height: Float, radius: Float, color: Int) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + Paint().setColor(color).use { paint -> + canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) + } + } + + /** + * Draw a rounded rectangle outline with the specified thickness. + * + * @param x rectangle x + * @param y rectangle y + * @param width rectangle width + * @param height rectangle height + * @param radius corner radius + * @param color outline color + * @param thickness outline thickness + */ + @JvmStatic + fun roundedOutline( + x: Float, + y: Float, + width: Float, + height: Float, + radius: Float, + color: Int, + thickness: Float = 1f, + ) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + val t = thickness.coerceAtLeast(1f) + val half = t / 2f + val innerRadius = (radius - half).coerceAtLeast(0f) + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = t + isAntiAlias = true + }.use { paint -> + canvas.drawRRect(RRect.makeXYWH(x + half, y + half, width - t, height - t, innerRadius), paint) + } + } + + /** + * Draw a rectangle filled with a two-color linear gradient. + * + * @param x rectangle x + * @param y rectangle y + * @param width rectangle width + * @param height rectangle height + * @param colorStart start color ARGB + * @param colorEnd end color ARGB + * @param direction gradient direction + */ + @JvmStatic + fun gradientRect( + x: Float, y: Float, width: Float, height: Float, + colorStart: Int, colorEnd: Int, direction: SkiaGradient, + ) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + val x1 = when (direction) { + SkiaGradient.LEFT_TO_RIGHT -> x + width + SkiaGradient.TOP_TO_BOTTOM -> x + } + + val y1 = when (direction) { + SkiaGradient.TOP_TO_BOTTOM -> y + height + SkiaGradient.LEFT_TO_RIGHT -> y + } + + Shader.makeLinearGradient( + x, y, x1, y1, + intArrayOf(colorStart, colorEnd) + ).use { shader -> + Paint().apply { + this.shader = shader + isAntiAlias = true + }.use { paint -> + canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) + } + } + } + + /** + * Draw a rounded rectangle filled with a two-color linear gradient. + * + * @param x rectangle x + * @param y rectangle y + * @param width rectangle width + * @param height rectangle height + * @param radius corner radius + * @param colorStart start color ARGB + * @param colorEnd end color ARGB + * @param direction gradient direction + */ + @JvmStatic + fun gradientRoundedRect( + x: Float, + y: Float, + width: Float, + height: Float, + radius: Float, + colorStart: Int, + colorEnd: Int, + direction: SkiaGradient, + ) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + val x1 = when (direction) { + SkiaGradient.LEFT_TO_RIGHT -> x + width + SkiaGradient.TOP_TO_BOTTOM -> x + } + + val y1 = when (direction) { + SkiaGradient.TOP_TO_BOTTOM -> y + height + SkiaGradient.LEFT_TO_RIGHT -> y + } + + Shader.makeLinearGradient( + x, y, x1, y1, + intArrayOf(colorStart, colorEnd) + ).use { shader -> + Paint().apply { + this.shader = shader + isAntiAlias = true + }.use { paint -> + canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) + } + } + } + + /** + * Draw a rectangle with rounded corners on one side only. + * + * @param x rectangle x + * @param y rectangle y + * @param width rectangle width + * @param height rectangle height + * @param radius corner radius + * @param color fill color ARGB + * @param side which side should have rounded corners + */ + @JvmStatic + fun halfRoundedRect( + x: Float, y: Float, width: Float, height: Float, + radius: Float, color: Int, side: SkiaSide = SkiaSide.TOP, + ) { + val canvas = this.canvas ?: return + if (width <= 0f || height <= 0f) return + + val r = radius.coerceAtLeast(0f) + + val radii = when (side) { + SkiaSide.TOP -> floatArrayOf(r, r, r, r, 0f, 0f, 0f, 0f) + SkiaSide.BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, r, r, r, r) + SkiaSide.LEFT -> floatArrayOf(r, r, 0f, 0f, 0f, 0f, r, r) + SkiaSide.RIGHT -> floatArrayOf(0f, 0f, r, r, r, r, 0f, 0f) + } + + Paint().apply { + setColor(color) + isAntiAlias = true + }.use { paint -> + canvas.drawRRect(RRect.makeComplexXYWH(x, y, width, height, radii), paint) + } + } + +} + + diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt new file mode 100644 index 00000000..27709692 --- /dev/null +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt @@ -0,0 +1,65 @@ +package org.cobalt.render.skia + +import io.github.humbleui.skija.Data +import io.github.humbleui.skija.Font +import io.github.humbleui.skija.FontEdging +import io.github.humbleui.skija.FontHinting +import io.github.humbleui.skija.FontMgr +import io.github.humbleui.skija.Paint +import io.github.humbleui.skija.TextLine +import java.io.IOException + +object SkiaText { + private val fonts = mutableMapOf() + + /** Primary UI font used throughout the client. */ + val primaryFont: Font = loadFont("assets/cobalt/font/ProductSans-Bold.ttf") + + private val canvas get() = SkiaContext.canvas + + /** Load and cache a font from the given resource path. */ + fun loadFont(resourcePath: String) = fonts.computeIfAbsent(resourcePath) { + val bytes = javaClass.classLoader + ?.getResourceAsStream(resourcePath) + ?.use { it.readAllBytes() } + ?: throw IOException("Font resource not found: $resourcePath") + + val font = FontMgr.getDefault().makeFromData(Data.makeFromBytes(bytes)) + ?: throw IllegalArgumentException("Invalid font data: $resourcePath") + + Font(font).apply { + isSubpixel = false + hinting = FontHinting.NORMAL + edging = FontEdging.ANTI_ALIAS + } + } + + data class TextStyle(val fontSize: Float, val color: Int) + + @JvmStatic + fun text(font: Font, text: String, x: Float, y: Float, style: TextStyle) { + val canvas = this.canvas ?: return + font.size = style.fontSize + + TextLine.make(text, font).use { line -> + val baseline = y - line.ascent - 1f + + Paint().setColor(style.color).use { paint -> + canvas.drawTextLine(line, x, baseline, paint) + } + } + } + + @JvmStatic + /** Measure and return the width of the given text using the font and size. */ + fun textWidth(font: Font, text: String, fontSize: Float): Float { + font.size = fontSize + + TextLine.make(text, font).use { line -> + return line.width + } + } + +} + + diff --git a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt b/src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt similarity index 98% rename from src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt rename to src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt index 73fe1bc9..82827980 100644 --- a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt +++ b/src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.util.skia +package org.cobalt.render.skia import io.github.humbleui.skija.BackendRenderTarget import io.github.humbleui.skija.impl.Stats diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt b/src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt similarity index 99% rename from src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt rename to src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt index 74744224..9c1ccccb 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt +++ b/src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.util.skia.gl +package org.cobalt.render.skia.gl import java.util.* diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt b/src/main/kotlin/org/cobalt/render/skia/gl/State.kt similarity index 92% rename from src/main/kotlin/org/cobalt/util/skia/gl/State.kt rename to src/main/kotlin/org/cobalt/render/skia/gl/State.kt index 5fcb3613..ca77708c 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/State.kt +++ b/src/main/kotlin/org/cobalt/render/skia/gl/State.kt @@ -1,3 +1,5 @@ +@file:Suppress("WildcardImport") // Imports 60+ items, so a wildcard import is fine here. + /* * This file is part of https://github.com/Lyzev/Skija. * @@ -15,8 +17,7 @@ * * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ - -package org.cobalt.util.skia.gl +package org.cobalt.render.skia.gl import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45.* @@ -150,10 +151,30 @@ class State(private val glVersion: Int) { * @return this [State] instance after restoration. */ fun pop(): State { + restoreProgramAndTexture() + + restoreSamplersAndBindings() + + restoreBlendAndCapabilities() + + restorePolygonViewportAndScissor() + + restorePixelStoresAndBuffers() + + restoreDepthMask() + + return this + } + + private fun restoreProgramAndTexture() { with(props) { glUseProgram(lastProgram[0]) glBindTexture(GL_TEXTURE_2D, lastTexture[0]) + } + } + private fun restoreSamplersAndBindings() { + with(props) { if (glVersion >= GL_VERSION_3_3 || GL.getCapabilities().GL_ARB_sampler_objects) { glBindSampler(DEFAULT_SAMPLER_UNIT, lastSampler[0]) } @@ -161,7 +182,13 @@ class State(private val glVersion: Int) { glActiveTexture(lastActiveTexture[0]) glBindVertexArray(lastVertexArrayObject[0]) glBindBuffer(GL_ARRAY_BUFFER, lastArrayBuffer[0]) + } + } + + private fun restoreBlendAndCapabilities() { + with(props) { glBlendEquationSeparate(lastBlendEquationRgb[0], lastBlendEquationAlpha[0]) + glBlendFuncSeparate( lastBlendSrcRgb[0], lastBlendDstRgb[0], @@ -171,12 +198,16 @@ class State(private val glVersion: Int) { if (lastEnableBlend) glEnable(GL_BLEND) else glDisable(GL_BLEND) + if (lastEnableCullFace) glEnable(GL_CULL_FACE) else glDisable(GL_CULL_FACE) + if (lastEnableDepthTest) glEnable(GL_DEPTH_TEST) else glDisable(GL_DEPTH_TEST) + if (lastEnableStencilTest) glEnable(GL_STENCIL_TEST) else glDisable(GL_STENCIL_TEST) + if (lastEnableScissorTest) glEnable(GL_SCISSOR_TEST) else glDisable(GL_SCISSOR_TEST) @@ -184,7 +215,11 @@ class State(private val glVersion: Int) { if (lastEnablePrimitiveRestart) glEnable(GL_PRIMITIVE_RESTART) else glDisable(GL_PRIMITIVE_RESTART) } + } + } + private fun restorePolygonViewportAndScissor() { + with(props) { if (glVersion >= GL_VERSION_2_0) { glPolygonMode(GL_FRONT_AND_BACK, lastPolygonMode[0]) } @@ -195,13 +230,18 @@ class State(private val glVersion: Int) { lastViewport[VIEWPORT_W], lastViewport[VIEWPORT_H] ) + glScissor( lastScissorBox[SCISSOR_X], lastScissorBox[SCISSOR_Y], lastScissorBox[SCISSOR_W], lastScissorBox[SCISSOR_H] ) + } + } + private fun restorePixelStoresAndBuffers() { + with(props) { glPixelStorei(GL_PACK_SWAP_BYTES, lastPackSwapBytes[0]) glPixelStorei(GL_PACK_LSB_FIRST, lastPackLsbFirst[0]) glPixelStorei(GL_PACK_ROW_LENGTH, lastPackRowLength[0]) @@ -223,11 +263,13 @@ class State(private val glVersion: Int) { glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, lastUnpackImageHeight[0]) glPixelStorei(GL_UNPACK_SKIP_IMAGES, lastUnpackSkipImages[0]) } + } + } + private fun restoreDepthMask() { + with(props) { glDepthMask(lastDepthMask) } - - return this } } diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt b/src/main/kotlin/org/cobalt/render/skia/gl/States.kt similarity index 98% rename from src/main/kotlin/org/cobalt/util/skia/gl/States.kt rename to src/main/kotlin/org/cobalt/render/skia/gl/States.kt index 5ef1a242..c7ff5068 100644 --- a/src/main/kotlin/org/cobalt/util/skia/gl/States.kt +++ b/src/main/kotlin/org/cobalt/render/skia/gl/States.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.util.skia.gl +package org.cobalt.render.skia.gl import java.util.* import org.lwjgl.opengl.GL30.glGetIntegerv diff --git a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt index beec50d1..6d983eef 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt @@ -3,7 +3,7 @@ package org.cobalt.ui.notification import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent -import org.cobalt.util.skia.SkiaRenderer +import org.cobalt.render.skia.SkiaRenderer /** * Manager responsible for displaying on-screen notifications. @@ -43,7 +43,7 @@ object NotificationManager { * notification draw call. */ @SubscribeEvent - fun onSkiaDraw(event: SkiaDrawEvent) { + fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { val windowScale = SkiaRenderer.getWindowScale() notificationsList diff --git a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt index de1d56cd..3143df40 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt @@ -7,7 +7,7 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.ui.animation.BounceAnimation -import org.cobalt.util.skia.SkiaRenderer +import org.cobalt.render.skia.SkiaRenderer internal object ConfigScreen : Screen(Component.empty()) { @@ -18,7 +18,7 @@ internal object ConfigScreen : Screen(Component.empty()) { } @SubscribeEvent - fun onSkiaDraw(event: SkiaDrawEvent) { + fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { if (minecraft.screen != this) { return } @@ -49,7 +49,12 @@ internal object ConfigScreen : Screen(Component.empty()) { openAnim.start() } - override fun extractBlurredBackground(graphics: GuiGraphicsExtractor) {} - override fun extractMenuBackground(graphics: GuiGraphicsExtractor) {} + override fun extractBlurredBackground(graphics: GuiGraphicsExtractor) { + return + } + + override fun extractMenuBackground(graphics: GuiGraphicsExtractor) { + return + } } diff --git a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt index 06191b64..57170ce6 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt @@ -24,7 +24,7 @@ object HudEditorScreen : Screen(Component.empty()) { * @param event the Skia draw event (unused in the current implementation) */ @SubscribeEvent - fun onSkiaDraw(event: SkiaDrawEvent) { + fun onSkiaDraw(@Suppress("UNUSED_PARAMETER") event: SkiaDrawEvent) { if (minecraft.screen != this) { return } diff --git a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt index d5841ffb..2352cde7 100644 --- a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt +++ b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt @@ -26,7 +26,7 @@ object TickScheduler { /** Internal event handler invoked at the end of each client tick to flush scheduled tasks. */ @SubscribeEvent - fun onClientTick(event: TickEvent.End) { + fun onClientTick(@Suppress("UNUSED_PARAMETER") event: TickEvent.End) { currentTick++ var task: ScheduledTask? diff --git a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt b/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt index 8034c764..2f579c28 100644 --- a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt +++ b/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt @@ -58,7 +58,7 @@ object RotationManager { * to the active rotation controller when a rotation is in progress. */ @SubscribeEvent - fun onWorldRender(event: WorldRenderEvent) { + fun onWorldRender(@Suppress("UnusedParameter") event: WorldRenderEvent) { if (!rotation.isRotating()) return rotation.onRotationWorldRender() } diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt deleted file mode 100644 index f06b8416..00000000 --- a/src/main/kotlin/org/cobalt/util/skia/SkiaRenderer.kt +++ /dev/null @@ -1,403 +0,0 @@ -package org.cobalt.util.skia - -import io.github.humbleui.skija.BlendMode -import io.github.humbleui.skija.Canvas -import io.github.humbleui.skija.ClipMode -import io.github.humbleui.skija.ColorFilter -import io.github.humbleui.skija.Data -import io.github.humbleui.skija.Font -import io.github.humbleui.skija.FontEdging -import io.github.humbleui.skija.FontHinting -import io.github.humbleui.skija.FontMgr -import io.github.humbleui.skija.Paint -import io.github.humbleui.skija.PaintMode -import io.github.humbleui.skija.SamplingMode -import io.github.humbleui.skija.Shader -import io.github.humbleui.skija.TextLine -import io.github.humbleui.types.RRect -import io.github.humbleui.types.Rect -import java.io.IOException -import org.cobalt.Cobalt.minecraft - - /** High-level Skia drawing helpers used by UI and module renderers. - * Provides convenience functions for text, shapes, images and scissor management. - */ -object SkiaRenderer { - private const val BASE_WIDTH = 1920f - private const val BASE_HEIGHT = 1080f - - private data class ImageCacheKey( - val identifier: String, - val radius: Float?, - val colorMask: Int?, - ) - - private val fonts = mutableMapOf() - private val images = mutableMapOf() - private var scissorStackDepth = 0 - - /** - * Primary UI font used throughout the client. Loaded and cached at - * initialization from the bundled assets. - */ - val primaryFont = loadFont("assets/cobalt/font/ProductSans-Bold.ttf") - - private val canvas: Canvas? - get() = SkiaContext.canvas - - /** Calculate a window scale factor relative to a 1920x1080 baseline for consistent UI sizing. */ - fun getWindowScale(): Float { - val windowWidth = minecraft.window.width.toFloat() - val windowHeight = minecraft.window.height.toFloat() - - return minOf(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) - } - - /** Save the current Skia canvas state. */ - @JvmStatic - fun save() = - this.canvas?.save() - - /** Restore the previously saved Skia canvas state. */ - @JvmStatic - fun restore() = - this.canvas?.restore() - - /** Translate the canvas by the given x/y offset. */ - @JvmStatic - fun translate(x: Float, y: Float) = - this.canvas?.translate(x, y) - - /** Rotate the canvas by the given angle in degrees. */ - @JvmStatic - fun rotate(angleDeg: Float) = - this.canvas?.rotate(angleDeg) - - /** Scale the canvas by the specified X and Y factors. */ - @JvmStatic - fun scale(x: Float, y: Float) = - this.canvas?.scale(x, y) - - /** Push a scissor/clip rectangle onto the canvas stack. Subsequent draws will be clipped. - * Coordinates are in canvas space. - */ - @JvmStatic - fun pushScissor(x: Float, y: Float, width: Float, height: Float) { - val canvas = this.canvas ?: return - - if (width <= 0 || height <= 0) { - return - } - - canvas.save() - canvas.clipRect(Rect.makeXYWH(x, y, width, height), ClipMode.INTERSECT, true) - scissorStackDepth++ - } - - /** Pop the last scissor/clip rectangle and restore the previous canvas state. */ - @JvmStatic - fun popScissor() { - if (scissorStackDepth <= 0) return - canvas?.restore() - scissorStackDepth-- - } - - /** Load and cache a font from the given resource path. */ - @JvmStatic - fun loadFont(resourcePath: String) = fonts.computeIfAbsent(resourcePath) { - val bytes = javaClass.classLoader - ?.getResourceAsStream(resourcePath) - ?.use { it.readAllBytes() } - ?: throw IOException("Font resource not found: $resourcePath") - - val font = FontMgr.getDefault().makeFromData(Data.makeFromBytes(bytes)) - ?: throw IllegalArgumentException("Invalid font data: $resourcePath") - - Font(font).apply { - isSubpixel = false - hinting = FontHinting.NORMAL - edging = FontEdging.ANTI_ALIAS - } - } - - /** Draw text using the specified font at the given position and size. */ - @JvmStatic - fun text(font: Font, text: String, x: Float, y: Float, fontSize: Float, color: Int) { - val canvas = this.canvas ?: return - font.size = fontSize - - TextLine.make(text, font).use { line -> - val baseline = y - line.ascent - 1f - - Paint().setColor(color).use { paint -> - canvas.drawTextLine(line, x, baseline, paint) - } - } - } - - /** Measure and return the width of the given text using the font and size. */ - @JvmStatic - fun textWidth(font: Font, text: String, fontSize: Float): Float { - font.size = fontSize - - TextLine.make(text, font).use { line -> - return line.width - } - } - - /** Load and cache an image by identifier, optionally applying rounding and color masking. */ - @JvmStatic - fun loadImage( - identifier: String, - radius: Float? = null, - colorMask: Int? = null, - ): SkiaImage { - val normalizedRadius = radius?.takeIf { it > 0f } - val key = ImageCacheKey(identifier, normalizedRadius, colorMask) - - return images.computeIfAbsent(key) { - SkiaImage(identifier, normalizedRadius, colorMask) - } - } - - /** Draw a SkiaImage raster into the specified destination rectangle. */ - @JvmStatic - fun image(image: SkiaImage, x: Float, y: Float, width: Float, height: Float) { - val canvas = this.canvas ?: return - if (width <= 0 || height <= 0) return - - val sourceImage = image.getOrGenerateRaster(width.toInt(), height.toInt()) ?: return - - Paint().use { paint -> - image.colorMask?.let { - paint.colorFilter = ColorFilter.makeBlend(it, BlendMode.SRC_ATOP) - } - - if (image.radius != null && image.radius > 0f) { - canvas.save() - canvas.clipRRect(RRect.makeXYWH(x, y, width, height, image.radius), ClipMode.INTERSECT, true) - } - - canvas.drawImageRect( - sourceImage, - Rect.makeWH(sourceImage.width.toFloat(), sourceImage.height.toFloat()), - Rect.makeXYWH(x, y, width, height), - SamplingMode.MITCHELL, - paint, - false - ) - - if (image.radius != null && image.radius > 0f) { - canvas.restore() - } - } - } - - /** Draw a straight line between two points. */ - @JvmStatic - fun line(x1: Float, x2: Float, y1: Float, y2: Float, color: Int, thickness: Float = 1f) { - val canvas = this.canvas ?: return - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = thickness.coerceAtLeast(0f) - isAntiAlias = true - }.use { paint -> - canvas.drawLine(x1, y1, x2, y2, paint) - } - } - - /** Draw a filled rectangle. */ - @JvmStatic - fun rect(x: Float, y: Float, width: Float, height: Float, color: Int) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - Paint().setColor(color).use { paint -> - canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) - } - } - - /** Draw a stroked rectangle outline with the given thickness. */ - @JvmStatic - fun outline(x: Float, y: Float, width: Float, height: Float, color: Int, thickness: Float = 1f) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - val thickness = thickness.coerceAtLeast(0f) - val half = thickness / 2f - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = thickness - isAntiAlias = true - }.use { paint -> - canvas.drawRect( - Rect.makeXYWH(x + half, y + half, width - thickness, height - thickness), - paint, - ) - } - } - - /** Draw a filled rounded rectangle with the specified corner radius. */ - @JvmStatic - fun roundedRect(x: Float, y: Float, width: Float, height: Float, radius: Float, color: Int) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - Paint().setColor(color).use { paint -> - canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) - } - } - - /** Draw a rounded rectangle outline with the specified thickness. */ - @JvmStatic - fun roundedOutline( - x: Float, - y: Float, - width: Float, - height: Float, - radius: Float, - color: Int, - thickness: Float = 1f, - ) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - val thickness = thickness.coerceAtLeast(1f) - val half = thickness / 2f - val innerRadius = (radius - half).coerceAtLeast(0f) - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = thickness - isAntiAlias = true - }.use { paint -> - canvas.drawRRect( - RRect.makeXYWH(x + half, y + half, width - thickness, height - thickness, innerRadius), - paint, - ) - } - } - - /** Draw a rectangle filled with a two-color linear gradient. */ - @JvmStatic - fun gradientRect( - x: Float, y: Float, width: Float, height: Float, - colorStart: Int, colorEnd: Int, direction: SkiaGradient, - ) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - val x1 = when (direction) { - SkiaGradient.LEFT_TO_RIGHT -> x + width - SkiaGradient.TOP_TO_BOTTOM -> x - } - - val y1 = when (direction) { - SkiaGradient.TOP_TO_BOTTOM -> y + height - SkiaGradient.LEFT_TO_RIGHT -> y - } - - Shader.makeLinearGradient( - x, y, x1, y1, - intArrayOf(colorStart, colorEnd) - ).use { shader -> - Paint().apply { - this.shader = shader - isAntiAlias = true - }.use { paint -> - canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) - } - } - } - - /** Draw a rounded rectangle filled with a two-color linear gradient. */ - @JvmStatic - fun gradientRoundedRect( - x: Float, - y: Float, - width: Float, - height: Float, - radius: Float, - colorStart: Int, - colorEnd: Int, - direction: SkiaGradient, - ) { - val canvas = this.canvas ?: return - - if (width <= 0f || height <= 0f) { - return - } - - val x1 = when (direction) { - SkiaGradient.LEFT_TO_RIGHT -> x + width - SkiaGradient.TOP_TO_BOTTOM -> x - } - - val y1 = when (direction) { - SkiaGradient.TOP_TO_BOTTOM -> y + height - SkiaGradient.LEFT_TO_RIGHT -> y - } - - Shader.makeLinearGradient( - x, y, x1, y1, - intArrayOf(colorStart, colorEnd) - ).use { shader -> - Paint().apply { - this.shader = shader - isAntiAlias = true - }.use { paint -> - canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) - } - } - } - - /** Draw a rectangle with rounded corners on one side only. - * - * @param side which side should have rounded corners - */ - @JvmStatic - fun halfRoundedRect( - x: Float, y: Float, width: Float, height: Float, - radius: Float, color: Int, side: SkiaSide = SkiaSide.TOP, - ) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - - val r = radius.coerceAtLeast(0f) - - val radii = when (side) { - SkiaSide.TOP -> floatArrayOf(r, r, r, r, 0f, 0f, 0f, 0f) - SkiaSide.BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, r, r, r, r) - SkiaSide.LEFT -> floatArrayOf(r, r, 0f, 0f, 0f, 0f, r, r) - SkiaSide.RIGHT -> floatArrayOf(0f, 0f, r, r, r, r, 0f, 0f) - } - - Paint().apply { - setColor(color) - isAntiAlias = true - }.use { paint -> - canvas.drawRRect(RRect.makeComplexXYWH(x, y, width, height, radii), paint) - } - } - -} From c466332bc5ddef1dfc1e4606067eb6131fbc4377 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 18 Apr 2026 20:39:03 +0200 Subject: [PATCH 11/18] refactor: fix last detekt issues --- src/main/kotlin/org/cobalt/command/Command.kt | 106 +++++--- src/main/kotlin/org/cobalt/event/EventBus.kt | 137 +++++----- src/main/kotlin/org/cobalt/math/Math.kt | 19 ++ .../kotlin/org/cobalt/module/ModuleManager.kt | 9 +- .../module/impl/render/PerformanceHUD.kt | 79 ++++-- .../kotlin/org/cobalt/render/RenderUtils.kt | 125 ++++++--- .../org/cobalt/render/skia/SkiaContext.kt | 34 +-- .../org/cobalt/render/skia/SkiaImage.kt | 35 ++- .../org/cobalt/render/skia/SkiaImages.kt | 77 ++++-- .../org/cobalt/render/skia/SkiaRenderer.kt | 9 +- .../org/cobalt/render/skia/SkiaShapes.kt | 238 ++---------------- .../kotlin/org/cobalt/render/skia/SkiaText.kt | 37 ++- .../kotlin/org/cobalt/render/skia/gl/State.kt | 180 +++++++------ .../cobalt/ui/notification/Notification.kt | 5 +- .../ui/notification/NotificationManager.kt | 9 +- .../org/cobalt/ui/screen/ConfigScreen.kt | 9 +- 16 files changed, 591 insertions(+), 517 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/math/Math.kt diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index 0420b708..b9b46770 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -7,6 +7,7 @@ import com.mojang.brigadier.arguments.IntegerArgumentType import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext import kotlin.reflect.KFunction import kotlin.reflect.KParameter import kotlin.reflect.full.declaredFunctions @@ -53,52 +54,71 @@ abstract class Command(val name: String) { val parameters = function.parameters val valueParams = parameters.filter { it.kind == KParameter.Kind.VALUE } - if (valueParams.isEmpty()) { - literal.executes { - function.call(this) - return@executes 1 - } + return if (valueParams.isEmpty()) buildNoArgSubCommand(literal, function) + else buildArgSubCommand(literal, function, parameters, valueParams) + } - return literal + private fun buildNoArgSubCommand( + literal: LiteralArgumentBuilder, + function: KFunction<*>, + ): LiteralArgumentBuilder { + literal.executes { + function.call(this@Command) + return@executes 1 } + return literal + } + + private fun buildArgSubCommand( + literal: LiteralArgumentBuilder, + function: KFunction<*>, + parameters: List, + valueParams: List, + ): LiteralArgumentBuilder { val arguments = valueParams.mapIndexed { index, param -> val name = param.name ?: "argument$index" createArgument(name, param.type.classifier) } arguments.last().executes { ctx -> - val mappedValues = valueParams.mapIndexed { index, param -> - val argumentName = param.name ?: "argument$index" - when (param.type.classifier) { - Double::class -> DoubleArgumentType.getDouble(ctx, argumentName) - Int::class -> IntegerArgumentType.getInteger(ctx, argumentName) - String::class -> StringArgumentType.getString(ctx, argumentName) - Boolean::class -> BoolArgumentType.getBool(ctx, argumentName) - Float::class -> FloatArgumentType.getFloat(ctx, argumentName) - else -> error("Unsupported type: ${param.type}") - } - } - - val argsMap = mutableMapOf() + executeFunctionWithContext(function, parameters, valueParams, ctx) + return@executes 1 + } - val instanceParam = parameters.firstOrNull { it.kind == KParameter.Kind.INSTANCE } - if (instanceParam != null) argsMap[instanceParam] = this + val argumentTree = buildArgumentTree(arguments) - for (i in valueParams.indices) { - argsMap[valueParams[i]] = mappedValues[i] - } + return literal.then(argumentTree) + } - function.callBy(argsMap) + private fun buildArgumentTree( + arguments: List>, + ): RequiredArgumentBuilder { + return arguments.reduceRight { arg, acc -> arg.then(acc) } + } - return@executes 1 + private fun executeFunctionWithContext( + function: KFunction<*>, + parameters: List, + valueParams: List, + ctx: CommandContext, + ) { + val mappedValues = valueParams.mapIndexed { index, param -> + val argumentName = param.name ?: "argument$index" + valueExtractors[param.type.classifier]?.invoke(ctx, argumentName) + ?: error("Unsupported type: ${param.type}") } - val argumentTree = arguments.reduceRight { arg, acc -> - arg.then(acc) + val argsMap = mutableMapOf() + + val instanceParam = parameters.firstOrNull { it.kind == KParameter.Kind.INSTANCE } + if (instanceParam != null) argsMap[instanceParam] = this@Command + + for (i in valueParams.indices) { + argsMap[valueParams[i]] = mappedValues[i] } - return literal.then(argumentTree) + function.callBy(argsMap) } /** Create a Brigadier RequiredArgumentBuilder for a supported parameter type. */ @@ -106,14 +126,26 @@ abstract class Command(val name: String) { name: String, type: Any?, ): RequiredArgumentBuilder { - return when (type) { - Double::class -> RequiredArgumentBuilder.argument(name, DoubleArgumentType.doubleArg()) - Int::class -> RequiredArgumentBuilder.argument(name, IntegerArgumentType.integer()) - String::class -> RequiredArgumentBuilder.argument(name, StringArgumentType.word()) - Boolean::class -> RequiredArgumentBuilder.argument(name, BoolArgumentType.bool()) - Float::class -> RequiredArgumentBuilder.argument(name, FloatArgumentType.floatArg()) - else -> throw IllegalArgumentException("Unsupported parameter type: $type") - } + val argType = argumentTypeSuppliers[type]?.invoke() + ?: throw IllegalArgumentException("Unsupported parameter type: $type") + + return RequiredArgumentBuilder.argument(name, argType) } + private val argumentTypeSuppliers: Map com.mojang.brigadier.arguments.ArgumentType<*>> = mapOf( + Double::class to { DoubleArgumentType.doubleArg() }, + Int::class to { IntegerArgumentType.integer() }, + String::class to { StringArgumentType.word() }, + Boolean::class to { BoolArgumentType.bool() }, + Float::class to { FloatArgumentType.floatArg() }, + ) + + private val valueExtractors: Map, String) -> Any?> = mapOf( + Double::class to { c, n -> DoubleArgumentType.getDouble(c, n) }, + Int::class to { c, n -> IntegerArgumentType.getInteger(c, n) }, + String::class to { c, n -> StringArgumentType.getString(c, n) }, + Boolean::class to { c, n -> BoolArgumentType.getBool(c, n) }, + Float::class to { c, n -> FloatArgumentType.getFloat(c, n) }, + ) + } diff --git a/src/main/kotlin/org/cobalt/event/EventBus.kt b/src/main/kotlin/org/cobalt/event/EventBus.kt index 1ca4e985..ff5068b4 100644 --- a/src/main/kotlin/org/cobalt/event/EventBus.kt +++ b/src/main/kotlin/org/cobalt/event/EventBus.kt @@ -2,6 +2,7 @@ package org.cobalt.event import org.cobalt.event.annotation.SubscribeEvent import java.lang.invoke.MethodHandles +import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import org.slf4j.LoggerFactory @@ -25,48 +26,52 @@ object EventBus { /** Register all methods annotated with [SubscribeEvent] from the given listener instance. */ @JvmStatic fun register(listener: Any) { - if (handlers.any { it.listener === listener }) { - return - } + if (handlers.any { it.listener === listener }) return + + val toAdd = createHandlersForListener(listener) + + if (toAdd.isNotEmpty()) handlers.addAll(toAdd) + cache.clear() + } + + private fun createHandlersForListener(listener: Any): List { + val result = mutableListOf() listener.javaClass.declaredMethods.forEach { method -> - val annotation = method.getAnnotation(SubscribeEvent::class.java) - ?: return@forEach - - val params = method.parameterTypes - - if (params.size != 1 || !Event::class.java.isAssignableFrom(params[0])) { - return@forEach - } - - if (!method.trySetAccessible()) { - logger.error( - "EventBus: could not access method ${listener.javaClass.name}#${method.name}, skipping" - ) - return@forEach - } - - val eventType = params.first() - val lookup = MethodHandles.privateLookupIn(listener.javaClass, MethodHandles.lookup()) - val handle = lookup.unreflect(method).bindTo(listener) - - val invoker: (Event) -> Unit = { event -> - handle.invoke(event) - } - - handlers.add( - Handler( - listener = listener, - eventType = eventType, - priority = annotation.priority, - ignoreCancelled = annotation.ignoreCancelled, - once = annotation.once, - invoker = invoker - ) - ) + createHandlerFromMethod(listener, method)?.let { result.add(it) } } + return result + } - cache.clear() + private fun createHandlerFromMethod(listener: Any, method: Method): Handler? { + val annotation = method.getAnnotation(SubscribeEvent::class.java) + val params = method.parameterTypes + + if (annotation == null || params.size != 1 || !Event::class.java.isAssignableFrom(params[0])) return null + + if (!method.trySetAccessible()) { + logger.error("EventBus: could not access method ${listener.javaClass.name}#${method.name}, skipping") + return null + } + + val eventType = params.first() + return buildHandler(listener, method, annotation, eventType) + } + + private fun buildHandler(listener: Any, method: Method, annotation: SubscribeEvent, eventType: Class<*>): Handler { + val lookup = MethodHandles.privateLookupIn(listener.javaClass, MethodHandles.lookup()) + val handle = lookup.unreflect(method).bindTo(listener) + + val invoker: (Event) -> Unit = { event -> handle.invoke(event) } + + return Handler( + listener = listener, + eventType = eventType, + priority = annotation.priority, + ignoreCancelled = annotation.ignoreCancelled, + once = annotation.once, + invoker = invoker + ) } /** Unregister all handlers for the given listener instance. */ @@ -80,33 +85,11 @@ object EventBus { @JvmStatic fun post(event: Event): Event { val eventClass = event.javaClass - val matched = cache.computeIfAbsent(eventClass) { - handlers - .filter { it.eventType.isAssignableFrom(eventClass) } - .sortedBy { it.priority.ordinal } - .toTypedArray() - } + val matched = cache.computeIfAbsent(eventClass) { computeMatchedHandlers(eventClass) } - var toRemove: MutableList? = null + val toRemove = processMatchedHandlers(matched, event) - for (handler in matched) { - if ( - event is Event.Cancellable && - event.isCancelled() && - !handler.ignoreCancelled - ) { - continue - } - - handler.invoker(event) - - if (handler.once) { - if (toRemove == null) toRemove = mutableListOf() - toRemove.add(handler) - } - } - - if (toRemove != null) { + if (toRemove.isNotEmpty()) { handlers.removeAll(toRemove.toSet()) cache.clear() } @@ -114,4 +97,32 @@ object EventBus { return event } + private fun processMatchedHandlers(matched: Array, event: Event): MutableList { + val toRemove = mutableListOf() + + for (handler in matched) { + if (shouldSkipHandler(handler, event)) continue + invokeHandler(handler, event, toRemove) + } + + return toRemove + } + + private fun shouldSkipHandler(handler: Handler, event: Event): Boolean { + return (event is Event.Cancellable && event.isCancelled() && !handler.ignoreCancelled) + } + + private fun invokeHandler(handler: Handler, event: Event, toRemove: MutableList) { + handler.invoker(event) + if (handler.once) toRemove.add(handler) + } + + + private fun computeMatchedHandlers(eventClass: Class<*>): Array { + return handlers + .filter { it.eventType.isAssignableFrom(eventClass) } + .sortedBy { it.priority.ordinal } + .toTypedArray() + } + } diff --git a/src/main/kotlin/org/cobalt/math/Math.kt b/src/main/kotlin/org/cobalt/math/Math.kt new file mode 100644 index 00000000..58679b71 --- /dev/null +++ b/src/main/kotlin/org/cobalt/math/Math.kt @@ -0,0 +1,19 @@ +package org.cobalt.math + +/** Simple 3D vector */ +data class SimpleVec3( + /** X property */ + val x: Float, + /** Y property */ + val y: Float, + /** Z property */ + val z: Float = 0f +) + +/** Dimensions wrapper */ +data class Dimensions( + /** Width property */ + val width: Float, + /** Height property */ + val height: Float +) diff --git a/src/main/kotlin/org/cobalt/module/ModuleManager.kt b/src/main/kotlin/org/cobalt/module/ModuleManager.kt index 7360519d..bfc1928c 100644 --- a/src/main/kotlin/org/cobalt/module/ModuleManager.kt +++ b/src/main/kotlin/org/cobalt/module/ModuleManager.kt @@ -6,6 +6,7 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.module.impl.render.PerformanceHUD import org.cobalt.render.skia.SkiaRenderer +import org.cobalt.math.SimpleVec3 /** Manager responsible for registering, storing and dispatching modules. */ object ModuleManager { @@ -68,9 +69,9 @@ object ModuleManager { val originY = renderable.yPos val moduleScale = renderable.scale * windowScale - SkiaRenderer.translate(originX, originY) - SkiaRenderer.scale(moduleScale, moduleScale) - SkiaRenderer.translate(-originX, -originY) + SkiaRenderer.translate(SimpleVec3(originX, originY)) + SkiaRenderer.scale(SimpleVec3(moduleScale, moduleScale)) + SkiaRenderer.translate(SimpleVec3(-originX, -originY)) renderable.renderModule() @@ -79,3 +80,5 @@ object ModuleManager { } } + + diff --git a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt index 2de915f5..c4eeec34 100644 --- a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt +++ b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt @@ -2,6 +2,8 @@ package org.cobalt.module.impl.render import kotlin.math.roundToInt import org.cobalt.Cobalt.minecraft +import org.cobalt.math.Dimensions +import org.cobalt.math.SimpleVec3 import org.cobalt.module.ModuleCategory import org.cobalt.module.RenderableModule import org.cobalt.ui.ColorPalette @@ -31,39 +33,74 @@ object PerformanceHUD : RenderableModule( override fun renderModule() { val width = getWidth() val height = getHeight() - val centerY = yPos + height / 2 - SkiaShapes.roundedRect(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.PANEL) - SkiaShapes.roundedOutline(xPos, yPos, width, height, CORNER_RADIUS, ColorPalette.BORDER, OUTLINE_THICKNESS) + drawBackground(width, height) + drawStats(height) + } + + private fun drawBackground(width: Float, height: Float) { + SkiaShapes.roundedRect(SimpleVec3(xPos, yPos), Dimensions(width, height), CORNER_RADIUS, ColorPalette.PANEL) + SkiaShapes.roundedOutline( + SimpleVec3(xPos, yPos), + Dimensions(width, height), + CORNER_RADIUS, + ColorPalette.BORDER, + OUTLINE_THICKNESS + ) + } + + private fun drawStats(height: Float) { + val centerY = yPos + height / 2 var currentX = xPos + PADDING val textY = centerY - FONT_SIZE / 2 for ((index, stat) in getStats().withIndex()) { if (index > 0) { - currentX += DIVIDER_GAP - - val midY = yPos + height * MID_FACTOR - SkiaShapes.line( - currentX, - currentX, - midY - DIVIDER_HALF_HEIGHT, - midY + DIVIDER_HALF_HEIGHT, - ColorPalette.BORDER, - OUTLINE_THICKNESS - ) - - currentX += DIVIDER_GAP + currentX = drawDivider(currentX, height) } - SkiaText.text(SkiaText.primaryFont, stat.value, currentX, textY, SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_PRIMARY)) - currentX += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - - SkiaText.text(SkiaText.primaryFont, stat.unit, currentX, textY, SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_DISABLED)) - currentX += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) + currentX = drawStatText(stat, currentX, textY) } } + private fun drawDivider(startX: Float, height: Float): Float { + var x = startX + DIVIDER_GAP + + val midY = yPos + height * MID_FACTOR + SkiaShapes.line( + SimpleVec3(x, midY - DIVIDER_HALF_HEIGHT), + SimpleVec3(x, midY + DIVIDER_HALF_HEIGHT), + ColorPalette.BORDER, + OUTLINE_THICKNESS + ) + + x += DIVIDER_GAP + return x + } + + private fun drawStatText(stat: Stat, startX: Float, textY: Float): Float { + var x = startX + + SkiaText.text( + SkiaText.primaryFont, + stat.value, + SimpleVec3(x, textY), + SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_PRIMARY) + ) + x += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + + SkiaText.text( + SkiaText.primaryFont, + stat.unit, + SimpleVec3(x, textY), + SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_DISABLED) + ) + x += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) + + return x + } + override fun getWidth(): Float { var width = PADDING * 2 diff --git a/src/main/kotlin/org/cobalt/render/RenderUtils.kt b/src/main/kotlin/org/cobalt/render/RenderUtils.kt index e71bb1bc..09c912af 100644 --- a/src/main/kotlin/org/cobalt/render/RenderUtils.kt +++ b/src/main/kotlin/org/cobalt/render/RenderUtils.kt @@ -128,13 +128,8 @@ object RenderUtils { return } - val poseStack = context.poseStack() - val bufferSource = context.bufferSource() val cameraPos = minecraft.gameRenderer.mainCamera.position() - val matrix = poseStack.last().pose() - val poseEntry = poseStack.last() - val fillColor = Color(color.red, color.green, color.blue, ALPHA) val corners = arrayOf( Vec3(box.minX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.maxZ), Vec3(box.minX, box.minY, box.maxZ), @@ -142,6 +137,23 @@ object RenderUtils { Vec3(box.maxX, box.maxY, box.maxZ), Vec3(box.minX, box.maxY, box.maxZ), ) + drawBoxQuads(context, corners, color, esp, cameraPos) + drawBoxLines(context, corners, color, esp, lineWidth, cameraPos) + } + + private fun drawBoxQuads( + context: LevelRenderContext, + corners: Array, + color: Color, + esp: Boolean, + cameraPos: Vec3 + ) { + val poseStack = context.poseStack() + val bufferSource = context.bufferSource() + val matrix = poseStack.last().pose() + + val fillColor = Color(color.red, color.green, color.blue, ALPHA) + val quadBuffer = bufferSource.getBuffer(Layers.getQuads(esp)) for (index in BOX_QUADS) { @@ -155,6 +167,20 @@ object RenderUtils { } bufferSource.endBatch(Layers.getQuads(esp)) + } + + private fun drawBoxLines( + context: LevelRenderContext, + corners: Array, + color: Color, + esp: Boolean, + lineWidth: Float, + cameraPos: Vec3 + ) { + val poseStack = context.poseStack() + val bufferSource = context.bufferSource() + val matrix = poseStack.last().pose() + val poseEntry = poseStack.last() val lineBuffer = bufferSource.getBuffer(Layers.getLines(esp)) @@ -163,23 +189,38 @@ object RenderUtils { val lineEnd = corners[BOX_LINES[i + 1]] val lineNormal = lineEnd.subtract(lineStart).normalize() - for (vertex in listOf(lineStart, lineEnd)) { - lineBuffer.addVertex( - matrix, - (vertex.x - cameraPos.x).toFloat(), - (vertex.y - cameraPos.y).toFloat(), - (vertex.z - cameraPos.z).toFloat(), - ) - .setLineWidth(lineWidth) - .setColor(color.red, color.green, color.blue, color.alpha) - .setNormal(poseEntry, lineNormal.x.toFloat(), lineNormal.y.toFloat(), lineNormal.z.toFloat()) - } + addBlockLineVertices(lineBuffer, matrix, poseEntry, lineStart, lineEnd, lineNormal, color, lineWidth, cameraPos) } bufferSource.endBatch(Layers.getLines(esp)) } - /** Draw a colored line between two world-space points. + private fun addBlockLineVertices( + lineBuffer: com.mojang.blaze3d.vertex.VertexConsumer, + matrix: org.joml.Matrix4f, + poseEntry: com.mojang.blaze3d.vertex.PoseStack.Pose, + lineStart: Vec3, + lineEnd: Vec3, + lineNormal: Vec3, + color: Color, + lineWidth: Float, + cameraPos: Vec3 + ) { + for (vertex in listOf(lineStart, lineEnd)) { + lineBuffer.addVertex( + matrix, + (vertex.x - cameraPos.x).toFloat(), + (vertex.y - cameraPos.y).toFloat(), + (vertex.z - cameraPos.z).toFloat(), + ) + .setLineWidth(lineWidth) + .setColor(color.red, color.green, color.blue, color.alpha) + .setNormal(poseEntry, lineNormal.x.toFloat(), lineNormal.y.toFloat(), lineNormal.z.toFloat()) + } + } + + /** + * Draw a colored line between two world-space points. * * @param context the level render context to draw with * @param from start point in world coordinates @@ -195,34 +236,48 @@ object RenderUtils { color: Color, style: LineStyle = LineStyle(), ) { - if (color.alpha == 0) { - return - } - + if (color.alpha == 0) return val frustum = context.levelState().cameraRenderState.cullFrustum + if (!FrustumUtils.isVisible( + frustum, min(from.x, to.x), min(from.y, to.y), min(from.z, to.z), + max(from.x, to.x), max(from.y, to.y), max(from.z, to.z) + )) return - if ( - !FrustumUtils.isVisible( - frustum, - min(from.x, to.x), min(from.y, to.y), min(from.z, to.z), - max(from.x, to.x), max(from.y, to.y), max(from.z, to.z), - ) - ) { - return - } + drawVisibleLine(context, from, to, color, style) + } - val poseStack = context.poseStack() + private fun drawVisibleLine( + context: LevelRenderContext, + from: Vec3, + to: Vec3, + color: Color, + style: LineStyle + ) { val bufferSource = context.bufferSource() + val lineBuffer = bufferSource.getBuffer(Layers.getLines(style.esp)) + + addLineVertices(context, lineBuffer, from, to, color, style) + + bufferSource.endBatch(Layers.getLines(style.esp)) + } + + private fun addLineVertices( + context: LevelRenderContext, + lineBuffer: com.mojang.blaze3d.vertex.VertexConsumer, + from: Vec3, + to: Vec3, + color: Color, + style: LineStyle + ) { + val poseStack = context.poseStack() val cameraPos = minecraft.gameRenderer.mainCamera.position() val poseEntry = poseStack.last() - val matrix = poseEntry.pose() - val lineBuffer = bufferSource.getBuffer(Layers.getLines(style.esp)) val lineNormal = to.subtract(from).normalize() for (vertex in listOf(from, to)) { lineBuffer .addVertex( - matrix, + poseEntry.pose(), (vertex.x - cameraPos.x).toFloat(), (vertex.y - cameraPos.y).toFloat(), (vertex.z - cameraPos.z).toFloat() @@ -231,8 +286,6 @@ object RenderUtils { .setColor(color.red, color.green, color.blue, color.alpha) .setNormal(poseEntry, lineNormal.x.toFloat(), lineNormal.y.toFloat(), lineNormal.z.toFloat()) } - - bufferSource.endBatch(Layers.getLines(style.esp)) } private val BOX_QUADS = intArrayOf( diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt index eb6d5ab4..bb89dfa7 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt @@ -52,10 +52,18 @@ internal object SkiaContext { } fun initSkia(width: Int, height: Int) { - if (context == null) { - context = DirectContext.makeGL() - } + ensureContext() + + recreateRenderTarget(width, height) + + canvas = surface?.canvas + } + private fun ensureContext() { + if (context == null) context = DirectContext.makeGL() + } + + private fun recreateRenderTarget(width: Int, height: Int) { surface?.close() renderTarget?.close() @@ -67,6 +75,7 @@ internal object SkiaContext { DEFAULT_PREFER_SAMPLES, FramebufferFormat.GR_GL_RGBA8 ) + surface = Surface.wrapBackendRenderTarget( requireNotNull(context), requireNotNull(renderTarget), @@ -74,28 +83,25 @@ internal object SkiaContext { ColorType.RGBA_8888, ColorSpace.getSRGB() ) - - canvas = surface?.canvas } fun draw() { - if (context == null || surface == null) return + val ctx = context ?: return + val srf = surface ?: return States.push() GL11.glDisable(GL11.GL_CULL_FACE) GL11.glClearColor(CLEAR_R, CLEAR_G, CLEAR_B, CLEAR_A_TRANSPARENT) - context?.resetGLAll() + ctx.resetGLAll() - canvas?.let { canvas -> - context?.let { context -> - renderTarget?.let { renderTarget -> - EventBus.post(SkiaDrawEvent(context, renderTarget, canvas)) - } - } + val cvs = canvas + val rt = renderTarget + if (cvs != null && rt != null) { + EventBus.post(SkiaDrawEvent(ctx, rt, cvs)) } - context?.flushAndSubmit(surface) + ctx.flushAndSubmit(srf) States.pop() } diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt index aa212744..eac633b8 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt @@ -68,28 +68,49 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In if (!isSvg) return image val dom = svgDom ?: return null - if (cachedRaster != null && width == lastWidth && height == lastHeight) { - return cachedRaster + if (!isCachedMatch(width, height)) { + cachedRaster?.close() + val generated = generateRaster(dom, width, height) + if (generated != null) { + cachedRaster = generated + lastWidth = width + lastHeight = height + } } - cachedRaster?.close() + return cachedRaster + } + private fun isCachedMatch(width: Int, height: Int): Boolean { + return cachedRaster != null && width == lastWidth && height == lastHeight + } + + private fun generateRaster(dom: SVGDOM, width: Int, height: Int): Image? { val root = dom.root ?: return null val sourceWidth = root.width.value.takeIf { it > 0 } ?: width.toFloat() val sourceHeight = root.height.value.takeIf { it > 0 } ?: height.toFloat() + return renderDomToSurface(dom, width, height, sourceWidth, sourceHeight) + } + + private fun renderDomToSurface( + dom: SVGDOM, + width: Int, + height: Int, + sourceWidth: Float, + sourceHeight: Float, + ): Image? { + var snapshot: Image? = null Surface.makeRaster(ImageInfo.makeN32Premul(width, height)).use { surface -> surface.canvas.apply { scale(width / sourceWidth, height / sourceHeight) dom.render(this) } - cachedRaster = surface.makeImageSnapshot() + snapshot = surface.makeImageSnapshot() } - lastWidth = width - lastHeight = height - return cachedRaster + return snapshot } /** diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt index 8c64204f..f3ced821 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt @@ -1,12 +1,16 @@ package org.cobalt.render.skia import io.github.humbleui.skija.BlendMode +import io.github.humbleui.skija.Canvas import io.github.humbleui.skija.ClipMode import io.github.humbleui.skija.ColorFilter +import io.github.humbleui.skija.Image import io.github.humbleui.skija.Paint import io.github.humbleui.skija.SamplingMode import io.github.humbleui.types.RRect import io.github.humbleui.types.Rect +import org.cobalt.math.SimpleVec3 +import org.cobalt.math.Dimensions /** Utilities for loading and drawing cached Skia images. * Images may be rounded and color-masked when drawn. @@ -47,43 +51,80 @@ object SkiaImages { * Draw the provided [SkiaImage] into the destination rectangle. * * @param image the cached image to draw - * @param x destination x coordinate - * @param y destination y coordinate - * @param width destination width in pixels - * @param height destination height in pixels + * @param pos destination coordinate + * @param dim destination dimension */ @JvmStatic - fun image(image: SkiaImage, x: Float, y: Float, width: Float, height: Float) { + fun image(image: SkiaImage, pos: SimpleVec3, dim: Dimensions) { + if (!isValidDimension(dim)) return val canvas = this.canvas ?: return - if (width <= 0 || height <= 0) return - val sourceImage = image.getOrGenerateRaster(width.toInt(), height.toInt()) ?: return + val sourceImage = image.getOrGenerateRaster(dim.width.toInt(), dim.height.toInt()) ?: return + drawConfiguredImage(canvas, image, pos, dim, sourceImage) + } + + private fun drawConfiguredImage( + canvas: Canvas, + image: SkiaImage, + pos: SimpleVec3, + dim: Dimensions, + sourceImage: Image + ) { Paint().use { paint -> - image.colorMask?.let { - paint.colorFilter = ColorFilter.makeBlend(it, BlendMode.SRC_ATOP) - } + configurePaint(paint, image.colorMask) + drawWithOptionalClip(canvas, image, pos, dim, sourceImage, paint) + } + } - if (image.radius != null && image.radius > 0f) { - canvas.save() - canvas.clipRRect(RRect.makeXYWH(x, y, width, height, image.radius), ClipMode.INTERSECT, true) - } + private fun isValidDimension(dim: Dimensions) = dim.width > 0 && dim.height > 0 + + private fun drawWithOptionalClip( + canvas: Canvas, + image: SkiaImage, + pos: SimpleVec3, + dim: Dimensions, + sourceImage: Image, + paint: Paint + ) { + val rrect = if (image.radius != null && image.radius > 0f) { + RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, image.radius) + } else null + withOptionalClip(canvas, rrect) { canvas.drawImageRect( sourceImage, Rect.makeWH(sourceImage.width.toFloat(), sourceImage.height.toFloat()), - Rect.makeXYWH(x, y, width, height), + Rect.makeXYWH(pos.x, pos.y, dim.width, dim.height), SamplingMode.MITCHELL, paint, false ) + } + } - if (image.radius != null && image.radius > 0f) { + private fun configurePaint(paint: Paint, colorMask: Int?) { + if (colorMask != null) { + paint.colorFilter = ColorFilter.makeBlend(colorMask, BlendMode.SRC_ATOP) + } + } + + private inline fun withOptionalClip( + canvas: io.github.humbleui.skija.Canvas, + rrect: RRect?, + block: () -> Unit, + ) { + if (rrect != null) { + canvas.save() + canvas.clipRRect(rrect, ClipMode.INTERSECT, true) + try { + block() + } finally { canvas.restore() } + } else { + block() } } } - - diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt index 5f0b389d..135a4b49 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt @@ -2,6 +2,8 @@ package org.cobalt.render.skia import io.github.humbleui.skija.Canvas import org.cobalt.Cobalt.minecraft +import org.cobalt.math.SimpleVec3 +import kotlin.math.min /** High-level Skia drawing helpers used by UI and module renderers. * Provides convenience functions for text, shapes, images and scissor management. @@ -18,7 +20,7 @@ object SkiaRenderer { val windowWidth = minecraft.window.width.toFloat() val windowHeight = minecraft.window.height.toFloat() - return minOf(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) + return min(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) } /** Save the current Skia canvas state. */ @@ -31,10 +33,9 @@ object SkiaRenderer { /** Translate the canvas by the given x/y offset. */ @JvmStatic - fun translate(x: Float, y: Float) = this.canvas?.translate(x, y) + fun translate(pos: SimpleVec3) = this.canvas?.translate(pos.x, pos.y) /** Scale the canvas by the specified X and Y factors. */ @JvmStatic - fun scale(x: Float, y: Float) = this.canvas?.scale(x, y) - + fun scale(scale: SimpleVec3) = this.canvas?.scale(scale.x, scale.y) } diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt index 906606ba..7a857987 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt @@ -1,57 +1,25 @@ package org.cobalt.render.skia -import io.github.humbleui.skija.ClipMode import io.github.humbleui.skija.Paint import io.github.humbleui.skija.PaintMode -import io.github.humbleui.skija.Shader import io.github.humbleui.types.RRect -import io.github.humbleui.types.Rect +import org.cobalt.math.Dimensions +import org.cobalt.math.SimpleVec3 /** Shape and scissor drawing helpers backed by the Skia canvas. */ object SkiaShapes { - private var scissorStackDepth = 0 private val canvas get() = SkiaContext.canvas - /** - * Push a scissor/clip rectangle onto the canvas stack. Subsequent draws - * will be clipped to the rectangle. - * - * @param x clip rectangle x coordinate - * @param y clip rectangle y coordinate - * @param width clip rectangle width - * @param height clip rectangle height - */ - @JvmStatic - fun pushScissor(x: Float, y: Float, width: Float, height: Float) { - val canvas = this.canvas ?: return - - if (width <= 0 || height <= 0) return - - canvas.save() - canvas.clipRect(Rect.makeXYWH(x, y, width, height), ClipMode.INTERSECT, true) - scissorStackDepth++ - } - - /** Pop the last scissor/clip rectangle and restore the previous canvas state. */ - @JvmStatic - fun popScissor() { - if (scissorStackDepth <= 0) return - canvas?.restore() - scissorStackDepth-- - } - /** * Draw a straight line between two points. * - * @param x1 start x - * @param x2 end x - * @param y1 start y - * @param y2 end y + * @param start start point + * @param end end point * @param color ARGB color * @param thickness line thickness */ @JvmStatic - fun line(x1: Float, x2: Float, y1: Float, y2: Float, color: Int, thickness: Float = 1f) { + fun line(start: SimpleVec3, end: SimpleVec3, color: Int, thickness: Float = 1f) { val canvas = this.canvas ?: return Paint().apply { @@ -60,83 +28,49 @@ object SkiaShapes { strokeWidth = thickness.coerceAtLeast(0f) isAntiAlias = true }.use { paint -> - canvas.drawLine(x1, y1, x2, y2, paint) - } - } - - /** Draw a filled rectangle. */ - @JvmStatic - fun rect(x: Float, y: Float, width: Float, height: Float, color: Int) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - - Paint().setColor(color).use { paint -> - canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) + canvas.drawLine(start.x, start.y, end.x, end.y, paint) } } /** - * Draw a stroked rectangle outline with the given thickness. + * Draw a filled rounded rectangle with the specified corner radius. * - * @param x rectangle x - * @param y rectangle y - * @param width rectangle width - * @param height rectangle height - * @param color ARGB color for the outline - * @param thickness outline thickness + * @param pos the position + * @param dim the dimensions + * @param radius corner radius + * @param color outline color */ @JvmStatic - fun outline(x: Float, y: Float, width: Float, height: Float, color: Int, thickness: Float = 1f) { + fun roundedRect(pos: SimpleVec3, dim: Dimensions, radius: Float, color: Int) { val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - val t = thickness.coerceAtLeast(0f) - val half = t / 2f - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = t - isAntiAlias = true - }.use { paint -> - canvas.drawRect(Rect.makeXYWH(x + half, y + half, width - t, height - t), paint) - } - } - - /** Draw a filled rounded rectangle with the specified corner radius. */ - @JvmStatic - fun roundedRect(x: Float, y: Float, width: Float, height: Float, radius: Float, color: Int) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return + if (dim.width <= 0f || dim.height <= 0f) return Paint().setColor(color).use { paint -> - canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) + canvas.drawRRect(RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, radius.coerceAtLeast(0f)), paint) } } /** * Draw a rounded rectangle outline with the specified thickness. * - * @param x rectangle x - * @param y rectangle y - * @param width rectangle width - * @param height rectangle height + * @param pos the position + * @param dim the dimensions * @param radius corner radius * @param color outline color * @param thickness outline thickness */ @JvmStatic fun roundedOutline( - x: Float, - y: Float, - width: Float, - height: Float, + pos: SimpleVec3, + dim: Dimensions, radius: Float, color: Int, thickness: Float = 1f, ) { + if (!isValid(dim)) { return } + val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return val t = thickness.coerceAtLeast(1f) val half = t / 2f @@ -148,137 +82,9 @@ object SkiaShapes { strokeWidth = t isAntiAlias = true }.use { paint -> - canvas.drawRRect(RRect.makeXYWH(x + half, y + half, width - t, height - t, innerRadius), paint) - } - } - - /** - * Draw a rectangle filled with a two-color linear gradient. - * - * @param x rectangle x - * @param y rectangle y - * @param width rectangle width - * @param height rectangle height - * @param colorStart start color ARGB - * @param colorEnd end color ARGB - * @param direction gradient direction - */ - @JvmStatic - fun gradientRect( - x: Float, y: Float, width: Float, height: Float, - colorStart: Int, colorEnd: Int, direction: SkiaGradient, - ) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - - val x1 = when (direction) { - SkiaGradient.LEFT_TO_RIGHT -> x + width - SkiaGradient.TOP_TO_BOTTOM -> x - } - - val y1 = when (direction) { - SkiaGradient.TOP_TO_BOTTOM -> y + height - SkiaGradient.LEFT_TO_RIGHT -> y - } - - Shader.makeLinearGradient( - x, y, x1, y1, - intArrayOf(colorStart, colorEnd) - ).use { shader -> - Paint().apply { - this.shader = shader - isAntiAlias = true - }.use { paint -> - canvas.drawRect(Rect.makeXYWH(x, y, width, height), paint) - } - } - } - - /** - * Draw a rounded rectangle filled with a two-color linear gradient. - * - * @param x rectangle x - * @param y rectangle y - * @param width rectangle width - * @param height rectangle height - * @param radius corner radius - * @param colorStart start color ARGB - * @param colorEnd end color ARGB - * @param direction gradient direction - */ - @JvmStatic - fun gradientRoundedRect( - x: Float, - y: Float, - width: Float, - height: Float, - radius: Float, - colorStart: Int, - colorEnd: Int, - direction: SkiaGradient, - ) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - - val x1 = when (direction) { - SkiaGradient.LEFT_TO_RIGHT -> x + width - SkiaGradient.TOP_TO_BOTTOM -> x - } - - val y1 = when (direction) { - SkiaGradient.TOP_TO_BOTTOM -> y + height - SkiaGradient.LEFT_TO_RIGHT -> y - } - - Shader.makeLinearGradient( - x, y, x1, y1, - intArrayOf(colorStart, colorEnd) - ).use { shader -> - Paint().apply { - this.shader = shader - isAntiAlias = true - }.use { paint -> - canvas.drawRRect(RRect.makeXYWH(x, y, width, height, radius.coerceAtLeast(0f)), paint) - } - } - } - - /** - * Draw a rectangle with rounded corners on one side only. - * - * @param x rectangle x - * @param y rectangle y - * @param width rectangle width - * @param height rectangle height - * @param radius corner radius - * @param color fill color ARGB - * @param side which side should have rounded corners - */ - @JvmStatic - fun halfRoundedRect( - x: Float, y: Float, width: Float, height: Float, - radius: Float, color: Int, side: SkiaSide = SkiaSide.TOP, - ) { - val canvas = this.canvas ?: return - if (width <= 0f || height <= 0f) return - - val r = radius.coerceAtLeast(0f) - - val radii = when (side) { - SkiaSide.TOP -> floatArrayOf(r, r, r, r, 0f, 0f, 0f, 0f) - SkiaSide.BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, r, r, r, r) - SkiaSide.LEFT -> floatArrayOf(r, r, 0f, 0f, 0f, 0f, r, r) - SkiaSide.RIGHT -> floatArrayOf(0f, 0f, r, r, r, r, 0f, 0f) - } - - Paint().apply { - setColor(color) - isAntiAlias = true - }.use { paint -> - canvas.drawRRect(RRect.makeComplexXYWH(x, y, width, height, radii), paint) + canvas.drawRRect(RRect.makeXYWH(pos.x + half, pos.y + half, dim.width - t, dim.height - t, innerRadius), paint) } } + private fun isValid(dim: Dimensions) = dim.width > 0f && dim.height > 0f } - - diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt index 27709692..a35bf821 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt +++ b/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt @@ -7,8 +7,10 @@ import io.github.humbleui.skija.FontHinting import io.github.humbleui.skija.FontMgr import io.github.humbleui.skija.Paint import io.github.humbleui.skija.TextLine +import org.cobalt.math.SimpleVec3 import java.io.IOException +/** Text drawing module */ object SkiaText { private val fonts = mutableMapOf() @@ -17,7 +19,11 @@ object SkiaText { private val canvas get() = SkiaContext.canvas - /** Load and cache a font from the given resource path. */ + /** + * Load and cache a font + * @param resourcePath the path + * @return the font + */ fun loadFont(resourcePath: String) = fonts.computeIfAbsent(resourcePath) { val bytes = javaClass.classLoader ?.getResourceAsStream(resourcePath) @@ -34,24 +40,43 @@ object SkiaText { } } + /** + * Text styling + * @property fontSize font size + * @property color color + */ data class TextStyle(val fontSize: Float, val color: Int) + /** + * Draw text + * @param font the font + * @param text the text + * @param pos the position + * @param style the style + */ @JvmStatic - fun text(font: Font, text: String, x: Float, y: Float, style: TextStyle) { + fun text(font: Font, text: String, pos: SimpleVec3, style: TextStyle) { val canvas = this.canvas ?: return + font.size = style.fontSize TextLine.make(text, font).use { line -> - val baseline = y - line.ascent - 1f + val baseline = pos.y - line.ascent - 1f Paint().setColor(style.color).use { paint -> - canvas.drawTextLine(line, x, baseline, paint) + canvas.drawTextLine(line, pos.x, baseline, paint) } } } + /** + * Get text width + * @param font the font + * @param text the text + * @param fontSize the font size + * @return the width + */ @JvmStatic - /** Measure and return the width of the given text using the font and size. */ fun textWidth(font: Font, text: String, fontSize: Float): Float { font.size = fontSize @@ -61,5 +86,3 @@ object SkiaText { } } - - diff --git a/src/main/kotlin/org/cobalt/render/skia/gl/State.kt b/src/main/kotlin/org/cobalt/render/skia/gl/State.kt index ca77708c..394431ef 100644 --- a/src/main/kotlin/org/cobalt/render/skia/gl/State.kt +++ b/src/main/kotlin/org/cobalt/render/skia/gl/State.kt @@ -75,69 +75,86 @@ class State(private val glVersion: Int) { glGetIntegerv(GL_CURRENT_PROGRAM, lastProgram) glGetIntegerv(GL_TEXTURE_BINDING_2D, lastTexture) - if (glVersion >= GL_VERSION_3_3 || GL.getCapabilities().GL_ARB_sampler_objects) { - glGetIntegerv(GL_SAMPLER_BINDING, lastSampler) - } + pushBindings() + pushEnables() + pushPixelStores() - glGetIntegerv(GL_ARRAY_BUFFER_BINDING, lastArrayBuffer) - glGetIntegerv(GL_VERTEX_ARRAY_BINDING, lastVertexArrayObject) + glPixelStorei(GL_UNPACK_ALIGNMENT, DEFAULT_UNPACK_ALIGNMENT) + glPixelStorei(GL_UNPACK_ROW_LENGTH, DEFAULT_UNPACK_ROW_LENGTH) + glPixelStorei(GL_UNPACK_SKIP_PIXELS, DEFAULT_UNPACK_SKIP_PIXELS) + glPixelStorei(GL_UNPACK_SKIP_ROWS, DEFAULT_UNPACK_SKIP_ROWS) + } - if (glVersion >= GL_VERSION_2_0) { - glGetIntegerv(GL_POLYGON_MODE, lastPolygonMode) - } + return this + } - glGetIntegerv(GL_VIEWPORT, lastViewport) - glGetIntegerv(GL_SCISSOR_BOX, lastScissorBox) - glGetIntegerv(GL_BLEND_SRC_RGB, lastBlendSrcRgb) - glGetIntegerv(GL_BLEND_DST_RGB, lastBlendDstRgb) - glGetIntegerv(GL_BLEND_SRC_ALPHA, lastBlendSrcAlpha) - glGetIntegerv(GL_BLEND_DST_ALPHA, lastBlendDstAlpha) - glGetIntegerv(GL_BLEND_EQUATION_RGB, lastBlendEquationRgb) - glGetIntegerv(GL_BLEND_EQUATION_ALPHA, lastBlendEquationAlpha) - - lastEnableBlend = glIsEnabled(GL_BLEND) - lastEnableCullFace = glIsEnabled(GL_CULL_FACE) - lastEnableDepthTest = glIsEnabled(GL_DEPTH_TEST) - lastEnableStencilTest = glIsEnabled(GL_STENCIL_TEST) - lastEnableScissorTest = glIsEnabled(GL_SCISSOR_TEST) + private fun Properties.pushBindings() { + if (glVersion >= GL_VERSION_3_3 || GL.getCapabilities().GL_ARB_sampler_objects) { + glGetIntegerv(GL_SAMPLER_BINDING, lastSampler) + } - if (glVersion >= GL_VERSION_3_1) { - lastEnablePrimitiveRestart = glIsEnabled(GL_PRIMITIVE_RESTART) - } + glGetIntegerv(GL_ARRAY_BUFFER_BINDING, lastArrayBuffer) + glGetIntegerv(GL_VERTEX_ARRAY_BINDING, lastVertexArrayObject) - lastDepthMask = glGetBoolean(GL_DEPTH_WRITEMASK) + if (glVersion >= GL_VERSION_2_0) { + glGetIntegerv(GL_POLYGON_MODE, lastPolygonMode) + } - glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, lastPixelUnpackBufferBinding) - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, DEFAULT_PIXEL_UNPACK_BINDING) + glGetIntegerv(GL_VIEWPORT, lastViewport) + glGetIntegerv(GL_SCISSOR_BOX, lastScissorBox) + glGetIntegerv(GL_BLEND_SRC_RGB, lastBlendSrcRgb) + glGetIntegerv(GL_BLEND_DST_RGB, lastBlendDstRgb) + glGetIntegerv(GL_BLEND_SRC_ALPHA, lastBlendSrcAlpha) + glGetIntegerv(GL_BLEND_DST_ALPHA, lastBlendDstAlpha) + glGetIntegerv(GL_BLEND_EQUATION_RGB, lastBlendEquationRgb) + glGetIntegerv(GL_BLEND_EQUATION_ALPHA, lastBlendEquationAlpha) + } - glGetIntegerv(GL_PACK_SWAP_BYTES, lastPackSwapBytes) - glGetIntegerv(GL_PACK_LSB_FIRST, lastPackLsbFirst) - glGetIntegerv(GL_PACK_ROW_LENGTH, lastPackRowLength) - glGetIntegerv(GL_PACK_SKIP_PIXELS, lastPackSkipPixels) - glGetIntegerv(GL_PACK_SKIP_ROWS, lastPackSkipRows) - glGetIntegerv(GL_PACK_ALIGNMENT, lastPackAlignment) + private fun Properties.pushEnables() { + lastEnableBlend = glIsEnabled(GL_BLEND) + lastEnableCullFace = glIsEnabled(GL_CULL_FACE) + lastEnableDepthTest = glIsEnabled(GL_DEPTH_TEST) + lastEnableStencilTest = glIsEnabled(GL_STENCIL_TEST) + lastEnableScissorTest = glIsEnabled(GL_SCISSOR_TEST) - glGetIntegerv(GL_UNPACK_SWAP_BYTES, lastUnpackSwapBytes) - glGetIntegerv(GL_UNPACK_LSB_FIRST, lastUnpackLsbFirst) - glGetIntegerv(GL_UNPACK_ALIGNMENT, lastUnpackAlignment) - glGetIntegerv(GL_UNPACK_ROW_LENGTH, lastUnpackRowLength) - glGetIntegerv(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels) - glGetIntegerv(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows) + if (glVersion >= GL_VERSION_3_1) { + lastEnablePrimitiveRestart = glIsEnabled(GL_PRIMITIVE_RESTART) + } - if (glVersion >= GL_VERSION_1_2) { - glGetIntegerv(GL_PACK_IMAGE_HEIGHT, lastPackImageHeight) - glGetIntegerv(GL_PACK_SKIP_IMAGES, lastPackSkipImages) - glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, lastUnpackImageHeight) - glGetIntegerv(GL_UNPACK_SKIP_IMAGES, lastUnpackSkipImages) - } + lastDepthMask = glGetBoolean(GL_DEPTH_WRITEMASK) + } - glPixelStorei(GL_UNPACK_ALIGNMENT, DEFAULT_UNPACK_ALIGNMENT) - glPixelStorei(GL_UNPACK_ROW_LENGTH, DEFAULT_UNPACK_ROW_LENGTH) - glPixelStorei(GL_UNPACK_SKIP_PIXELS, DEFAULT_UNPACK_SKIP_PIXELS) - glPixelStorei(GL_UNPACK_SKIP_ROWS, DEFAULT_UNPACK_SKIP_ROWS) + private fun Properties.pushPixelStores() { + glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, lastPixelUnpackBufferBinding) + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, DEFAULT_PIXEL_UNPACK_BINDING) + + pushPackPixelStores() + pushUnpackPixelStores() + + if (glVersion >= GL_VERSION_1_2) { + glGetIntegerv(GL_PACK_IMAGE_HEIGHT, lastPackImageHeight) + glGetIntegerv(GL_PACK_SKIP_IMAGES, lastPackSkipImages) + glGetIntegerv(GL_UNPACK_IMAGE_HEIGHT, lastUnpackImageHeight) + glGetIntegerv(GL_UNPACK_SKIP_IMAGES, lastUnpackSkipImages) } + } - return this + private fun Properties.pushPackPixelStores() { + glGetIntegerv(GL_PACK_SWAP_BYTES, lastPackSwapBytes) + glGetIntegerv(GL_PACK_LSB_FIRST, lastPackLsbFirst) + glGetIntegerv(GL_PACK_ROW_LENGTH, lastPackRowLength) + glGetIntegerv(GL_PACK_SKIP_PIXELS, lastPackSkipPixels) + glGetIntegerv(GL_PACK_SKIP_ROWS, lastPackSkipRows) + glGetIntegerv(GL_PACK_ALIGNMENT, lastPackAlignment) + } + + private fun Properties.pushUnpackPixelStores() { + glGetIntegerv(GL_UNPACK_SWAP_BYTES, lastUnpackSwapBytes) + glGetIntegerv(GL_UNPACK_LSB_FIRST, lastUnpackLsbFirst) + glGetIntegerv(GL_UNPACK_ALIGNMENT, lastUnpackAlignment) + glGetIntegerv(GL_UNPACK_ROW_LENGTH, lastUnpackRowLength) + glGetIntegerv(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels) + glGetIntegerv(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows) } /** @@ -196,28 +213,22 @@ class State(private val glVersion: Int) { lastBlendDstAlpha[0] ) - if (lastEnableBlend) glEnable(GL_BLEND) - else glDisable(GL_BLEND) - - if (lastEnableCullFace) glEnable(GL_CULL_FACE) - else glDisable(GL_CULL_FACE) - - if (lastEnableDepthTest) glEnable(GL_DEPTH_TEST) - else glDisable(GL_DEPTH_TEST) - - if (lastEnableStencilTest) glEnable(GL_STENCIL_TEST) - else glDisable(GL_STENCIL_TEST) - - if (lastEnableScissorTest) glEnable(GL_SCISSOR_TEST) - else glDisable(GL_SCISSOR_TEST) + setEnable(GL_BLEND, lastEnableBlend) + setEnable(GL_CULL_FACE, lastEnableCullFace) + setEnable(GL_DEPTH_TEST, lastEnableDepthTest) + setEnable(GL_STENCIL_TEST, lastEnableStencilTest) + setEnable(GL_SCISSOR_TEST, lastEnableScissorTest) if (glVersion >= GL_VERSION_3_1) { - if (lastEnablePrimitiveRestart) glEnable(GL_PRIMITIVE_RESTART) - else glDisable(GL_PRIMITIVE_RESTART) + setEnable(GL_PRIMITIVE_RESTART, lastEnablePrimitiveRestart) } } } + private fun setEnable(capability: Int, enable: Boolean) { + if (enable) glEnable(capability) else glDisable(capability) + } + private fun restorePolygonViewportAndScissor() { with(props) { if (glVersion >= GL_VERSION_2_0) { @@ -242,20 +253,8 @@ class State(private val glVersion: Int) { private fun restorePixelStoresAndBuffers() { with(props) { - glPixelStorei(GL_PACK_SWAP_BYTES, lastPackSwapBytes[0]) - glPixelStorei(GL_PACK_LSB_FIRST, lastPackLsbFirst[0]) - glPixelStorei(GL_PACK_ROW_LENGTH, lastPackRowLength[0]) - glPixelStorei(GL_PACK_SKIP_PIXELS, lastPackSkipPixels[0]) - glPixelStorei(GL_PACK_SKIP_ROWS, lastPackSkipRows[0]) - glPixelStorei(GL_PACK_ALIGNMENT, lastPackAlignment[0]) - - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, lastPixelUnpackBufferBinding[0]) - glPixelStorei(GL_UNPACK_SWAP_BYTES, lastUnpackSwapBytes[0]) - glPixelStorei(GL_UNPACK_LSB_FIRST, lastUnpackLsbFirst[0]) - glPixelStorei(GL_UNPACK_ALIGNMENT, lastUnpackAlignment[0]) - glPixelStorei(GL_UNPACK_ROW_LENGTH, lastUnpackRowLength[0]) - glPixelStorei(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels[0]) - glPixelStorei(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows[0]) + restorePackPixelStores() + restoreUnpackPixelStores() if (glVersion >= GL_VERSION_1_2) { glPixelStorei(GL_PACK_IMAGE_HEIGHT, lastPackImageHeight[0]) @@ -266,6 +265,25 @@ class State(private val glVersion: Int) { } } + private fun Properties.restorePackPixelStores() { + glPixelStorei(GL_PACK_SWAP_BYTES, lastPackSwapBytes[0]) + glPixelStorei(GL_PACK_LSB_FIRST, lastPackLsbFirst[0]) + glPixelStorei(GL_PACK_ROW_LENGTH, lastPackRowLength[0]) + glPixelStorei(GL_PACK_SKIP_PIXELS, lastPackSkipPixels[0]) + glPixelStorei(GL_PACK_SKIP_ROWS, lastPackSkipRows[0]) + glPixelStorei(GL_PACK_ALIGNMENT, lastPackAlignment[0]) + } + + private fun Properties.restoreUnpackPixelStores() { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, lastPixelUnpackBufferBinding[0]) + glPixelStorei(GL_UNPACK_SWAP_BYTES, lastUnpackSwapBytes[0]) + glPixelStorei(GL_UNPACK_LSB_FIRST, lastUnpackLsbFirst[0]) + glPixelStorei(GL_UNPACK_ALIGNMENT, lastUnpackAlignment[0]) + glPixelStorei(GL_UNPACK_ROW_LENGTH, lastUnpackRowLength[0]) + glPixelStorei(GL_UNPACK_SKIP_PIXELS, lastUnpackSkipPixels[0]) + glPixelStorei(GL_UNPACK_SKIP_ROWS, lastUnpackSkipRows[0]) + } + private fun restoreDepthMask() { with(props) { glDepthMask(lastDepthMask) diff --git a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt index 3db40956..a0a1a99a 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt @@ -39,10 +39,7 @@ data class Notification( * description within the component bounds. This method is called every * frame while the notification is visible. */ - override fun renderComponent() { - - } - + override fun renderComponent() { return } } diff --git a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt index 6d983eef..8771bbe0 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt @@ -4,6 +4,7 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.render.skia.SkiaRenderer +import org.cobalt.math.SimpleVec3 /** * Manager responsible for displaying on-screen notifications. @@ -53,9 +54,9 @@ object NotificationManager { val originX = notification.xPos val originY = notification.yPos - SkiaRenderer.translate(originX, originY) - SkiaRenderer.scale(windowScale, windowScale) - SkiaRenderer.translate(-originX, -originY) + SkiaRenderer.translate(SimpleVec3(originX, originY)) + SkiaRenderer.scale(SimpleVec3(windowScale, windowScale)) + SkiaRenderer.translate(SimpleVec3(-originX, -originY)) notification.renderComponent() @@ -64,3 +65,5 @@ object NotificationManager { } } + + diff --git a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt index 3143df40..1d2ae40f 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt @@ -8,6 +8,7 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.ui.animation.BounceAnimation import org.cobalt.render.skia.SkiaRenderer +import org.cobalt.math.SimpleVec3 internal object ConfigScreen : Screen(Component.empty()) { @@ -33,9 +34,9 @@ internal object ConfigScreen : Screen(Component.empty()) { val cy = height / 2f SkiaRenderer.save() - SkiaRenderer.translate(cx, cy) - SkiaRenderer.scale(scale, scale) - SkiaRenderer.translate(-cx, -cy) + SkiaRenderer.translate(SimpleVec3(cx, cy)) + SkiaRenderer.scale(SimpleVec3(scale, scale)) + SkiaRenderer.translate(SimpleVec3(-cx, -cy)) } // TODO: draw the actual UI here.. @@ -58,3 +59,5 @@ internal object ConfigScreen : Screen(Component.empty()) { } } + + From c6becd50ffe8837a7df01b4ccbd9ba5b8dd1452a Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 18 Apr 2026 20:58:06 +0200 Subject: [PATCH 12/18] refactor: remove useless comment --- src/main/kotlin/org/cobalt/Cobalt.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index 82636a35..468da548 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -35,7 +35,6 @@ object Cobalt : ClientModInitializer { ModuleManager.registerModules() CommandManager.register(MainCommand) - // Dispatch Events LevelRenderEvents.END_MAIN.register { context -> EventBus.post(WorldRenderEvent(context)) } From 41fef4c70061deebd12a2d4ac237750cf94ef6d4 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 18 Apr 2026 21:05:27 +0200 Subject: [PATCH 13/18] feat: fix bug with command function call --- src/main/kotlin/org/cobalt/command/Command.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index b9b46770..fc51456c 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -33,7 +33,7 @@ abstract class Command(val name: String) { if (function.findAnnotation() != null) { root.executes { - function.call(this) + function.call(this@Command) return@executes 1 } continue From e6c5a27d69902ee51b6119936c27939f73b4413b Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 19 Apr 2026 10:16:14 +0200 Subject: [PATCH 14/18] refactor: should be the last detekt issue --- src/main/kotlin/org/cobalt/command/Command.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index e370947d..312189d5 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -27,9 +27,14 @@ abstract class Command(val name: String, val aliases: List = emptyList> { val mainRoot = LiteralArgumentBuilder.literal(name) - val functions = this::class.declaredFunctions - for (function in functions) { + registerFunctions(mainRoot) + + return listOf(mainRoot) + buildAliases(mainRoot) + } + + private fun registerFunctions(mainRoot: LiteralArgumentBuilder) { + for (function in this::class.declaredFunctions) { function.isAccessible = true if (function.findAnnotation() != null) { @@ -44,15 +49,20 @@ abstract class Command(val name: String, val aliases: List = emptyList + private fun buildAliases( + mainRoot: LiteralArgumentBuilder + ): List> { + return aliases.filter { it.isNotBlank() }.map { alias -> val aliasRoot = LiteralArgumentBuilder.literal(alias) + mainRoot.arguments.forEach { child -> aliasRoot.then(child) } + mainRoot.command?.let { aliasRoot.executes(it) } + aliasRoot } - - return listOf(mainRoot) + aliasRoots } /** Construct a subcommand literal from a handler function and its parameters. */ From c72605a515199f17d0e68ac62bf0b8cd3051150b Mon Sep 17 00:00:00 2001 From: Nathan <209938737+quiteboring@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:10:11 -0400 Subject: [PATCH 15/18] chore: my attempt at better kdocs (i suck at this) --- config/detekt/detekt.yml | 2 +- .../client/AbstractClientPlayerAccessor.java | 4 +- .../cobalt/mixin/client/MinecraftMixin.java | 6 +- .../org/cobalt/mixin/gui/ChatScreenMixin.java | 25 -- .../mixin/gui/ClientPacketListenerMixin.java | 8 +- .../mixin/gui/CommandSuggestionsMixin.java | 2 +- .../cobalt/mixin/platform/WindowMixin.java | 2 +- .../cobalt/mixin/render/FrustumInvoker.java | 14 + src/main/kotlin/org/cobalt/Cobalt.kt | 23 +- src/main/kotlin/org/cobalt/command/Command.kt | 15 +- .../org/cobalt/command/CommandManager.kt | 53 +++- .../command/annotation/DefaultHandler.kt | 3 + .../cobalt/command/annotation/SubCommand.kt | 5 +- .../org/cobalt/command/impl/MainCommand.kt | 12 +- src/main/kotlin/org/cobalt/dsl/Render.kt | 50 ++- src/main/kotlin/org/cobalt/dsl/Utils.kt | 25 +- src/main/kotlin/org/cobalt/event/Event.kt | 32 +- src/main/kotlin/org/cobalt/event/EventBus.kt | 71 +++-- .../cobalt/event/annotation/SubscribeEvent.kt | 13 +- .../org/cobalt/event/impl/ChatSendEvent.kt | 14 +- .../org/cobalt/event/impl/PacketEvent.kt | 25 +- .../org/cobalt/event/impl/SkiaDrawEvent.kt | 16 +- .../kotlin/org/cobalt/event/impl/TickEvent.kt | 13 +- .../org/cobalt/event/impl/WorldRenderEvent.kt | 13 +- src/main/kotlin/org/cobalt/math/Math.kt | 22 +- src/main/kotlin/org/cobalt/module/Module.kt | 101 ++++-- .../kotlin/org/cobalt/module/ModuleManager.kt | 57 +++- .../module/impl/render/PerformanceHUD.kt | 36 +-- .../org/cobalt/render/skia/SkiaEnums.kt | 40 --- .../org/cobalt/render/skia/SkiaRenderer.kt | 41 --- .../org/cobalt/render/skia/SkiaShapes.kt | 90 ------ .../org/cobalt/render/skia/gl/Properties.kt | 179 ----------- src/main/kotlin/org/cobalt/ui/ColorPalette.kt | 41 +-- src/main/kotlin/org/cobalt/ui/UIComponent.kt | 25 +- .../org/cobalt/ui/animation/Animation.kt | 26 +- .../cobalt/ui/animation/BounceAnimation.kt | 3 + .../cobalt/ui/notification/Notification.kt | 22 +- .../ui/notification/NotificationManager.kt | 40 +-- .../org/cobalt/ui/screen/ConfigScreen.kt | 15 +- .../org/cobalt/ui/screen/HudEditorScreen.kt | 16 +- src/main/kotlin/org/cobalt/util/ChatUtils.kt | 66 +++- src/main/kotlin/org/cobalt/util/ColorUtils.kt | 45 ++- .../kotlin/org/cobalt/util/FrustumUtils.kt | 24 +- src/main/kotlin/org/cobalt/util/MouseUtils.kt | 21 -- .../cobalt/{render => util}/RenderUtils.kt | 243 +++++++------- .../kotlin/org/cobalt/util/ServerUtils.kt | 65 ++-- src/main/kotlin/org/cobalt/util/WebUtils.kt | 17 +- .../kotlin/org/cobalt/util/WindowUtils.kt | 51 +++ .../kotlin/org/cobalt/util/helper/Layers.kt | 20 +- .../org/cobalt/util/helper/Pipelines.kt | 27 +- .../org/cobalt/util/helper/TickScheduler.kt | 16 +- .../cobalt/util/rotation/DefaultRotations.kt | 100 ------ .../org/cobalt/util/rotation/IRotation.kt | 46 --- .../cobalt/util/rotation/RotationManager.kt | 66 ---- .../{render => util}/skia/SkiaContext.kt | 8 +- .../kotlin/org/cobalt/util/skia/SkiaEnums.kt | 33 ++ .../cobalt/{render => util}/skia/SkiaImage.kt | 63 ++-- .../{render => util}/skia/SkiaImages.kt | 72 +++-- .../kotlin/org/cobalt/util/skia/SkiaShapes.kt | 296 ++++++++++++++++++ .../cobalt/{render => util}/skia/SkiaText.kt | 73 +++-- .../org/cobalt/util/skia/SkiaTransforms.kt | 47 +++ .../skia/WrappedBackendRenderTarget.kt | 25 +- .../org/cobalt/util/skia/gl/Properties.kt | 89 ++++++ .../cobalt/{render => util}/skia/gl/State.kt | 35 +-- .../cobalt/{render => util}/skia/gl/States.kt | 20 +- src/main/resources/cobalt.mixins.json | 1 - 66 files changed, 1489 insertions(+), 1280 deletions(-) delete mode 100644 src/main/java/org/cobalt/mixin/gui/ChatScreenMixin.java delete mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt delete mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt delete mode 100644 src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt delete mode 100644 src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt delete mode 100644 src/main/kotlin/org/cobalt/util/MouseUtils.kt rename src/main/kotlin/org/cobalt/{render => util}/RenderUtils.kt (61%) create mode 100644 src/main/kotlin/org/cobalt/util/WindowUtils.kt delete mode 100644 src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt delete mode 100644 src/main/kotlin/org/cobalt/util/rotation/IRotation.kt delete mode 100644 src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt rename src/main/kotlin/org/cobalt/{render => util}/skia/SkiaContext.kt (95%) create mode 100644 src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt rename src/main/kotlin/org/cobalt/{render => util}/skia/SkiaImage.kt (67%) rename src/main/kotlin/org/cobalt/{render => util}/skia/SkiaImages.kt (63%) create mode 100644 src/main/kotlin/org/cobalt/util/skia/SkiaShapes.kt rename src/main/kotlin/org/cobalt/{render => util}/skia/SkiaText.kt (51%) create mode 100644 src/main/kotlin/org/cobalt/util/skia/SkiaTransforms.kt rename src/main/kotlin/org/cobalt/{render => util}/skia/WrappedBackendRenderTarget.kt (75%) create mode 100644 src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt rename src/main/kotlin/org/cobalt/{render => util}/skia/gl/State.kt (87%) rename src/main/kotlin/org/cobalt/{render => util}/skia/gl/States.kt (70%) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index de06dd78..b59c1e65 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -57,7 +57,7 @@ complexity: CyclomaticComplexMethod: active: true - allowedComplexity: 5 + allowedComplexity: 6 NestedBlockDepth: active: true diff --git a/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java b/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java index 62f2fd38..10168103 100644 --- a/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java +++ b/src/main/java/org/cobalt/mixin/client/AbstractClientPlayerAccessor.java @@ -9,9 +9,9 @@ public interface AbstractClientPlayerAccessor { /** - * Returns the backing {@link PlayerInfo} instance from {@link AbstractClientPlayer}. + * Returns the client-side player info. * - * @return the current player info for this client player + * @return the {@link net.minecraft.client.multiplayer.PlayerInfo} associated with this player */ @Accessor("playerInfo") PlayerInfo getClientPlayerInfo(); diff --git a/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java b/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java index cea354d9..376d8386 100644 --- a/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java +++ b/src/main/java/org/cobalt/mixin/client/MinecraftMixin.java @@ -23,7 +23,7 @@ import org.cobalt.Cobalt; import org.cobalt.event.EventBus; import org.cobalt.event.impl.TickEvent; -import org.cobalt.render.skia.SkiaContext; +import org.cobalt.util.skia.SkiaContext; import org.lwjgl.glfw.GLFW; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -45,7 +45,7 @@ private void registerSkia(GameConfig gameConfig, CallbackInfo ci) { int finalWidth = Math.max(width[0], 1); int finalHeight = Math.max(height[0], 1); - SkiaContext.INSTANCE.initSkia(finalWidth, finalHeight); + SkiaContext.INSTANCE.initSkia$cobalt(finalWidth, finalHeight); } @Inject( @@ -57,7 +57,7 @@ private void registerSkia(GameConfig gameConfig, CallbackInfo ci) { require = 1 ) private void onBeforeFlipFrame(boolean advanceGameTime, CallbackInfo ci) { - SkiaContext.INSTANCE.draw(); + SkiaContext.INSTANCE.draw$cobalt(); } @Inject(method = "tick", at = @At("HEAD")) diff --git a/src/main/java/org/cobalt/mixin/gui/ChatScreenMixin.java b/src/main/java/org/cobalt/mixin/gui/ChatScreenMixin.java deleted file mode 100644 index 3f2bf163..00000000 --- a/src/main/java/org/cobalt/mixin/gui/ChatScreenMixin.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cobalt.mixin.gui; - -import net.minecraft.client.gui.screens.ChatScreen; -import org.cobalt.event.EventBus; -import org.cobalt.event.impl.ChatSendEvent; -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.CallbackInfo; - -@Mixin(ChatScreen.class) -public class ChatScreenMixin { - - @Inject(method = "handleChatInput", at = @At("HEAD"), cancellable = true) - private void handleChatMessage(String msg, boolean addToRecent, CallbackInfo ci) { - ChatSendEvent event = new ChatSendEvent(msg); - EventBus.post(event); - - if (event.isCancelled()) { - ci.cancel(); - } - } - - -} diff --git a/src/main/java/org/cobalt/mixin/gui/ClientPacketListenerMixin.java b/src/main/java/org/cobalt/mixin/gui/ClientPacketListenerMixin.java index 104f482b..b56cae84 100644 --- a/src/main/java/org/cobalt/mixin/gui/ClientPacketListenerMixin.java +++ b/src/main/java/org/cobalt/mixin/gui/ClientPacketListenerMixin.java @@ -2,6 +2,8 @@ import net.minecraft.client.multiplayer.ClientPacketListener; import org.cobalt.command.CommandManager; +import org.cobalt.event.EventBus; +import org.cobalt.event.impl.ChatSendEvent; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -12,8 +14,10 @@ public class ClientPacketListenerMixin { @Inject(method = "sendChat", at = @At("HEAD"), cancellable = true) public void sendChatMessage(String content, CallbackInfo ci) { - if (content.startsWith(String.valueOf(CommandManager.getPrefix()))) { - CommandManager.handleCommandExecution(content); + ChatSendEvent event = new ChatSendEvent(content); + EventBus.post(event); + + if (event.isCancelled()) { ci.cancel(); } } diff --git a/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java b/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java index 94662eb7..72d31fca 100644 --- a/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java +++ b/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java @@ -64,7 +64,7 @@ public void refresh(CallbackInfo ci, @Local(name = "reader") StringReader reader return; } - if (!reader.canRead() || reader.peek() != CommandManager.getPrefix()) { + if (!reader.canRead() || reader.peek() != CommandManager.PREFIX) { reader.setCursor(0); return; } diff --git a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java index aa1cfaf4..7dccad38 100644 --- a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java +++ b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java @@ -19,7 +19,7 @@ package org.cobalt.mixin.platform; import com.mojang.blaze3d.platform.Window; -import org.cobalt.render.skia.SkiaContext; +import org.cobalt.util.skia.SkiaContext; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; diff --git a/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java b/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java index 1b74d5d2..fc4be8cc 100644 --- a/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java +++ b/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java @@ -7,6 +7,20 @@ @Mixin(Frustum.class) public interface FrustumInvoker { + /** + * Tests whether an axis-aligned bounding box is inside, intersecting, or outside the view frustum. + * + * @param minX the x-coordinate of the minimum corner + * @param minY the y-coordinate of the minimum corner + * @param minZ the z-coordinate of the minimum corner + * @param maxX the x-coordinate of the maximum corner + * @param maxY the y-coordinate of the maximum corner + * @param maxZ the z-coordinate of the maximum corner + * + * @return {@link org.joml.FrustumIntersection#INSIDE}, + * {@link org.joml.FrustumIntersection#INTERSECT}, + * or a plane index if outside the frustum + */ @Invoker int invokeCubeInFrustum(double minX, double minY, double minZ, double maxX, double maxY, double maxZ); diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index 468da548..ae2a1bf5 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -5,32 +5,39 @@ import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.ModContainer import net.minecraft.client.Minecraft +import org.cobalt.Cobalt.MOD_CONTAINER +import org.cobalt.Cobalt.MOD_NAME +import org.cobalt.Cobalt.MOD_VERSION +import org.cobalt.Cobalt.minecraft import org.cobalt.command.CommandManager import org.cobalt.command.impl.MainCommand import org.cobalt.event.EventBus import org.cobalt.event.impl.WorldRenderEvent import org.cobalt.module.ModuleManager -/** Main mod entrypoint and shared constants for the Cobalt client mod. */ +/** + * Main mod entrypoint and contains shared constants for Cobalt. + * + * @property minecraft global Minecraft client instance + * @property MOD_CONTAINER Fabric mod container for this mod + * @property MOD_NAME display name of the mod from metadata + * @property MOD_VERSION version string from mod metadata + */ object Cobalt : ClientModInitializer { - /** Cached Minecraft client instance. */ - @JvmField - val minecraft: Minecraft = Minecraft.getInstance() + @JvmStatic + val minecraft: Minecraft + get() = Minecraft.getInstance() - /** The Fabric ModContainer for this mod. */ @JvmField val MOD_CONTAINER: ModContainer = FabricLoader.getInstance().getModContainer("cobalt").orElseThrow() - /** Human-readable mod name. */ @JvmField val MOD_NAME: String = MOD_CONTAINER.metadata.name - /** Friendly mod version string. */ @JvmField val MOD_VERSION: String = MOD_CONTAINER.metadata.version.friendlyString - /** Called when the client initializes; registers modules and commands and wires render events. */ override fun onInitializeClient() { ModuleManager.registerModules() CommandManager.register(MainCommand) diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index 312189d5..8bc50b29 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -17,15 +17,15 @@ import net.minecraft.client.multiplayer.ClientSuggestionProvider import org.cobalt.command.annotation.DefaultHandler import org.cobalt.command.annotation.SubCommand -/** Base class for defining chat commands; reflection is used to discover handlers and subcommands. +/** + * Base class for chat commands with option for subcommands. * - * @property name the primary literal name of this command - * @property aliases any secondary names for this command + * @property name primary command name + * @property aliases alternate command names */ abstract class Command(val name: String, val aliases: List = emptyList()) { - /** Build a Brigadier LiteralArgumentBuilder for this command, wiring discovered handlers and subcommands. */ - fun build(): List> { + internal fun build(): List> { val mainRoot = LiteralArgumentBuilder.literal(name) registerFunctions(mainRoot) @@ -56,16 +56,12 @@ abstract class Command(val name: String, val aliases: List = emptyList> { return aliases.filter { it.isNotBlank() }.map { alias -> val aliasRoot = LiteralArgumentBuilder.literal(alias) - mainRoot.arguments.forEach { child -> aliasRoot.then(child) } - mainRoot.command?.let { aliasRoot.executes(it) } - aliasRoot } } - /** Construct a subcommand literal from a handler function and its parameters. */ private fun buildSubCommand(function: KFunction<*>): LiteralArgumentBuilder { val literal = LiteralArgumentBuilder.literal(function.name) @@ -139,7 +135,6 @@ abstract class Command(val name: String, val aliases: List = emptyList() + internal val dispatcher = CommandDispatcher() - /** Character prefix used to identify chat commands. */ - @JvmStatic - val prefix: Char = '.' + /** + * Prefix for custom commands. + */ + internal const val PREFIX: Char = '.' - /** Register a top-level command into the Brigadier dispatcher. */ + init { + EventBus.register(this) + } + + /** + * Registers a command with the dispatcher. + * + * @param command the command instance to register + */ @JvmStatic fun register(command: Command) { command.build().forEach { dispatcher.register(it) } } - /** Execute a command line string as if entered by the player; logs and notifies on failure. */ - @JvmStatic - fun handleCommandExecution(content: String) { + @Suppress("UndocumentedPublicFunction") + @SubscribeEvent + fun handleCommandExecution(@Suppress("UnusedParameter") event: ChatSendEvent) { + val content = event.message + + if (!content.startsWith(PREFIX)) { + return + } + val player = minecraft.player ?: return - val commandLine = content.removePrefix(prefix.toString()).trim() + val commandLine = content.removePrefix(PREFIX.toString()).trim() - if (commandLine.isEmpty()) { return } + if (commandLine.isEmpty()) { + return + } try { dispatcher.execute(commandLine, player.connection.suggestionsProvider) } catch (exception: CommandSyntaxException) { ChatUtils.sendSystemMessage("${ChatFormatting.RED}${exception.message}") } + + event.setCancelled(true) } + } diff --git a/src/main/kotlin/org/cobalt/command/annotation/DefaultHandler.kt b/src/main/kotlin/org/cobalt/command/annotation/DefaultHandler.kt index dc080f80..31170244 100644 --- a/src/main/kotlin/org/cobalt/command/annotation/DefaultHandler.kt +++ b/src/main/kotlin/org/cobalt/command/annotation/DefaultHandler.kt @@ -1,5 +1,8 @@ package org.cobalt.command.annotation +/** + * Marks a function as the default command handler. + */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class DefaultHandler diff --git a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt index 134d3e03..4f2eab38 100644 --- a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt +++ b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt @@ -1,10 +1,9 @@ package org.cobalt.command.annotation /** - * Marks a function as a named sub-command for a command handler. + * Marks a function as a sub-command. * - * @param name optional sub-command label; when empty the function name is - * used by the dispatcher + * @param name optional label that defaults to the function name */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) diff --git a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt index e88b79e9..a9f91c3c 100644 --- a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt +++ b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt @@ -7,10 +7,9 @@ import org.cobalt.command.annotation.SubCommand import org.cobalt.ui.screen.ConfigScreen import org.cobalt.ui.screen.HudEditorScreen import org.cobalt.util.helper.TickScheduler -import org.cobalt.util.rotation.DefaultRotations -import org.cobalt.util.rotation.RotationManager internal object MainCommand : Command(name = "cobalt", aliases = listOf("cb")) { + private const val DELAY_TICKS = 1L @DefaultHandler @@ -27,13 +26,4 @@ internal object MainCommand : Command(name = "cobalt", aliases = listOf("cb")) { } } - @SubCommand - fun rotate(yaw: Double, pitch: Double) { - RotationManager.setActiveRotation( - DefaultRotations, - yaw = yaw, - pitch = pitch - ) - } - } diff --git a/src/main/kotlin/org/cobalt/dsl/Render.kt b/src/main/kotlin/org/cobalt/dsl/Render.kt index d8a291b3..0fb626b1 100644 --- a/src/main/kotlin/org/cobalt/dsl/Render.kt +++ b/src/main/kotlin/org/cobalt/dsl/Render.kt @@ -6,24 +6,60 @@ import net.minecraft.core.BlockPos import net.minecraft.world.entity.Entity import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 -import org.cobalt.render.RenderUtils +import org.cobalt.util.RenderUtils -/** Draw a wireframe outline of a block at the given world block position. */ +/** + * Draw a unit cube wireframe and optional translucent fill at the given block position. + * + * @param pos the block position to draw + * @param color the color to use for outline/fill + * @param esp when true uses ESP render type variants + * @param lineWidth line thickness for outlines + */ fun LevelRenderContext.drawBlockPos(pos: BlockPos, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawBlockPos(this, pos, color, esp, lineWidth) -/** Draw an outline around an entity using the provided color and line width. */ +/** + * Draw an outline around the provided entity's bounding box, interpolated for rendering. + * + * @param entity the entity whose bounding box will be outlined + * @param color the outline color + * @param esp when true uses ESP render type variants + * @param lineWidth outline thickness + */ fun LevelRenderContext.drawEntityOutline(entity: Entity, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawEntityOutline(this, entity, color, esp, lineWidth) -/** Draw a tracer (line) from the camera/renderer position to the given world vector. */ +/** + * Draw a line from the camera position toward the supplied world-space target point. + * + * @param to world-space target coordinate for the tracer + * @param color tracer color + * @param esp when true uses ESP render type variants + * @param lineWidth tracer thickness + */ fun LevelRenderContext.drawTracer(to: Vec3, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawTracer(this, to, color, esp, lineWidth) -/** Draw a colored axis-aligned bounding box (AABB) in world space. */ +/** + * Draw a colored axis-aligned bounding box (AABB) with optional translucent fill and outline. + * + * @param box the world-space axis-aligned bounding box + * @param color color used for fill/outline + * @param esp when true uses ESP render type variants + * @param lineWidth outline thickness + */ fun LevelRenderContext.drawBox(box: AABB, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = RenderUtils.drawBox(this, box, color, esp, lineWidth) -/** Draw a colored line between two world-space points using the renderer context. */ +/** + * Draw a colored line between two world-space points. + * + * @param from start point in world coordinates + * @param to end point in world coordinates + * @param color the color to use for the line + * @param esp whether ESP rendering is enabled + * @param lineWidth thickness of the line + */ fun LevelRenderContext.drawLine(from: Vec3, to: Vec3, color: Color, esp: Boolean = false, lineWidth: Float = 1f) = - RenderUtils.drawLine(this, from, to, color, RenderUtils.LineStyle(esp, lineWidth)) + RenderUtils.drawLine(this, from, to, color, esp, lineWidth) diff --git a/src/main/kotlin/org/cobalt/dsl/Utils.kt b/src/main/kotlin/org/cobalt/dsl/Utils.kt index 5c6807cd..814a70e2 100644 --- a/src/main/kotlin/org/cobalt/dsl/Utils.kt +++ b/src/main/kotlin/org/cobalt/dsl/Utils.kt @@ -2,27 +2,38 @@ package org.cobalt.dsl import org.cobalt.Cobalt.minecraft - -/** Extract the red component (0-255) from an ARGB integer. */ +/** + * Extracts the red component from an ARGB color integer. + */ inline val Int.red get() = this shr 16 and 0xFF -/** Extract the green component (0-255) from an ARGB integer. */ +/** + * Extracts the green component from an ARGB color integer. + */ inline val Int.green get() = this shr 8 and 0xFF -/** Extract the blue component (0-255) from an ARGB integer. */ +/** + * Extracts the blue component from an ARGB color integer. + */ inline val Int.blue get() = this and 0xFF -/** Extract the alpha component (0-255) from an ARGB integer. */ +/** + * Extracts the alpha component from an ARGB color integer. + */ inline val Int.alpha get() = this shr 24 and 0xFF -/** Current mouse X position in screen coordinates as a Float. */ +/** + * The current X position of the mouse cursor. + */ inline val mouseX: Float get() = minecraft.mouseHandler.xpos().toFloat() -/** Current mouse Y position in screen coordinates as a Float. */ +/** + * The current Y position of the mouse cursor. + */ inline val mouseY: Float get() = minecraft.mouseHandler.ypos().toFloat() diff --git a/src/main/kotlin/org/cobalt/event/Event.kt b/src/main/kotlin/org/cobalt/event/Event.kt index 627a174d..dfc64636 100644 --- a/src/main/kotlin/org/cobalt/event/Event.kt +++ b/src/main/kotlin/org/cobalt/event/Event.kt @@ -1,26 +1,40 @@ package org.cobalt.event -/** Base event type used by the module event system. */ +/** + * Base class for all custom Cobalt events. + */ abstract class Event { - /** Base class for cancellable events which can be prevented from propagating. */ + /** + * Base class for events that can be canceled. + */ abstract class Cancellable : Event() { private var cancelled = false - /** Return true when this event has been cancelled and should not be processed further. */ + /** + * Returns whether this event has been canceled. + * + * @return true if the event is canceled, false otherwise + */ fun isCancelled(): Boolean { return cancelled } - /** Mark this event as cancelled or not. */ + /** + * Sets the canceled state of this event. + * + * @param cancelled whether the event should be canceled + */ fun setCancelled(cancelled: Boolean) { this.cancelled = cancelled } } - /** Event delivery priority used by subscribers to control ordering. */ + /** + * Priority levels used to determine event listener execution order. + */ enum class Priority { /** Highest delivery priority; handlers with this priority run before others. */ @@ -38,10 +52,10 @@ abstract class Event { /** Lowest delivery priority; runs last. */ LOWEST; - /** Numeric weight corresponding to the priority's ordinal. */ - fun weight(): Int { - return ordinal - } + /** + * Numeric weight used for sorting priorities. + */ + fun weight(): Int = ordinal } diff --git a/src/main/kotlin/org/cobalt/event/EventBus.kt b/src/main/kotlin/org/cobalt/event/EventBus.kt index ff5068b4..9c0e838e 100644 --- a/src/main/kotlin/org/cobalt/event/EventBus.kt +++ b/src/main/kotlin/org/cobalt/event/EventBus.kt @@ -1,13 +1,16 @@ package org.cobalt.event -import org.cobalt.event.annotation.SubscribeEvent import java.lang.invoke.MethodHandles import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList +import org.cobalt.event.annotation.SubscribeEvent import org.slf4j.LoggerFactory -/** Lightweight event bus used to register listeners and post events to handlers with priorities. */ +/** + * Central event bus responsible for registering, unregistering, + * and dispatching events to subscribed listeners. + */ object EventBus { private data class Handler( @@ -23,7 +26,11 @@ object EventBus { private val cache = ConcurrentHashMap, Array>() private val logger = LoggerFactory.getLogger(this::class.java) - /** Register all methods annotated with [SubscribeEvent] from the given listener instance. */ + /** + * Registers all methods annotated with [SubscribeEvent] from the given listener instance. + * + * @param listener the object containing event subscriber methods + */ @JvmStatic fun register(listener: Any) { if (handlers.any { it.listener === listener }) return @@ -35,6 +42,40 @@ object EventBus { cache.clear() } + /** + * Unregisters all event handlers associated with the given listener instance. + * + * @param listener the listener whose event handlers should be removed + */ + @JvmStatic + fun unregister(listener: Any) { + handlers.removeIf { it.listener === listener } + cache.clear() + } + + /** + * Posts an event to all registered listeners. + * + * Event handlers are filtered by type, priority, and cancellation rules. + * + * @param event the event to dispatch + * @return the same event instance after processing + */ + @JvmStatic + fun post(event: Event): Event { + val eventClass = event.javaClass + val matched = cache.computeIfAbsent(eventClass) { computeMatchedHandlers(eventClass) } + + val toRemove = processMatchedHandlers(matched, event) + + if (toRemove.isNotEmpty()) { + handlers.removeAll(toRemove.toSet()) + cache.clear() + } + + return event + } + private fun createHandlersForListener(listener: Any): List { val result = mutableListOf() listener.javaClass.declaredMethods.forEach { method -> @@ -74,29 +115,6 @@ object EventBus { ) } - /** Unregister all handlers for the given listener instance. */ - @JvmStatic - fun unregister(listener: Any) { - handlers.removeIf { it.listener === listener } - cache.clear() - } - - /** Post an event to all matching handlers and return the event. */ - @JvmStatic - fun post(event: Event): Event { - val eventClass = event.javaClass - val matched = cache.computeIfAbsent(eventClass) { computeMatchedHandlers(eventClass) } - - val toRemove = processMatchedHandlers(matched, event) - - if (toRemove.isNotEmpty()) { - handlers.removeAll(toRemove.toSet()) - cache.clear() - } - - return event - } - private fun processMatchedHandlers(matched: Array, event: Event): MutableList { val toRemove = mutableListOf() @@ -117,7 +135,6 @@ object EventBus { if (handler.once) toRemove.add(handler) } - private fun computeMatchedHandlers(eventClass: Class<*>): Array { return handlers .filter { it.eventType.isAssignableFrom(eventClass) } diff --git a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt index 25a289b8..92dce0f4 100644 --- a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt +++ b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt @@ -2,14 +2,15 @@ package org.cobalt.event.annotation import org.cobalt.event.Event -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -/** Annotation to mark methods as event subscribers. +/** + * Marks a function as an event subscriber. * - * @param ignoreCancelled if true the subscriber will still receive events that were cancelled - * @param priority delivery priority for ordering subscribers - * @param once when true the subscriber will be removed after the first invocation + * @param ignoreCancelled whether to receive canceled events + * @param priority subscriber execution priority + * @param once if true, subscriber is removed after first call */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) annotation class SubscribeEvent( val ignoreCancelled: Boolean = false, val priority: Event.Priority = Event.Priority.MEDIUM, diff --git a/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt b/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt index a769e53d..db13fdd8 100644 --- a/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/ChatSendEvent.kt @@ -2,8 +2,12 @@ package org.cobalt.event.impl import org.cobalt.event.Event -/** Event fired when the player sends a chat message; cancellable. */ -class ChatSendEvent( - /** The raw chat message content that is being sent. */ - val message: String -) : Event.Cancellable() +/** + * Custom event fired when the player sends a chat message. + * + * This event extends [Event.Cancellable] and can be canceled to prevent + * the message from being sent. + * + * @property message the message being sent + */ +class ChatSendEvent(val message: String) : Event.Cancellable() diff --git a/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt b/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt index ec2e2c34..566ddc44 100644 --- a/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/PacketEvent.kt @@ -3,16 +3,33 @@ package org.cobalt.event.impl import net.minecraft.network.protocol.Packet import org.cobalt.event.Event -/** Base event for network packet send/receive operations; cancellable. */ +/** + * Base type for all packet-related events. + * + * @property packet the packet being sent or received + */ abstract class PacketEvent( - /** The network packet associated with the event. */ val packet: Packet<*>, ) : Event.Cancellable() { - /** Event fired when a packet is sent from the client. */ + /** + * Custom event fired when a packet is sent to the server. + * + * This event is cancellable via [Event.Cancellable], which prevents the + * packet from being sent. + * + * @property packet the packet being sent + */ class Send(packet: Packet<*>) : PacketEvent(packet) - /** Event fired when a packet is received by the client. */ + /** + * Custom event fired when a packet is received from the server. + * + * This event is cancellable via [Event.Cancellable], which prevents the + * packet from being processed. + * + * @property packet the packet being received + */ class Receive(packet: Packet<*>) : PacketEvent(packet) } diff --git a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt index 02010af6..c91cdae9 100644 --- a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt @@ -3,14 +3,20 @@ package org.cobalt.event.impl import io.github.humbleui.skija.Canvas import io.github.humbleui.skija.DirectContext import org.cobalt.event.Event -import org.cobalt.render.skia.WrappedBackendRenderTarget +import org.cobalt.util.skia.WrappedBackendRenderTarget -/** Event fired when Skia drawing is performed for a render pass. */ +/** + * Custom event fired when Skia performs its draw step for the current frame. + * + * This is the callback point where custom rendering can be performed on the + * Skia canvas before it is submitted. + * + * @property context the Skia direct rendering context + * @property renderTarget the backend render target used for rendering + * @property canvas the Skia canvas for drawing operations + */ class SkiaDrawEvent( - /** The Skia DirectContext used for GPU operations. */ val context: DirectContext, - /** The wrapped backend render target representing the surface being rendered to. */ val renderTarget: WrappedBackendRenderTarget, - /** The Skia Canvas used to issue draw commands. */ val canvas: Canvas ) : Event() diff --git a/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt b/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt index 4ece4c75..154f58b8 100644 --- a/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/TickEvent.kt @@ -2,13 +2,18 @@ package org.cobalt.event.impl import org.cobalt.event.Event -/** Events that represent client tick boundaries. */ +/** + * Base event for client tick lifecycle events. + */ abstract class TickEvent : Event() { - /** Event fired at the start of the client tick. */ + /** + * Custom event fired at the start of the client tick. + */ class Start : TickEvent() - /** Event fired at the end of the client tick. */ + /** + * Custom event fired at the end of the client tick. + */ class End : TickEvent() - } diff --git a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt index 544402bf..5491e662 100644 --- a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt @@ -1,14 +1,15 @@ package org.cobalt.event.impl import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext -import net.minecraft.client.Camera -import net.minecraft.client.DeltaTracker -import net.minecraft.client.renderer.MultiBufferSource -import net.minecraft.client.renderer.culling.Frustum import org.cobalt.event.Event -/** Event emitted during the world render pass providing the rendering context. */ +/** + * Custom event fired at the end of world rendering. + * + * Intended for use with [org.cobalt.util.RenderUtils] to perform custom world rendering. + * + * @property context the Fabric [LevelRenderContext] for the current render frame + */ class WorldRenderEvent( - /** The LevelRenderContext for the current render pass. */ val context: LevelRenderContext ) : Event() diff --git a/src/main/kotlin/org/cobalt/math/Math.kt b/src/main/kotlin/org/cobalt/math/Math.kt index 58679b71..80037f1c 100644 --- a/src/main/kotlin/org/cobalt/math/Math.kt +++ b/src/main/kotlin/org/cobalt/math/Math.kt @@ -1,19 +1,23 @@ package org.cobalt.math -/** Simple 3D vector */ -data class SimpleVec3( - /** X property */ +/** + * Simple 2D vector + * + * @property x x property + * @property y y property + */ +data class Vec2f( val x: Float, - /** Y property */ val y: Float, - /** Z property */ - val z: Float = 0f ) -/** Dimensions wrapper */ +/** + * Dimensions wrapper + * + * @property width width property + * @property height height property + */ data class Dimensions( - /** Width property */ val width: Float, - /** Height property */ val height: Float ) diff --git a/src/main/kotlin/org/cobalt/module/Module.kt b/src/main/kotlin/org/cobalt/module/Module.kt index 957b934f..aba0a974 100644 --- a/src/main/kotlin/org/cobalt/module/Module.kt +++ b/src/main/kotlin/org/cobalt/module/Module.kt @@ -1,74 +1,137 @@ package org.cobalt.module -/** Base class for all client modules/features. */ +/** + * Base class for all client modules/features. + * + * A module represents a toggleable feature within the client. + * + * @property name the module name + * @property category the category used to group modules in the UI + * + * @see RenderableModule for modules with rendering capabilities + */ abstract class Module( - /** Human-readable module name. */ val name: String, - /** Category used to group modules in UI. */ val category: ModuleCategory, ) { - /** Whether this module is currently enabled. */ - var enabled: Boolean = false + private var enabled: Boolean = false - /** Called when the module is registered with the module manager. */ + /** + * Called when the module is registered in the module system. + */ open fun onRegistration() {} - /** Return true when this module supports render callbacks. */ + /** + * Returns whether this module is currently enabled. + * + * @return true if the module is enabled, false otherwise + */ + fun isEnabled(): Boolean = enabled + + /** + * Sets the enabled state of this module. + * + * @param enabled whether the module should be enabled or disabled + */ + fun setEnabled(enabled: Boolean) { + this.enabled = enabled + } + + /** + * Returns whether this module supports rendering capabilities. + * + * @return true if this module is a [RenderableModule], false otherwise + */ fun isRenderable(): Boolean { return this is RenderableModule } } -/** Module variant that exposes screen-space rendering hooks and layout properties. */ +/** + * Module variant that exposes screen-space rendering hooks and layout properties. + * + * Extends [Module] with rendering capabilities. + * + * @property xPos UI X position for rendering the module. + * @property yPos UI Y position for rendering the module. + */ abstract class RenderableModule( name: String, category: ModuleCategory, - /** UI X position for rendering the module. */ var xPos: Float, - /** UI Y position for rendering the module. */ var yPos: Float, ) : Module(name, category) { - /** UI scale factor for rendering the module. */ + /** + * UI scale factor for rendering the module. + */ var scale: Float = 1.0f - /** Return the current width of the module's rendered area. */ + /** + * Returns the rendered width of this module in screen space. + * + * @return the module width in pixels + */ abstract fun getWidth(): Float - /** Return the current height of the module's rendered area. */ + /** + * Returns the rendered height of this module in screen space. + * + * @return the module height in pixels + */ abstract fun getHeight(): Float - /** Render the module's UI onto the current canvas/context. */ + /** + * Renders this module to the screen. + */ abstract fun renderModule() } -/** Logical grouping for modules used by UI and registration. */ +/** + * Represents a grouping category for modules used in the UI. + * + * @property displayName the name shown in the UI + */ class ModuleCategory private constructor( - /** Human-friendly display name for this category. */ val displayName: String ) { + companion object { private val entries = mutableMapOf() - /** Pre-registered render category used for renderable modules. */ + /** + * Predefined category for rendering-related modules. + */ @JvmField val RENDER = register(displayName = "Render") - /** Register or return an existing ModuleCategory for the given display name. */ + /** + * Registers a new module category or returns an existing one. + * + * Categories are stored case-insensitively. + * + * @param displayName the name of the category + * @return the existing or newly created [ModuleCategory] + */ fun register(displayName: String): ModuleCategory { return entries.getOrPut(displayName.lowercase()) { ModuleCategory(displayName) } } - /** Return all registered module categories. */ + /** + * Returns all registered module categories. + * + * @return a collection of all [ModuleCategory] instances + */ fun getCategories(): Collection { return entries.values } } + } diff --git a/src/main/kotlin/org/cobalt/module/ModuleManager.kt b/src/main/kotlin/org/cobalt/module/ModuleManager.kt index bfc1928c..81dbe89d 100644 --- a/src/main/kotlin/org/cobalt/module/ModuleManager.kt +++ b/src/main/kotlin/org/cobalt/module/ModuleManager.kt @@ -4,11 +4,14 @@ import org.cobalt.Cobalt.minecraft import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent +import org.cobalt.math.Vec2f import org.cobalt.module.impl.render.PerformanceHUD -import org.cobalt.render.skia.SkiaRenderer -import org.cobalt.math.SimpleVec3 +import org.cobalt.util.WindowUtils +import org.cobalt.util.skia.SkiaTransforms -/** Manager responsible for registering, storing and dispatching modules. */ +/** + * Central registry and lifecycle manager for all client modules. + */ object ModuleManager { private val modules = mutableSetOf() @@ -17,8 +20,7 @@ object ModuleManager { EventBus.register(this) } - /** Register built-in modules and perform their onRegistration lifecycle call. */ - fun registerModules() { + internal fun registerModules() { val builtIn = arrayOf( PerformanceHUD ) @@ -28,7 +30,11 @@ object ModuleManager { } } - /** Add a module to the manager; will throw if a module with the same name is already registered. */ + /** + * Adds a module to the registry. + * + * @throws IllegalStateException if a module with the same name is already registered + */ fun addModule(module: Module) { if (!modules.add(module)) { error("'${module.name}' is already registered") @@ -37,45 +43,64 @@ object ModuleManager { module.onRegistration() } - /** Lookup a module by its name (case-insensitive). */ + /** + * Removes a module from the registry. + * + * @param module the module to remove + * @return true if the module was removed, false if it was not registered + */ + fun removeModule(module: Module): Boolean { + return modules.remove(module) + } + + /** + * Returns a module by name (case-insensitive). + * + * @param moduleName the name of the module + * @return the matching module, or null if not found + */ fun getModule(moduleName: String): Module? { return modules.find { module -> module.name.equals(moduleName, true) } } - /** Return the set of registered modules. */ + /** + * Returns all registered modules. + * + * @return a set of all modules in the registry + */ fun getModules(): Set { return modules } - /** Draw all enabled modules that implement renderable behavior during the Skia render pass. */ + @Suppress("UndocumentedPublicFunction") @SubscribeEvent fun drawRenderableModules(@Suppress("UnusedParameter") event: SkiaDrawEvent) { if (minecraft.level == null) { return } - val windowScale = SkiaRenderer.getWindowScale() + val windowScale = WindowUtils.getWindowScale() modules - .filter { module -> module.enabled && module.isRenderable() } + .filter { module -> module.isEnabled() && module.isRenderable() } .forEach { module -> val renderable = module as RenderableModule - SkiaRenderer.save() + SkiaTransforms.save() val originX = renderable.xPos val originY = renderable.yPos val moduleScale = renderable.scale * windowScale - SkiaRenderer.translate(SimpleVec3(originX, originY)) - SkiaRenderer.scale(SimpleVec3(moduleScale, moduleScale)) - SkiaRenderer.translate(SimpleVec3(-originX, -originY)) + SkiaTransforms.translate(Vec2f(originX, originY)) + SkiaTransforms.scale(Vec2f(moduleScale, moduleScale)) + SkiaTransforms.translate(Vec2f(-originX, -originY)) renderable.renderModule() - SkiaRenderer.restore() + SkiaTransforms.restore() } } diff --git a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt index c4eeec34..108eda21 100644 --- a/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt +++ b/src/main/kotlin/org/cobalt/module/impl/render/PerformanceHUD.kt @@ -3,17 +3,17 @@ package org.cobalt.module.impl.render import kotlin.math.roundToInt import org.cobalt.Cobalt.minecraft import org.cobalt.math.Dimensions -import org.cobalt.math.SimpleVec3 +import org.cobalt.math.Vec2f import org.cobalt.module.ModuleCategory import org.cobalt.module.RenderableModule import org.cobalt.ui.ColorPalette import org.cobalt.util.ServerUtils -import org.cobalt.render.skia.SkiaShapes -import org.cobalt.render.skia.SkiaText +import org.cobalt.util.skia.SkiaShapes +import org.cobalt.util.skia.SkiaText private const val DEFAULT_OFFSET = 5.0f -object PerformanceHUD : RenderableModule( +internal object PerformanceHUD : RenderableModule( name = "Performance HUD", category = ModuleCategory.RENDER, xPos = DEFAULT_OFFSET, @@ -39,9 +39,9 @@ object PerformanceHUD : RenderableModule( } private fun drawBackground(width: Float, height: Float) { - SkiaShapes.roundedRect(SimpleVec3(xPos, yPos), Dimensions(width, height), CORNER_RADIUS, ColorPalette.PANEL) - SkiaShapes.roundedOutline( - SimpleVec3(xPos, yPos), + SkiaShapes.drawRoundedRect(Vec2f(xPos, yPos), Dimensions(width, height), CORNER_RADIUS, ColorPalette.PANEL) + SkiaShapes.drawRoundedOutline( + Vec2f(xPos, yPos), Dimensions(width, height), CORNER_RADIUS, ColorPalette.BORDER, @@ -68,9 +68,9 @@ object PerformanceHUD : RenderableModule( var x = startX + DIVIDER_GAP val midY = yPos + height * MID_FACTOR - SkiaShapes.line( - SimpleVec3(x, midY - DIVIDER_HALF_HEIGHT), - SimpleVec3(x, midY + DIVIDER_HALF_HEIGHT), + SkiaShapes.drawLine( + Vec2f(x, midY - DIVIDER_HALF_HEIGHT), + Vec2f(x, midY + DIVIDER_HALF_HEIGHT), ColorPalette.BORDER, OUTLINE_THICKNESS ) @@ -82,21 +82,21 @@ object PerformanceHUD : RenderableModule( private fun drawStatText(stat: Stat, startX: Float, textY: Float): Float { var x = startX - SkiaText.text( + SkiaText.drawText( SkiaText.primaryFont, stat.value, - SimpleVec3(x, textY), + Vec2f(x, textY), SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_PRIMARY) ) - x += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + x += SkiaText.getTextWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - SkiaText.text( + SkiaText.drawText( SkiaText.primaryFont, stat.unit, - SimpleVec3(x, textY), + Vec2f(x, textY), SkiaText.TextStyle(FONT_SIZE, ColorPalette.TEXT_DISABLED) ) - x += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) + x += SkiaText.getTextWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) return x } @@ -109,8 +109,8 @@ object PerformanceHUD : RenderableModule( width += PADDING + 2 * TEXT_SPACING } - width += SkiaText.textWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING - width += SkiaText.textWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) + width += SkiaText.getTextWidth(SkiaText.primaryFont, stat.value, FONT_SIZE) + TEXT_SPACING + width += SkiaText.getTextWidth(SkiaText.primaryFont, stat.unit, FONT_SIZE) } return width diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt deleted file mode 100644 index 7431054e..00000000 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaEnums.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.cobalt.render.skia - -/** - * Lightweight enums used by the Skia-based renderer helpers. - * - * These enums provide small, strongly-typed descriptors that make drawing code - * easier to read and less error-prone than using raw booleans or integers. - */ -enum class SkiaGradient { - /** - * Indicates a gradient that interpolates colors from the top edge toward the - * bottom edge of the target rectangle (y increases). - */ - TOP_TO_BOTTOM, - - /** - * Indicates a gradient that interpolates colors from the left edge toward the - * right edge of the target rectangle (x increases). - */ - LEFT_TO_RIGHT -} - -/** - * Represents a side of a rectangle or box and is useful for APIs that need to - * specify a particular edge (for example, where to draw a border or place a - * badge). - */ -enum class SkiaSide { - /** The top edge of a rectangle. */ - TOP, - - /** The bottom edge of a rectangle. */ - BOTTOM, - - /** The left edge of a rectangle. */ - LEFT, - - /** The right edge of a rectangle. */ - RIGHT -} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt deleted file mode 100644 index 135a4b49..00000000 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaRenderer.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.cobalt.render.skia - -import io.github.humbleui.skija.Canvas -import org.cobalt.Cobalt.minecraft -import org.cobalt.math.SimpleVec3 -import kotlin.math.min - - /** High-level Skia drawing helpers used by UI and module renderers. - * Provides convenience functions for text, shapes, images and scissor management. - */ -object SkiaRenderer { - private const val BASE_WIDTH = 1920f - private const val BASE_HEIGHT = 1080f - - private val canvas: Canvas? - get() = SkiaContext.canvas - - /** Calculate a window scale factor relative to a 1920x1080 baseline for consistent UI sizing. */ - fun getWindowScale(): Float { - val windowWidth = minecraft.window.width.toFloat() - val windowHeight = minecraft.window.height.toFloat() - - return min(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) - } - - /** Save the current Skia canvas state. */ - @JvmStatic - fun save() = this.canvas?.save() - - /** Restore the previously saved Skia canvas state. */ - @JvmStatic - fun restore() = this.canvas?.restore() - - /** Translate the canvas by the given x/y offset. */ - @JvmStatic - fun translate(pos: SimpleVec3) = this.canvas?.translate(pos.x, pos.y) - - /** Scale the canvas by the specified X and Y factors. */ - @JvmStatic - fun scale(scale: SimpleVec3) = this.canvas?.scale(scale.x, scale.y) -} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt b/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt deleted file mode 100644 index 7a857987..00000000 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaShapes.kt +++ /dev/null @@ -1,90 +0,0 @@ -package org.cobalt.render.skia - -import io.github.humbleui.skija.Paint -import io.github.humbleui.skija.PaintMode -import io.github.humbleui.types.RRect -import org.cobalt.math.Dimensions -import org.cobalt.math.SimpleVec3 - -/** Shape and scissor drawing helpers backed by the Skia canvas. */ -object SkiaShapes { - private val canvas get() = SkiaContext.canvas - - /** - * Draw a straight line between two points. - * - * @param start start point - * @param end end point - * @param color ARGB color - * @param thickness line thickness - */ - @JvmStatic - fun line(start: SimpleVec3, end: SimpleVec3, color: Int, thickness: Float = 1f) { - val canvas = this.canvas ?: return - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = thickness.coerceAtLeast(0f) - isAntiAlias = true - }.use { paint -> - canvas.drawLine(start.x, start.y, end.x, end.y, paint) - } - } - - /** - * Draw a filled rounded rectangle with the specified corner radius. - * - * @param pos the position - * @param dim the dimensions - * @param radius corner radius - * @param color outline color - */ - @JvmStatic - fun roundedRect(pos: SimpleVec3, dim: Dimensions, radius: Float, color: Int) { - val canvas = this.canvas ?: return - - if (dim.width <= 0f || dim.height <= 0f) return - - Paint().setColor(color).use { paint -> - canvas.drawRRect(RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, radius.coerceAtLeast(0f)), paint) - } - } - - /** - * Draw a rounded rectangle outline with the specified thickness. - * - * @param pos the position - * @param dim the dimensions - * @param radius corner radius - * @param color outline color - * @param thickness outline thickness - */ - @JvmStatic - fun roundedOutline( - pos: SimpleVec3, - dim: Dimensions, - radius: Float, - color: Int, - thickness: Float = 1f, - ) { - if (!isValid(dim)) { return } - - val canvas = this.canvas ?: return - - val t = thickness.coerceAtLeast(1f) - val half = t / 2f - val innerRadius = (radius - half).coerceAtLeast(0f) - - Paint().apply { - setColor(color) - mode = PaintMode.STROKE - strokeWidth = t - isAntiAlias = true - }.use { paint -> - canvas.drawRRect(RRect.makeXYWH(pos.x + half, pos.y + half, dim.width - t, dim.height - t, innerRadius), paint) - } - } - - private fun isValid(dim: Dimensions) = dim.width > 0f && dim.height > 0f -} diff --git a/src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt b/src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt deleted file mode 100644 index 9c1ccccb..00000000 --- a/src/main/kotlin/org/cobalt/render/skia/gl/Properties.kt +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of https://github.com/Lyzev/Skija. - * - * Copyright (c) 2025. Lyzev - * - * Skija is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, version 3 of the License, or - * (at your option) any later version. - * - * Skija 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Skija. If not, see . - */ - -package org.cobalt.render.skia.gl - -import java.util.* - -/** - * Container for cached OpenGL state used by the renderer. - * - * Each property holds the last known value for the corresponding GL state so - * the library can avoid redundant GL calls when the state is already set. - */ -class Properties { - - /** The last active texture unit (GL_ACTIVE_TEXTURE). Stored as a single-element IntArray. */ - val lastActiveTexture = IntArray(1) - - /** The last linked/used program object (GL_CURRENT_PROGRAM). */ - val lastProgram = IntArray(1) - - /** The last bound texture id. */ - val lastTexture = IntArray(1) - - /** The last bound sampler id. */ - val lastSampler = IntArray(1) - - /** The last bound array buffer id (GL_ARRAY_BUFFER). */ - val lastArrayBuffer = IntArray(1) - - /** The last bound vertex array object id (GL_VERTEX_ARRAY). */ - val lastVertexArrayObject = IntArray(1) - - /** The last polygon rasterization mode(s) (e.g., GL_POLYGON_MODE). Two elements for front and back. */ - val lastPolygonMode = IntArray(2) - - /** The last viewport rectangle (x, y, width, height). */ - val lastViewport = IntArray(4) - - /** The last scissor box rectangle (x, y, width, height). */ - val lastScissorBox = IntArray(4) - - /** The last blend source factor for RGB (GL_BLEND_SRC_RGB). */ - val lastBlendSrcRgb = IntArray(1) - - /** The last blend destination factor for RGB (GL_BLEND_DST_RGB). */ - val lastBlendDstRgb = IntArray(1) - - /** The last blend source factor for alpha (GL_BLEND_SRC_ALPHA). */ - val lastBlendSrcAlpha = IntArray(1) - - /** The last blend destination factor for alpha (GL_BLEND_DST_ALPHA). */ - val lastBlendDstAlpha = IntArray(1) - - /** The last blend equation for RGB (GL_BLEND_EQUATION_RGB). */ - val lastBlendEquationRgb = IntArray(1) - - /** The last blend equation for alpha (GL_BLEND_EQUATION_ALPHA). */ - val lastBlendEquationAlpha = IntArray(1) - - /** The last bound pixel unpack buffer (GL_PIXEL_UNPACK_BUFFER). */ - val lastPixelUnpackBufferBinding = IntArray(1) - - /** The last pixel unpack alignment (GL_UNPACK_ALIGNMENT). */ - val lastUnpackAlignment = IntArray(1) - - /** The last pixel unpack row length (GL_UNPACK_ROW_LENGTH). */ - val lastUnpackRowLength = IntArray(1) - - /** The last pixel unpack skip pixels (GL_UNPACK_SKIP_PIXELS). */ - val lastUnpackSkipPixels = IntArray(1) - - /** The last pixel unpack skip rows (GL_UNPACK_SKIP_ROWS). */ - val lastUnpackSkipRows = IntArray(1) - - /** The last pack swap bytes flag (GL_PACK_SWAP_BYTES). */ - val lastPackSwapBytes = IntArray(1) - - /** The last pack LSB first flag (GL_PACK_LSB_FIRST). */ - val lastPackLsbFirst = IntArray(1) - - /** The last pack row length (GL_PACK_ROW_LENGTH). */ - val lastPackRowLength = IntArray(1) - - /** The last pack image height (GL_PACK_IMAGE_HEIGHT). */ - val lastPackImageHeight = IntArray(1) - - /** The last pack skip pixels (GL_PACK_SKIP_PIXELS). */ - val lastPackSkipPixels = IntArray(1) - - /** The last pack skip rows (GL_PACK_SKIP_ROWS). */ - val lastPackSkipRows = IntArray(1) - - /** The last pack skip images (GL_PACK_SKIP_IMAGES). */ - val lastPackSkipImages = IntArray(1) - - /** The last pack alignment (GL_PACK_ALIGNMENT). */ - val lastPackAlignment = IntArray(1) - - /** The last unpack swap bytes flag (GL_UNPACK_SWAP_BYTES). */ - val lastUnpackSwapBytes = IntArray(1) - - /** The last unpack LSB first flag (GL_UNPACK_LSB_FIRST). */ - val lastUnpackLsbFirst = IntArray(1) - - /** The last unpack image height (GL_UNPACK_IMAGE_HEIGHT). */ - val lastUnpackImageHeight = IntArray(1) - - /** The last unpack skip images (GL_UNPACK_SKIP_IMAGES). */ - val lastUnpackSkipImages = IntArray(1) - - // Internal bitset for boolean GL state flags. - private val flags = BitSet(7) - - /** Whether blending was last enabled (GL_BLEND). */ - var lastEnableBlend - get() = flags[0] - set(value) { - flags[0] = value - } - - /** Whether face culling was last enabled (GL_CULL_FACE). */ - var lastEnableCullFace - get() = flags[1] - set(value) { - flags[1] = value - } - - /** Whether depth testing was last enabled (GL_DEPTH_TEST). */ - var lastEnableDepthTest - get() = flags[2] - set(value) { - flags[2] = value - } - - /** Whether stencil testing was last enabled (GL_STENCIL_TEST). */ - var lastEnableStencilTest - get() = flags[3] - set(value) { - flags[3] = value - } - - /** Whether scissor testing was last enabled (GL_SCISSOR_TEST). */ - var lastEnableScissorTest - get() = flags[4] - set(value) { - flags[4] = value - } - - /** Whether primitive restart was last enabled (GL_PRIMITIVE_RESTART). */ - var lastEnablePrimitiveRestart - get() = flags[5] - set(value) { - flags[5] = value - } - - /** The last depth mask value (whether depth writes were enabled). */ - var lastDepthMask - get() = flags[6] - set(value) { - flags[6] = value - } - -} diff --git a/src/main/kotlin/org/cobalt/ui/ColorPalette.kt b/src/main/kotlin/org/cobalt/ui/ColorPalette.kt index 0e485e8f..d1447f11 100644 --- a/src/main/kotlin/org/cobalt/ui/ColorPalette.kt +++ b/src/main/kotlin/org/cobalt/ui/ColorPalette.kt @@ -2,58 +2,37 @@ package org.cobalt.ui import java.awt.Color -// TODO: bring this over to a theme manager +/** + * Global color definitions used across the UI. + * + * TODO: Bring to a theme manager + */ +@Suppress("UndocumentedPublicProperty") object ColorPalette { - /** Primary background color used for main surfaces (RGB int). */ + // Backgrounds val BACKGROUND_PRIMARY = Color(18, 18, 18).rgb - - /** Secondary background color for panels and elevated surfaces (RGB int). */ val BACKGROUND_SECONDARY = Color(24, 24, 24).rgb - - /** Panel background color for containers (RGB int). */ val PANEL = Color(30, 30, 30).rgb - - /** Hover background color for interactive elements (RGB int). */ val HOVER = Color(37, 37, 37).rgb - - /** Border color for separators and outlines (RGB int). */ val BORDER = Color(42, 42, 42).rgb - /** Primary accent color for highlights and buttons (RGB int). */ + // Accent colors val ACCENT_PRIMARY = Color(79, 140, 255).rgb - - /** Accent color used on hover states (RGB int). */ val ACCENT_HOVER = Color(106, 162, 255).rgb - - /** Accent color used for active/pressed states (RGB int). */ val ACCENT_ACTIVE = Color(58, 116, 230).rgb - - /** Accent glow color with alpha for soft glow effects (RGBA int). */ val ACCENT_GLOW = Color(47, 95, 191, 64).rgb - /** Primary text color for high-contrast text (RGB int). */ + // Text colors val TEXT_PRIMARY = Color(230, 230, 230).rgb - - /** Secondary text color for less prominent text (RGB int). */ val TEXT_SECONDARY = Color(179, 179, 179).rgb - - /** Muted text color for de-emphasized labels (RGB int). */ val TEXT_MUTED = Color(122, 122, 122).rgb - - /** Disabled text color for inactive elements (RGB int). */ val TEXT_DISABLED = Color(95, 95, 95).rgb - /** Success semantic color for positive states (RGB int). */ + // Semantic states val SUCCESS = Color(63, 191, 127).rgb - - /** Warning semantic color for cautionary states (RGB int). */ val WARNING = Color(230, 181, 102).rgb - - /** Error semantic color for negative states (RGB int). */ val ERROR = Color(224, 90, 90).rgb - - /** Informational semantic color for neutral/utility states (RGB int). */ val INFO = Color(93, 169, 233).rgb } diff --git a/src/main/kotlin/org/cobalt/ui/UIComponent.kt b/src/main/kotlin/org/cobalt/ui/UIComponent.kt index 9554fa62..5007f8c2 100644 --- a/src/main/kotlin/org/cobalt/ui/UIComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/UIComponent.kt @@ -1,21 +1,32 @@ package org.cobalt.ui -/** Base UI component with position and size used by HUD/editor screens. */ +/** + * Base class for all drawable UI components. + * + * @property xPos X position of the component + * @property yPos Y position of the component + * @property width component width + * @property height component height + */ abstract class UIComponent( - /** Screen-space X coordinate. */ var xPos: Float, - /** Screen-space Y coordinate. */ var yPos: Float, - /** Component width in pixels. */ open val width: Float = 0.0f, - /** Component height in pixels. */ open val height: Float = 0.0f, ) { - /** Render the component's contents onto the current canvas/context. */ + /** + * Render the component's contents. + */ abstract fun renderComponent() - /** Update the component's screen-space position and return this instance. */ + /** + * Updates the component position. + * + * @param xPos new X coordinate in screen space + * @param yPos new Y coordinate in screen space + * @return this component for chaining + */ fun updateBounds(xPos: Float, yPos: Float): UIComponent { this.xPos = xPos this.yPos = yPos diff --git a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt index c81302ee..b0c5f03b 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/Animation.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/Animation.kt @@ -5,7 +5,9 @@ package org.cobalt.ui.animation -/** Generic animation base for interpolating values over a duration in milliseconds. */ +/** + * Base class for value animations over a fixed duration. + */ abstract class Animation(private val duration: Long) { companion object { @@ -19,15 +21,19 @@ abstract class Animation(private val duration: Long) { private var animating = false private var reversed = false - /** Compute the interpolated value between start and end for the current animation progress. + /** + * Computes the interpolated value between start and end based on progress. * * @param start starting value * @param end ending value - * @param reverse whether the animation is reversed + * @param reverse whether interpolation direction is reversed + * @return the interpolated value at the current animation state */ abstract fun get(start: T, end: T, reverse: Boolean = false): T - /** Start or toggle the animation; handles reversal when already animating. */ + /** + * Starts the animation, or reverses its direction if already running. + */ fun start() { val currentTime = System.currentTimeMillis() @@ -44,7 +50,11 @@ abstract class Animation(private val duration: Long) { return } - /** Return animation progress as a percentage between 0 and 100. */ + /** + * Returns the current animation progress as a percentage. + * + * @return animation progress between 0 and 100 + */ fun getPercent(): Float { if (!animating) return PERCENT_MAX val percent = ((System.currentTimeMillis() - startTime) / duration.toFloat() * PERCENT_MAX) @@ -57,7 +67,11 @@ abstract class Animation(private val duration: Long) { return percent.coerceAtMost(PERCENT_MAX) } - /** Whether the animation is currently running. */ + /** + * Returns whether the animation is currently running. + * + * @return true if the animation is in progress, false otherwise + */ fun isAnimating(): Boolean { return animating } diff --git a/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt b/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt index 5260e9e7..1cbd70ea 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/BounceAnimation.kt @@ -2,6 +2,9 @@ package org.cobalt.ui.animation import kotlin.math.pow +/** + * Bounce-style animation that applies an easing function with overshoot. + */ class BounceAnimation(duration: Long) : Animation(duration) { companion object { diff --git a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt index a0a1a99a..0e2ce425 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt @@ -3,20 +3,16 @@ package org.cobalt.ui.notification import kotlin.time.Duration import org.cobalt.ui.UIComponent -/** Simple on-screen notification displayed for a given duration. +/** + * On-screen notification UI element. * * @property title short headline text shown prominently * @property description body text shown below the title - * @property duration how long the notification should be visible + * @property duration how long the notification remains visible */ data class Notification( - /** Short headline text shown prominently. */ val title: String, - - /** Body text shown below the title. */ val description: String, - - /** How long the notification should be visible. */ val duration: Duration ) : UIComponent( xPos = DEFAULT_X, @@ -33,18 +29,17 @@ data class Notification( } /** - * Render the notification UI. - * - * Implementations should draw the notification background, title and - * description within the component bounds. This method is called every - * frame while the notification is visible. + * Renders the notification contents. */ - override fun renderComponent() { return } + override fun renderComponent() { + // TODO: draw the actual notification here.. + } } /** Types of notifications used to indicate severity or purpose. */ enum class NotificationType { + /** Indicates a successful operation. */ SUCCESS, @@ -56,4 +51,5 @@ enum class NotificationType { /** Informational message without success/error semantics. */ INFO + } diff --git a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt index 8771bbe0..40b03171 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt @@ -3,19 +3,14 @@ package org.cobalt.ui.notification import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent -import org.cobalt.render.skia.SkiaRenderer -import org.cobalt.math.SimpleVec3 +import org.cobalt.math.Vec2f +import org.cobalt.util.skia.SkiaTransforms /** - * Manager responsible for displaying on-screen notifications. - * - * Notifications pushed to this manager will be rendered each frame via the - * Skia renderer. The manager registers itself on the global event bus in the - * initializer so it receives draw callbacks. + * Central manager for Cobalt notifications. */ object NotificationManager { - // Internal storage for active notifications. private val notificationsList = mutableSetOf() init { @@ -23,44 +18,33 @@ object NotificationManager { } /** - * Enqueue a notification for rendering. + * Adds a notification to be rendered. * - * The notification will be retained until it decides to remove itself or - * the manager is cleared. Notifications are rendered in an unspecified - * iteration order. - * - * @param notification the notification to display + * @param notification the notification instance to display */ fun pushNotification(notification: Notification) { notificationsList.add(notification) } - /** - * Event handler invoked during the Skia draw pass. - * - * This method iterates over all active notifications and renders each one - * using the `SkiaRenderer`. Notifications are drawn with the appropriate - * window scale and the renderer state is saved/restored around each - * notification draw call. - */ + @Suppress("UndocumentedPublicFunction") @SubscribeEvent fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { - val windowScale = SkiaRenderer.getWindowScale() + val windowScale = SkiaTransforms.getWindowScale() notificationsList .forEach { notification -> - SkiaRenderer.save() + SkiaTransforms.save() val originX = notification.xPos val originY = notification.yPos - SkiaRenderer.translate(SimpleVec3(originX, originY)) - SkiaRenderer.scale(SimpleVec3(windowScale, windowScale)) - SkiaRenderer.translate(SimpleVec3(-originX, -originY)) + SkiaTransforms.translate(Vec2f(originX, originY)) + SkiaTransforms.scale(Vec2f(windowScale, windowScale)) + SkiaTransforms.translate(Vec2f(-originX, -originY)) notification.renderComponent() - SkiaRenderer.restore() + SkiaTransforms.restore() } } diff --git a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt index 1d2ae40f..9c257147 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/ConfigScreen.kt @@ -6,9 +6,9 @@ import net.minecraft.network.chat.Component import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent +import org.cobalt.math.Vec2f import org.cobalt.ui.animation.BounceAnimation -import org.cobalt.render.skia.SkiaRenderer -import org.cobalt.math.SimpleVec3 +import org.cobalt.util.skia.SkiaTransforms internal object ConfigScreen : Screen(Component.empty()) { @@ -18,6 +18,7 @@ internal object ConfigScreen : Screen(Component.empty()) { EventBus.register(this) } + @Suppress("UndocumentedPublicFunction") @SubscribeEvent fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { if (minecraft.screen != this) { @@ -33,16 +34,16 @@ internal object ConfigScreen : Screen(Component.empty()) { val cx = width / 2f val cy = height / 2f - SkiaRenderer.save() - SkiaRenderer.translate(SimpleVec3(cx, cy)) - SkiaRenderer.scale(SimpleVec3(scale, scale)) - SkiaRenderer.translate(SimpleVec3(-cx, -cy)) + SkiaTransforms.save() + SkiaTransforms.translate(Vec2f(cx, cy)) + SkiaTransforms.scale(Vec2f(scale, scale)) + SkiaTransforms.translate(Vec2f(-cx, -cy)) } // TODO: draw the actual UI here.. if (openAnim.isAnimating()) { - SkiaRenderer.restore() + SkiaTransforms.restore() } } diff --git a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt index 57170ce6..d5a53735 100644 --- a/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt +++ b/src/main/kotlin/org/cobalt/ui/screen/HudEditorScreen.kt @@ -6,25 +6,15 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent -/** - * Screen instance used for editing HUD elements. The object registers itself - * on the global event bus to receive Skia draw callbacks while the screen - * is active. - */ -object HudEditorScreen : Screen(Component.empty()) { +internal object HudEditorScreen : Screen(Component.empty()) { init { EventBus.register(this) } - /** - * Handle Skia draw events to render the HUD editor overlay when this - * screen is the currently displayed Minecraft screen. - * - * @param event the Skia draw event (unused in the current implementation) - */ + @Suppress("UndocumentedPublicFunction") @SubscribeEvent - fun onSkiaDraw(@Suppress("UNUSED_PARAMETER") event: SkiaDrawEvent) { + fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { if (minecraft.screen != this) { return } diff --git a/src/main/kotlin/org/cobalt/util/ChatUtils.kt b/src/main/kotlin/org/cobalt/util/ChatUtils.kt index 440a57c8..bc08f5cd 100644 --- a/src/main/kotlin/org/cobalt/util/ChatUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ChatUtils.kt @@ -7,9 +7,13 @@ import org.cobalt.Cobalt import org.cobalt.Cobalt.minecraft import org.slf4j.LoggerFactory - /** Utilities for sending chat/system messages and building message components. */ +/** + * Utility for sending chat and system messages. + */ object ChatUtils { + private val logger = LoggerFactory.getLogger(this::class.java) + private val defaultPrefix = Component.literal("") .append(Component.literal("[").withStyle(ChatFormatting.DARK_GRAY)) .append(ColorUtils.buildTextGradient(Cobalt.MOD_NAME, 0x4CADD0, 0xB2F9FF)) @@ -20,12 +24,13 @@ object ChatUtils { .append(ColorUtils.buildTextGradient("${Cobalt.MOD_NAME} Debug", 0x369876, 0x71FF9E)) .append(Component.literal("] ").withStyle(ChatFormatting.DARK_GRAY)) - private val logger = LoggerFactory.getLogger(this::class.java) + private var lastDebugMessage: String? = null - /** Send a system chat message to the player using the configured message prefix. + /** + * Sends a system message to the local player with optional formatting. * - * @param message the plain text message to send - * @param type the MessageType that controls prefixing/formatting + * @param message the message content to display + * @param type determines how the message is formatted (prefix, debug, or raw) */ @JvmStatic fun sendSystemMessage(message: String, type: MessageType = MessageType.DEFAULT) { @@ -38,22 +43,38 @@ object ChatUtils { val component = when (type) { MessageType.DEFAULT -> defaultPrefix.copy().append(stringToComponent(message)) - MessageType.DEBUG -> debugPrefix.copy().append(stringToComponent(message)) MessageType.RAW -> stringToComponent(message) + MessageType.DEBUG -> { + if (lastDebugMessage == message) { + return + } + + lastDebugMessage = message + debugPrefix.copy().append(stringToComponent(message)) + } } player.sendSystemMessage(component) } - /** Convert a plain string into a MutableComponent. */ + /** + * Converts a plain string into a Minecraft chat component. + * + * @param string the raw text to convert + * @return a mutable chat component representing the input string + */ @JvmStatic fun stringToComponent(string: String): MutableComponent { return Component.literal(string) } - /** Send a raw chat message as though typed by the player. */ + /** + * Sends a chat message as the player. + * + * @param message the message to send in chat + */ @JvmStatic - fun sendMessageAsPlayer(message: String) { + fun sendPlayerMessage(message: String) { val player = minecraft.player if (player == null) { @@ -64,7 +85,11 @@ object ChatUtils { player.connection.sendChat(message) } - /** Send a client-side command string to the server. */ + /** + * Sends a command as the player to the server. + * + * @param command the command string without the leading slash + */ @JvmStatic fun sendCommand(command: String) { val player = minecraft.player @@ -79,12 +104,25 @@ object ChatUtils { } -/** Type of message to send via ChatUtils. */ +/** + * Type of message formatting used by [ChatUtils]. + * + * Determines whether a prefix is applied or if the message is sent raw. + */ enum class MessageType { + /** Default message includes the mod prefix. */ DEFAULT, - /** Debug messages include the debug prefix. */ - DEBUG, + /** Raw messages are sent without any prefix. */ - RAW + RAW, + + /** + * Debug message includes the debug prefix. + * + * Additionally, duplicate consecutive messages are ignored to prevent spam. + * If the same message is sent twice in a row, it will not be re-displayed. + */ + DEBUG + } diff --git a/src/main/kotlin/org/cobalt/util/ColorUtils.kt b/src/main/kotlin/org/cobalt/util/ColorUtils.kt index e408c9d2..0fc47e5e 100644 --- a/src/main/kotlin/org/cobalt/util/ColorUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ColorUtils.kt @@ -10,20 +10,23 @@ import org.cobalt.dsl.blue import org.cobalt.dsl.green import org.cobalt.dsl.red -/** Color-related utility helpers for building text gradients and extracting ARGB components. */ +/** + * Utility functions for color manipulation and text styling. + */ object ColorUtils { private const val MIN_TEXT_LENGTH = 1 private const val SHIFT_RED = 16 private const val SHIFT_GREEN = 8 - /** Build a MutableComponent where each character of the input text is colored with an interpolated - * color between startColor and endColor. + /** + * Creates a gradient-colored text component where each character + * is interpolated between two ARGB colors. * - * @param text the text to colorize - * @param startColor ARGB integer color used at the start of the text - * @param endColor ARGB integer color used at the end of the text - * @return a MutableComponent with per-character gradient coloring + * @param text input text + * @param startColor starting ARGB color + * @param endColor ending ARGB color + * @return gradient-colored [MutableComponent] */ @JvmStatic fun buildTextGradient(text: String, startColor: Int, endColor: Int): MutableComponent { @@ -52,19 +55,39 @@ object ColorUtils { return result } - /** Return the red component (0-255) of the supplied ARGB color integer. */ + /** + * Extracts the red channel from an ARGB color value. + * + * @param color the ARGB color integer + * @return the red component (0–255) + */ @JvmStatic fun getRed(color: Int) = color.red - /** Return the green component (0-255) of the supplied ARGB color integer. */ + /** + * Extracts the green channel from an ARGB color value. + * + * @param color the ARGB color integer + * @return the green component (0–255) + */ @JvmStatic fun getGreen(color: Int) = color.green - /** Return the blue component (0-255) of the supplied ARGB color integer. */ + /** + * Extracts the blue channel from an ARGB color value. + * + * @param color the ARGB color integer + * @return the blue component (0–255) + */ @JvmStatic fun getBlue(color: Int) = color.blue - /** Return the alpha component (0-255) of the supplied ARGB color integer. */ + /** + * Extracts the alpha channel from an ARGB color value. + * + * @param color the ARGB color integer + * @return the alpha component (0–255) + */ @JvmStatic fun getAlpha(color: Int) = color.alpha diff --git a/src/main/kotlin/org/cobalt/util/FrustumUtils.kt b/src/main/kotlin/org/cobalt/util/FrustumUtils.kt index fc0deebe..783494ff 100644 --- a/src/main/kotlin/org/cobalt/util/FrustumUtils.kt +++ b/src/main/kotlin/org/cobalt/util/FrustumUtils.kt @@ -5,18 +5,34 @@ import net.minecraft.world.phys.AABB import org.cobalt.mixin.render.FrustumInvoker import org.joml.FrustumIntersection -/** Utilities for frustum visibility testing used by rendering helpers. */ +/** + * Utility functions for frustum-based visibility checks. + */ object FrustumUtils { - /** Check whether an AABB is inside or intersects the provided frustum. */ + /** + * Checks whether an axis-aligned bounding box is visible within the given frustum. + * + * @param frustum the view frustum used for visibility testing + * @param box the bounding box to test + * @return true if the box is inside or intersects the frustum, false otherwise + */ @JvmStatic fun isVisible(frustum: Frustum, box: AABB): Boolean { return isVisible(frustum, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ) } - /** Check whether the specified axis-aligned cube bounds are visible in the frustum. + /** + * Checks whether an axis-aligned bounding box is visible within the given frustum. * - * @return true when the bounds are inside or intersect the frustum + * @param frustum the view frustum used for visibility testing + * @param minX minimum X coordinate of the box + * @param minY minimum Y coordinate of the box + * @param minZ minimum Z coordinate of the box + * @param maxX maximum X coordinate of the box + * @param maxY maximum Y coordinate of the box + * @param maxZ maximum Z coordinate of the box + * @return true if the box is inside or intersects the frustum, false otherwise */ @JvmStatic fun isVisible( diff --git a/src/main/kotlin/org/cobalt/util/MouseUtils.kt b/src/main/kotlin/org/cobalt/util/MouseUtils.kt deleted file mode 100644 index 51ba81c0..00000000 --- a/src/main/kotlin/org/cobalt/util/MouseUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.cobalt.util - -import org.cobalt.dsl.mouseX -import org.cobalt.dsl.mouseY - -/** Convenience accessors for the current mouse position in screen coordinates. */ -object MouseUtils { - - /** Return the current mouse X position as a Float. */ - @JvmStatic - fun getMouseX(): Float { - return mouseX - } - - /** Return the current mouse Y position as a Float. */ - @JvmStatic - fun getMouseY(): Float { - return mouseY - } - -} diff --git a/src/main/kotlin/org/cobalt/render/RenderUtils.kt b/src/main/kotlin/org/cobalt/util/RenderUtils.kt similarity index 61% rename from src/main/kotlin/org/cobalt/render/RenderUtils.kt rename to src/main/kotlin/org/cobalt/util/RenderUtils.kt index 09c912af..2debee07 100644 --- a/src/main/kotlin/org/cobalt/render/RenderUtils.kt +++ b/src/main/kotlin/org/cobalt/util/RenderUtils.kt @@ -1,5 +1,7 @@ -package org.cobalt.render +package org.cobalt.util +import com.mojang.blaze3d.vertex.PoseStack +import com.mojang.blaze3d.vertex.VertexConsumer import java.awt.Color import kotlin.math.max import kotlin.math.min @@ -8,22 +10,19 @@ import net.minecraft.core.BlockPos import net.minecraft.world.entity.Entity import net.minecraft.world.phys.AABB import net.minecraft.world.phys.Vec3 -import org.cobalt.Cobalt.minecraft -import org.cobalt.util.FrustumUtils +import org.cobalt.Cobalt import org.cobalt.util.helper.Layers +import org.joml.Matrix4f -/** Utility rendering helpers for drawing boxes, outlines, tracers and lines in world space. */ +/** + * Utility rendering helpers for drawing boxes, outlines, tracers and lines in world space. + */ object RenderUtils { - private const val ALPHA = 100 - /** Style options for drawing lines. - * - * @param esp when true uses ESP render type variants - * @param lineWidth line thickness - */ - data class LineStyle(val esp: Boolean = false, val lineWidth: Float = 1f) + private const val ALPHA = 100 - /** Draw a unit cube wireframe and optional translucent fill at the given block position. + /** + * Draw a unit cube wireframe and optional translucent fill at the given block position. * * @param context the level render context to draw with * @param pos the block position to draw @@ -33,25 +32,26 @@ object RenderUtils { */ @JvmStatic fun drawBlockPos( - context: LevelRenderContext, - pos: BlockPos, - color: Color, - esp: Boolean = false, - lineWidth: Float = 1f, + context: LevelRenderContext, + pos: BlockPos, + color: Color, + esp: Boolean = false, + lineWidth: Float = 1f, ) { val box = AABB( - pos.x.toDouble(), - pos.y.toDouble(), - pos.z.toDouble(), - pos.x + 1.0, - pos.y + 1.0, - pos.z + 1.0 + pos.x.toDouble(), + pos.y.toDouble(), + pos.z.toDouble(), + pos.x + 1.0, + pos.y + 1.0, + pos.z + 1.0 ) drawBox(context, box, color, esp, lineWidth) } - /** Draw an outline around the provided entity's bounding box, interpolated for rendering. + /** + * Draw an outline around the provided entity's bounding box, interpolated for rendering. * * @param context the level render context to draw with * @param entity the entity whose bounding box will be outlined @@ -60,13 +60,13 @@ object RenderUtils { * @param lineWidth outline thickness */ fun drawEntityOutline( - context: LevelRenderContext, - entity: Entity, - color: Color, - esp: Boolean = false, - lineWidth: Float = 1f, + context: LevelRenderContext, + entity: Entity, + color: Color, + esp: Boolean = false, + lineWidth: Float = 1f, ) { - val partialTicks = minecraft.deltaTracker.getGameTimeDeltaPartialTick(true) + val partialTicks = Cobalt.minecraft.deltaTracker.getGameTimeDeltaPartialTick(true) val interpolatedX = entity.xOld + (entity.x - entity.xOld) * partialTicks val interpolatedY = entity.yOld + (entity.y - entity.yOld) * partialTicks @@ -89,17 +89,17 @@ object RenderUtils { */ @JvmStatic fun drawTracer( - context: LevelRenderContext, - to: Vec3, - color: Color, - esp: Boolean = true, - lineWidth: Float = 1f, + context: LevelRenderContext, + to: Vec3, + color: Color, + esp: Boolean = true, + lineWidth: Float = 1f, ) { - val camera = minecraft.gameRenderer.mainCamera + val camera = Cobalt.minecraft.gameRenderer.mainCamera val cameraPos = camera.position() val from = cameraPos.add(Vec3.directionFromRotation(camera.xRot(), camera.yRot())) - drawLine(context, from, to, color, LineStyle(esp, lineWidth)) + drawLine(context, from, to, color, esp, lineWidth) } /** Draw a colored axis-aligned bounding box (AABB) with optional translucent fill and outline. @@ -112,15 +112,13 @@ object RenderUtils { */ @JvmStatic fun drawBox( - context: LevelRenderContext, - box: AABB, - color: Color, - esp: Boolean = false, - lineWidth: Float = 1f, + context: LevelRenderContext, + box: AABB, + color: Color, + esp: Boolean = false, + lineWidth: Float = 1f, ) { - if (color.alpha == 0) { - return - } + if (color.alpha == 0) return val frustum = context.levelState().cameraRenderState.cullFrustum @@ -128,25 +126,57 @@ object RenderUtils { return } - val cameraPos = minecraft.gameRenderer.mainCamera.position() + val cameraPos = Cobalt.minecraft.gameRenderer.mainCamera.position() val corners = arrayOf( - Vec3(box.minX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.minZ), - Vec3(box.maxX, box.minY, box.maxZ), Vec3(box.minX, box.minY, box.maxZ), - Vec3(box.minX, box.maxY, box.minZ), Vec3(box.maxX, box.maxY, box.minZ), - Vec3(box.maxX, box.maxY, box.maxZ), Vec3(box.minX, box.maxY, box.maxZ), + Vec3(box.minX, box.minY, box.minZ), Vec3(box.maxX, box.minY, box.minZ), + Vec3(box.maxX, box.minY, box.maxZ), Vec3(box.minX, box.minY, box.maxZ), + Vec3(box.minX, box.maxY, box.minZ), Vec3(box.maxX, box.maxY, box.minZ), + Vec3(box.maxX, box.maxY, box.maxZ), Vec3(box.minX, box.maxY, box.maxZ), ) drawBoxQuads(context, corners, color, esp, cameraPos) drawBoxLines(context, corners, color, esp, lineWidth, cameraPos) } + /** Draw a colored line between two world-space points. + * + * @param context the level render context to draw with + * @param from start point in world coordinates + * @param to end point in world coordinates + * @param color the color to use for the line + * @param esp whether ESP rendering is enabled + * @param lineWidth thickness of the line + */ + @JvmStatic + fun drawLine( + context: LevelRenderContext, + from: Vec3, + to: Vec3, + color: Color, + esp: Boolean = false, + lineWidth: Float = 1f, + ) { + if (color.alpha == 0) return + + val frustum = context.levelState().cameraRenderState.cullFrustum + + if (!FrustumUtils.isVisible( + frustum, + min(from.x, to.x), min(from.y, to.y), min(from.z, to.z), + max(from.x, to.x), max(from.y, to.y), max(from.z, to.z) + ) + ) return + + drawVisibleLine(context, from, to, color, esp, lineWidth) + } + private fun drawBoxQuads( - context: LevelRenderContext, - corners: Array, - color: Color, - esp: Boolean, - cameraPos: Vec3 + context: LevelRenderContext, + corners: Array, + color: Color, + esp: Boolean, + cameraPos: Vec3, ) { val poseStack = context.poseStack() val bufferSource = context.bufferSource() @@ -170,12 +200,12 @@ object RenderUtils { } private fun drawBoxLines( - context: LevelRenderContext, - corners: Array, - color: Color, - esp: Boolean, - lineWidth: Float, - cameraPos: Vec3 + context: LevelRenderContext, + corners: Array, + color: Color, + esp: Boolean, + lineWidth: Float, + cameraPos: Vec3, ) { val poseStack = context.poseStack() val bufferSource = context.bufferSource() @@ -196,15 +226,15 @@ object RenderUtils { } private fun addBlockLineVertices( - lineBuffer: com.mojang.blaze3d.vertex.VertexConsumer, - matrix: org.joml.Matrix4f, - poseEntry: com.mojang.blaze3d.vertex.PoseStack.Pose, - lineStart: Vec3, - lineEnd: Vec3, - lineNormal: Vec3, - color: Color, - lineWidth: Float, - cameraPos: Vec3 + lineBuffer: VertexConsumer, + matrix: Matrix4f, + poseEntry: PoseStack.Pose, + lineStart: Vec3, + lineEnd: Vec3, + lineNormal: Vec3, + color: Color, + lineWidth: Float, + cameraPos: Vec3, ) { for (vertex in listOf(lineStart, lineEnd)) { lineBuffer.addVertex( @@ -219,70 +249,43 @@ object RenderUtils { } } - /** - * Draw a colored line between two world-space points. - * - * @param context the level render context to draw with - * @param from start point in world coordinates - * @param to end point in world coordinates - * @param color the color to use for the line - * @param style line drawing style (esp and lineWidth) - */ - @JvmStatic - fun drawLine( - context: LevelRenderContext, - from: Vec3, - to: Vec3, - color: Color, - style: LineStyle = LineStyle(), - ) { - if (color.alpha == 0) return - val frustum = context.levelState().cameraRenderState.cullFrustum - if (!FrustumUtils.isVisible( - frustum, min(from.x, to.x), min(from.y, to.y), min(from.z, to.z), - max(from.x, to.x), max(from.y, to.y), max(from.z, to.z) - )) return - - drawVisibleLine(context, from, to, color, style) - } - private fun drawVisibleLine( - context: LevelRenderContext, - from: Vec3, - to: Vec3, - color: Color, - style: LineStyle + context: LevelRenderContext, + from: Vec3, + to: Vec3, + color: Color, + esp: Boolean, + lineWidth: Float, ) { val bufferSource = context.bufferSource() - val lineBuffer = bufferSource.getBuffer(Layers.getLines(style.esp)) + val lineBuffer = bufferSource.getBuffer(Layers.getLines(esp)) - addLineVertices(context, lineBuffer, from, to, color, style) + addLineVertices(context, lineBuffer, from, to, color, lineWidth) - bufferSource.endBatch(Layers.getLines(style.esp)) + bufferSource.endBatch(Layers.getLines(esp)) } private fun addLineVertices( - context: LevelRenderContext, - lineBuffer: com.mojang.blaze3d.vertex.VertexConsumer, - from: Vec3, - to: Vec3, - color: Color, - style: LineStyle + context: LevelRenderContext, + lineBuffer: VertexConsumer, + from: Vec3, + to: Vec3, + color: Color, + lineWidth: Float, ) { val poseStack = context.poseStack() - val cameraPos = minecraft.gameRenderer.mainCamera.position() + val cameraPos = Cobalt.minecraft.gameRenderer.mainCamera.position() val poseEntry = poseStack.last() val lineNormal = to.subtract(from).normalize() for (vertex in listOf(from, to)) { - lineBuffer - .addVertex( - poseEntry.pose(), - (vertex.x - cameraPos.x).toFloat(), - (vertex.y - cameraPos.y).toFloat(), - (vertex.z - cameraPos.z).toFloat() - ) - .setLineWidth(style.lineWidth) + lineBuffer.addVertex( + poseEntry.pose(), + (vertex.x - cameraPos.x).toFloat(), + (vertex.y - cameraPos.y).toFloat(), + (vertex.z - cameraPos.z).toFloat() + ) + .setLineWidth(lineWidth) .setColor(color.red, color.green, color.blue, color.alpha) .setNormal(poseEntry, lineNormal.x.toFloat(), lineNormal.y.toFloat(), lineNormal.z.toFloat()) } diff --git a/src/main/kotlin/org/cobalt/util/ServerUtils.kt b/src/main/kotlin/org/cobalt/util/ServerUtils.kt index b4f576c0..e2bd6418 100644 --- a/src/main/kotlin/org/cobalt/util/ServerUtils.kt +++ b/src/main/kotlin/org/cobalt/util/ServerUtils.kt @@ -8,67 +8,58 @@ import org.cobalt.event.impl.PacketEvent import org.cobalt.mixin.client.AbstractClientPlayerAccessor private const val DEFAULT_TPS = 20f -private const val TPS_ALPHA = 0.05 -private const val TPS_BETA = 0.95 -private const val TPS_SCALE = 20000.0 -private const val TPS_MAX = 20.0 +private const val TICKS_PER_SECOND = 20.0 +private const val MS_PER_SECOND = 1000.0 +private const val TPS_SMOOTHING = 0.05f /** - * Utility object for retrieving and tracking server-side metrics such as - * average TPS (ticks per second) and the current player ping. - * - * The object listens for incoming packets and updates an internal smoothed - * TPS estimate whenever the server time packet is received. + * Utility for server-related information. */ object ServerUtils { - private var lastTickTime = 0L + + private var lastTickTime = -1L /** - * Smoothed average server ticks per second (TPS). - * - * This value is updated when a `ClientboundSetTimePacket` is received and is - * smoothed over time to avoid rapid fluctuations. The setter is private; the - * value should be read-only from callers. + * Average of the server's ticks per second (TPS). */ var averageTps = DEFAULT_TPS private set /** - * The current player's network latency (ping) in milliseconds. + * Current network latency to the server in milliseconds. * - * Obtained from the Minecraft client player info via an accessor mixin. - * Returns 0 when no player info is available. + * @return player's ping value, or 0 if unavailable */ val currentPing - get() = (minecraft.player as AbstractClientPlayerAccessor).clientPlayerInfo?.latency ?: 0 + get() = (minecraft.player as AbstractClientPlayerAccessor) + .clientPlayerInfo?.latency ?: 0 init { EventBus.register(this) } - /** - * Event handler for incoming packets. When a `ClientboundSetTimePacket` is - * received we use its arrival timestamp to estimate the server tick time and - * update [averageTps] with a small smoothing factor. - */ + @Suppress("UndocumentedPublicFunction") @SubscribeEvent - fun onPacketReceive(event: PacketEvent.Receive) { - if (event.packet is ClientboundSetTimePacket) { - val now = System.currentTimeMillis() + fun onPacketReceive(@Suppress("UnusedParameter") event: PacketEvent.Receive) { + if (event.packet !is ClientboundSetTimePacket) return + + val now = System.currentTimeMillis() + + val last = lastTickTime + lastTickTime = now - if (lastTickTime == 0L) { - lastTickTime = now - return - } + if (last == -1L) return - val delta = now - lastTickTime - lastTickTime = now + val delta = now - last + if (delta <= 0) return - if (delta <= 0) return + val tps = (MS_PER_SECOND * TICKS_PER_SECOND / delta) + .coerceAtMost(TICKS_PER_SECOND) + .toFloat() - val tps = (TPS_SCALE / delta).coerceIn(0.0, TPS_MAX) - averageTps = (averageTps * TPS_BETA + tps * TPS_ALPHA).toFloat() - } + averageTps = + averageTps * (1 - TPS_SMOOTHING) + + tps * TPS_SMOOTHING } } diff --git a/src/main/kotlin/org/cobalt/util/WebUtils.kt b/src/main/kotlin/org/cobalt/util/WebUtils.kt index e0729d1e..ecdc1541 100644 --- a/src/main/kotlin/org/cobalt/util/WebUtils.kt +++ b/src/main/kotlin/org/cobalt/util/WebUtils.kt @@ -5,15 +5,20 @@ import java.net.HttpURLConnection import java.net.URI import org.cobalt.Cobalt -/** Small HTTP helpers for fetching resources. */ +/** + * Utility for web-related operations. + */ object WebUtils { - /** Open an InputStream for the given URL using a simple GET request. + /** + * Opens an InputStream for the given URL using an HTTP GET request. * - * @param url the URL to fetch - * @param timeout connect/read timeout in milliseconds - * @param cache whether to allow URLConnection caching - * @return an InputStream for reading the response body; caller is responsible for closing it + * The caller is responsible for closing the returned stream. + * + * @param url target URL + * @param timeout connection and read timeout in milliseconds + * @param cache whether URLConnection caching is enabled + * @return [InputStream] of the HTTP response body */ @JvmStatic fun getInputStream(url: String, timeout: Int = 5000, cache: Boolean = true): InputStream { diff --git a/src/main/kotlin/org/cobalt/util/WindowUtils.kt b/src/main/kotlin/org/cobalt/util/WindowUtils.kt new file mode 100644 index 00000000..cf654dcb --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/WindowUtils.kt @@ -0,0 +1,51 @@ +package org.cobalt.util + +import kotlin.math.min +import org.cobalt.Cobalt.minecraft +import org.cobalt.dsl.mouseX +import org.cobalt.dsl.mouseY + +/** + * Utility for handling mouse input and cursor state within the client. + * + * Provides access to mouse position and controls for cursor interaction. + */ +object WindowUtils { + + private const val BASE_WIDTH = 1920f + private const val BASE_HEIGHT = 1080f + + /** + * Calculates a UI scaling factor based on the current window size. + * + * The scale is derived by comparing the current window dimensions to a + * fixed base resolution (1920x1080) and returning the smaller ratio + * to maintain aspect consistency. + * + * @return scale factor used for UI rendering + */ + @JvmStatic + fun getWindowScale(): Float { + val windowWidth = minecraft.window.width.toFloat() + val windowHeight = minecraft.window.height.toFloat() + + return min(windowWidth / BASE_WIDTH, windowHeight / BASE_HEIGHT) + } + + /** + * Gets the current mouse X position in screen space. + * + * @return mouse X coordinate in window space + */ + @JvmStatic + fun getMouseX(): Float = mouseX + + /** + * Gets the current mouse Y position in screen space. + * + * @return mouse Y coordinate in window space + */ + @JvmStatic + fun getMouseY(): Float = mouseY + +} diff --git a/src/main/kotlin/org/cobalt/util/helper/Layers.kt b/src/main/kotlin/org/cobalt/util/helper/Layers.kt index ff6783df..ed5e5643 100644 --- a/src/main/kotlin/org/cobalt/util/helper/Layers.kt +++ b/src/main/kotlin/org/cobalt/util/helper/Layers.kt @@ -6,7 +6,9 @@ import net.minecraft.client.renderer.rendertype.OutputTarget import net.minecraft.client.renderer.rendertype.RenderSetup import net.minecraft.client.renderer.rendertype.RenderType -/** Pre-configured RenderType providers used by RenderUtils for lines and quads (ESP variants). */ +/** + * Collection of predefined RenderType layers used for custom rendering. + */ object Layers { private val LINES: RenderType = RenderType.create( @@ -41,12 +43,24 @@ object Layers { .createRenderSetup() ) - /** Return a RenderType for drawing quads; pass true to use the ESP variant. */ + /** + * Returns the appropriate quad RenderType based on ESP mode. + * + * @param esp whether ESP rendering is enabled + * @return the corresponding quad RenderType + */ + @JvmStatic fun getQuads(esp: Boolean): RenderType { return if (esp) QUADS_ESP else QUADS } - /** Return a RenderType for drawing lines; pass true to use the ESP variant. */ + /** + * Returns the appropriate line RenderType based on ESP mode. + * + * @param esp whether ESP rendering is enabled + * @return the corresponding line RenderType + */ + @JvmStatic fun getLines(esp: Boolean): RenderType { return if (esp) LINES_ESP else LINES } diff --git a/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt b/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt index 771cd2d2..00c16b95 100644 --- a/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt +++ b/src/main/kotlin/org/cobalt/util/helper/Pipelines.kt @@ -6,18 +6,23 @@ import java.util.* import net.minecraft.client.renderer.RenderPipelines import net.minecraft.resources.Identifier -/** Central registry for custom render pipelines used by the client. */ +/** + * Collection of custom RenderPipeline definitions used for rendering. + */ object Pipelines { private const val NAMESPACE = "cobalt" - private const val PIPELINE_DIR = "pipeline/" - private const val LINES_ESP_PATH = PIPELINE_DIR + "lines_esp" - private const val QUADS_PATH = PIPELINE_DIR + "quads" - private const val QUADS_ESP_PATH = PIPELINE_DIR + "quads_esp" + private const val PIPELINE_DIR = "pipeline" + private const val LINES_ESP_PATH = "$PIPELINE_DIR/lines_esp" + private const val QUADS_PATH = "$PIPELINE_DIR/quads" + private const val QUADS_ESP_PATH = "$PIPELINE_DIR/quads_esp" private val NO_DEPTH_STENCIL: Optional = Optional.empty() - /** Pipeline for rendering ESP-style lines (no depth/stencil). */ + /** + * Render pipeline for line rendering with ESP enabled (visible through walls). + */ + @JvmStatic val LINES_ESP: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.LINES_SNIPPET) .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, LINES_ESP_PATH)) @@ -25,7 +30,10 @@ object Pipelines { .build() ) - /** Pipeline for rendering filled debug quads (default depth/stencil). */ + /** + * Render pipeline for standard quad rendering with normal depth testing. + */ + @JvmStatic val QUADS: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.DEBUG_FILLED_SNIPPET) .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, QUADS_PATH)) @@ -33,7 +41,10 @@ object Pipelines { .build() ) - /** Pipeline for rendering filled quads used in ESP (no depth/stencil). */ + /** + * Render pipeline for quad rendering with ESP enabled (visible through walls). + */ + @JvmStatic val QUADS_ESP: RenderPipeline = RenderPipelines.register( RenderPipeline.builder(RenderPipelines.DEBUG_FILLED_SNIPPET) .withLocation(Identifier.fromNamespaceAndPath(NAMESPACE, QUADS_ESP_PATH)) diff --git a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt index 2352cde7..1b101a62 100644 --- a/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt +++ b/src/main/kotlin/org/cobalt/util/helper/TickScheduler.kt @@ -5,8 +5,9 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent -/** Schedule Runnable tasks to run after a number of client ticks. - * Tasks are executed on TickEvent.End and are ordered by scheduled tick. */ +/** + * Utility for scheduling delayed tasks based on client tick updates. + */ object TickScheduler { private val taskQueue = PriorityQueue(Comparator.comparingLong(ScheduledTask::executeTick)) @@ -18,15 +19,20 @@ object TickScheduler { EventBus.register(this) } - /** Schedule an action to execute after the given number of ticks. */ + /** + * Schedules a task to be executed after a given number of client ticks. + * + * @param delayTicks number of ticks to wait before executing the task + * @param action the task to execute after the delay + */ @JvmStatic fun schedule(delayTicks: Long, action: Runnable) { taskQueue.offer(ScheduledTask(currentTick + delayTicks, action)) } - /** Internal event handler invoked at the end of each client tick to flush scheduled tasks. */ + @Suppress("UndocumentedPublicFunction") @SubscribeEvent - fun onClientTick(@Suppress("UNUSED_PARAMETER") event: TickEvent.End) { + fun onClientTick(@Suppress("UnusedParameter") event: TickEvent.End) { currentTick++ var task: ScheduledTask? diff --git a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt b/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt deleted file mode 100644 index 53edb003..00000000 --- a/src/main/kotlin/org/cobalt/util/rotation/DefaultRotations.kt +++ /dev/null @@ -1,100 +0,0 @@ -package org.cobalt.util.rotation - -import kotlin.math.abs -import org.cobalt.Cobalt.minecraft -import org.cobalt.util.ChatUtils -import org.cobalt.util.MessageType - -object DefaultRotations : IRotation { - - private const val FULL_CIRCLE = 360.0 - private const val HALF_CIRCLE = 180.0 - private const val NORMALIZE_OFFSET = FULL_CIRCLE + HALF_CIRCLE - private const val ANGLE_TOLERANCE = 0.5 - private const val ZERO_ANGLE = 0.0 - - private var rotating = false - private var targetYaw = ZERO_ANGLE - private var targetPitch = ZERO_ANGLE - private var currentYaw = ZERO_ANGLE - private var currentPitch = ZERO_ANGLE - private var currentSpeed = ZERO_ANGLE - - private val player = minecraft.player - - override fun onRotationStart(yaw: Double, pitch: Double, speed: Double) { - rotating = true - targetYaw = yaw - targetPitch = pitch - currentYaw = getPlayerYaw() - currentPitch = getPlayerPitch() - currentSpeed = speed - ChatUtils.sendSystemMessage("Rotation started to $yaw, $pitch", MessageType.DEBUG) - } - - override fun onRotationEnd() { - rotating = false - ChatUtils.sendSystemMessage("Ended rotation.", MessageType.DEBUG) - } - - override fun onRotationWorldRender() { - if (!rotating) return - - val player = player?: return - - val currentYaw = player.yRot.toDouble() - val currentPitch = player.xRot.toDouble() - - - val newYaw = lerpAngle(currentYaw, targetYaw, currentSpeed) - val newPitch = lerp(currentPitch, targetPitch, currentSpeed) - - applyRotation(newYaw, newPitch) - - if ( - angleDistance(newYaw, targetYaw) < ANGLE_TOLERANCE && - abs(newPitch - targetPitch) < ANGLE_TOLERANCE - ) { - stopRotation() - } - } - - private fun lerpAngle(current: Double, target: Double, alpha: Double): Double { - val delta = ((target - current + NORMALIZE_OFFSET) % FULL_CIRCLE) - HALF_CIRCLE - return current + delta * alpha - } - - private fun lerp(a: Double, b: Double, t: Double): Double { - return a + (b - a) * t - } - - private fun angleDistance(a: Double, b: Double): Double { - val d = ((b - a + NORMALIZE_OFFSET) % FULL_CIRCLE) - HALF_CIRCLE - return abs(d) - } - - private fun distance(a: Double, b: Double): Double { - return abs(a - b) - } - - override fun isRotating(): Boolean = rotating - - private fun applyRotation(yaw: Double, pitch: Double) { - val player = player ?: return - - val y = yaw.toFloat() - val p = pitch.toFloat() - - player.yRot = y - player.xRot = p - } - - private fun getPlayerYaw(): Double { - return player?.yRot?.toDouble() ?: ZERO_ANGLE - } - - private fun getPlayerPitch(): Double { - return player?.xRot?.toDouble() ?: ZERO_ANGLE - } - -} diff --git a/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt b/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt deleted file mode 100644 index 96747a4e..00000000 --- a/src/main/kotlin/org/cobalt/util/rotation/IRotation.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.cobalt.util.rotation - -/** - * Contract for components that perform or manage rotation over time. - * - * Implementations should handle starting, updating and stopping rotations - * and expose whether a rotation is currently in progress. - */ -interface IRotation { - - /** - * Called during world rendering when rotation-related rendering or updates - * should be performed. Typically used to update camera orientation visuals - * or perform per-frame interpolation while a rotation is active. - */ - fun onRotationWorldRender() - - /** - * Called when a rotation finishes or is explicitly stopped. Implementations - * should perform any cleanup required when rotation ends. - */ - fun onRotationEnd() - - /** - * Start a rotation towards the given yaw and pitch. - * - * @param yaw target yaw in degrees - * @param pitch target pitch in degrees - * @param speed interpolation speed (higher = faster). Defaults to 0.15. - */ - fun onRotationStart(yaw: Double, pitch: Double, speed: Double = 0.15) - - /** - * Returns true when a rotation is currently in progress. - */ - fun isRotating(): Boolean - - /** - * Convenience helper to stop rotation. Default implementation delegates to - * [onRotationEnd]. Implementations may override to perform additional work. - */ - fun stopRotation() { - onRotationEnd() - } - -} diff --git a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt b/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt deleted file mode 100644 index 2f579c28..00000000 --- a/src/main/kotlin/org/cobalt/util/rotation/RotationManager.kt +++ /dev/null @@ -1,66 +0,0 @@ -package org.cobalt.util.rotation - -import org.cobalt.event.EventBus -import org.cobalt.event.annotation.SubscribeEvent -import org.cobalt.event.impl.WorldRenderEvent - -/** - * Global manager for the currently active rotation controller. - * - * The manager holds an [IRotation] implementation which performs rotation - * logic. Use [setActiveRotation] to switch controllers, [getActiveRotation] - * to query the current controller and [resetRotation] to return to the - * default implementation. This object registers itself on the event bus and - * forwards per-frame world render updates to the active rotation when - * applicable. - */ -object RotationManager { - - private var rotation: IRotation = DefaultRotations - - init { - EventBus.register(this) - } - - /** - * Make [newRotation] the active rotation controller and start a rotation - * towards the provided [yaw] and [pitch]. If a rotation is already in - * progress it will be stopped before switching controllers. - */ - fun setActiveRotation(newRotation: IRotation, yaw: Double, pitch: Double) { - if (rotation.isRotating()) { - rotation.stopRotation() - } - - rotation = newRotation - rotation.onRotationStart(yaw, pitch) - } - - /** - * Returns the currently active [IRotation] controller. - */ - fun getActiveRotation(): IRotation = rotation - - /** - * Reset the active rotation controller to the default implementation and - * stop any ongoing rotation. - */ - fun resetRotation() { - if (rotation.isRotating()) { - rotation.stopRotation() - } - - rotation = DefaultRotations - } - - /** - * Event handler invoked during world rendering. Forwards the render event - * to the active rotation controller when a rotation is in progress. - */ - @SubscribeEvent - fun onWorldRender(@Suppress("UnusedParameter") event: WorldRenderEvent) { - if (!rotation.isRotating()) return - rotation.onRotationWorldRender() - } - -} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt similarity index 95% rename from src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt rename to src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt index bb89dfa7..e3984e36 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaContext.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaContext.kt @@ -16,7 +16,7 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.render.skia +package org.cobalt.util.skia import io.github.humbleui.skija.Canvas import io.github.humbleui.skija.ColorSpace @@ -27,7 +27,7 @@ import io.github.humbleui.skija.Surface import io.github.humbleui.skija.SurfaceOrigin import org.cobalt.event.EventBus import org.cobalt.event.impl.SkiaDrawEvent -import org.cobalt.render.skia.gl.States +import org.cobalt.util.skia.gl.States import org.lwjgl.opengl.GL11 private const val DEFAULT_SAMPLES = 0 @@ -51,7 +51,7 @@ internal object SkiaContext { EventBus.register(this) } - fun initSkia(width: Int, height: Int) { + internal fun initSkia(width: Int, height: Int) { ensureContext() recreateRenderTarget(width, height) @@ -85,7 +85,7 @@ internal object SkiaContext { ) } - fun draw() { + internal fun draw() { val ctx = context ?: return val srf = surface ?: return diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt new file mode 100644 index 00000000..aa43f3d5 --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaEnums.kt @@ -0,0 +1,33 @@ +package org.cobalt.util.skia + +/** + * Direction for gradient interpolation in Skia rendering helpers + */ +enum class SkiaGradient { + + /** Gradient flows from top to bottom (vertical interpolation). */ + TOP_TO_BOTTOM, + + /** Gradient flows from left to right (horizontal interpolation). */ + LEFT_TO_RIGHT + +} + +/** + * A side of a rectangle, used for edge-based drawing operations. + */ +enum class SkiaSide { + + /** The top edge of a rectangle. */ + TOP, + + /** The bottom edge of a rectangle. */ + BOTTOM, + + /** The left edge of a rectangle. */ + LEFT, + + /** The right edge of a rectangle. */ + RIGHT + +} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt similarity index 67% rename from src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt rename to src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt index eac633b8..b92254c9 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaImage.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaImage.kt @@ -1,4 +1,4 @@ -package org.cobalt.render.skia +package org.cobalt.util.skia import io.github.humbleui.skija.Data import io.github.humbleui.skija.Image @@ -12,27 +12,22 @@ import kotlinx.coroutines.runBlocking import org.cobalt.util.WebUtils /** - * Represents a lazily-loaded image resource. Supports both raster images and - * SVG documents. Raster images are loaded as deferred Skia [Image] objects, - * while SVGs are parsed into an [SVGDOM] and can be rasterized on demand. + * Image wrapper used by [SkiaImages]. * - * @property radius optional corner radius to apply when rendering the image - * @property colorMask optional ARGB color mask applied when drawing + * @property isSvg whether the image source is an SVG file + * @property image raster image instance (null if SVG) + * @property svgDom parsed SVG document (null if raster image) + * + * @param identifier path or URL to the image resource + * @property radius optional corner radius for rendering + * @property colorMask optional ARGB color mask applied to the image + * + * @see SkiaImages */ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: Int? = null) { - /** True when the identifier points to an SVG resource (case-insensitive). */ val isSvg = identifier.endsWith(".svg", ignoreCase = true) - - /** - * Deferred Skia [Image] used for raster formats (png/jpg/etc.). This will be - * null for SVG resources. - */ val image: Image? - - /** - * Parsed [SVGDOM] for SVG resources. Null for raster images. - */ val svgDom: SVGDOM? private var cachedRaster: Image? = null @@ -52,17 +47,13 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In } /** - * Return a raster [Image] sized to the requested [width]/[height]. + * Returns a rasterized image for the given size. * - * For raster inputs this returns the deferred image (no resizing). For SVG - * inputs this will render the SVG to a raster surface, cache the generated - * snapshot and return it. Subsequent calls with the same dimensions will - * return the cached snapshot. + * For SVG sources, the image is generated and cached per size. * - * @param width desired pixel width of the rasterized image - * @param height desired pixel height of the rasterized image - * @return a Skia [Image] at the requested dimensions, or null if the image - * could not be loaded or rendered + * @param width target width + * @param height target height + * @return raster image or null if unavailable */ fun getOrGenerateRaster(width: Int, height: Int): Image? { if (!isSvg) return image @@ -81,6 +72,15 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In return cachedRaster } + /** + * Releases all underlying Skia resources. + */ + fun delete() { + image?.close() + svgDom?.close() + cachedRaster?.close() + } + private fun isCachedMatch(width: Int, height: Int): Boolean { return cachedRaster != null && width == lastWidth && height == lastHeight } @@ -113,18 +113,8 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In return snapshot } - /** - * Release any native resources (Skia images and DOMs) held by this - * instance. After calling this method the instance should not be used to - * produce images. - */ - fun delete() { - image?.close() - svgDom?.close() - cachedRaster?.close() - } - companion object { + private fun getByteArray(path: String): ByteArray { val trimmedPath = path.trim() return if (trimmedPath.startsWith("http")) runBlocking { WebUtils.getInputStream(trimmedPath).readBytes() } @@ -134,6 +124,7 @@ class SkiaImage(identifier: String, val radius: Float? = null, val colorMask: In else this::class.java.getResourceAsStream(trimmedPath)?.readBytes() ?: throw FileNotFoundException(trimmedPath) } } + } } diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaImages.kt similarity index 63% rename from src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt rename to src/main/kotlin/org/cobalt/util/skia/SkiaImages.kt index f3ced821..ed08aba4 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaImages.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaImages.kt @@ -1,4 +1,4 @@ -package org.cobalt.render.skia +package org.cobalt.util.skia import io.github.humbleui.skija.BlendMode import io.github.humbleui.skija.Canvas @@ -9,29 +9,26 @@ import io.github.humbleui.skija.Paint import io.github.humbleui.skija.SamplingMode import io.github.humbleui.types.RRect import io.github.humbleui.types.Rect -import org.cobalt.math.SimpleVec3 import org.cobalt.math.Dimensions +import org.cobalt.math.Vec2f +import org.cobalt.util.skia.SkiaContext.canvas -/** Utilities for loading and drawing cached Skia images. - * Images may be rounded and color-masked when drawn. +/** + * Utility for loading, caching, and drawing images via Skia. */ object SkiaImages { - private data class ImageCacheKey( - val identifier: String, - val radius: Float?, - val colorMask: Int?, - ) private val images = mutableMapOf() - private val canvas get() = SkiaContext.canvas /** - * Load or create a cached image for the given identifier. + * Loads an image configuration and caches it by identifier, radius, and color mask. * - * @param identifier resource identifier for the image - * @param radius optional corner radius to apply when rendering - * @param colorMask optional ARGB color mask to blend over the image - * @return a cached or newly created [SkiaImage] + * Radius values <= 0 are normalized to `null` (no rounded clipping). + * + * @param identifier image/resource identifier + * @param radius optional corner radius for rounded clipping + * @param colorMask optional ARGB color mask applied during draw + * @return cached [SkiaImage] instance for the given configuration */ @JvmStatic fun loadImage( @@ -48,16 +45,18 @@ object SkiaImages { } /** - * Draw the provided [SkiaImage] into the destination rectangle. + * Draws a configured image to the current Skia canvas. + * + * Returns early when dimensions are invalid, the canvas is unavailable, + * or raster generation fails. * - * @param image the cached image to draw - * @param pos destination coordinate - * @param dim destination dimension + * @param image configured [SkiaImage] to draw + * @param pos target position in screen space */ @JvmStatic - fun image(image: SkiaImage, pos: SimpleVec3, dim: Dimensions) { + fun drawImage(image: SkiaImage, pos: Vec2f, dim: Dimensions) { if (!isValidDimension(dim)) return - val canvas = this.canvas ?: return + val canvas = canvas ?: return val sourceImage = image.getOrGenerateRaster(dim.width.toInt(), dim.height.toInt()) ?: return @@ -67,9 +66,9 @@ object SkiaImages { private fun drawConfiguredImage( canvas: Canvas, image: SkiaImage, - pos: SimpleVec3, + pos: Vec2f, dim: Dimensions, - sourceImage: Image + sourceImage: Image, ) { Paint().use { paint -> configurePaint(paint, image.colorMask) @@ -77,21 +76,19 @@ object SkiaImages { } } - private fun isValidDimension(dim: Dimensions) = dim.width > 0 && dim.height > 0 - private fun drawWithOptionalClip( canvas: Canvas, image: SkiaImage, - pos: SimpleVec3, + pos: Vec2f, dim: Dimensions, sourceImage: Image, - paint: Paint + paint: Paint, ) { - val rrect = if (image.radius != null && image.radius > 0f) { + val roundedRect = if (image.radius != null && image.radius > 0f) { RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, image.radius) } else null - withOptionalClip(canvas, rrect) { + withOptionalClip(canvas, roundedRect) { canvas.drawImageRect( sourceImage, Rect.makeWH(sourceImage.width.toFloat(), sourceImage.height.toFloat()), @@ -110,13 +107,13 @@ object SkiaImages { } private inline fun withOptionalClip( - canvas: io.github.humbleui.skija.Canvas, - rrect: RRect?, + canvas: Canvas, + roundedRect: RRect?, block: () -> Unit, ) { - if (rrect != null) { + if (roundedRect != null) { canvas.save() - canvas.clipRRect(rrect, ClipMode.INTERSECT, true) + canvas.clipRRect(roundedRect, ClipMode.INTERSECT, true) try { block() } finally { @@ -127,4 +124,13 @@ object SkiaImages { } } + private fun isValidDimension(dim: Dimensions) = + dim.width > 0 && dim.height > 0 + + private data class ImageCacheKey( + val identifier: String, + val radius: Float?, + val colorMask: Int?, + ) + } diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaShapes.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaShapes.kt new file mode 100644 index 00000000..0debf835 --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaShapes.kt @@ -0,0 +1,296 @@ +package org.cobalt.util.skia + +import io.github.humbleui.skija.Paint +import io.github.humbleui.skija.PaintMode +import io.github.humbleui.skija.Shader +import io.github.humbleui.types.RRect +import io.github.humbleui.types.Rect +import org.cobalt.math.Dimensions +import org.cobalt.math.Vec2f +import org.cobalt.util.skia.SkiaContext.canvas + +/** + * Utility for drawing basic shape primitives via Skia. + */ +object SkiaShapes { + + /** + * Draws a line segment between two points. + * + * No-op when no canvas is available. + * + * @param start line start position + * @param end line end position + * @param color ARGB line color + * @param thickness stroke width in pixels (clamped to >= 0) + */ + @JvmStatic + fun drawLine(start: Vec2f, end: Vec2f, color: Int, thickness: Float = 1f) { + val canvas = canvas ?: return + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = thickness.coerceAtLeast(0f) + isAntiAlias = true + }.use { paint -> + canvas.drawLine(start.x, start.y, end.x, end.y, paint) + } + } + + /** + * Draws a filled rectangle. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position of the rectangle + * @param dim width and height of the rectangle + * @param color ARGB fill color + */ + @JvmStatic + fun drawRect(pos: Vec2f, dim: Dimensions, color: Int) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + Paint().setColor(color).use { paint -> + canvas.drawRect(Rect.makeXYWH(pos.x, pos.y, dim.width, dim.height), paint) + } + } + + /** + * Draws a rectangle filled with a linear gradient. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param colorStart starting ARGB color of the gradient + * @param colorEnd ending ARGB color of the gradient + * @param direction direction of the gradient flow + */ + @JvmStatic + fun drawGradientRect(pos: Vec2f, dim: Dimensions, colorStart: Int, colorEnd: Int, direction: SkiaGradient) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + createLinearGradientShader(pos, dim, colorStart, colorEnd, direction).use { shader -> + Paint().apply { + this.shader = shader + isAntiAlias = true + }.use { paint -> + canvas.drawRect( + Rect.makeXYWH(pos.x, pos.y, dim.width, dim.height), + paint + ) + } + } + } + + /** + * Draws a rectangle outline. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param color ARGB stroke color + * @param thickness stroke width in pixels (clamped to >= 0) + */ + @JvmStatic + fun drawOutline(pos: Vec2f, dim: Dimensions, color: Int, thickness: Float = 1f) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + val thickness = thickness.coerceAtLeast(0f) + val half = thickness / 2f + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = thickness + isAntiAlias = true + }.use { paint -> + canvas.drawRect( + Rect.makeXYWH(pos.x + half, pos.y + half, dim.width - thickness, dim.height - thickness), + paint, + ) + } + } + + /** + * Draws a filled rounded rectangle. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param radius corner radius in pixels (clamped to >= 0) + * @param color ARGB fill color + */ + @JvmStatic + fun drawRoundedRect(pos: Vec2f, dim: Dimensions, radius: Float, color: Int) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + Paint().setColor(color).use { paint -> + canvas.drawRRect(RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, radius.coerceAtLeast(0f)), paint) + } + } + + /** + * Draws a rounded rectangle filled with a linear gradient. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param radius corner radius in pixels + * @param colorStart starting ARGB color of the gradient + * @param colorEnd ending ARGB color of the gradient + * @param direction direction of the gradient flow + */ + @JvmStatic + fun drawGradientRoundedRect( + pos: Vec2f, dim: Dimensions, + radius: Float, colorStart: Int, colorEnd: Int, direction: SkiaGradient, + ) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + createLinearGradientShader(pos, dim, colorStart, colorEnd, direction).use { shader -> + Paint().apply { + this.shader = shader + isAntiAlias = true + }.use { paint -> + canvas.drawRRect( + RRect.makeXYWH(pos.x, pos.y, dim.width, dim.height, radius.coerceAtLeast(0f)), + paint + ) + } + } + } + + /** + * Draws a rounded rectangle outline. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param radius outer corner radius in pixels + * @param color ARGB stroke color + * @param thickness outline width in pixels (clamped to >= 1) + */ + @JvmStatic + fun drawRoundedOutline( + pos: Vec2f, dim: Dimensions, + radius: Float, color: Int, thickness: Float = 1f, + ) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + + val thickness = thickness.coerceAtLeast(1f) + val half = thickness / 2f + val innerRadius = (radius - half).coerceAtLeast(0f) + + Paint().apply { + setColor(color) + mode = PaintMode.STROKE + strokeWidth = thickness + isAntiAlias = true + }.use { paint -> + canvas.drawRRect( + RRect.makeXYWH(pos.x + half, pos.y + half, dim.width - thickness, dim.height - thickness, innerRadius), paint + ) + } + } + + /** + * Draws a rectangle with selectively rounded corners on one side. + * + * Returns early when dimensions are invalid or no canvas is available. + * + * @param pos top-left position + * @param dim rectangle size + * @param radius corner radius in pixels + * @param color ARGB fill color + * @param side which side(s) should be rounded + */ + @JvmStatic + fun drawHalfRoundedRect( + pos: Vec2f, dim: Dimensions, + radius: Float, color: Int, side: SkiaSide = SkiaSide.TOP, + ) { + if (!isValid(dim)) { + return + } + + val canvas = canvas ?: return + val radius = radius.coerceAtLeast(0f) + val radii = buildRadii(side, radius) + + Paint().apply { + setColor(color) + isAntiAlias = true + }.use { paint -> + canvas.drawRRect(RRect.makeComplexXYWH(pos.x, pos.y, dim.width, dim.height, radii), paint) + } + } + + private fun createLinearGradientShader( + pos: Vec2f, + dim: Dimensions, + colorStart: Int, + colorEnd: Int, + direction: SkiaGradient, + ): Shader { + val (x1, y1) = calculateGradientEnd(pos, dim, direction) + + return Shader.makeLinearGradient( + pos.x, pos.y, + x1, y1, + intArrayOf(colorStart, colorEnd) + ) + } + + private fun calculateGradientEnd( + pos: Vec2f, + dim: Dimensions, + direction: SkiaGradient, + ): Pair { + return when (direction) { + SkiaGradient.LEFT_TO_RIGHT -> pos.x + dim.width to pos.y + SkiaGradient.TOP_TO_BOTTOM -> pos.x to pos.y + dim.height + } + } + + private fun buildRadii(side: SkiaSide, radius: Float): FloatArray { + return when (side) { + SkiaSide.TOP -> floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f) + SkiaSide.BOTTOM -> floatArrayOf(0f, 0f, 0f, 0f, radius, radius, radius, radius) + SkiaSide.LEFT -> floatArrayOf(radius, radius, 0f, 0f, 0f, 0f, radius, radius) + SkiaSide.RIGHT -> floatArrayOf(0f, 0f, radius, radius, radius, radius, 0f, 0f) + } + } + + private fun isValid(dim: Dimensions) = dim.width > 0f && dim.height > 0f + +} diff --git a/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaText.kt similarity index 51% rename from src/main/kotlin/org/cobalt/render/skia/SkiaText.kt rename to src/main/kotlin/org/cobalt/util/skia/SkiaText.kt index a35bf821..b6b1d3df 100644 --- a/src/main/kotlin/org/cobalt/render/skia/SkiaText.kt +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaText.kt @@ -1,4 +1,4 @@ -package org.cobalt.render.skia +package org.cobalt.util.skia import io.github.humbleui.skija.Data import io.github.humbleui.skija.Font @@ -7,23 +7,33 @@ import io.github.humbleui.skija.FontHinting import io.github.humbleui.skija.FontMgr import io.github.humbleui.skija.Paint import io.github.humbleui.skija.TextLine -import org.cobalt.math.SimpleVec3 import java.io.IOException +import org.cobalt.math.Vec2f +import org.cobalt.util.skia.SkiaContext.canvas -/** Text drawing module */ +/** + * Utility for font loading and text rendering via Skia. + */ object SkiaText { - private val fonts = mutableMapOf() - /** Primary UI font used throughout the client. */ + /** + * Primary UI font used throughout the client. + */ + @JvmField val primaryFont: Font = loadFont("assets/cobalt/font/ProductSans-Bold.ttf") - private val canvas get() = SkiaContext.canvas + private val fonts = mutableMapOf() /** - * Load and cache a font - * @param resourcePath the path - * @return the font + * Loads a font from classpath resources and caches it by path. + * + * @param resourcePath classpath path to a font file + * @return loaded [Font] instance configured for UI rendering + * + * @throws IOException if the resource cannot be found + * @throws IllegalArgumentException if the font bytes are invalid */ + @JvmStatic fun loadFont(resourcePath: String) = fonts.computeIfAbsent(resourcePath) { val bytes = javaClass.classLoader ?.getResourceAsStream(resourcePath) @@ -41,22 +51,18 @@ object SkiaText { } /** - * Text styling - * @property fontSize font size - * @property color color - */ - data class TextStyle(val fontSize: Float, val color: Int) - - /** - * Draw text - * @param font the font - * @param text the text - * @param pos the position - * @param style the style + * Draws text at the given position using the provided font and style. + * + * No-op when no canvas is available. + * + * @param font font instance to use + * @param text text content to render + * @param pos target position in screen space + * @param style text style containing font size and color */ @JvmStatic - fun text(font: Font, text: String, pos: SimpleVec3, style: TextStyle) { - val canvas = this.canvas ?: return + fun drawText(font: Font, text: String, pos: Vec2f, style: TextStyle) { + val canvas = canvas ?: return font.size = style.fontSize @@ -70,14 +76,15 @@ object SkiaText { } /** - * Get text width - * @param font the font - * @param text the text - * @param fontSize the font size - * @return the width + * Measures the rendered width of text for the given font size. + * + * @param font font instance to measure with + * @param text text content to measure + * @param fontSize font size used for measurement + * @return text width in pixels */ @JvmStatic - fun textWidth(font: Font, text: String, fontSize: Float): Float { + fun getTextWidth(font: Font, text: String, fontSize: Float): Float { font.size = fontSize TextLine.make(text, font).use { line -> @@ -85,4 +92,12 @@ object SkiaText { } } + /** + * Immutable text rendering style. + * + * @property fontSize font size in pixels + * @property color ARGB color value + */ + data class TextStyle(val fontSize: Float, val color: Int) + } diff --git a/src/main/kotlin/org/cobalt/util/skia/SkiaTransforms.kt b/src/main/kotlin/org/cobalt/util/skia/SkiaTransforms.kt new file mode 100644 index 00000000..257d3ec9 --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/skia/SkiaTransforms.kt @@ -0,0 +1,47 @@ +package org.cobalt.util.skia + +import org.cobalt.math.Vec2f +import org.cobalt.util.skia.SkiaContext.canvas + +/** + * Utility for applying transform and state operations to the current Skia canvas. + */ +object SkiaTransforms { + + /** + * Saves the current canvas state. + * + * No-op when no canvas is available. + */ + @JvmStatic + fun save() = canvas?.save() + + /** + * Restores the most recently saved canvas state. + * + * No-op when no canvas is available. + */ + @JvmStatic + fun restore() = canvas?.restore() + + /** + * Translates the canvas by the given position offset. + * + * No-op when no canvas is available. + * + * @param pos translation offset on X/Y axes + */ + @JvmStatic + fun translate(pos: Vec2f) = canvas?.translate(pos.x, pos.y) + + /** + * Scales the canvas by the given X/Y factors. + * + * No-op when no canvas is available. + * + * @param scale scale factors on X/Y axes + */ + @JvmStatic + fun scale(scale: Vec2f) = canvas?.scale(scale.x, scale.y) + +} diff --git a/src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt similarity index 75% rename from src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt rename to src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt index 82827980..ffc81b61 100644 --- a/src/main/kotlin/org/cobalt/render/skia/WrappedBackendRenderTarget.kt +++ b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt @@ -16,37 +16,25 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.render.skia +package org.cobalt.util.skia import io.github.humbleui.skija.BackendRenderTarget +import io.github.humbleui.skija.BackendRenderTarget._nMakeGL import io.github.humbleui.skija.impl.Stats import org.jetbrains.annotations.Contract -/** - * A thin wrapper around Skija's [BackendRenderTarget] that carries additional - * information about the GL framebuffer used to back the render target. - * - * @property width framebuffer width in pixels - * @property height framebuffer height in pixels - * @property sampleCnt number of samples for multisampling - * @property stencilBits number of stencil bits in the framebuffer - * @property fbId OpenGL framebuffer id - * @property fbFormat OpenGL framebuffer format - * @param ptr native pointer passed to the Skija BackendRenderTarget base - */ -class WrappedBackendRenderTarget( +internal class WrappedBackendRenderTarget( val width: Int, val height: Int, val sampleCnt: Int, val stencilBits: Int, val fbId: Int, val fbFormat: Int, - ptr: Long + ptr: Long, ) : BackendRenderTarget(ptr) { companion object { - @Contract("_, _, _, _, _, _ -> new") /** * Create a new [WrappedBackendRenderTarget] backed by an OpenGL framebuffer. * @@ -60,8 +48,9 @@ class WrappedBackendRenderTarget( * @param fbId OpenGL framebuffer id * @param fbFormat OpenGL framebuffer format */ - fun makeGL( - width: Int, height: Int, sampleCnt: Int, stencilBits: Int, fbId: Int, fbFormat: Int + @Contract("_, _, _, _, _, _ -> new") + internal fun makeGL( + width: Int, height: Int, sampleCnt: Int, stencilBits: Int, fbId: Int, fbFormat: Int, ): WrappedBackendRenderTarget { Stats.onNativeCall() return WrappedBackendRenderTarget( diff --git a/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt b/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt new file mode 100644 index 00000000..e5e4b49f --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/skia/gl/Properties.kt @@ -0,0 +1,89 @@ +package org.cobalt.util.skia.gl + +import java.util.* + +/** + * Represents the OpenGL state. + */ +@Suppress("UndocumentedPublicProperty") +class Properties { + + val lastActiveTexture = IntArray(1) + val lastProgram = IntArray(1) + val lastTexture = IntArray(1) + val lastSampler = IntArray(1) + val lastArrayBuffer = IntArray(1) + val lastVertexArrayObject = IntArray(1) + val lastPolygonMode = IntArray(2) + val lastViewport = IntArray(4) + val lastScissorBox = IntArray(4) + val lastBlendSrcRgb = IntArray(1) + val lastBlendDstRgb = IntArray(1) + val lastBlendSrcAlpha = IntArray(1) + val lastBlendDstAlpha = IntArray(1) + val lastBlendEquationRgb = IntArray(1) + val lastBlendEquationAlpha = IntArray(1) + + val lastPixelUnpackBufferBinding = IntArray(1) + val lastUnpackAlignment = IntArray(1) + val lastUnpackRowLength = IntArray(1) + val lastUnpackSkipPixels = IntArray(1) + val lastUnpackSkipRows = IntArray(1) + val lastPackSwapBytes = IntArray(1) + val lastPackLsbFirst = IntArray(1) + val lastPackRowLength = IntArray(1) + val lastPackImageHeight = IntArray(1) + val lastPackSkipPixels = IntArray(1) + val lastPackSkipRows = IntArray(1) + val lastPackSkipImages = IntArray(1) + val lastPackAlignment = IntArray(1) + val lastUnpackSwapBytes = IntArray(1) + val lastUnpackLsbFirst = IntArray(1) + val lastUnpackImageHeight = IntArray(1) + val lastUnpackSkipImages = IntArray(1) + + private val flags = BitSet(7) + + var lastEnableBlend + get() = flags[0] + set(value) { + flags[0] = value + } + + var lastEnableCullFace + get() = flags[1] + set(value) { + flags[1] = value + } + + var lastEnableDepthTest + get() = flags[2] + set(value) { + flags[2] = value + } + + var lastEnableStencilTest + get() = flags[3] + set(value) { + flags[3] = value + } + + var lastEnableScissorTest + get() = flags[4] + set(value) { + flags[4] = value + } + + var lastEnablePrimitiveRestart + get() = flags[5] + set(value) { + flags[5] = value + } + + var lastDepthMask + get() = flags[6] + set(value) { + flags[6] = value + } + +} diff --git a/src/main/kotlin/org/cobalt/render/skia/gl/State.kt b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt similarity index 87% rename from src/main/kotlin/org/cobalt/render/skia/gl/State.kt rename to src/main/kotlin/org/cobalt/util/skia/gl/State.kt index 394431ef..0d1dd352 100644 --- a/src/main/kotlin/org/cobalt/render/skia/gl/State.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/State.kt @@ -17,7 +17,7 @@ * * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.render.skia.gl +package org.cobalt.util.skia.gl import org.lwjgl.opengl.GL import org.lwjgl.opengl.GL45.* @@ -45,28 +45,19 @@ private const val SCISSOR_W = 2 private const val SCISSOR_H = 3 /** - * Represents a snapshot of relevant OpenGL state that can be pushed and - * restored. The snapshot reads multiple GL bindings and pixel store - * parameters so rendering code can change GL state and then restore it to - * the previous values. + * Represents the OpenGL state. * - * @param glVersion computed GL version (major * 100 + minor * 10) used to - * determine which GL features are available when capturing/restoring state. + * @property glVersion The current OpenGL version. */ class State(private val glVersion: Int) { private val props = Properties() /** - * Capture the current GL state into this [State] instance. + * Saves the current OpenGL state. * - * This method queries a wide set of GL bindings (textures, buffers, - * vertex arrays), pixel store parameters and enabled/disabled flags and - * stores them inside the internal [Properties] object. It also resets a - * handful of pixel store parameters (unpack alignment/row/skip) to safe - * defaults required by the renderer. - * - * @return this [State] instance for convenience. + * @return this [State] instance for convenience + * @see pop */ fun push(): State { with(props) { @@ -158,28 +149,18 @@ class State(private val glVersion: Int) { } /** - * Restore GL state previously captured by [push]. - * - * The stored values are applied back to the GL context, including bound - * program, textures, samplers, vertex arrays, pixel store parameters and - * enabled/disabled capabilities. The method returns this [State] - * instance for chaining if desired. + * Restores the state that was saved with [push]. * * @return this [State] instance after restoration. + * @see push */ fun pop(): State { restoreProgramAndTexture() - restoreSamplersAndBindings() - restoreBlendAndCapabilities() - restorePolygonViewportAndScissor() - restorePixelStoresAndBuffers() - restoreDepthMask() - return this } diff --git a/src/main/kotlin/org/cobalt/render/skia/gl/States.kt b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt similarity index 70% rename from src/main/kotlin/org/cobalt/render/skia/gl/States.kt rename to src/main/kotlin/org/cobalt/util/skia/gl/States.kt index c7ff5068..06277560 100644 --- a/src/main/kotlin/org/cobalt/render/skia/gl/States.kt +++ b/src/main/kotlin/org/cobalt/util/skia/gl/States.kt @@ -16,23 +16,18 @@ * You should have received a copy of the GNU General Public License along with Skija. If not, see . */ -package org.cobalt.render.skia.gl +package org.cobalt.util.skia.gl -import java.util.* -import org.lwjgl.opengl.GL30.glGetIntegerv +import java.util.Stack import org.lwjgl.opengl.GL30.GL_MAJOR_VERSION import org.lwjgl.opengl.GL30.GL_MINOR_VERSION +import org.lwjgl.opengl.GL30.glGetIntegerv private const val GL_MAJOR_MULTIPLIER = 100 private const val GL_MINOR_MULTIPLIER = 10 /** - * Utility that manages a stack of OpenGL state snapshots. - * - * Use [push] to capture the current GL state and [pop] to restore the most - * recently captured state. The object's initializer reads the OpenGL major - * and minor version and stores a computed integer representation for use by - * created [State] instances. + * Stores and restores OpenGL states. */ object States { @@ -40,17 +35,14 @@ object States { private val states = Stack() /** - * Capture the current GL state and push a snapshot onto the internal stack. - * - * A new [State] is created using the GL version detected at startup and - * its [State.push] method is invoked to record the GL state. + * Pushes the current OpenGL state onto the stack. */ fun push() { states += State(glVersion).push() } /** - * Restore and remove the most recently pushed GL state snapshot. + * Pops the last OpenGL state from the stack and restores it. * * Throws an [IllegalArgumentException] if there is no saved state to * restore. diff --git a/src/main/resources/cobalt.mixins.json b/src/main/resources/cobalt.mixins.json index 281bf5f8..aa922ea1 100644 --- a/src/main/resources/cobalt.mixins.json +++ b/src/main/resources/cobalt.mixins.json @@ -6,7 +6,6 @@ "client": [ "client.AbstractClientPlayerAccessor", "client.MinecraftMixin", - "gui.ChatScreenMixin", "gui.ClientPacketListenerMixin", "gui.CommandSuggestionsMixin", "gui.GuiRendererMixin", From 94a63c546b0461330a4a6431f362eed755fa638b Mon Sep 17 00:00:00 2001 From: Nathan <209938737+quiteboring@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:16:57 -0400 Subject: [PATCH 16/18] fix: compile errors --- .../mixin/gui/CommandSuggestionsMixin.java | 2 +- .../cobalt/mixin/platform/WindowMixin.java | 2 +- .../ui/notification/NotificationManager.kt | 3 ++- .../util/skia/WrappedBackendRenderTarget.kt | 20 ++++++++++++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java b/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java index 72d31fca..f828a05f 100644 --- a/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java +++ b/src/main/java/org/cobalt/mixin/gui/CommandSuggestionsMixin.java @@ -72,7 +72,7 @@ public void refresh(CallbackInfo ci, @Local(name = "reader") StringReader reader reader.skip(); int cursor = this.input.getCursorPosition(); - CommandDispatcher dispatcher = CommandManager.getDispatcher(); + CommandDispatcher dispatcher = CommandManager.getDispatcher$cobalt(); if (this.currentParse == null) { ClientSuggestionProvider suggestionProvider = this.minecraft.player.connection.getSuggestionsProvider(); diff --git a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java index 7dccad38..77baedd1 100644 --- a/src/main/java/org/cobalt/mixin/platform/WindowMixin.java +++ b/src/main/java/org/cobalt/mixin/platform/WindowMixin.java @@ -33,7 +33,7 @@ private void onFramebufferResize(long handle, int newWidth, int newHeight, Callb int finalWidth = Math.max(newWidth, 1); int finalHeight = Math.max(newHeight, 1); - SkiaContext.INSTANCE.initSkia(finalWidth, finalHeight); + SkiaContext.INSTANCE.initSkia$cobalt(finalWidth, finalHeight); } } diff --git a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt index 40b03171..d6e6ec88 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/NotificationManager.kt @@ -4,6 +4,7 @@ import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.SkiaDrawEvent import org.cobalt.math.Vec2f +import org.cobalt.util.WindowUtils import org.cobalt.util.skia.SkiaTransforms /** @@ -29,7 +30,7 @@ object NotificationManager { @Suppress("UndocumentedPublicFunction") @SubscribeEvent fun onSkiaDraw(@Suppress("UnusedParameter") event: SkiaDrawEvent) { - val windowScale = SkiaTransforms.getWindowScale() + val windowScale = WindowUtils.getWindowScale() notificationsList .forEach { notification -> diff --git a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt index ffc81b61..23acf15c 100644 --- a/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt +++ b/src/main/kotlin/org/cobalt/util/skia/WrappedBackendRenderTarget.kt @@ -23,7 +23,19 @@ import io.github.humbleui.skija.BackendRenderTarget._nMakeGL import io.github.humbleui.skija.impl.Stats import org.jetbrains.annotations.Contract -internal class WrappedBackendRenderTarget( +/** + * [BackendRenderTarget] wrapper that stores the OpenGL framebuffer metadata + * used to create and track the native render-target pointer. + * + * @property width framebuffer width in pixels + * @property height framebuffer height in pixels + * @property sampleCnt number of samples used for multisampling + * @property stencilBits number of stencil bits in the framebuffer + * @property fbId OpenGL framebuffer object id + * @property fbFormat OpenGL framebuffer format enum value + * @param ptr native backend render-target pointer owned by [BackendRenderTarget] + */ +class WrappedBackendRenderTarget( val width: Int, val height: Int, val sampleCnt: Int, @@ -38,8 +50,9 @@ internal class WrappedBackendRenderTarget( /** * Create a new [WrappedBackendRenderTarget] backed by an OpenGL framebuffer. * - * The native helper [_nMakeGL] is invoked to allocate the platform-specific - * backend render target and its pointer is stored in the created wrapper. + * The native helper [_nMakeGL] allocates the underlying backend render target. + * The resulting pointer is then passed to [BackendRenderTarget] through the + * [WrappedBackendRenderTarget] constructor. * * @param width framebuffer width in pixels * @param height framebuffer height in pixels @@ -47,6 +60,7 @@ internal class WrappedBackendRenderTarget( * @param stencilBits number of stencil bits in the framebuffer * @param fbId OpenGL framebuffer id * @param fbFormat OpenGL framebuffer format + * @return a new [WrappedBackendRenderTarget] instance with native pointer state */ @Contract("_, _, _, _, _, _ -> new") internal fun makeGL( From eb3a9a4f63c6323a2dea296d6103321e627e22e7 Mon Sep 17 00:00:00 2001 From: Nathan <209938737+quiteboring@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:17:59 -0400 Subject: [PATCH 17/18] chore: cleanup --- src/main/java/org/cobalt/mixin/render/FrustumInvoker.java | 5 ++--- src/main/kotlin/org/cobalt/event/EventBus.kt | 2 +- src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt | 2 +- src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt | 2 +- src/main/kotlin/org/cobalt/math/Math.kt | 2 +- src/main/kotlin/org/cobalt/module/Module.kt | 2 +- src/main/kotlin/org/cobalt/ui/notification/Notification.kt | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java b/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java index fc4be8cc..b8649f6d 100644 --- a/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java +++ b/src/main/java/org/cobalt/mixin/render/FrustumInvoker.java @@ -16,10 +16,9 @@ public interface FrustumInvoker { * @param maxX the x-coordinate of the maximum corner * @param maxY the y-coordinate of the maximum corner * @param maxZ the z-coordinate of the maximum corner - * * @return {@link org.joml.FrustumIntersection#INSIDE}, - * {@link org.joml.FrustumIntersection#INTERSECT}, - * or a plane index if outside the frustum + * {@link org.joml.FrustumIntersection#INTERSECT}, + * or a plane index if outside the frustum */ @Invoker int invokeCubeInFrustum(double minX, double minY, double minZ, double maxX, double maxY, double maxZ); diff --git a/src/main/kotlin/org/cobalt/event/EventBus.kt b/src/main/kotlin/org/cobalt/event/EventBus.kt index 9c0e838e..68fa6512 100644 --- a/src/main/kotlin/org/cobalt/event/EventBus.kt +++ b/src/main/kotlin/org/cobalt/event/EventBus.kt @@ -19,7 +19,7 @@ object EventBus { val priority: Event.Priority, val ignoreCancelled: Boolean, val once: Boolean, - val invoker: (Event) -> Unit + val invoker: (Event) -> Unit, ) private val handlers = CopyOnWriteArrayList() diff --git a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt index c91cdae9..d6e07d41 100644 --- a/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/SkiaDrawEvent.kt @@ -18,5 +18,5 @@ import org.cobalt.util.skia.WrappedBackendRenderTarget class SkiaDrawEvent( val context: DirectContext, val renderTarget: WrappedBackendRenderTarget, - val canvas: Canvas + val canvas: Canvas, ) : Event() diff --git a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt index 5491e662..2ca89a98 100644 --- a/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt +++ b/src/main/kotlin/org/cobalt/event/impl/WorldRenderEvent.kt @@ -11,5 +11,5 @@ import org.cobalt.event.Event * @property context the Fabric [LevelRenderContext] for the current render frame */ class WorldRenderEvent( - val context: LevelRenderContext + val context: LevelRenderContext, ) : Event() diff --git a/src/main/kotlin/org/cobalt/math/Math.kt b/src/main/kotlin/org/cobalt/math/Math.kt index 80037f1c..739b8a06 100644 --- a/src/main/kotlin/org/cobalt/math/Math.kt +++ b/src/main/kotlin/org/cobalt/math/Math.kt @@ -19,5 +19,5 @@ data class Vec2f( */ data class Dimensions( val width: Float, - val height: Float + val height: Float, ) diff --git a/src/main/kotlin/org/cobalt/module/Module.kt b/src/main/kotlin/org/cobalt/module/Module.kt index aba0a974..a6ba69e4 100644 --- a/src/main/kotlin/org/cobalt/module/Module.kt +++ b/src/main/kotlin/org/cobalt/module/Module.kt @@ -96,7 +96,7 @@ abstract class RenderableModule( * @property displayName the name shown in the UI */ class ModuleCategory private constructor( - val displayName: String + val displayName: String, ) { companion object { diff --git a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt index 0e2ce425..a74904e4 100644 --- a/src/main/kotlin/org/cobalt/ui/notification/Notification.kt +++ b/src/main/kotlin/org/cobalt/ui/notification/Notification.kt @@ -13,7 +13,7 @@ import org.cobalt.ui.UIComponent data class Notification( val title: String, val description: String, - val duration: Duration + val duration: Duration, ) : UIComponent( xPos = DEFAULT_X, yPos = DEFAULT_Y, From cbf3fa12cc12c2d2143be9b4d1308188c3c167d6 Mon Sep 17 00:00:00 2001 From: Nathan <209938737+quiteboring@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:18:22 -0400 Subject: [PATCH 18/18] chore: cleanup pt2 --- src/main/kotlin/org/cobalt/command/Command.kt | 2 +- src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/Command.kt b/src/main/kotlin/org/cobalt/command/Command.kt index 8bc50b29..dbe09118 100644 --- a/src/main/kotlin/org/cobalt/command/Command.kt +++ b/src/main/kotlin/org/cobalt/command/Command.kt @@ -52,7 +52,7 @@ abstract class Command(val name: String, val aliases: List = emptyList + mainRoot: LiteralArgumentBuilder, ): List> { return aliases.filter { it.isNotBlank() }.map { alias -> val aliasRoot = LiteralArgumentBuilder.literal(alias) diff --git a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt index 92dce0f4..ab34a481 100644 --- a/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt +++ b/src/main/kotlin/org/cobalt/event/annotation/SubscribeEvent.kt @@ -14,5 +14,5 @@ import org.cobalt.event.Event annotation class SubscribeEvent( val ignoreCancelled: Boolean = false, val priority: Event.Priority = Event.Priority.MEDIUM, - val once: Boolean = false + val once: Boolean = false, )