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..075ff1e --- /dev/null +++ b/src/main/kotlin/org/polyfrost/polyhitbox/api/HitboxColors.kt @@ -0,0 +1,56 @@ +package org.polyfrost.polyhitbox.api + +import net.minecraft.world.entity.Entity +import org.slf4j.LoggerFactory + +enum class HitboxElement { + SIDE, + OUTLINE, + EYE_HEIGHT, + 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(context: HitboxColorContext, argb: Int): Int +} + +object HitboxColors { + + private val logger = LoggerFactory.getLogger("PolyHitbox") + + @Volatile + private var providers: Array = emptyArray() + + @JvmStatic + fun register(provider: HitboxColorProvider) { + synchronized(this) { + if (providers.none { it === provider }) providers += provider + } + } + + @JvmStatic + fun unregister(provider: HitboxColorProvider) { + synchronized(this) { + providers = providers.filterNot { it === provider }.toTypedArray() + } + } + + 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 d14a9cf..1ce1448 100644 --- a/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt +++ b/src/main/kotlin/org/polyfrost/polyhitbox/render/HitboxRenderer.kt @@ -9,6 +9,11 @@ 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 import org.polyfrost.polyhitbox.config.ModConfig @@ -51,6 +56,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 +146,7 @@ object HitboxRenderer { viewer = player selfInFirstPerson = if (mc.options.cameraType.isFirstPerson) mc.cameraEntity else null vanillaToggle = vanillaHitboxesEnabled() + colorProviders = HitboxColors.snapshot() return true } @@ -203,6 +210,36 @@ 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( + 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 fun tint(entity: Entity, element: HitboxElement, hover: Boolean, iframe: Boolean, argb: Int): Int { + val providers = colorProviders + if (providers.isEmpty()) return argb + val context = Context(entity, element, hover, iframe) + var result = argb + 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 + } + private fun drawEntity(vc: VertexConsumer, entity: Entity, config: HitboxConfig) { val delta = partialTicks.toDouble() val px = Mth.lerp(delta, entity.xo, entity.x) @@ -223,20 +260,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, 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 = 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 = 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 = 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