aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at
diff options
context:
space:
mode:
authorBrandon <brandon.wamboldt@gmail.com>2023-09-07 08:03:26 -0300
committerGitHub <noreply@github.com>2023-09-07 13:03:26 +0200
commit6be7873d9475686fdc404212cebd45f22edbff87 (patch)
tree2de35d9806c73e1ca361cbac704f84c09e844540 /src/main/java/at
parent5d2f4ea1989b1a3077a26fa7f9fd2935daf9d1bb (diff)
downloadskyhanni-6be7873d9475686fdc404212cebd45f22edbff87.tar.gz
skyhanni-6be7873d9475686fdc404212cebd45f22edbff87.tar.bz2
skyhanni-6be7873d9475686fdc404212cebd45f22edbff87.zip
Add highlight and outline feature for rare sea creatures (#439)
Add highlight and outline feature for rare sea creatures #439
Diffstat (limited to 'src/main/java/at')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/FishingConfig.java6
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/MiscConfig.java6
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/RenderEntityOutlineEvent.kt118
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/fishing/SeaCreatureFeatures.kt100
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/hooks/RenderGlobalHook.kt20
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/hooks/RendererLivingEntityHook.kt20
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/transformers/CustomRenderGlobal.java17
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRenderGlobal.java42
-rw-r--r--src/main/java/at/hannibal2/skyhanni/mixins/transformers/MixinRendererLivingEntity.java23
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/EntityOutlineRenderer.kt376
11 files changed, 731 insertions, 0 deletions
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
@@ -192,6 +192,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",
desc = "Counts how many sharks have been caught."
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
@@ -1036,6 +1036,12 @@ public class MiscConfig {
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<Entity>?) :
+ 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<Entity, Int>? = null
+
+ /**
+ * The entities we can outline. Note that this set and [.entitiesToOutline] are disjoint at all times.
+ */
+ var entitiesToChooseFrom: HashSet<Entity>? = 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<Entity> = 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<Entity> = 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<Entity> { 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<EntityLivingBase>()
+
+ @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<out EntityLivingBase>, 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<Boolean?>) {
+ 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<Boolean> 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<Entity, Int>?, var noXrayCache: HashMap<Entity, Int>?, var noOutlineCache: HashSet<Entity>?)
+} \ No newline at end of file