From 736dcfc7a015841a55c7636e2e37aea61863a087 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Sat, 8 Aug 2026 03:19:47 -0400 Subject: [PATCH 1/4] Add coloring api --- README.md | 16 +++++++++ .../polyfrost/polyhitbox/api/HitboxColors.kt | 34 +++++++++++++++++++ .../polyhitbox/render/HitboxRenderer.kt | 21 +++++++++--- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt diff --git a/README.md b/README.md index eb52063..7e683e4 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,22 @@ A hitbox modification mod - Different color during invulnerability frames (i-frames) - Hide with the HUD (F1) +## For mod developers + +Other mods can recolour hitboxes per entity through `org.polyfrost.polyhitbox.api.HitboxColors`: + +```java +HitboxColors.register((entity, element, argb) -> + element == HitboxElement.OUTLINE ? myColorFor(entity, argb) : argb); +``` + +Providers run on the render thread, once per element per visible entity per frame, after the +configured hover and i-frame colours have been applied. + +Everything in `org.polyfrost.polyhitbox.api` is stable. Nothing else is — in particular +`render.HitboxRenderer` is private, is rewritten between patch releases, and injecting into it will +break your users' game rather than just your feature. + ## Gallery ![settings-page.png](images/settings-page.png) diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt new file mode 100644 index 0000000..f46b100 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt @@ -0,0 +1,34 @@ +package org.polyfrost.polyhitbox.api + +import net.minecraft.world.entity.Entity + +enum class HitboxElement { + SIDE, + OUTLINE, + EYE_HEIGHT, + VIEW_RAY, +} + +fun interface HitboxColorProvider { + fun color(entity: Entity, element: HitboxElement, argb: Int): Int +} + +object HitboxColors { + + @Volatile + private var providers: Array = emptyArray() + + @JvmStatic + fun register(provider: HitboxColorProvider) { + synchronized(this) { providers += provider } + } + + @JvmStatic + fun unregister(provider: HitboxColorProvider) { + synchronized(this) { + providers = providers.filterNot { it === provider }.toTypedArray() + } + } + + internal fun snapshot(): Array = providers +} diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt index d14a9cf..77e78d0 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt @@ -9,6 +9,9 @@ import net.minecraft.util.Mth import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player +import org.polyfrost.polyhitbox.api.HitboxColorProvider +import org.polyfrost.polyhitbox.api.HitboxColors +import org.polyfrost.polyhitbox.api.HitboxElement import org.polyfrost.polyhitbox.config.HitboxCategory import org.polyfrost.polyhitbox.config.HitboxConfig import org.polyfrost.polyhitbox.config.ModConfig @@ -51,6 +54,7 @@ object HitboxRenderer { private var viewer: Player? = null private var selfInFirstPerson: Entity? = null private var vanillaToggle = false + private var colorProviders: Array = emptyArray() private var offX = 0.0 private var offY = 0.0 @@ -140,6 +144,7 @@ object HitboxRenderer { viewer = player selfInFirstPerson = if (mc.options.cameraType.isFirstPerson) mc.cameraEntity else null vanillaToggle = vanillaHitboxesEnabled() + colorProviders = HitboxColors.snapshot() return true } @@ -203,6 +208,14 @@ object HitboxRenderer { private fun pick(iframe: Boolean, hover: Boolean, iframeArgb: Int, hoverArgb: Int, baseArgb: Int): Int = if (iframe) iframeArgb else if (hover) hoverArgb else baseArgb + private fun tint(entity: Entity, element: HitboxElement, argb: Int): Int { + val providers = colorProviders + if (providers.isEmpty()) return argb + var result = argb + for (provider in providers) result = provider.color(entity, element, result) + return result + } + private fun drawEntity(vc: VertexConsumer, entity: Entity, config: HitboxConfig) { val delta = partialTicks.toDouble() val px = Mth.lerp(delta, entity.xo, entity.x) @@ -223,20 +236,20 @@ object HitboxRenderer { val iframe = config.iframeColor && inIframes(entity) if (config.showSide) { - val c = pick(iframe, hover, config.sideIframeArgb, config.sideHoverArgb, config.sideArgb) + val c = tint(entity, HitboxElement.SIDE, pick(iframe, hover, config.sideIframeArgb, config.sideHoverArgb, config.sideArgb)) fillBox(vc, minX, minY, minZ, maxX, maxY, maxZ, c) } if (config.showOutline) { - val c = pick(iframe, hover, config.outlineIframeArgb, config.outlineHoverArgb, config.outlineArgb) + val c = tint(entity, HitboxElement.OUTLINE, pick(iframe, hover, config.outlineIframeArgb, config.outlineHoverArgb, config.outlineArgb)) styledBox(vc, config, minX, minY, minZ, maxX, maxY, maxZ, c, config.outlineThickness) } if (config.showEyeHeight) { - val c = pick(iframe, hover, config.eyeHeightIframeArgb, config.eyeHeightHoverArgb, config.eyeHeightArgb) + val c = tint(entity, HitboxElement.EYE_HEIGHT, pick(iframe, hover, config.eyeHeightIframeArgb, config.eyeHeightHoverArgb, config.eyeHeightArgb)) val eyeY = minY + entity.eyeHeight styledBox(vc, config, minX, eyeY - 0.01, minZ, maxX, eyeY + 0.01, maxZ, c, config.eyeHeightThickness) } if (config.showViewRay) { - val c = pick(iframe, hover, config.viewRayIframeArgb, config.viewRayHoverArgb, config.viewRayArgb) + val c = tint(entity, HitboxElement.VIEW_RAY, pick(iframe, hover, config.viewRayIframeArgb, config.viewRayHoverArgb, config.viewRayArgb)) val eyeY = minY + entity.eyeHeight val view = entity.getViewVector(partialTicks) val ax = px - camX From 765582d5703844538fe5680ae01e7616d53021a7 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Sat, 8 Aug 2026 04:14:49 -0400 Subject: [PATCH 2/4] Add requested changes --- README.md | 14 +++++++-- .../polyfrost/polyhitbox/api/HitboxColors.kt | 22 ++++++++++++- .../polyhitbox/render/HitboxRenderer.kt | 31 ++++++++++++++++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7e683e4..18027a7 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,20 @@ A hitbox modification mod Other mods can recolour hitboxes per entity through `org.polyfrost.polyhitbox.api.HitboxColors`: ```java -HitboxColors.register((entity, element, argb) -> - element == HitboxElement.OUTLINE ? myColorFor(entity, argb) : argb); +HitboxColors.register((context, argb) -> + context.getElement() == HitboxElement.OUTLINE + ? myColorFor(context.getEntity(), argb) + : argb); ``` Providers run on the render thread, once per element per visible entity per frame, after the -configured hover and i-frame colours have been applied. +configured hover and i-frame colours have been applied. `context.has(HitboxCondition.HOVERED)` and +`context.has(HitboxCondition.IFRAME)` report the entity's actual state regardless of whether the +matching colour option is enabled; more conditions may be added to `HitboxCondition` later. + +The context is only valid for the duration of the call — read what you need, don't retain it. A +provider that throws is unregistered and the failure is logged, so a broken integration degrades to +its own feature going away rather than breaking hitbox rendering. Everything in `org.polyfrost.polyhitbox.api` is stable. Nothing else is — in particular `render.HitboxRenderer` is private, is rewritten between patch releases, and injecting into it will diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt index f46b100..0875a04 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt @@ -1,6 +1,7 @@ package org.polyfrost.polyhitbox.api import net.minecraft.world.entity.Entity +import org.slf4j.LoggerFactory enum class HitboxElement { SIDE, @@ -9,12 +10,26 @@ enum class HitboxElement { VIEW_RAY, } +enum class HitboxCondition { + HOVERED, + IFRAME, +} + +interface HitboxColorContext { + val entity: Entity + val element: HitboxElement + + fun has(condition: HitboxCondition): Boolean +} + fun interface HitboxColorProvider { - fun color(entity: Entity, element: HitboxElement, argb: Int): Int + fun color(context: HitboxColorContext, argb: Int): Int } object HitboxColors { + private val logger = LoggerFactory.getLogger("PolyHitbox") + @Volatile private var providers: Array = emptyArray() @@ -31,4 +46,9 @@ object HitboxColors { } internal fun snapshot(): Array = providers + + internal fun disable(provider: HitboxColorProvider, error: Throwable) { + unregister(provider) + logger.error("Unregistered hitbox color provider {} because it threw.", provider.javaClass.name, error) + } } diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt index 77e78d0..a2a04b5 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt @@ -9,8 +9,10 @@ import net.minecraft.util.Mth import net.minecraft.world.entity.Entity import net.minecraft.world.entity.LivingEntity import net.minecraft.world.entity.player.Player +import org.polyfrost.polyhitbox.api.HitboxColorContext import org.polyfrost.polyhitbox.api.HitboxColorProvider import org.polyfrost.polyhitbox.api.HitboxColors +import org.polyfrost.polyhitbox.api.HitboxCondition import org.polyfrost.polyhitbox.api.HitboxElement import org.polyfrost.polyhitbox.config.HitboxCategory import org.polyfrost.polyhitbox.config.HitboxConfig @@ -208,11 +210,38 @@ object HitboxRenderer { private fun pick(iframe: Boolean, hover: Boolean, iframeArgb: Int, hoverArgb: Int, baseArgb: Int): Int = if (iframe) iframeArgb else if (hover) hoverArgb else baseArgb + private class Context : HitboxColorContext { + override lateinit var entity: Entity + override lateinit var element: HitboxElement + var hovered = false + var iframe = false + + override fun has(condition: HitboxCondition): Boolean = when (condition) { + HitboxCondition.HOVERED -> hovered + HitboxCondition.IFRAME -> iframe + } + } + + private val context = Context() + private fun tint(entity: Entity, element: HitboxElement, argb: Int): Int { val providers = colorProviders if (providers.isEmpty()) return argb + context.entity = entity + context.element = element + context.hovered = entity === hovered + context.iframe = inIframes(entity) var result = argb - for (provider in providers) result = provider.color(entity, element, result) + var failed = false + for (provider in providers) { + try { + result = provider.color(context, result) + } catch (error: Throwable) { + HitboxColors.disable(provider, error) + failed = true + } + } + if (failed) colorProviders = HitboxColors.snapshot() return result } From a85f90e847e7caaf90c7a303a9717d50454130a2 Mon Sep 17 00:00:00 2001 From: lowercasebtw Date: Sat, 8 Aug 2026 04:22:10 -0400 Subject: [PATCH 3/4] Remove un-needed documentation --- README.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/README.md b/README.md index 18027a7..eb52063 100644 --- a/README.md +++ b/README.md @@ -19,30 +19,6 @@ A hitbox modification mod - Different color during invulnerability frames (i-frames) - Hide with the HUD (F1) -## For mod developers - -Other mods can recolour hitboxes per entity through `org.polyfrost.polyhitbox.api.HitboxColors`: - -```java -HitboxColors.register((context, argb) -> - context.getElement() == HitboxElement.OUTLINE - ? myColorFor(context.getEntity(), argb) - : argb); -``` - -Providers run on the render thread, once per element per visible entity per frame, after the -configured hover and i-frame colours have been applied. `context.has(HitboxCondition.HOVERED)` and -`context.has(HitboxCondition.IFRAME)` report the entity's actual state regardless of whether the -matching colour option is enabled; more conditions may be added to `HitboxCondition` later. - -The context is only valid for the duration of the call — read what you need, don't retain it. A -provider that throws is unregistered and the failure is logged, so a broken integration degrades to -its own feature going away rather than breaking hitbox rendering. - -Everything in `org.polyfrost.polyhitbox.api` is stable. Nothing else is — in particular -`render.HitboxRenderer` is private, is rewritten between patch releases, and injecting into it will -break your users' game rather than just your feature. - ## Gallery ![settings-page.png](images/settings-page.png) From b42427ea750619437a9562100df2e3f245cea61b Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Mon, 10 Aug 2026 06:15:21 +0700 Subject: [PATCH 4/4] fixes --- .../polyfrost/polyhitbox/api/HitboxColors.kt | 4 ++- .../polyhitbox/render/HitboxRenderer.kt | 29 ++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt index 0875a04..075ff1e 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt @@ -35,7 +35,9 @@ object HitboxColors { @JvmStatic fun register(provider: HitboxColorProvider) { - synchronized(this) { providers += provider } + synchronized(this) { + if (providers.none { it === provider }) providers += provider + } } @JvmStatic diff --git a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt index a2a04b5..1ce1448 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt @@ -210,27 +210,22 @@ object HitboxRenderer { private fun pick(iframe: Boolean, hover: Boolean, iframeArgb: Int, hoverArgb: Int, baseArgb: Int): Int = if (iframe) iframeArgb else if (hover) hoverArgb else baseArgb - private class Context : HitboxColorContext { - override lateinit var entity: Entity - override lateinit var element: HitboxElement - var hovered = false - var iframe = false - + private class Context( + override val entity: Entity, + override val element: HitboxElement, + private val hovered: Boolean, + private val iframe: Boolean, + ) : HitboxColorContext { override fun has(condition: HitboxCondition): Boolean = when (condition) { HitboxCondition.HOVERED -> hovered HitboxCondition.IFRAME -> iframe } } - private val context = Context() - - private fun tint(entity: Entity, element: HitboxElement, argb: Int): Int { + private fun tint(entity: Entity, element: HitboxElement, hover: Boolean, iframe: Boolean, argb: Int): Int { val providers = colorProviders if (providers.isEmpty()) return argb - context.entity = entity - context.element = element - context.hovered = entity === hovered - context.iframe = inIframes(entity) + val context = Context(entity, element, hover, iframe) var result = argb var failed = false for (provider in providers) { @@ -265,20 +260,20 @@ object HitboxRenderer { val iframe = config.iframeColor && inIframes(entity) if (config.showSide) { - val c = tint(entity, HitboxElement.SIDE, pick(iframe, hover, config.sideIframeArgb, config.sideHoverArgb, config.sideArgb)) + val c = tint(entity, HitboxElement.SIDE, hover, iframe, pick(iframe, hover, config.sideIframeArgb, config.sideHoverArgb, config.sideArgb)) fillBox(vc, minX, minY, minZ, maxX, maxY, maxZ, c) } if (config.showOutline) { - val c = tint(entity, HitboxElement.OUTLINE, pick(iframe, hover, config.outlineIframeArgb, config.outlineHoverArgb, config.outlineArgb)) + val c = tint(entity, HitboxElement.OUTLINE, hover, iframe, pick(iframe, hover, config.outlineIframeArgb, config.outlineHoverArgb, config.outlineArgb)) styledBox(vc, config, minX, minY, minZ, maxX, maxY, maxZ, c, config.outlineThickness) } if (config.showEyeHeight) { - val c = tint(entity, HitboxElement.EYE_HEIGHT, pick(iframe, hover, config.eyeHeightIframeArgb, config.eyeHeightHoverArgb, config.eyeHeightArgb)) + val c = tint(entity, HitboxElement.EYE_HEIGHT, hover, iframe, pick(iframe, hover, config.eyeHeightIframeArgb, config.eyeHeightHoverArgb, config.eyeHeightArgb)) val eyeY = minY + entity.eyeHeight styledBox(vc, config, minX, eyeY - 0.01, minZ, maxX, eyeY + 0.01, maxZ, c, config.eyeHeightThickness) } if (config.showViewRay) { - val c = tint(entity, HitboxElement.VIEW_RAY, pick(iframe, hover, config.viewRayIframeArgb, config.viewRayHoverArgb, config.viewRayArgb)) + val c = tint(entity, HitboxElement.VIEW_RAY, hover, iframe, pick(iframe, hover, config.viewRayIframeArgb, config.viewRayHoverArgb, config.viewRayArgb)) val eyeY = minY + entity.eyeHeight val view = entity.getViewVector(partialTicks) val ax = px - camX