From 6be7873d9475686fdc404212cebd45f22edbff87 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 7 Sep 2023 08:03:26 -0300 Subject: Add highlight and outline feature for rare sea creatures (#439) Add highlight and outline feature for rare sea creatures #439 --- src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt | 3 + .../skyhanni/config/features/FishingConfig.java | 6 + .../skyhanni/config/features/MiscConfig.java | 6 + .../skyhanni/events/RenderEntityOutlineEvent.kt | 118 +++++++ .../features/fishing/SeaCreatureFeatures.kt | 100 ++++++ .../skyhanni/mixins/hooks/RenderGlobalHook.kt | 20 ++ .../mixins/hooks/RendererLivingEntityHook.kt | 20 ++ .../mixins/transformers/CustomRenderGlobal.java | 17 + .../mixins/transformers/MixinRenderGlobal.java | 42 +++ .../transformers/MixinRendererLivingEntity.java | 23 ++ .../skyhanni/utils/EntityOutlineRenderer.kt | 376 +++++++++++++++++++++ 11 files changed, 731 insertions(+) create mode 100644 src/main/java/at/hannibal2/skyhanni/events/RenderEntityOutlineEvent.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/fishing/SeaCreatureFeatures.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/mixins/hooks/RenderGlobalHook.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/mixins/hooks/RendererLivingEntityHook.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/mixins/transformers/CustomRenderGlobal.java create mode 100644 src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRenderGlobal.java create mode 100644 src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRendererLivingEntity.java create mode 100644 src/main/java/at/hannibal2/skyhanni/utils/EntityOutlineRenderer.kt (limited to 'src/main/java/at/hannibal2/skyhanni') diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index 5b0806042..17aa52347 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -103,6 +103,7 @@ import at.hannibal2.skyhanni.features.summonings.SummoningSoulsName import at.hannibal2.skyhanni.mixins.hooks.RenderLivingEntityHelper import at.hannibal2.skyhanni.test.* import at.hannibal2.skyhanni.test.command.CopyNearbyParticlesCommand +import at.hannibal2.skyhanni.utils.EntityOutlineRenderer import at.hannibal2.skyhanni.utils.MinecraftConsoleFilter.Companion.initLogging import at.hannibal2.skyhanni.utils.NEUVersionCheck.checkIfNeuIsLoaded import at.hannibal2.skyhanni.utils.TabListData @@ -140,6 +141,7 @@ class SkyHanniMod { loadModule(HypixelData()) loadModule(DungeonData()) loadModule(ScoreboardData()) + loadModule(SeaCreatureFeatures()) loadModule(SeaCreatureManager()) loadModule(ItemRenderBackground()) loadModule(EntityData()) @@ -171,6 +173,7 @@ class SkyHanniMod { loadModule(TitleData()) loadModule(BlockData()) loadModule(DefaultConfigFeatures) + loadModule(EntityOutlineRenderer) // APIs loadModule(BazaarApi()) diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/FishingConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/FishingConfig.java index 3dccd1b3c..896c572f0 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/FishingConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/FishingConfig.java @@ -191,6 +191,12 @@ public class FishingConfig { } + @Expose + @ConfigOption(name = "Rare Sea Creature Highlight", desc = "Highlight rare sea creatures in blue color.") + @ConfigEditorBoolean + @FeatureToggle + public boolean rareSeaCreatureHighlight = false; + @Expose @ConfigOption( name = "Shark Fish Counter", diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/MiscConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/MiscConfig.java index 3830f175b..efceff92b 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/MiscConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/MiscConfig.java @@ -1035,6 +1035,12 @@ public class MiscConfig { @FeatureToggle public boolean configButtonOnPause = true; + @Expose + @ConfigOption(name = "Entity Outlines", desc = "Enable entity outlines for applicable features.") + @ConfigEditorBoolean + @FeatureToggle + public boolean enableEntityOutlines = true; + @Expose public Position inventoryLoadPos = new Position(394, 124, false, true); diff --git a/src/main/java/at/hannibal2/skyhanni/events/RenderEntityOutlineEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/RenderEntityOutlineEvent.kt new file mode 100644 index 000000000..5efc7cd94 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/events/RenderEntityOutlineEvent.kt @@ -0,0 +1,118 @@ +package at.hannibal2.skyhanni.events + +import net.minecraft.client.Minecraft +import net.minecraft.entity.Entity +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.item.EntityItemFrame +import java.util.function.Consumer + +class RenderEntityOutlineEvent(theType: Type?, potentialEntities: HashSet?) : + LorenzEvent() { + + /** + * The phase of the event (see [Type] + */ + var type: Type? = null + + /** + * The entities to outline. This is progressively cumulated from [.entitiesToChooseFrom] + */ + var entitiesToOutline: HashMap? = null + + /** + * The entities we can outline. Note that this set and [.entitiesToOutline] are disjoint at all times. + */ + var entitiesToChooseFrom: HashSet? = null + + /** + * Constructs the event, given the type and optional entities to outline. + * + * + * This will modify {@param potentialEntities} internally, so make a copy before passing it if necessary. + * + * @param theType of the event (see [Type] + */ + init { + type = theType + entitiesToChooseFrom = potentialEntities + if (potentialEntities != null) { + entitiesToOutline = HashMap(potentialEntities.size) + } + } + + /** + * Conditionally queue entities around which to render entities + * Selects from the pool of [.entitiesToChooseFrom] to speed up the predicate testing on subsequent calls. + * Is more efficient (theoretically) than calling [.queueEntityToOutline] for each entity because lists are handled internally. + * + * + * This function loops through all entities and so is not very efficient. + * It's advisable to encapsulate calls to this function with global checks (those not dependent on an individual entity) for efficiency purposes. + * + * @param outlineColor a function to test + */ + fun queueEntitiesToOutline(outlineColor: ((entity: Entity) -> Int?)? = null) { + if (outlineColor == null) { + return + } + if (entitiesToChooseFrom == null) { + computeAndCacheEntitiesToChooseFrom() + } + val itr: MutableIterator = entitiesToChooseFrom!!.iterator() + while (itr.hasNext()) { + val e: Entity = itr.next() + val i: Int? = outlineColor(e) + if (i != null) { + entitiesToOutline!![e] = i + itr.remove() + } + } + } + + /** + * Adds a single entity to the list of the entities to outline + * + * @param entity the entity to add + * @param outlineColor the color with which to outline + */ + fun queueEntityToOutline(entity: Entity?, outlineColor: Int) { + if (entity == null) { + return + } + if (entitiesToChooseFrom == null) { + computeAndCacheEntitiesToChooseFrom() + } + if (!entitiesToChooseFrom!!.contains(entity)) { + return + } + entitiesToOutline!![entity] = outlineColor + entitiesToChooseFrom!!.remove(entity) + } + + /** + * Used for on-the-fly generation of entities. Driven by event handlers in a decentralized fashion + */ + private fun computeAndCacheEntitiesToChooseFrom() { + val entities: List = Minecraft.getMinecraft().theWorld.getLoadedEntityList() + // Only render outlines around non-null entities within the camera frustum + entitiesToChooseFrom = HashSet(entities.size) + // Only consider entities that aren't invisible armorstands to increase FPS significantly + entities.forEach(Consumer { e: Entity? -> + if (e != null && !(e is EntityArmorStand && e.isInvisible()) && e !is EntityItemFrame) { + entitiesToChooseFrom!!.add(e) + } + }) + entitiesToOutline = HashMap(entitiesToChooseFrom!!.size) + } + + /** + * The phase of the event. + * [.XRAY] means that this directly precedes entities whose outlines are rendered through walls (Vanilla 1.9+) + * [.NO_XRAY] means that this directly precedes entities whose outlines are rendered only when visible to the client + */ + enum class Type { + XRAY, + NO_XRAY + } + +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/features/fishing/SeaCreatureFeatures.kt b/src/main/java/at/hannibal2/skyhanni/features/fishing/SeaCreatureFeatures.kt new file mode 100644 index 000000000..9f7ef0fa5 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/fishing/SeaCreatureFeatures.kt @@ -0,0 +1,100 @@ +package at.hannibal2.skyhanni.features.fishing + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.events.EntityMaxHealthUpdateEvent +import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent +import at.hannibal2.skyhanni.events.RenderEntityOutlineEvent +import at.hannibal2.skyhanni.events.withAlpha +import at.hannibal2.skyhanni.features.damageindicator.DamageIndicatorManager +import at.hannibal2.skyhanni.mixins.hooks.RenderLivingEntityHelper +import at.hannibal2.skyhanni.utils.* +import at.hannibal2.skyhanni.utils.EntityUtils.getSkinTexture +import at.hannibal2.skyhanni.utils.EntityUtils.hasMaxHealth +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer +import at.hannibal2.skyhanni.utils.LorenzUtils.baseMaxHealth +import at.hannibal2.skyhanni.utils.LorenzUtils.editCopy +import net.minecraft.client.Minecraft +import net.minecraft.client.entity.EntityOtherPlayerMP +import net.minecraft.client.gui.FontRenderer +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityItem +import net.minecraft.entity.monster.EntityGuardian +import net.minecraft.entity.monster.EntitySkeleton +import net.minecraft.entity.monster.EntityZombie +import net.minecraft.entity.player.EntityPlayer +import net.minecraft.scoreboard.ScorePlayerTeam +import net.minecraft.scoreboard.Team.EnumVisible +import net.minecraftforge.client.event.RenderWorldLastEvent +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.awt.Color + + +class SeaCreatureFeatures { + private val config get() = SkyHanniMod.feature.fishing + private var rareSeaCreatures = listOf() + + @SubscribeEvent + fun onEntityHealthUpdate(event: EntityMaxHealthUpdateEvent) { + if (!isEnabled()) return + val entity = event.entity as? EntityLivingBase ?: return + if (DamageIndicatorManager.isBoss(entity)) return + + val maxHealth = event.maxHealth + for (creatureType in RareSeaCreatureType.entries) { + if (EntityGuardian::class.java.isInstance(entity) && entity.baseMaxHealth > 1000) { + LorenzUtils.consoleLog("FISHING CREATURE NAME: ${entity.baseMaxHealth} ${(entity as EntityGuardian).customNameTag}") + } + + if (!creatureType.health.any { entity.hasMaxHealth(it, false, maxHealth) }) continue + if (!creatureType.clazz.isInstance(entity)) continue + + if (EntityPlayer::class.java.isInstance(entity)) { + LorenzUtils.consoleLog("FISHING CREATURE NAME: ${(entity as EntityPlayer).customNameTag}") + } + + if (creatureType.nametag.isNotBlank() && EntityPlayer::class.java.isInstance(entity) && (entity as EntityPlayer).name != creatureType.nametag) { + continue + } + + rareSeaCreatures = rareSeaCreatures.editCopy { add(entity) } + RenderLivingEntityHelper.setEntityColor(entity, LorenzColor.RED.toColor().withAlpha(50)) + { config.rareSeaCreatureHighlight } + RenderLivingEntityHelper.setNoHurtTime(entity) { config.rareSeaCreatureHighlight } + } + } + + @SubscribeEvent + fun onWorldChange(event: LorenzWorldChangeEvent) { + rareSeaCreatures = emptyList() + } + + @SubscribeEvent + fun onRenderEntityOutlines(event: RenderEntityOutlineEvent) { + if (isEnabled() && config.rareSeaCreatureHighlight && event.type === RenderEntityOutlineEvent.Type.XRAY) { + event.queueEntitiesToOutline(getEntityOutlineColor) + } + } + + private fun isEnabled() = LorenzUtils.inSkyBlock && !LorenzUtils.inDungeons && !LorenzUtils.inKuudraFight + + private val getEntityOutlineColor: (entity: Entity) -> Int? = { e -> + if (EntityLivingBase::class.java.isInstance(e) && e in rareSeaCreatures && e.distanceToPlayer() < 30) { + LorenzColor.GREEN.toColor().rgb + } else { + null + } + } + + enum class RareSeaCreatureType(val clazz: Class, val nametag: String, vararg val health: Int) { + WATER_HYDRA(EntityZombie::class.java, "Water Hydra", 500_000, 1_500_000), + SEA_EMPEROR(EntityGuardian::class.java, "The Sea Emperors", 750_000, 800_000, 2_250_000, 2_400_000), + ZOMBIE_MINER(EntityPlayer::class.java, "", 2_000_000, 6_000_000), + PHANTOM_FISHERMAN(EntityPlayer::class.java, "Phantom Fisher", 1_000_000, 3_000_000), + GRIM_REAPER(EntityPlayer::class.java, "Grim Reaper", 3_000_000, 9_000_000), + YETI(EntityPlayer::class.java, "", 2_000_000, 6_000_000), + NUTCRACKER(EntityPlayer::class.java, "", 4_000_000, 12_000_000), + GREAT_WHITE_SHARK(EntityPlayer::class.java, "GWS ", 1_500_000, 4_500_000), + ; + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RenderGlobalHook.kt b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RenderGlobalHook.kt new file mode 100644 index 000000000..57305c5bd --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RenderGlobalHook.kt @@ -0,0 +1,20 @@ +package at.hannibal2.skyhanni.mixins.hooks + +import at.hannibal2.skyhanni.utils.EntityOutlineRenderer +import at.hannibal2.skyhanni.utils.RenderUtils +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.culling.ICamera +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable + +class RenderGlobalHook { + fun renderEntitiesOutlines(camera: ICamera?, partialTicks: Float): Boolean { + val vec = RenderUtils.exactLocation(Minecraft.getMinecraft().renderViewEntity, partialTicks) + return EntityOutlineRenderer.renderEntityOutlines(camera!!, partialTicks, vec) + } + + fun shouldRenderEntityOutlines(cir: CallbackInfoReturnable) { + if (EntityOutlineRenderer.shouldRenderEntityOutlines()) { + cir.returnValue = true + } + } +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RendererLivingEntityHook.kt b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RendererLivingEntityHook.kt new file mode 100644 index 000000000..a06e6d89e --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/mixins/hooks/RendererLivingEntityHook.kt @@ -0,0 +1,20 @@ +package at.hannibal2.skyhanni.mixins.hooks + +import at.hannibal2.skyhanni.utils.EntityOutlineRenderer +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.entity.EntityLivingBase + +class RendererLivingEntityHook { + fun setOutlineColor(red: Float, green: Float, blue: Float, alpha: Float, entity: EntityLivingBase) { + val color = EntityOutlineRenderer.getCustomOutlineColor(entity); + + if (color != null) { + val colorRed = (color shr 16 and 255).toFloat() / 255.0f + val colorGreen = (color shr 8 and 255).toFloat() / 255.0f + val colorBlue = (color and 255).toFloat() / 255.0f + GlStateManager.color(colorRed, colorGreen, colorBlue, alpha); + } else { + GlStateManager.color(red, green, blue, alpha); + } + } +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/CustomRenderGlobal.java b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/CustomRenderGlobal.java new file mode 100644 index 000000000..bd440d3ea --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/CustomRenderGlobal.java @@ -0,0 +1,17 @@ +package at.hannibal2.skyhanni.mixins.transformers; + +import net.minecraft.client.renderer.RenderGlobal; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.client.shader.ShaderGroup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(RenderGlobal.class) +public interface CustomRenderGlobal { + @Accessor("entityOutlineFramebuffer") + Framebuffer getEntityOutlineFramebuffer_skyhanni(); + + @Accessor("entityOutlineShader") + ShaderGroup getEntityOutlineShader_skyhanni(); + +} \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRenderGlobal.java b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRenderGlobal.java new file mode 100644 index 000000000..630fb1381 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRenderGlobal.java @@ -0,0 +1,42 @@ +package at.hannibal2.skyhanni.mixins.transformers; + +import at.hannibal2.skyhanni.mixins.hooks.RenderGlobalHook; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderGlobal; +import at.hannibal2.skyhanni.utils.EntityOutlineRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.culling.ICamera; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RenderGlobal.class) +public abstract class MixinRenderGlobal { + + @Shadow + abstract boolean isRenderEntityOutlines(); + + @Unique + private final RenderGlobalHook skyHanni$hook = new RenderGlobalHook(); + + @Redirect(method = "renderEntities", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;isRenderEntityOutlines()Z")) + public boolean renderEntitiesOutlines(RenderGlobal self, Entity renderViewEntity, ICamera camera, float partialTicks) { + return skyHanni$hook.renderEntitiesOutlines(camera, partialTicks) && this.isRenderEntityOutlines(); + } + + @Inject(method = "isRenderEntityOutlines", at = @At(value = "HEAD"), cancellable = true) + public void isRenderEntityOutlinesWrapper(CallbackInfoReturnable cir) { + skyHanni$hook.shouldRenderEntityOutlines(cir); + } + + @Inject(method = "renderEntityOutlineFramebuffer", at = @At(value = "RETURN")) + public void afterFramebufferDraw(CallbackInfo callbackInfo) { + GlStateManager.enableDepth(); + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRendererLivingEntity.java b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRendererLivingEntity.java new file mode 100644 index 000000000..281137ae5 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRendererLivingEntity.java @@ -0,0 +1,23 @@ +package at.hannibal2.skyhanni.mixins.transformers; + +import at.hannibal2.skyhanni.mixins.hooks.RendererLivingEntityHook; +import at.hannibal2.skyhanni.utils.EntityOutlineRenderer; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.entity.RendererLivingEntity; +import net.minecraft.entity.EntityLivingBase; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(RendererLivingEntity.class) +public class MixinRendererLivingEntity { + + @Unique + private final RendererLivingEntityHook skyHanni$hook = new RendererLivingEntityHook(); + + @Redirect(method = "setScoreTeamColor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GlStateManager;color(FFFF)V")) + public void setOutlineColor(float colorRed, float colorGreen, float colorBlue, float colorAlpha, EntityLivingBase entity) { + skyHanni$hook.setOutlineColor(colorRed, colorGreen, colorBlue, colorAlpha, entity); + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/EntityOutlineRenderer.kt b/src/main/java/at/hannibal2/skyhanni/utils/EntityOutlineRenderer.kt new file mode 100644 index 000000000..454df1454 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/EntityOutlineRenderer.kt @@ -0,0 +1,376 @@ +package at.hannibal2.skyhanni.utils + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.RenderEntityOutlineEvent +import at.hannibal2.skyhanni.mixins.transformers.CustomRenderGlobal +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.GlStateManager +import net.minecraft.client.renderer.OpenGlHelper +import net.minecraft.client.renderer.RenderHelper +import net.minecraft.client.renderer.culling.ICamera +import net.minecraft.client.shader.Framebuffer +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityLivingBase +import net.minecraft.util.BlockPos +import net.minecraftforge.client.MinecraftForgeClient +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import net.minecraftforge.fml.common.gameevent.TickEvent +import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL13 +import org.lwjgl.opengl.GL30 +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method + +/** + * Class to handle all entity outlining, including xray and no-xray rendering + * Features that include entity outlining should subscribe to the {@link RenderEntityOutlineEvent}. + * + * Credit to SkyblockAddons and Biscuit Development + * https://github.com/BiscuitDevelopment/SkyblockAddons/blob/main/src/main/java/codes/biscuit/skyblockaddons/features/EntityOutlines/EntityOutlineRenderer.java + * + */ + object EntityOutlineRenderer { + private val entityRenderCache: CachedInfo = CachedInfo(null, null, null) + private var stopLookingForOptifine = false + private var isFastRender: Method? = null + private var isShaders: Method? = null + private var isAntialiasing: Method? = null + private var emptyLastTick = false + private val swapBuffer by lazy { initSwapBuffer() } + private val logger = LorenzLogger("entity_outline_renderer") + private val config get() = SkyHanniMod.feature.misc + private val mc get() = Minecraft.getMinecraft() + private val BUF_FLOAT_4: java.nio.FloatBuffer = org.lwjgl.BufferUtils.createFloatBuffer(4) + + /** + * @return a new framebuffer with the size of the main framebuffer + */ + private fun initSwapBuffer(): Framebuffer { + val main = mc.framebuffer + val framebuffer = Framebuffer(main.framebufferTextureWidth, main.framebufferTextureHeight, true) + framebuffer.setFramebufferFilter(GL11.GL_NEAREST) + framebuffer.setFramebufferColor(0.0f, 0.0f, 0.0f, 0.0f) + return framebuffer + } + + private fun updateFramebufferSize() { + val width = mc.displayWidth + val height = mc.displayHeight + if (swapBuffer.framebufferWidth != width || swapBuffer.framebufferHeight != height) { + swapBuffer.createBindFramebuffer(width, height) + } + val rg = mc.renderGlobal as CustomRenderGlobal + val outlineBuffer = rg.entityOutlineFramebuffer_skyhanni + if (outlineBuffer.framebufferWidth != width || outlineBuffer.framebufferHeight != height) { + outlineBuffer.createBindFramebuffer(width, height) + rg.entityOutlineShader_skyhanni.createBindFramebuffers(width, height) + } + } + + /** + * Renders xray and no-xray entity outlines. + * + * @param camera the current camera + * @param partialTicks the progress to the next tick + * @param x the camera x position + * @param y the camera y position + * @param z the camera z position + */ + @JvmStatic + fun renderEntityOutlines(camera: ICamera, partialTicks: Float, vector: LorenzVec): Boolean { + val shouldRenderOutlines = shouldRenderEntityOutlines() + + if (!(shouldRenderOutlines && !isCacheEmpty() && MinecraftForgeClient.getRenderPass() == 0)) { + return !shouldRenderOutlines + } + + val renderGlobal = mc.renderGlobal as CustomRenderGlobal + val renderManager = mc.renderManager + mc.theWorld.theProfiler.endStartSection("entityOutlines") + updateFramebufferSize() + + // Clear and bind the outline framebuffer + renderGlobal.entityOutlineFramebuffer_skyhanni.framebufferClear() + renderGlobal.entityOutlineFramebuffer_skyhanni.bindFramebuffer(false) + + // Vanilla options + RenderHelper.disableStandardItemLighting() + GlStateManager.disableFog() + mc.renderManager.setRenderOutlines(true) + + // Enable outline mode + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL13.GL_COMBINE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_REPLACE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL13.GL_CONSTANT); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_REPLACE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA); + + // Render x-ray outlines first, ignoring the depth buffer bit + if (!isXrayCacheEmpty()) { + // Xray is enabled by disabling depth testing + GlStateManager.depthFunc(GL11.GL_ALWAYS) + + entityRenderCache.xrayCache?.forEach { (key, value) -> + // Test if the entity should render, given the player's camera position + if (!shouldRender(camera, key, vector)) return@forEach + + try { + if (key !is EntityLivingBase) outlineColor(value) + renderManager.renderEntityStatic(key, partialTicks, true) + } catch (ignored: Exception) { + } + } + + // Reset depth function + GlStateManager.depthFunc(GL11.GL_LEQUAL) + } + + // Render no-xray outlines second, taking into consideration the depth bit + if (!isNoXrayCacheEmpty()) { + if (!isNoOutlineCacheEmpty()) { + // Render other entities + terrain that may occlude an entity outline into a depth buffer + swapBuffer.framebufferClear() + copyBuffers(mc.framebuffer, swapBuffer, GL11.GL_DEPTH_BUFFER_BIT) + swapBuffer.bindFramebuffer(false) + + // Copy terrain + other entities depth into outline frame buffer to now switch to no-xray outlines + entityRenderCache.noOutlineCache?.forEach { entity -> + // Test if the entity should render, given the player's instantaneous camera position + if (!shouldRender(camera, entity, vector)) return@forEach + + try { + renderManager.renderEntityStatic(entity, partialTicks, true) + } catch (ignored: Exception) { + } + } + + // Copy the entire depth buffer of everything that might occlude outline to outline framebuffer + copyBuffers(swapBuffer, renderGlobal.entityOutlineFramebuffer_skyhanni, GL11.GL_DEPTH_BUFFER_BIT) + renderGlobal.entityOutlineFramebuffer_skyhanni.bindFramebuffer(false) + } else { + copyBuffers(mc.framebuffer, renderGlobal.entityOutlineFramebuffer_skyhanni, GL11.GL_DEPTH_BUFFER_BIT) + } + + // Xray disabled by re-enabling traditional depth testing + entityRenderCache.noXrayCache?.forEach { (key, value) -> + // Test if the entity should render, given the player's instantaneous camera position + if (!shouldRender(camera, key, vector)) return@forEach + + try { + if (key !is EntityLivingBase) outlineColor(value) + renderManager.renderEntityStatic(key, partialTicks, true) + } catch (ignored: Exception) { + } + } + } + + // Disable outline mode + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_RGB, GL11.GL_MODULATE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_RGB, GL11.GL_TEXTURE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_RGB, GL11.GL_SRC_COLOR); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_COMBINE_ALPHA, GL11.GL_MODULATE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_SOURCE0_ALPHA, GL11.GL_TEXTURE); + GL11.glTexEnvi(GL11.GL_TEXTURE_ENV, GL13.GL_OPERAND0_ALPHA, GL11.GL_SRC_ALPHA); + + // Vanilla options + RenderHelper.enableStandardItemLighting() + mc.renderManager.setRenderOutlines(false) + + // Load the outline shader + GlStateManager.depthMask(false) + renderGlobal.entityOutlineShader_skyhanni.loadShaderGroup(partialTicks) + GlStateManager.depthMask(true) + + // Reset GL/framebuffers for next render layers + GlStateManager.enableLighting() + mc.framebuffer.bindFramebuffer(false) + GlStateManager.enableFog() + GlStateManager.enableBlend() + GlStateManager.enableColorMaterial() + GlStateManager.enableDepth() + GlStateManager.enableAlpha() + + return !shouldRenderOutlines + } + + @JvmStatic + fun getCustomOutlineColor(entity: Entity?): Int? { + if (entityRenderCache.xrayCache?.containsKey(entity) == true) { + return entityRenderCache.xrayCache!![entity] + } + return if (entityRenderCache.noXrayCache?.containsKey(entity) == true) { + entityRenderCache.noXrayCache!![entity] + } else null + } + + /** + * Caches optifine settings and determines whether outlines should be rendered + * + * @return `true` iff outlines should be rendered + */ + @JvmStatic + fun shouldRenderEntityOutlines(): Boolean { + val renderGlobal = mc.renderGlobal as CustomRenderGlobal + + // Vanilla Conditions + if (renderGlobal.entityOutlineFramebuffer_skyhanni == null || renderGlobal.entityOutlineShader_skyhanni == null || mc.thePlayer == null) return false + + // Skyblock Conditions + if (!LorenzUtils.inSkyBlock) { + return false + } + + // Main toggle for outlines features + if (!config.enableEntityOutlines) { + return false + } + + // Optifine Conditions + if (!stopLookingForOptifine && isFastRender == null) { + try { + val config = Class.forName("Config") + try { + isFastRender = config.getMethod("isFastRender") + isShaders = config.getMethod("isShaders") + isAntialiasing = config.getMethod("isAntialiasing") + } catch (ex: Exception) { + logger.log("Couldn't find Optifine methods for entity outlines.") + stopLookingForOptifine = true + } + } catch (ex: Exception) { + logger.log("Couldn't find Optifine for entity outlines.") + stopLookingForOptifine = true + } + } + var isFastRenderValue = false + var isShadersValue = false + var isAntialiasingValue = false + if (isFastRender != null) { + try { + isFastRenderValue = isFastRender!!.invoke(null) as Boolean + isShadersValue = isShaders!!.invoke(null) as Boolean + isAntialiasingValue = isAntialiasing!!.invoke(null) as Boolean + } catch (ex: IllegalAccessException) { + logger.log("An error occurred while calling Optifine methods for entity outlines... $ex") + } catch (ex: InvocationTargetException) { + logger.log("An error occurred while calling Optifine methods for entity outlines... $ex") + } + } + return !isFastRenderValue && !isShadersValue && !isAntialiasingValue + } + + /** + * Apply the same rendering standards as in [net.minecraft.client.renderer.RenderGlobal.renderEntities] lines 659 to 669 + * + * @param camera the current camera + * @param entity the entity to render + * @param x the camera x position + * @param y the camera y position + * @param z the camera z position + * @return whether the entity should be rendered + */ + private fun shouldRender(camera: ICamera, entity: Entity, vector: LorenzVec): Boolean = + // Only render the view entity when sleeping or in 3rd person mode + if (entity === mc.renderViewEntity && + !(mc.renderViewEntity is EntityLivingBase && (mc.renderViewEntity as EntityLivingBase).isPlayerSleeping || + mc.gameSettings.thirdPersonView != 0)) { + false + } else mc.theWorld.isBlockLoaded(BlockPos(entity)) && (mc.renderManager.shouldRender(entity, camera, vector.x, vector.y, vector.z) || entity.riddenByEntity === mc.thePlayer) + // Only render if renderManager would render and the world is loaded at the entity + + private fun outlineColor(color: Int) { + BUF_FLOAT_4.put(0, (color shr 16 and 255).toFloat() / 255.0f) + BUF_FLOAT_4.put(1, (color shr 8 and 255).toFloat() / 255.0f) + BUF_FLOAT_4.put(2, (color and 255).toFloat() / 255.0f) + BUF_FLOAT_4.put(3, 1f) + GL11.glTexEnv(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_COLOR, BUF_FLOAT_4) + } + + /** + * Function that copies a portion of a framebuffer to another framebuffer. + * + * + * Note that this requires GL3.0 to function properly + * + * + * The major use of this function is to copy the depth-buffer portion of the world framebuffer to the entity outline framebuffer. + * This enables us to perform no-xray outlining on entities, as we can use the world framebuffer's depth testing on the outline frame buffer + * + * @param frameToCopy the framebuffer from which we are copying data + * @param frameToPaste the framebuffer onto which we are copying the data + * @param buffersToCopy the bit mask indicating the sections to copy (see [GL11.GL_DEPTH_BUFFER_BIT], [GL11.GL_COLOR_BUFFER_BIT], [GL11.GL_STENCIL_BUFFER_BIT]) + */ + private fun copyBuffers(frameToCopy: Framebuffer?, frameToPaste: Framebuffer?, buffersToCopy: Int) { + if (OpenGlHelper.isFramebufferEnabled()) { + OpenGlHelper.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, frameToCopy!!.framebufferObject) + OpenGlHelper.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, frameToPaste!!.framebufferObject) + GL30.glBlitFramebuffer(0, 0, frameToCopy.framebufferWidth, frameToCopy.framebufferHeight, + 0, 0, frameToPaste.framebufferWidth, frameToPaste.framebufferHeight, + buffersToCopy, GL11.GL_NEAREST) + } + } + + fun isCacheEmpty() = isXrayCacheEmpty() && isNoXrayCacheEmpty() + + private fun isXrayCacheEmpty() = entityRenderCache.xrayCache?.isEmpty() ?: true + private fun isNoXrayCacheEmpty() = entityRenderCache.noXrayCache?.isEmpty() ?: true + private fun isNoOutlineCacheEmpty() = entityRenderCache.noOutlineCache?.isEmpty() ?: true + + /** + * Updates the cache at the start of every minecraft tick to improve efficiency. + * Identifies and caches all entities in the world that should be outlined. + * + * + * Calls to [.shouldRender] are frustum based, rely on partialTicks, + * and so can't be updated on a per-tick basis without losing information. + * + * + * This works since entities are only updated once per tick, so the inclusion or exclusion of an entity + * to be outlined can be cached each tick with no loss of data + * + * @param event the client tick event + */ + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (event.phase == EventPriority.NORMAL) { + val rg = mc.renderGlobal as CustomRenderGlobal + + if (mc.theWorld != null && shouldRenderEntityOutlines()) { + // These events need to be called in this specific order for the xray to have priority over the no xray + // Get all entities to render xray outlines + val xrayOutlineEvent = RenderEntityOutlineEvent(RenderEntityOutlineEvent.Type.XRAY, null) + MinecraftForge.EVENT_BUS.post(xrayOutlineEvent) + // Get all entities to render no xray outlines, using pre-filtered entities (no need to test xray outlined entities) + val noxrayOutlineEvent = RenderEntityOutlineEvent(RenderEntityOutlineEvent.Type.NO_XRAY, xrayOutlineEvent.entitiesToChooseFrom) + MinecraftForge.EVENT_BUS.post(noxrayOutlineEvent) + // Cache the entities for future use + entityRenderCache.xrayCache = xrayOutlineEvent.entitiesToOutline + entityRenderCache.noXrayCache = noxrayOutlineEvent.entitiesToOutline + entityRenderCache.noOutlineCache = noxrayOutlineEvent.entitiesToChooseFrom + emptyLastTick = if (isCacheEmpty()) { + if (!emptyLastTick) { + rg.entityOutlineFramebuffer_skyhanni.framebufferClear() + } + true + } else { + false + } + } else if (!emptyLastTick) { + entityRenderCache.xrayCache = null + entityRenderCache.noXrayCache = null + entityRenderCache.noOutlineCache = null + if (rg.entityOutlineFramebuffer_skyhanni != null) rg.entityOutlineFramebuffer_skyhanni.framebufferClear() + emptyLastTick = true + } + } + } + + private class CachedInfo(var xrayCache: HashMap?, var noXrayCache: HashMap?, var noOutlineCache: HashSet?) +} \ No newline at end of file -- cgit