diff options
author | Thunderblade73 <85900443+Thunderblade73@users.noreply.github.com> | 2024-04-03 20:50:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-03 20:50:31 +0200 |
commit | 2f85351bacddb9ab3704a53c778d558a755bcc06 (patch) | |
tree | f479045f271f04a66a1ba69a61cb1b9893261eb5 | |
parent | 76be6ad6de39c7078550394e8ec24a494ddb3bcc (diff) | |
download | skyhanni-2f85351bacddb9ab3704a53c778d558a755bcc06.tar.gz skyhanni-2f85351bacddb9ab3704a53c778d558a755bcc06.tar.bz2 skyhanni-2f85351bacddb9ab3704a53c778d558a755bcc06.zip |
Backend: Mob Detection (#712)
Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com>
Co-authored-by: Cal <cwolfson58@gmail.com>
17 files changed, 1887 insertions, 4 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index e14589155..440c1922e 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -55,6 +55,9 @@ import at.hannibal2.skyhanni.data.TrackerManager import at.hannibal2.skyhanni.data.jsonobjects.local.FriendsJson import at.hannibal2.skyhanni.data.jsonobjects.local.JacobContestsJson import at.hannibal2.skyhanni.data.jsonobjects.local.KnownFeaturesJson +import at.hannibal2.skyhanni.data.mob.MobData +import at.hannibal2.skyhanni.data.mob.MobDebug +import at.hannibal2.skyhanni.data.mob.MobDetection import at.hannibal2.skyhanni.data.jsonobjects.local.VisualWordsJson import at.hannibal2.skyhanni.data.repo.RepoManager import at.hannibal2.skyhanni.events.LorenzTickEvent @@ -441,6 +444,8 @@ class SkyHanniMod { loadModule(SeaCreatureManager()) loadModule(ItemRenderBackground()) loadModule(EntityData()) + loadModule(MobData()) + loadModule(MobDetection()) loadModule(EntityMovementData()) loadModule(TestExportTools) loadModule(ItemClickData()) @@ -827,6 +832,7 @@ class SkyHanniMod { loadModule(SkyHanniDebugsAndTests) loadModule(WorldEdit) PreInitFinishedEvent().postAndCatch() + loadModule(MobDebug()) } @Mod.EventHandler diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugMobConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugMobConfig.java new file mode 100644 index 000000000..9093459a5 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugMobConfig.java @@ -0,0 +1,81 @@ +package at.hannibal2.skyhanni.config.features.dev; + +import com.google.gson.annotations.Expose; +import io.github.moulberry.moulconfig.annotations.Accordion; +import io.github.moulberry.moulconfig.annotations.ConfigEditorBoolean; +import io.github.moulberry.moulconfig.annotations.ConfigEditorDropdown; +import io.github.moulberry.moulconfig.annotations.ConfigOption; + +public class DebugMobConfig { + + @Expose + @ConfigOption(name = "Mob Detection Enable", desc = "Turn off and on again to reset all Mobs.") + @ConfigEditorBoolean + public boolean enable = true; + + @Expose + @ConfigOption(name = "Mob Detection", desc = "Debug feature to check the Mob Detection.") + @Accordion + public MobDetection mobDetection = new MobDetection(); + + public enum HowToShow { + OFF("Off"), + ONLY_NAME("Only Name"), + ONLY_HIGHLIGHT("Only Highlight"), + NAME_AND_HIGHLIGHT("Both"); + + final String str; + + HowToShow(String str) { + this.str = str; + } + + @Override + public String toString() { + return str; + } + } + + public static class MobDetection { + + @Expose + @ConfigOption(name = "Log Events", desc = "Logs the spawn and despawn event with full mob info.") + @ConfigEditorBoolean + public boolean logEvents = false; + + @Expose + @ConfigOption(name = "Show RayHit", desc = "Highlights the mob that is currently in front of your view (only SkyblockMob).") + @ConfigEditorBoolean + public boolean showRayHit = false; + + @Expose + @ConfigOption(name = "Player Highlight", desc = "Highlight each entity that is a real Player in blue. (You are also include in the list but won't be highlighted for obvious reason).") + @ConfigEditorBoolean + public boolean realPlayerHighlight = false; + + @Expose + @ConfigOption(name = "DisplayNPC", desc = "Shows the internal mobs that are 'DisplayNPC' as highlight (in red) or the name.") + @ConfigEditorDropdown + public HowToShow displayNPC = HowToShow.OFF; + + @Expose + @ConfigOption(name = "SkyblockMob", desc = "Shows the internal mobs that are 'SkyblockMob' as highlight (in green) or the name.") + @ConfigEditorDropdown + public HowToShow skyblockMob = HowToShow.OFF; + + @Expose + @ConfigOption(name = "Summon", desc = "Shows the internal mobs that are 'Summon' as highlight (in yellow) or the name.") + @ConfigEditorDropdown + public HowToShow summon = HowToShow.OFF; + + @Expose + @ConfigOption(name = "Special", desc = "Shows the internal mobs that are 'Special' as highlight (in aqua) or the name.") + @ConfigEditorDropdown + public HowToShow special = HowToShow.OFF; + + @Expose + @ConfigOption(name = "Show Invisible", desc = "Shows the mob even though they are invisible (do to invisibility effect) if looked at directly.") + @ConfigEditorBoolean + public boolean showInvisible = false; + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java index 29d9e1779..f4cc9943e 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java @@ -70,4 +70,8 @@ public class DevConfig { @Category(name = "Minecraft Console", desc = "Minecraft Console Settings") public MinecraftConsoleConfig minecraftConsoles = new MinecraftConsoleConfig(); + @Expose + @Category(name = "Debug Mob", desc = "Every Debug related to the Mob System") + public DebugMobConfig mobDebug = new DebugMobConfig(); + } diff --git a/src/main/java/at/hannibal2/skyhanni/data/ClickType.kt b/src/main/java/at/hannibal2/skyhanni/data/ClickType.kt index 06c37d039..b008978f0 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/ClickType.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/ClickType.kt @@ -2,4 +2,4 @@ package at.hannibal2.skyhanni.data enum class ClickType { LEFT_CLICK, RIGHT_CLICK -}
\ No newline at end of file +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/Mob.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/Mob.kt new file mode 100644 index 000000000..4eab5d881 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/Mob.kt @@ -0,0 +1,177 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.data.mob.Mob.Type +import at.hannibal2.skyhanni.data.mob.MobFilter.summonOwnerPattern +import at.hannibal2.skyhanni.events.MobEvent +import at.hannibal2.skyhanni.utils.CollectionUtils.toSingletonListOrEmpty +import at.hannibal2.skyhanni.utils.EntityUtils.canBeSeen +import at.hannibal2.skyhanni.utils.EntityUtils.cleanName +import at.hannibal2.skyhanni.utils.EntityUtils.isCorrupted +import at.hannibal2.skyhanni.utils.EntityUtils.isRunic +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer +import at.hannibal2.skyhanni.utils.LocationUtils.union +import at.hannibal2.skyhanni.utils.MobUtils +import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.monster.EntityZombie +import net.minecraft.util.AxisAlignedBB + +/** + * Represents a Mob in Hypixel Skyblock. + * + * @property baseEntity The main entity representing the Mob. + * + * Avoid caching, as it may change without notice. + * @property mobType The type of the Mob. + * @property armorStand The armor stand entity associated with the Mob, if it has one. + * + * Avoid caching, as it may change without notice. + * @property name The name of the Mob. + * @property extraEntities Additional entities associated with the Mob. + * + * Avoid caching, as they may change without notice. + * @property owner Valid for: [Type.SUMMON], [Type.SLAYER] + * + * The owner of the Mob. + * @property hasStar Valid for: [Type.DUNGEON] + * + * Indicates whether the Mob has a star. + * @property attribute Valid for: [Type.DUNGEON] + * + * The attribute of the Mob. + * @property levelOrTier Valid for: [Type.BASIC], [Type.SLAYER] + * + * The level or tier of the Mob. + * @property hologram1 Valid for: [Type.BASIC], [Type.SLAYER] + * + * Gives back the first additional armor stand. + * + * (should be called in the [MobEvent.Spawn] since it is a lazy) + * @property hologram2 Valid for: [Type.BASIC], [Type.SLAYER] + * + * Gives back the second additional armor stand. + * + * (should be called in the [MobEvent.Spawn] since it is a lazy) + */ +class Mob( + var baseEntity: EntityLivingBase, + val mobType: Type, + var armorStand: EntityArmorStand? = null, + val name: String = "", + additionalEntities: List<EntityLivingBase>? = null, + ownerName: String? = null, + val hasStar: Boolean = false, + val attribute: MobFilter.DungeonAttribute? = null, + val levelOrTier: Int = -1, +) { + + val owner: MobUtils.OwnerShip? + + val hologram1Delegate = lazy { MobUtils.getArmorStand(armorStand ?: baseEntity, 1) } + val hologram2Delegate = lazy { MobUtils.getArmorStand(armorStand ?: baseEntity, 2) } + + val hologram1 by hologram1Delegate + val hologram2 by hologram2Delegate + + private val extraEntitiesList = additionalEntities?.toMutableList() ?: mutableListOf() + private var relativeBoundingBox: AxisAlignedBB? + + val extraEntities: List<EntityLivingBase> = extraEntitiesList + + enum class Type { + DISPLAY_NPC, SUMMON, BASIC, DUNGEON, BOSS, SLAYER, PLAYER, PROJECTILE, SPECIAL; + + fun isSkyblockMob() = when (this) { + BASIC, DUNGEON, BOSS, SLAYER -> true + else -> false + } + } + + val isCorrupted get() = baseEntity.isCorrupted() // Can change + val isRunic = baseEntity.isRunic() // Does not Change + + fun isInRender() = baseEntity.distanceToPlayer() < MobData.ENTITY_RENDER_RANGE_IN_BLOCKS + + fun canBeSeen() = baseEntity.canBeSeen() + + fun isInvisible() = if (baseEntity !is EntityZombie) baseEntity.isInvisible else false + + val boundingBox: AxisAlignedBB + get() = relativeBoundingBox?.offset(baseEntity.posX, baseEntity.posY, baseEntity.posZ) + ?: baseEntity.entityBoundingBox + + init { + removeExtraEntitiesFromChecking() + relativeBoundingBox = + if (extraEntities.isNotEmpty()) makeRelativeBoundingBox() else null // Inlined updateBoundingBox() + + owner = (ownerName ?: if (mobType == Type.SLAYER) hologram2?.let { + summonOwnerPattern.matchMatcher(it.cleanName()) { this.group("name") } + } else null)?.let { MobUtils.OwnerShip(it) } + } + + private fun removeExtraEntitiesFromChecking() = + extraEntities.count { MobData.retries[it.entityId] != null }.also { + MobData.externRemoveOfRetryAmount += it + } + + fun updateBoundingBox() { + relativeBoundingBox = if (extraEntities.isNotEmpty()) makeRelativeBoundingBox() else null + } + + private fun makeRelativeBoundingBox() = + (baseEntity.entityBoundingBox.union(extraEntities.filter { it !is EntityArmorStand } + .mapNotNull { it.entityBoundingBox }))?.offset(-baseEntity.posX, -baseEntity.posY, -baseEntity.posZ) + + fun fullEntityList() = + baseEntity.toSingletonListOrEmpty() + + armorStand.toSingletonListOrEmpty() + + extraEntities + + fun makeEntityToMobAssociation() = + fullEntityList().associateWith { this } + + internal fun internalAddEntity(entity: EntityLivingBase) { + if (baseEntity.entityId > entity.entityId) { + extraEntitiesList.add(0, baseEntity) + baseEntity = entity + } else { + extraEntitiesList.add(extraEntitiesList.lastIndex + 1, entity) + } + updateBoundingBox() + MobData.entityToMob[entity] = this + } + + internal fun internalAddEntity(entities: Collection<EntityLivingBase>) { + val list = entities.drop(1).toMutableList().apply { add(baseEntity) } + extraEntitiesList.addAll(0, list) + baseEntity = entities.first() + updateBoundingBox() + removeExtraEntitiesFromChecking() + MobData.entityToMob.putAll(entities.associateWith { this }) + } + + internal fun internalUpdateOfEntity(entity: EntityLivingBase) = when (entity.entityId) { + baseEntity.entityId -> baseEntity = entity + armorStand?.entityId ?: Int.MIN_VALUE -> armorStand = entity as EntityArmorStand + else -> { + extraEntitiesList.remove(entity) + extraEntitiesList.add(entity) + Unit // To make return type of this branch Unit + } + } + + override fun hashCode() = baseEntity.hashCode() + + override fun toString(): String = "$name - ${baseEntity.entityId}" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Mob + + return baseEntity == other.baseEntity + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/MobData.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/MobData.kt new file mode 100644 index 000000000..b8da58d41 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/MobData.kt @@ -0,0 +1,140 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.events.MobEvent +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.getLorenzVec +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.TreeMap +import at.hannibal2.skyhanni.data.mob.Mob.Type as MobType + +class MobData { + + class MobSet : HashSet<Mob>() { + val entityList get() = this.flatMap { listOf(it.baseEntity) + (it.extraEntities) } + } + + companion object { + + val players = MobSet() + val displayNPCs = MobSet() + val skyblockMobs = MobSet() + val summoningMobs = MobSet() + val special = MobSet() + val currentMobs = MobSet() + + val entityToMob = mutableMapOf<EntityLivingBase, Mob>() + + internal val currentEntityLiving = mutableSetOf<EntityLivingBase>() + internal val previousEntityLiving = mutableSetOf<EntityLivingBase>() + + internal val retries = TreeMap<Int, RetryEntityInstancing>() + + const val ENTITY_RENDER_RANGE_IN_BLOCKS = 80.0 // Entity DeRender after ~5 Chunks + const val DETECTION_RANGE = 22.0 + const val DISPLAY_NPC_DETECTION_RANGE = 24.0 // 24.0 + + var externRemoveOfRetryAmount = 0 + } + + internal enum class Result { + Found, NotYetFound, Illegal, SomethingWentWrong + } + + internal class MobResult(val result: Result, val mob: Mob?) { + operator fun component1() = result + operator fun component2() = mob + + companion object { + val illegal = MobResult(Result.Illegal, null) + val notYetFound = MobResult(Result.NotYetFound, null) + val somethingWentWrong = MobResult(Result.SomethingWentWrong, null) + fun found(mob: Mob) = MobResult(Result.Found, mob) + + fun EntityArmorStand?.makeMobResult(mob: (EntityArmorStand) -> Mob?) = + this?.let { armor -> + mob.invoke(armor)?.let { found(it) } ?: somethingWentWrong + } ?: notYetFound + } + } + + internal class RetryEntityInstancing( + var entity: EntityLivingBase, + var times: Int, + val roughType: MobType + ) { + override fun hashCode() = entity.entityId + override fun equals(other: Any?) = (other as? RetryEntityInstancing).hashCode() == this.hashCode() + fun toKeyValuePair() = entity.entityId to this + + fun outsideRange() = + entity.getLorenzVec().distanceChebyshevIgnoreY(LocationUtils.playerLocation()) > when (roughType) { + MobType.DISPLAY_NPC -> DISPLAY_NPC_DETECTION_RANGE + MobType.PLAYER -> Double.POSITIVE_INFINITY + else -> DETECTION_RANGE + } + } + + @SubscribeEvent + fun onMobEventSpawn(event: MobEvent.Spawn) { + entityToMob.putAll(event.mob.makeEntityToMobAssociation()) + currentMobs.add(event.mob) + } + + @SubscribeEvent + fun onMobEventDeSpawn(event: MobEvent.DeSpawn) { + event.mob.fullEntityList().forEach { entityToMob.remove(it) } + currentMobs.remove(event.mob) + } + + @SubscribeEvent + fun onSkyblockMobSpawnEvent(event: MobEvent.Spawn.SkyblockMob) { + skyblockMobs.add(event.mob) + } + + @SubscribeEvent + fun onSkyblockMobDeSpawnEvent(event: MobEvent.DeSpawn.SkyblockMob) { + skyblockMobs.remove(event.mob) + } + + @SubscribeEvent + fun onSummonSpawnEvent(event: MobEvent.Spawn.Summon) { + summoningMobs.add(event.mob) + } + + @SubscribeEvent + fun onSummonDeSpawnEvent(event: MobEvent.DeSpawn.Summon) { + summoningMobs.remove(event.mob) + } + + @SubscribeEvent + fun onSpecialSpawnEvent(event: MobEvent.Spawn.Special) { + special.add(event.mob) + } + + @SubscribeEvent + fun onSpecialDeSpawnEvent(event: MobEvent.DeSpawn.Special) { + special.remove(event.mob) + } + + @SubscribeEvent + fun onDisplayNPCSpawnEvent(event: MobEvent.Spawn.DisplayNPC) { + displayNPCs.add(event.mob) + } + + @SubscribeEvent + fun onDisplayNPCDeSpawnEvent(event: MobEvent.DeSpawn.DisplayNPC) { + displayNPCs.remove(event.mob) + } + + @SubscribeEvent + fun onRealPlayerSpawnEvent(event: MobEvent.Spawn.Player) { + players.add(event.mob) + } + + @SubscribeEvent + fun onRealPlayerDeSpawnEvent(event: MobEvent.DeSpawn.Player) { + players.remove(event.mob) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/MobDebug.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/MobDebug.kt new file mode 100644 index 000000000..63a39d502 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/MobDebug.kt @@ -0,0 +1,97 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.features.dev.DebugMobConfig.HowToShow +import at.hannibal2.skyhanni.events.LorenzRenderWorldEvent +import at.hannibal2.skyhanni.events.MobEvent +import at.hannibal2.skyhanni.test.command.CopyNearbyEntitiesCommand.getMobInfo +import at.hannibal2.skyhanni.utils.LocationUtils.getTopCenter +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzDebug +import at.hannibal2.skyhanni.utils.MobUtils +import at.hannibal2.skyhanni.utils.RenderUtils.drawFilledBoundingBox_nea +import at.hannibal2.skyhanni.utils.RenderUtils.drawString +import at.hannibal2.skyhanni.utils.RenderUtils.expandBlock +import net.minecraft.client.Minecraft +import net.minecraft.client.entity.EntityPlayerSP +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +class MobDebug { + + private val config get() = SkyHanniMod.feature.dev.mobDebug.mobDetection + + private var lastRayHit: Mob? = null + + private fun HowToShow.isHighlight() = + this == HowToShow.ONLY_HIGHLIGHT || this == HowToShow.NAME_AND_HIGHLIGHT + + private fun HowToShow.isName() = + this == HowToShow.ONLY_NAME || this == HowToShow.NAME_AND_HIGHLIGHT + + private fun Mob.isNotInvisible() = !this.isInvisible() || (config.showInvisible && this == lastRayHit) + + private fun MobData.MobSet.highlight(event: LorenzRenderWorldEvent, color: (Mob) -> (LorenzColor)) = + this.filter { it.isNotInvisible() }.forEach { + event.drawFilledBoundingBox_nea(it.boundingBox.expandBlock(), color.invoke(it).toColor(), 0.3f) + } + + private fun MobData.MobSet.showName(event: LorenzRenderWorldEvent) = + this.filter { it.canBeSeen() && it.isNotInvisible() }.map { it.boundingBox.getTopCenter() to it.name }.forEach { + event.drawString( + it.first.add(y = 0.5), "§5" + it.second, seeThroughBlocks = true + ) + } + + @SubscribeEvent + fun onWorldRenderDebug(event: LorenzRenderWorldEvent) { + if (config.showRayHit || config.showInvisible) { + lastRayHit = MobUtils.rayTraceForMobs(Minecraft.getMinecraft().thePlayer, event.partialTicks) + ?.firstOrNull { it.canBeSeen() && (config.showInvisible || it.isInvisible()) } + } + + if (config.skyblockMob.isHighlight()) { + MobData.skyblockMobs.highlight(event) { if (it.mobType == Mob.Type.BOSS) LorenzColor.DARK_GREEN else LorenzColor.GREEN } + } + if (config.displayNPC.isHighlight()) { + MobData.displayNPCs.highlight(event) { LorenzColor.RED } + } + if (config.realPlayerHighlight) { + MobData.players.highlight(event) { if (it.baseEntity is EntityPlayerSP) LorenzColor.CHROMA else LorenzColor.BLUE } + } + if (config.summon.isHighlight()) { + MobData.summoningMobs.highlight(event) { LorenzColor.YELLOW } + } + if (config.special.isHighlight()) { + MobData.special.highlight(event) { LorenzColor.AQUA } + } + if (config.skyblockMob.isName()) { + MobData.skyblockMobs.showName(event) + } + if (config.displayNPC.isName()) { + MobData.displayNPCs.showName(event) + } + if (config.summon.isName()) { + MobData.summoningMobs.showName(event) + } + if (config.special.isName()) { + MobData.special.showName(event) + } + if (config.showRayHit) { + lastRayHit?.let { + event.drawFilledBoundingBox_nea(it.boundingBox.expandBlock(), LorenzColor.GOLD.toColor(), 0.5f) + } + } + } + + @SubscribeEvent + fun onMobEvent(event: MobEvent) { + if (!config.logEvents) return + LorenzDebug.log( + "Mob ${if (event is MobEvent.Spawn) "Spawn" else "Despawn"}: ${ + getMobInfo(event.mob).joinToString( + ", " + ) + }" + ) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/MobDetection.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/MobDetection.kt new file mode 100644 index 000000000..c674c0593 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/MobDetection.kt @@ -0,0 +1,361 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.data.mob.MobFilter.isDisplayNPC +import at.hannibal2.skyhanni.data.mob.MobFilter.isRealPlayer +import at.hannibal2.skyhanni.data.mob.MobFilter.isSkyBlockMob +import at.hannibal2.skyhanni.events.DebugDataCollectEvent +import at.hannibal2.skyhanni.events.EntityHealthUpdateEvent +import at.hannibal2.skyhanni.events.IslandChangeEvent +import at.hannibal2.skyhanni.events.LorenzTickEvent +import at.hannibal2.skyhanni.events.MobEvent +import at.hannibal2.skyhanni.events.PacketEvent +import at.hannibal2.skyhanni.utils.CollectionUtils.drainForEach +import at.hannibal2.skyhanni.utils.CollectionUtils.drainTo +import at.hannibal2.skyhanni.utils.CollectionUtils.put +import at.hannibal2.skyhanni.utils.CollectionUtils.refreshReference +import at.hannibal2.skyhanni.utils.EntityUtils +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LorenzLogger +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.getLorenzVec +import net.minecraft.client.Minecraft +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.monster.EntityCreeper +import net.minecraft.entity.passive.EntityBat +import net.minecraft.entity.passive.EntityVillager +import net.minecraft.entity.player.EntityPlayer +import net.minecraft.network.play.server.S0CPacketSpawnPlayer +import net.minecraft.network.play.server.S0FPacketSpawnMob +import net.minecraft.network.play.server.S37PacketStatistics +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import net.minecraftforge.fml.common.network.FMLNetworkEvent +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicBoolean + +private const val MAX_RETRIES = 20 * 5 + +private const val MOB_DETECTION_LOG_PREFIX = "MobDetection: " + +class MobDetection { + + /* Unsupported "Mobs" + Nicked Players + Odanate + Silk Worm + Fairy (in Dungeon) + Totem of Corruption + Worm + Scatha + Butterfly + Exe + Wai + Zee + */ + + private val forceReset get() = !SkyHanniMod.feature.dev.mobDebug.enable + + private var shouldClear: AtomicBoolean = AtomicBoolean(false) + + private val logger = LorenzLogger("mob/detection") + + init { + MobFilter.bossMobNameFilter + MobFilter.mobNameFilter + MobFilter.dojoFilter + MobFilter.summonFilter + MobFilter.dungeonNameFilter + MobFilter.petCareNamePattern + MobFilter.slayerNameFilter + MobFilter.summonOwnerPattern + MobFilter.wokeSleepingGolemPattern + MobFilter.jerryPattern + MobFilter.jerryMagmaCubePattern + } + + private fun mobDetectionReset() { + MobData.currentMobs.map { + it.createDeSpawnEvent() + }.forEach { it.postAndCatch() } + } + + @SubscribeEvent + fun onTick(event: LorenzTickEvent) { + if (shouldClear.get()) { // Needs to work outside skyblock since it needs clearing when leaving skyblock and joining limbo + mobDetectionReset() + shouldClear.set(false) + } + if (!LorenzUtils.inSkyBlock) return + if (event.isMod(2)) return + + makeEntityReferenceUpdate() + + handleMobsFromPacket() + + handleRetries() + + MobData.previousEntityLiving.clear() + MobData.previousEntityLiving.addAll(MobData.currentEntityLiving) + MobData.currentEntityLiving.clear() + MobData.currentEntityLiving.addAll(EntityUtils.getEntities<EntityLivingBase>() + .filter { it !is EntityArmorStand }) + + if (forceReset) { + MobData.currentEntityLiving.clear() // Naturally removing the mobs using the despawn + } + + (MobData.currentEntityLiving - MobData.previousEntityLiving).forEach { addRetry(it) } // Spawn + (MobData.previousEntityLiving - MobData.currentEntityLiving).forEach { entityDeSpawn(it) } // Despawn + + if (forceReset) { + mobDetectionReset() // Ensure that all mobs are cleared 100% + } + } + + /** Splits the entity into player, displayNPC and other */ + private fun EntityLivingBase.getRoughType() = when { + this is EntityPlayer && this.isRealPlayer() -> Mob.Type.PLAYER + this.isDisplayNPC() -> Mob.Type.DISPLAY_NPC + this.isSkyBlockMob() && !islandException() -> Mob.Type.BASIC + else -> null + } + + private fun addRetry(entity: EntityLivingBase) = entity.getRoughType()?.let { type -> + val re = MobData.RetryEntityInstancing(entity, 0, type) + MobData.retries.put(re.toKeyValuePair()) + } + + private fun removeRetry(entity: EntityLivingBase) = MobData.retries.remove(entity.entityId) + + private fun getRetry(entity: EntityLivingBase) = MobData.retries[entity.entityId] + + /** @return always true */ + private fun mobDetectionError(string: String) = logger.log(string).let { true } + + /**@return a false means that it should try again (later)*/ + private fun entitySpawn(entity: EntityLivingBase, roughType: Mob.Type): Boolean { + when (roughType) { + Mob.Type.PLAYER -> MobEvent.Spawn.Player(MobFactories.player(entity)).postAndCatch() + + Mob.Type.DISPLAY_NPC -> return MobFilter.createDisplayNPC(entity) + Mob.Type.BASIC -> { + val (result, mob) = MobFilter.createSkyblockEntity(entity) + when (result) { + MobData.Result.NotYetFound -> return false + MobData.Result.Illegal -> return true // Remove entity from the spawning queue + MobData.Result.SomethingWentWrong -> return mobDetectionError("Something Went Wrong!") + MobData.Result.Found -> { + if (mob == null) return mobDetectionError("Mob is null even though result is Found") + when (mob.mobType) { + Mob.Type.SUMMON -> MobEvent.Spawn.Summon(mob) + + Mob.Type.BASIC, Mob.Type.DUNGEON, Mob.Type.BOSS, Mob.Type.SLAYER -> MobEvent.Spawn.SkyblockMob( + mob + ) + + Mob.Type.SPECIAL -> MobEvent.Spawn.Special(mob) + Mob.Type.PROJECTILE -> MobEvent.Spawn.Projectile(mob) + Mob.Type.DISPLAY_NPC -> MobEvent.Spawn.DisplayNPC(mob) // Needed for some special cases + Mob.Type.PLAYER -> return mobDetectionError("An Player Ended Here. How?") + }.postAndCatch() + } + } + } + + else -> return true + } + return true + } + + private val entityFromPacket = ConcurrentLinkedQueue<Pair<EntityPacketType, Int>>() + + /** For mobs that have default health of the entity */ + private enum class EntityPacketType { + SPIRIT_BAT, VILLAGER, CREEPER_VAIL + } + + /** Handles some mobs that have default health of the entity, specially using the [EntityHealthUpdateEvent] */ + private fun handleMobsFromPacket() = entityFromPacket.drainForEach { (type, id) -> + when (type) { + EntityPacketType.SPIRIT_BAT -> { + val entity = EntityUtils.getEntityByID(id) as? EntityBat ?: return@drainForEach + if (MobData.entityToMob[entity] != null) return@drainForEach + removeRetry(entity) + MobEvent.Spawn.Projectile(MobFactories.projectile(entity, "Spirit Scepter Bat")).postAndCatch() + } + + EntityPacketType.VILLAGER -> { + val entity = EntityUtils.getEntityByID(id) as? EntityVillager ?: return@drainForEach + val mob = MobData.entityToMob[entity] + if (mob != null && mob.mobType == Mob.Type.DISPLAY_NPC) { + MobEvent.DeSpawn.DisplayNPC(mob) + addRetry(entity) + return@drainForEach + } + getRetry(entity)?.let { + if (it.roughType == Mob.Type.DISPLAY_NPC) { + removeRetry(entity) + addRetry(entity) + } + } + } + + EntityPacketType.CREEPER_VAIL -> { + val entity = EntityUtils.getEntityByID(id) as? EntityCreeper ?: return@drainForEach + if (MobData.entityToMob[entity] != null) return@drainForEach + if (!entity.powered) return@drainForEach + removeRetry(entity) + MobEvent.Spawn.Special(MobFactories.special(entity, "Creeper Veil")).postAndCatch() + } + } + } + + @SubscribeEvent + fun onEntityHealthUpdateEvent(event: EntityHealthUpdateEvent) { + when { + event.entity is EntityBat && event.health == 6 -> { + entityFromPacket.add(EntityPacketType.SPIRIT_BAT to event.entity.entityId) + } + + event.entity is EntityVillager && event.health != 20 -> { + entityFromPacket.add(EntityPacketType.VILLAGER to event.entity.entityId) + } + + event.entity is EntityCreeper && event.health == 20 -> { + entityFromPacket.add(EntityPacketType.CREEPER_VAIL to event.entity.entityId) + } + } + } + + private fun islandException(): Boolean = when (LorenzUtils.skyBlockIsland) { + IslandType.GARDEN_GUEST -> true + IslandType.PRIVATE_ISLAND_GUEST -> true + else -> false + } + + private fun entityDeSpawn(entity: EntityLivingBase) { + MobData.entityToMob[entity]?.createDeSpawnEvent()?.postAndCatch() ?: removeRetry(entity) + allEntitiesViaPacketId.remove(entity.entityId) + } + + private fun Mob.createDeSpawnEvent() = when (this.mobType) { + Mob.Type.PLAYER -> MobEvent.DeSpawn.Player(this) + Mob.Type.SUMMON -> MobEvent.DeSpawn.Summon(this) + Mob.Type.SPECIAL -> MobEvent.DeSpawn.Special(this) + Mob.Type.PROJECTILE -> MobEvent.DeSpawn.Projectile(this) + Mob.Type.DISPLAY_NPC -> MobEvent.DeSpawn.DisplayNPC(this) + Mob.Type.BASIC, Mob.Type.DUNGEON, Mob.Type.BOSS, Mob.Type.SLAYER -> MobEvent.DeSpawn.SkyblockMob(this) + } + + private fun handleRetries() { + val iterator = MobData.retries.iterator() + while (iterator.hasNext()) { + val (_, retry) = iterator.next() + + if (MobData.externRemoveOfRetryAmount > 0) { + iterator.remove() + MobData.externRemoveOfRetryAmount-- + continue + } + + if (retry.outsideRange()) continue + + val entity = retry.entity + if (retry.times == MAX_RETRIES) { + logger.log( + "`${retry.entity.name}`${retry.entity.entityId} missed {\n " + + "is already Found: ${MobData.entityToMob[retry.entity] != null})." + + "\n Position: ${retry.entity.getLorenzVec()}\n " + + "DistanceC: ${ + entity.getLorenzVec().distanceChebyshevIgnoreY(LocationUtils.playerLocation()) + }\n" + + "Relative Position: ${entity.getLorenzVec().subtract(LocationUtils.playerLocation())}\n " + + "}" + ) + // Uncomment this to make it closed a loop + // iterator.remove() + // continue + } + if (!entitySpawn(entity, retry.roughType)) { + retry.times++ + continue + } + iterator.remove() + } + } + + private val entityUpdatePackets = ConcurrentLinkedQueue<Int>() + private val entitiesThatRequireUpdate = mutableSetOf<Int>() // needs to be distinct, therefore not using a queue + + /** Refreshes the references of the entities in entitiesThatRequireUpdate */ + private fun makeEntityReferenceUpdate() { + entitiesThatRequireUpdate.iterator().let { iterator -> + while (iterator.hasNext()) { + if (handleEntityUpdate(iterator.next())) iterator.remove() + } + } + entityUpdatePackets.drainTo(entitiesThatRequireUpdate) + } + + private fun handleEntityUpdate(entityID: Int): Boolean { + val entity = EntityUtils.getEntityByID(entityID) as? EntityLivingBase ?: return false + getRetry(entity)?.apply { this.entity = entity } + MobData.currentEntityLiving.refreshReference(entity) + MobData.previousEntityLiving.refreshReference(entity) + // update map + MobData.entityToMob[entity]?.internalUpdateOfEntity(entity) + return true + } + + @SubscribeEvent + fun onEntitySpawnPacket(event: PacketEvent.ReceiveEvent) { + when (val packet = event.packet) { + is S0FPacketSpawnMob -> addEntityUpdate(packet.entityID) + is S0CPacketSpawnPlayer -> addEntityUpdate(packet.entityID) + // is S0EPacketSpawnObject -> addEntityUpdate(packet.entityID) + is S37PacketStatistics -> // one of the first packets that is sent when switching servers inside the BungeeCord Network (please some prove this, I just found it out via Testing) + { + shouldClear.set(true) + allEntitiesViaPacketId.clear() + } + } + } + + @SubscribeEvent + fun onIslandChange(event: IslandChangeEvent) { + MobData.currentEntityLiving.remove(Minecraft.getMinecraft().thePlayer) // Fix for the Player + } + + private val allEntitiesViaPacketId = mutableSetOf<Int>() + + private fun addEntityUpdate(id: Int) = if (allEntitiesViaPacketId.contains(id)) { + entityUpdatePackets.add(id) + } else { + allEntitiesViaPacketId.add(id) + } + + @SubscribeEvent + fun onDisconnect(event: FMLNetworkEvent.ClientDisconnectionFromServerEvent) { + shouldClear.set(true) + } + + @SubscribeEvent + fun onDebugDataCollect(event: DebugDataCollectEvent) { + event.title("Mob Detection") + if (forceReset) { + event.addData("Mob Detection is manually disabled!") + } else { + event.addIrrelevant { + add("normal enabled") + add("Active Mobs: ${MobData.currentMobs.size}") + val inDistanceMobs = MobData.retries.count { it.value.outsideRange() } + add("Searching for Mobs: ${MobData.retries.size - inDistanceMobs}") + add("Mobs over Max Search Count: ${MobData.retries.count { it.value.times > MAX_RETRIES }}") + add("Mobs outside of Range: $inDistanceMobs") + } + } + } + +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/MobFactories.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/MobFactories.kt new file mode 100644 index 000000000..b8c9f9227 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/MobFactories.kt @@ -0,0 +1,133 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.utils.EntityUtils.cleanName +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimal +import at.hannibal2.skyhanni.utils.StringUtils.findMatcher +import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand + +object MobFactories { + fun slayer( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase> + ): Mob? = + MobFilter.slayerNameFilter.matchMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.SLAYER, + armorStand = armorStand, + name = this.group("name"), + additionalEntities = extraEntityList, + levelOrTier = this.group("tier").romanToDecimal() + ) + } + + fun boss( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase> = emptyList(), + overriddenName: String? = null + ): Mob? = + MobFilter.bossMobNameFilter.matchMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.BOSS, + armorStand = armorStand, + name = overriddenName ?: this.group("name"), + levelOrTier = group("level")?.takeIf { it.isNotEmpty() }?.toInt() ?: -1, + additionalEntities = extraEntityList + ) + } + + fun dungeon( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase> = emptyList() + ): Mob? = + MobFilter.dungeonNameFilter.matchMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.DUNGEON, + armorStand = armorStand, + name = this.group("name"), + additionalEntities = extraEntityList, + hasStar = this.group("star")?.isNotEmpty() ?: false, + attribute = this.group("attribute")?.takeIf { it.isNotEmpty() } + ?.let { + LorenzUtils.enumValueOfOrNull<MobFilter.DungeonAttribute>(it) + } + ) + } + + fun basic( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase>? = null + ): Mob? = + MobFilter.mobNameFilter.findMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.BASIC, + armorStand = armorStand, + name = this.group("name").removeCorruptedSuffix( + this.group("corrupted")?.isNotEmpty() ?: false + ), + additionalEntities = extraEntityList, + levelOrTier = this.group("level")?.takeIf { it.isNotEmpty() } + ?.toInt() ?: -1 + ) + } + + fun basic(baseEntity: EntityLivingBase, name: String) = + Mob(baseEntity = baseEntity, mobType = Mob.Type.BASIC, name = name) + + fun summon( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase> + ): Mob? = + MobFilter.summonFilter.matchMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.SUMMON, + armorStand = armorStand, + name = this.group("name"), + additionalEntities = extraEntityList, + ownerName = this.group("owner") + ) + } + + fun displayNPC(baseEntity: EntityLivingBase, armorStand: EntityArmorStand, clickArmorStand: EntityArmorStand): Mob = + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.DISPLAY_NPC, + armorStand = armorStand, + name = armorStand.cleanName(), + additionalEntities = listOf(clickArmorStand) + ) + + fun player(baseEntity: EntityLivingBase): Mob = Mob(baseEntity, Mob.Type.PLAYER, name = baseEntity.name) + fun projectile(baseEntity: EntityLivingBase, name: String): Mob = + Mob(baseEntity = baseEntity, mobType = Mob.Type.PROJECTILE, name = name) + + fun special(baseEntity: EntityLivingBase, name: String, armorStand: EntityArmorStand? = null) = + Mob(baseEntity = baseEntity, mobType = Mob.Type.SPECIAL, armorStand = armorStand, name = name) + + private fun String.removeCorruptedSuffix(case: Boolean) = if (case) this.dropLast(1) else this + fun dojo(baseEntity: EntityLivingBase, armorStand: EntityArmorStand): Mob? = + MobFilter.dojoFilter.matchMatcher(armorStand.cleanName()) { + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.SPECIAL, + armorStand = armorStand, + name = if (this.group("points").isNotEmpty()) "Points: " + this.group("points") else this.group("empty") + ) + } + + fun minionMob(baseEntity: EntityLivingBase) = + Mob(baseEntity, Mob.Type.SPECIAL, name = MobFilter.MINION_MOB_PREFIX + baseEntity.cleanName()) + +} diff --git a/src/main/java/at/hannibal2/skyhanni/data/mob/MobFilter.kt b/src/main/java/at/hannibal2/skyhanni/data/mob/MobFilter.kt new file mode 100644 index 000000000..be67bb6c4 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/mob/MobFilter.kt @@ -0,0 +1,565 @@ +package at.hannibal2.skyhanni.data.mob + +import at.hannibal2.skyhanni.data.IslandType +import at.hannibal2.skyhanni.data.mob.MobData.MobResult +import at.hannibal2.skyhanni.data.mob.MobData.MobResult.Companion.makeMobResult +import at.hannibal2.skyhanni.events.MobEvent +import at.hannibal2.skyhanni.features.dungeon.DungeonAPI +import at.hannibal2.skyhanni.utils.CollectionUtils.takeWhileInclusive +import at.hannibal2.skyhanni.utils.EntityUtils.cleanName +import at.hannibal2.skyhanni.utils.EntityUtils.isNPC +import at.hannibal2.skyhanni.utils.ItemUtils.getSkullTexture +import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceTo +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.LorenzUtils.baseMaxHealth +import at.hannibal2.skyhanni.utils.LorenzUtils.derpy +import at.hannibal2.skyhanni.utils.LorenzUtils.isInIsland +import at.hannibal2.skyhanni.utils.MobUtils +import at.hannibal2.skyhanni.utils.MobUtils.isDefaultValue +import at.hannibal2.skyhanni.utils.MobUtils.takeNonDefault +import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher +import at.hannibal2.skyhanni.utils.StringUtils.matches +import at.hannibal2.skyhanni.utils.getLorenzVec +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import net.minecraft.client.entity.EntityOtherPlayerMP +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.boss.EntityDragon +import net.minecraft.entity.boss.EntityWither +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.monster.EntityCaveSpider +import net.minecraft.entity.monster.EntityCreeper +import net.minecraft.entity.monster.EntityEnderman +import net.minecraft.entity.monster.EntityGiantZombie +import net.minecraft.entity.monster.EntityGuardian +import net.minecraft.entity.monster.EntityIronGolem +import net.minecraft.entity.monster.EntityMagmaCube +import net.minecraft.entity.monster.EntityPigZombie +import net.minecraft.entity.monster.EntitySlime +import net.minecraft.entity.monster.EntitySnowman +import net.minecraft.entity.monster.EntityWitch +import net.minecraft.entity.monster.EntityZombie +import net.minecraft.entity.passive.EntityAnimal +import net.minecraft.entity.passive.EntityBat +import net.minecraft.entity.passive.EntityChicken +import net.minecraft.entity.passive.EntityCow +import net.minecraft.entity.passive.EntityHorse +import net.minecraft.entity.passive.EntityMooshroom +import net.minecraft.entity.passive.EntityOcelot +import net.minecraft.entity.passive.EntityPig +import net.minecraft.entity.passive.EntityRabbit +import net.minecraft.entity.passive.EntitySheep +import net.minecraft.entity.passive.EntityVillager +import net.minecraft.entity.player.EntityPlayer + +@Suppress("RegExpRedundantEscape") +object MobFilter { + + private val repoGroup = RepoPattern.group("mob.detection") + + val mobNameFilter by repoGroup.pattern( + "filter.basic", + "(?:\\[\\w+(?<level>\\d+)\\] )?(?<corrupted>.Corrupted )?(?<name>.*) [\\d❤]+" + ) + val slayerNameFilter by repoGroup.pattern("filter.slayer", "^. (?<name>.*) (?<tier>[IV]+) \\d+.*") + val bossMobNameFilter by repoGroup.pattern( + "filter.boss", + "^. (?:\\[\\w+(?<level>\\d+)\\] )?(?<name>.*) (?:[\\d\\/Mk.,❤]+|█+) .$" + ) + val dungeonNameFilter by repoGroup.pattern( + "filter.dungeon", + "^(?:(?<star>✯)\\s)?(?:(?<attribute>${DungeonAttribute.toRegexLine})\\s)?(?:\\[[\\w\\d]+\\]\\s)?(?<name>.+)\\s[^\\s]+$" + ) + val summonFilter by repoGroup.pattern("filter.summon", "^(?<owner>\\w+)'s (?<name>.*) \\d+.*") + val dojoFilter by repoGroup.pattern("filter.dojo", "^(?:(?<points>\\d+) pts|(?<empty>\\w+))$") + val jerryPattern by repoGroup.pattern( + "jerry", + "(?:\\[\\w+(?<level>\\d+)\\] )?(?<owner>\\w+)'s (?<name>\\w+ Jerry) \\d+ Hits" + ) + + val petCareNamePattern by repoGroup.pattern("pattern.petcare", "^\\[\\w+ (?<level>\\d+)\\] (?<name>.*)") + val wokeSleepingGolemPattern by repoGroup.pattern("pattern.dungeon.woke.golem", "(?:§c§lWoke|§5§lSleeping) Golem§r") + val jerryMagmaCubePattern by repoGroup.pattern( + "pattern.jerry.magma.cube", + "§c(?:Cubie|Maggie|Cubert|Cübe|Cubette|Magmalene|Lucky 7|8ball|Mega Cube|Super Cube) §a\\d+§8\\/§a\\d+§c❤" + ) + val summonOwnerPattern by repoGroup.pattern("pattern.summon.owner", ".*Spawned by: (?<name>.*).*") + + private const val RAT_SKULL = + "ewogICJ0aW1lc3RhbXAiIDogMTYxODQxOTcwMTc1MywKICAicHJvZmlsZUlkIiA6ICI3MzgyZGRmYmU0ODU0NTVjODI1ZjkwMGY4OGZkMzJmOCIsCiAgInByb2ZpbGVOYW1lIiA6ICJCdUlJZXQiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYThhYmI0NzFkYjBhYjc4NzAzMDExOTc5ZGM4YjQwNzk4YTk0MWYzYTRkZWMzZWM2MWNiZWVjMmFmOGNmZmU4IiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=" + private const val HELLWISP_TENTACLE_SKULL = + "ewogICJ0aW1lc3RhbXAiIDogMTY0OTM4MzAyMTQxNiwKICAicHJvZmlsZUlkIiA6ICIzYjgwOTg1YWU4ODY0ZWZlYjA3ODg2MmZkOTRhMTVkOSIsCiAgInByb2ZpbGVOYW1lIiA6ICJLaWVyYW5fVmF4aWxpYW4iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDI3MDQ2Mzg0OTM2MzhiODVjMzhkZDYzZmZkYmUyMjJmZTUzY2ZkNmE1MDk3NzI4NzU2MTE5MzdhZTViNWUyMiIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + private const val RIFT_EYE_SKULL1 = + "ewogICJ0aW1lc3RhbXAiIDogMTY0ODA5MTkzNTcyMiwKICAicHJvZmlsZUlkIiA6ICJhNzdkNmQ2YmFjOWE0NzY3YTFhNzU1NjYxOTllYmY5MiIsCiAgInByb2ZpbGVOYW1lIiA6ICIwOEJFRDUiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjI2YmRlNDUwNDljN2I3ZDM0NjA1ZDgwNmEwNjgyOWI2Zjk1NWI4NTZhNTk5MWZkMzNlN2VhYmNlNDRjMDgzNCIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + private const val RIFT_EYE_SKULL2 = + "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTdkYjE5MjNkMDNjNGVmNGU5ZjZlODcyYzVhNmFkMjU3OGIxYWZmMmIyODFmYmMzZmZhNzQ2NmM4MjVmYjkifX19" + private const val NPC_TURD_SKULL = + "ewogICJ0aW1lc3RhbXAiIDogMTYzOTUxMjYxNzc5MywKICAicHJvZmlsZUlkIiA6ICIwZjczMDA3NjEyNGU0NGM3YWYxMTE1NDY5YzQ5OTY3OSIsCiAgInByb2ZpbGVOYW1lIiA6ICJPcmVfTWluZXIxMjMiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjM2MzBkOWIwMjA4OGVhMTkyNGE4NzIyNDJhYmM3NWI2MjYyYzJhY2E5MmFlY2Y4NzE0YTU3YTQxZWVhMGI5ZCIKICAgIH0KICB9Cn0=" + + const val MINION_MOB_PREFIX = "Minion Mob " + + enum class DungeonAttribute { + Flaming, Stormy, Speedy, Fortified, Healthy, Healing, Boomer, Golden, Stealth; + + companion object { + + val toRegexLine = DungeonAttribute.entries.joinToString("|") { it.name } + } + } + + private val extraDisplayNPCByName = setOf( + "Guy ", // Guy NPC (but only as visitor) + "vswiblxdxg", // Mayor Cole + "anrrtqytsl", // Weaponsmith + ) + + private val displayNPCCompressedNamePattern by repoGroup.pattern("displaynpc.name", "[a-z0-9]{10}") + + private fun displayNPCNameCheck(name: String) = name.startsWith('§') + || displayNPCCompressedNamePattern.matches(name) + || extraDisplayNPCByName.contains(name) + + private val listOfClickArmorStand = setOf( + "§e§lCLICK", + "§6§lSEASONAL SKINS", + "§e§lGATE KEEPER", + "§e§lBLACKSMITH", + "§e§lSHOP", + "§e§lTREASURES" + ) + + fun Entity.isSkyBlockMob(): Boolean = when { + this !is EntityLivingBase -> false + this is EntityArmorStand -> false + this is EntityPlayer && this.isRealPlayer() -> false + this.isDisplayNPC() -> false + this is EntityWither && this.entityId < 0 -> false + else -> true + } + + fun EntityPlayer.isRealPlayer() = uniqueID != null && uniqueID.version() == 4 + + fun EntityLivingBase.isDisplayNPC() = (this is EntityPlayer && isNPC() && displayNPCNameCheck(this.name)) + || (this is EntityVillager && this.maxHealth == 20.0f) // Villager NPCs in the Village + || (this is EntityWitch && this.entityId <= 500) // Alchemist NPC + || (this is EntityCow && this.entityId <= 500) // Shania NPC (in Rift and Outside) + || (this is EntitySnowman && this.entityId <= 500) // Sherry NPC (in Jerry Island) + + internal fun createDisplayNPC(entity: EntityLivingBase): Boolean { + val clickArmorStand = MobUtils.getArmorStandByRangeAll(entity, 1.5).firstOrNull { armorStand -> + listOfClickArmorStand.contains(armorStand.name) + } ?: return false + val armorStand = MobUtils.getArmorStand(clickArmorStand, -1) ?: return false + MobEvent.Spawn.DisplayNPC(MobFactories.displayNPC(entity, armorStand, clickArmorStand)).postAndCatch() + return true + } + + /** baseEntity must have passed the .isSkyBlockMob() function */ + internal fun createSkyblockEntity(baseEntity: EntityLivingBase): MobResult { + val nextEntity = MobUtils.getNextEntity(baseEntity, 1) as? EntityLivingBase + + exceptions(baseEntity, nextEntity)?.let { return it } + + // Check if Late Stack + nextEntity?.let { nextEntity -> + MobData.entityToMob[nextEntity]?.apply { internalAddEntity(baseEntity) }?.also { return MobResult.illegal } + } + + // Stack up the mob + var caughtSkyblockMob: Mob? = null + val extraEntityList = generateSequence(nextEntity) { + MobUtils.getNextEntity( + it, + 1 + ) as? EntityLivingBase + }.takeWhileInclusive { entity -> + !(entity is EntityArmorStand && !entity.isDefaultValue()) && MobData.entityToMob[entity]?.also { + caughtSkyblockMob = it + }?.run { false } ?: true + }.toList() + stackedMobsException(baseEntity, extraEntityList)?.let { return it } + + // If Late Stack add all entities + caughtSkyblockMob?.apply { internalAddEntity(extraEntityList.dropLast(1)) }?.also { return MobResult.illegal } + + val armorStand = extraEntityList.lastOrNull() as? EntityArmorStand ?: return MobResult.notYetFound + + if (armorStand.isDefaultValue()) return MobResult.notYetFound + return createSkyblockMob(baseEntity, armorStand, extraEntityList.dropLast(1))?.let { MobResult.found(it) } + ?: MobResult.notYetFound + } + + private fun createSkyblockMob( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand, + extraEntityList: List<EntityLivingBase> + ): Mob? = + MobFactories.summon(baseEntity, armorStand, extraEntityList) + ?: MobFactories.slayer(baseEntity, armorStand, extraEntityList) + ?: MobFactories.boss(baseEntity, armorStand, extraEntityList) + ?: if (DungeonAPI.inDungeon()) MobFactories.dungeon( + baseEntity, + armorStand, + extraEntityList + ) else (MobFactories.basic(baseEntity, armorStand, extraEntityList) + ?: MobFactories.dojo(baseEntity, armorStand)) + + private fun noArmorStandMobs(baseEntity: EntityLivingBase): MobResult? = when { + baseEntity is EntityBat -> createBat(baseEntity) + + baseEntity.isFarmMob() -> createFarmMobs(baseEntity)?.let { MobResult.found(it) } + baseEntity is EntityDragon -> MobResult.found(MobFactories.basic(baseEntity, baseEntity.cleanName())) + baseEntity is EntityGiantZombie && baseEntity.name == "Dinnerbone" -> MobResult.found( + MobFactories.projectile( + baseEntity, + "Giant Sword" + ) + ) // Will false trigger if there is another Dinnerbone Giant + baseEntity is EntityCaveSpider -> MobUtils.getArmorStand(baseEntity, -1) + ?.takeIf { summonOwnerPattern.matches(it.cleanName()) }?.let { + MobData.entityToMob[MobUtils.getNextEntity(baseEntity, -4)]?.internalAddEntity(baseEntity) + ?.let { MobResult.illegal } + } + + baseEntity is EntityWither && baseEntity.invulTime == 800 -> MobResult.found( + MobFactories.special( + baseEntity, + "Mini Wither" + ) + ) + + else -> null + } + + private fun exceptions(baseEntity: EntityLivingBase, nextEntity: EntityLivingBase?): MobResult? { + noArmorStandMobs(baseEntity)?.also { return it } + val armorStand = nextEntity as? EntityArmorStand + islandSpecificExceptions(baseEntity, armorStand, nextEntity)?.also { return it } + + if (armorStand == null) return null + armorStandOnlyMobs(baseEntity, armorStand)?.also { return it } + jerryPattern.matchMatcher(armorStand.cleanName()) { + val level = this.group("level")?.toInt() ?: -1 + val owner = this.group("owner") ?: return@matchMatcher + val name = this.group("name") ?: return@matchMatcher + return MobResult.found( + Mob( + baseEntity, + Mob.Type.BASIC, + armorStand, + name = name, + ownerName = owner, + levelOrTier = level + ) + ) + } + return when { + baseEntity is EntityPig && armorStand.name.endsWith("'s Pig") -> MobResult.illegal // Pig Pet + + baseEntity is EntityHorse && armorStand.name.endsWith("'s Skeleton Horse") -> MobResult.illegal// Skeleton Horse Pet + + baseEntity is EntityHorse && armorStand.name.endsWith("'s Horse") -> MobResult.illegal // Horse Pet + + baseEntity is EntityGuardian && armorStand.cleanName() + .matches("^\\d+".toRegex()) -> MobResult.illegal // Wierd Sea Guardian Ability + + else -> null + } + } + + private fun islandSpecificExceptions( + baseEntity: EntityLivingBase, + armorStand: EntityArmorStand?, + nextEntity: EntityLivingBase? + ): MobResult? { + return if (DungeonAPI.inDungeon()) { + when { + baseEntity is EntityZombie && armorStand != null && (armorStand.name == "§e﴾ §c§lThe Watcher§r§r §e﴿" || armorStand.name == "§3§lWatchful Eye§r") -> MobResult.found( + MobFactories.special(baseEntity, armorStand.cleanName(), armorStand) + ) + + baseEntity is EntityCaveSpider -> MobUtils.getClosedArmorStand(baseEntity, 2.0).takeNonDefault() + .makeMobResult { MobFactories.dungeon(baseEntity, it) } + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "Shadow Assassin" -> MobUtils.getClosedArmorStandWithName( + baseEntity, + 3.0, + "Shadow Assassin" + ).makeMobResult { MobFactories.dungeon(baseEntity, it) } + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "The Professor" -> MobUtils.getArmorStand( + baseEntity, + 9 + ).makeMobResult { MobFactories.boss(baseEntity, it) } + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && (nextEntity is EntityGiantZombie || nextEntity == null) && baseEntity.name.contains( + "Livid" + ) -> MobUtils.getClosedArmorStandWithName(baseEntity, 6.0, "﴾ Livid") + .makeMobResult { MobFactories.boss(baseEntity, it, overriddenName = "Real Livid") } + + baseEntity is EntityIronGolem && wokeSleepingGolemPattern.matches( + armorStand?.name ?: "" + ) -> MobResult.found(Mob(baseEntity, Mob.Type.DUNGEON, armorStand, "Sleeping Golem")) // Consistency fix + else -> null + } + } else when (LorenzUtils.skyBlockIsland) { + IslandType.PRIVATE_ISLAND -> when { + armorStand?.isDefaultValue() != false -> if (baseEntity.getLorenzVec() + .distanceChebyshevIgnoreY(LocationUtils.playerLocation()) < 15.0 + ) MobResult.found(MobFactories.minionMob(baseEntity)) else MobResult.notYetFound // TODO fix to always include Valid Mobs on Private Island + else -> null + } + + IslandType.THE_RIFT -> when { + baseEntity is EntitySlime && nextEntity is EntitySlime -> MobResult.illegal// Bacte Tentacle + baseEntity is EntitySlime && armorStand != null && armorStand.cleanName() + .startsWith("﴾ [Lv10] B") -> MobResult.found( + Mob( + baseEntity, + Mob.Type.BOSS, + armorStand, + name = "Bacte" + ) + ) + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "Branchstrutter " -> MobResult.found( + Mob(baseEntity, Mob.Type.DISPLAY_NPC, name = "Branchstrutter") + ) + + else -> null + } + + IslandType.CRIMSON_ISLE -> when { + baseEntity is EntitySlime && armorStand?.name == "§f§lCOLLECT!" -> MobResult.found( + MobFactories.special( + baseEntity, + "Heavy Pearl" + ) + ) + + baseEntity is EntityPig && nextEntity is EntityPig -> MobResult.illegal // Matriarch Tongue + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "BarbarianGuard " -> MobResult.found( + Mob(baseEntity, Mob.Type.DISPLAY_NPC, name = "Barbarian Guard") + ) + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "MageGuard " -> MobResult.found( + Mob(baseEntity, Mob.Type.DISPLAY_NPC, name = "Mage Guard") + ) + + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() && baseEntity.name == "Mage Outlaw" -> MobResult.found( + Mob(baseEntity, Mob.Type.BOSS, armorStand, name = "Mage Outlaw") + ) // fix for wierd name + baseEntity is EntityPigZombie && baseEntity.inventory?.get(4) + ?.getSkullTexture() == NPC_TURD_SKULL -> MobResult.found( + Mob( + baseEntity, + Mob.Type.DISPLAY_NPC, + name = "Turd" + ) + ) + + baseEntity is EntityOcelot -> if (createDisplayNPC(baseEntity)) MobResult.illegal else MobResult.notYetFound // Maybe a problem in the future + else -> null + } + + IslandType.DEEP_CAVERNS -> when { + baseEntity is EntityCreeper && baseEntity.baseMaxHealth.derpy() == 120 -> MobResult.found( + Mob( + baseEntity, + Mob.Type.BASIC, + name = "Sneaky Creeper", + levelOrTier = 3 + ) + ) + + else -> null + } + + IslandType.DWARVEN_MINES -> when { + baseEntity is EntityCreeper && baseEntity.baseMaxHealth.derpy() == 1_000_000 -> MobResult.found( + MobFactories.basic(baseEntity, "Ghost") + ) + + else -> null + } + + IslandType.CRYSTAL_HOLLOWS -> when { + baseEntity is EntityMagmaCube && armorStand != null && armorStand.cleanName() == "[Lv100] Bal ???❤" -> MobResult.found( + Mob(baseEntity, Mob.Type.BOSS, armorStand, "Bal", levelOrTier = 100) + ) + + else -> null + } + + IslandType.HUB -> when { + baseEntity is EntityOcelot && armorStand?.isDefaultValue() == false && armorStand.name.startsWith("§8[§7Lv155§8] §cAzrael§r") -> MobUtils.getArmorStand( + baseEntity, + 1 + ).makeMobResult { MobFactories.basic(baseEntity, it) } + + baseEntity is EntityOcelot && (nextEntity is EntityOcelot || nextEntity == null) -> MobUtils.getArmorStand( + baseEntity, + 3 + ).makeMobResult { MobFactories.basic(baseEntity, it) } + + baseEntity is EntityOtherPlayerMP && (baseEntity.name == "Minos Champion" || baseEntity.name == "Minos Inquisitor" || baseEntity.name == "Minotaur ") && armorStand != null -> MobUtils.getArmorStand( + baseEntity, + 2 + ).makeMobResult { MobFactories.basic(baseEntity, it, listOf(armorStand)) } + + baseEntity is EntityZombie && armorStand?.isDefaultValue() == true && MobUtils.getNextEntity( + baseEntity, + 4 + )?.name?.startsWith("§e") == true -> petCareHandler(baseEntity) + + baseEntity is EntityZombie && armorStand != null && !armorStand.isDefaultValue() -> null // Impossible Rat + baseEntity is EntityZombie -> ratHandler(baseEntity, nextEntity) // Possible Rat + else -> null + } + + IslandType.GARDEN -> when { + baseEntity is EntityOtherPlayerMP && baseEntity.isNPC() -> MobResult.found( + Mob( + baseEntity, + Mob.Type.DISPLAY_NPC, + name = baseEntity.cleanName() + ) + ) + + else -> null + } + + IslandType.KUUDRA_ARENA -> when { + baseEntity is EntityMagmaCube && nextEntity is EntityMagmaCube -> MobResult.illegal + baseEntity is EntityZombie && nextEntity is EntityZombie -> MobResult.illegal + baseEntity is EntityZombie && nextEntity is EntityGiantZombie -> MobResult.illegal + else -> null + } + + IslandType.WINTER -> when { + baseEntity is EntityMagmaCube && jerryMagmaCubePattern.matches( + MobUtils.getArmorStand( + baseEntity, + 2 + )?.name + ) -> + MobResult.found( + Mob( + baseEntity, + Mob.Type.BOSS, + MobUtils.getArmorStand(baseEntity, 2), + "Jerry Magma Cube" + ) + ) + + else -> null + } + + else -> null + } + } + + private fun petCareHandler(baseEntity: EntityLivingBase): MobResult { + val extraEntityList = listOf(1, 2, 3, 4).mapNotNull { MobUtils.getArmorStand(baseEntity, it) } + if (extraEntityList.size != 4) return MobResult.notYetFound + return petCareNamePattern.matchMatcher(extraEntityList[1].cleanName()) { + MobResult.found( + Mob( + baseEntity, + Mob.Type.SPECIAL, + armorStand = extraEntityList[1], + name = this.group("name"), + additionalEntities = extraEntityList, + levelOrTier = this.group("level").toInt() + ), + ) + } ?: MobResult.somethingWentWrong + } + + private fun stackedMobsException( + baseEntity: EntityLivingBase, + extraEntityList: List<EntityLivingBase> + ): MobResult? = + if (DungeonAPI.inDungeon()) { + when { + (baseEntity is EntityEnderman || baseEntity is EntityGiantZombie) && extraEntityList.lastOrNull()?.name == "§e﴾ §c§lLivid§r§r §a7M§c❤ §e﴿" -> MobResult.illegal // Livid Start Animation + else -> null + } + } else when (LorenzUtils.skyBlockIsland) { + IslandType.CRIMSON_ISLE -> when { + else -> null + } + + else -> null + } + + private fun armorStandOnlyMobs(baseEntity: EntityLivingBase, armorStand: EntityArmorStand): MobResult? { + if (baseEntity !is EntityZombie) return null + when { + armorStand.name.endsWith("'s Armadillo") -> return MobResult.illegal // Armadillo Pet + armorStand.name.endsWith("'s Rat") -> return MobResult.illegal // Rat Pet + baseEntity.riddenByEntity is EntityPlayer && MobUtils.getArmorStand(baseEntity, 2)?.inventory?.get(4) + ?.getSkullTexture() == RAT_SKULL -> return MobResult.illegal // Rat Morph + } + when (armorStand.inventory?.get(4)?.getSkullTexture()) { + HELLWISP_TENTACLE_SKULL -> return MobResult.illegal // Hellwisp Tentacle + RIFT_EYE_SKULL1 -> return MobResult.found(MobFactories.special(baseEntity, "Rift Teleport Eye", armorStand)) + RIFT_EYE_SKULL2 -> return MobResult.found(MobFactories.special(baseEntity, "Rift Teleport Eye", armorStand)) + } + return null + } + + fun EntityLivingBase.isFarmMob() = + this is EntityAnimal && this.baseMaxHealth.derpy() + .let { it == 50 || it == 20 || it == 130 } && LorenzUtils.skyBlockIsland != IslandType.PRIVATE_ISLAND + + private fun createFarmMobs(baseEntity: EntityLivingBase): Mob? = when (baseEntity) { + is EntityMooshroom -> MobFactories.basic(baseEntity, "Farm Mooshroom") + is EntityCow -> MobFactories.basic(baseEntity, "Farm Cow") + is EntityPig -> MobFactories.basic(baseEntity, "Farm Pig") + is EntityChicken -> MobFactories.basic(baseEntity, "Farm Chicken") + is EntityRabbit -> MobFactories.basic(baseEntity, "Farm Rabbit") + is EntitySheep -> MobFactories.basic(baseEntity, "Farm Sheep") + else -> null + } + + private fun createBat(baseEntity: EntityLivingBase): MobResult? = when (baseEntity.baseMaxHealth.derpy()) { + 5_000_000 -> MobResult.found(MobFactories.basic(baseEntity, "Cinderbat")) + 75_000 -> MobResult.found(MobFactories.basic(baseEntity, "Thorn Bat")) + 600 -> if (IslandType.GARDEN.isInIsland()) null else MobResult.notYetFound + 100 -> MobResult.found( + MobFactories.basic( + baseEntity, + if (DungeonAPI.inDungeon()) "Dungeon Secret Bat" else if (IslandType.PRIVATE_ISLAND.isInIsland()) "Private Island Bat" else "Mega Bat" + ) + ) + + 20 -> MobResult.found(MobFactories.projectile(baseEntity, "Vampire Mask Bat")) + // 6 -> MobFactories.projectile(baseEntity, "Spirit Scepter Bat") // moved to Packet Event because 6 is default Health of Bats + 5 -> MobResult.found(MobFactories.special(baseEntity, "Bat Pinata")) + else -> MobResult.notYetFound + } + + private fun ratHandler(baseEntity: EntityZombie, nextEntity: EntityLivingBase?): MobResult? = + generateSequence(ratSearchStart) { it + 1 }.take(ratSearchUpTo - ratSearchStart + 1).map { i -> + MobUtils.getArmorStand( + baseEntity, i + ) + }.firstOrNull { + it != null && it.distanceTo(baseEntity) < 4.0 && it.inventory?.get(4)?.getSkullTexture() == RAT_SKULL + }?.let { + MobResult.found( + Mob( + baseEntity = baseEntity, + mobType = Mob.Type.BASIC, + armorStand = it, + name = "Rat" + ) + ) + } + ?: if (nextEntity is EntityZombie) MobResult.notYetFound else null + + private const val ratSearchStart = 1 + private const val ratSearchUpTo = 11 +} diff --git a/src/main/java/at/hannibal2/skyhanni/events/MobEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/MobEvent.kt new file mode 100644 index 000000000..34725c546 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/events/MobEvent.kt @@ -0,0 +1,23 @@ +package at.hannibal2.skyhanni.events + +import at.hannibal2.skyhanni.data.mob.Mob + +open class MobEvent(val mob: Mob) : LorenzEvent() { + open class Spawn(mob: Mob) : MobEvent(mob) { + class SkyblockMob(mob: Mob) : Spawn(mob) + class Summon(mob: Mob) : Spawn(mob) + class Player(mob: Mob) : Spawn(mob) + class DisplayNPC(mob: Mob) : Spawn(mob) + class Special(mob: Mob) : Spawn(mob) + class Projectile(mob: Mob) : Spawn(mob) + } + + open class DeSpawn(mob: Mob) : MobEvent(mob) { + class SkyblockMob(mob: Mob) : DeSpawn(mob) + class Summon(mob: Mob) : DeSpawn(mob) + class Player(mob: Mob) : DeSpawn(mob) + class DisplayNPC(mob: Mob) : DeSpawn(mob) + class Special(mob: Mob) : DeSpawn(mob) + class Projectile(mob: Mob) : DeSpawn(mob) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/test/command/CopyNearbyEntitiesCommand.kt b/src/main/java/at/hannibal2/skyhanni/test/command/CopyNearbyEntitiesCommand.kt index 5ba4447f2..276e5477a 100644 --- a/src/main/java/at/hannibal2/skyhanni/test/command/CopyNearbyEntitiesCommand.kt +++ b/src/main/java/at/hannibal2/skyhanni/test/command/CopyNearbyEntitiesCommand.kt @@ -1,21 +1,32 @@ package at.hannibal2.skyhanni.test.command +import at.hannibal2.skyhanni.data.mob.Mob +import at.hannibal2.skyhanni.data.mob.MobData +import at.hannibal2.skyhanni.data.mob.MobFilter.isDisplayNPC +import at.hannibal2.skyhanni.data.mob.MobFilter.isRealPlayer +import at.hannibal2.skyhanni.data.mob.MobFilter.isSkyBlockMob import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.EntityUtils +import at.hannibal2.skyhanni.utils.EntityUtils.cleanName import at.hannibal2.skyhanni.utils.EntityUtils.getBlockInHand import at.hannibal2.skyhanni.utils.EntityUtils.getSkinTexture +import at.hannibal2.skyhanni.utils.EntityUtils.isNPC import at.hannibal2.skyhanni.utils.ItemUtils.cleanName import at.hannibal2.skyhanni.utils.ItemUtils.getSkullTexture import at.hannibal2.skyhanni.utils.ItemUtils.isEnchanted import at.hannibal2.skyhanni.utils.ItemUtils.name import at.hannibal2.skyhanni.utils.LocationUtils +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToPlayer import at.hannibal2.skyhanni.utils.LorenzUtils.baseMaxHealth import at.hannibal2.skyhanni.utils.OSUtils import at.hannibal2.skyhanni.utils.toLorenzVec import net.minecraft.client.entity.EntityOtherPlayerMP +import net.minecraft.entity.Entity import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.boss.EntityWither import net.minecraft.entity.item.EntityArmorStand import net.minecraft.entity.item.EntityItem +import net.minecraft.entity.monster.EntityCreeper import net.minecraft.entity.monster.EntityEnderman import net.minecraft.entity.monster.EntityMagmaCube import net.minecraft.entity.player.EntityPlayer @@ -34,18 +45,21 @@ object CopyNearbyEntitiesCommand { val resultList = mutableListOf<String>() var counter = 0 - for (entity in EntityUtils.getAllEntities()) { + for (entity in EntityUtils.getAllEntities().sortedBy { it.entityId }) { val position = entity.position val vec = position.toLorenzVec() val distance = start.distance(vec) + val mob = MobData.entityToMob[entity] if (distance < searchRadius) { val simpleName = entity.javaClass.simpleName resultList.add("entity: $simpleName") val displayName = entity.displayName resultList.add("name: '" + entity.name + "'") + if (entity is EntityArmorStand) resultList.add("cleanName: '" + entity.cleanName() + "'") resultList.add("displayName: '${displayName.formattedText}'") resultList.add("entityId: ${entity.entityId}") - resultList.add("uuid version: ${entity.uniqueID.version()} ${if (entity.uniqueID.version() != 4) "NPC " else ""}(${entity.uniqueID})") + resultList.add("Type of Mob: ${getType(entity, mob)}") + resultList.add("uuid version: ${entity.uniqueID.version()} (${entity.uniqueID})") resultList.add("location data:") resultList.add("- vec: $vec") resultList.add("- distance: $distance") @@ -137,6 +151,26 @@ object CopyNearbyEntitiesCommand { val skinTexture = entity.getSkinTexture() resultList.add("- skin texture: $skinTexture") } + + is EntityCreeper -> { + resultList.add("EntityCreeper:") + val creeperState = entity.creeperState + val ignite = entity.hasIgnited() + val powered = entity.powered + resultList.add("- creeperState: '$creeperState'") + resultList.add("- ignite: '$ignite'") + resultList.add("- powered: '$powered'") + } + + is EntityWither -> { + resultList.add("EntityWither:") + val invulTime = entity.invulTime + resultList.add("- invulTime: '$invulTime'") + } + } + if (mob != null && mob.mobType != Mob.Type.PLAYER) { + resultList.add("MobInfo: ") + resultList.addAll(getMobInfo(mob).map { "- $it" }) } resultList.add("") resultList.add("") @@ -168,4 +202,63 @@ object CopyNearbyEntitiesCommand { resultList.add("- type: $type") } } + + private fun getType(entity: Entity, mob: Mob?) = buildString { + if (entity is EntityLivingBase && entity.isDisplayNPC()) append("DisplayNPC, ") + if (entity is EntityPlayer && entity.isNPC()) append("NPC, ") + if (entity is EntityPlayer && entity.isRealPlayer()) append("RealPlayer, ") + if (mob?.mobType == Mob.Type.SUMMON) append("Summon, ") + if (entity.isSkyBlockMob()) { + append("SkyblockMob(") + + if (mob == null) { + append(if (entity.distanceToPlayer() > MobData.DETECTION_RANGE) "Not in Range" else "None") + append(")") + } else { + append(mob.mobType.name) + if (mob.baseEntity == entity) append("/Base") + append(")\"") + append(mob.name) + append("\"") + } + append(", ") + } + + if (isNotEmpty()) { + delete(length - 2, length) // Remove the last ", " + } else { + append("NONE") + } + } + + fun getMobInfo(mob: Mob) = buildList<String> { + add("Name: ${mob.name}") + add("Type: ${mob.mobType}") + add("Base Entity: ${mob.baseEntity.asString()}") + add("Armorstand: ${mob.armorStand?.asString()}") + if (mob.extraEntities.isNotEmpty()) { + add("Extra Entities") + addAll(mob.extraEntities.map { " " + it.asString() }) + } + if (mob.hologram1Delegate.isInitialized()) { + add("Hologram1: ${mob.hologram1?.asString()}") + } + if (mob.hologram2Delegate.isInitialized()) { + add("Hologram2: ${mob.hologram2?.asString()}") + } + if (mob.owner != null) { + add("Owner: ${mob.owner.ownerName}") + } + add("Level or Tier: ${mob.levelOrTier.takeIf { it != -1 }}") + if (mob.mobType == Mob.Type.DUNGEON) { + add("Is Starred: ${mob.hasStar}") + add("Attribute: ${mob.attribute ?: "NONE"}") + } + if (mob.boundingBox != mob.baseEntity.entityBoundingBox) { + add("Bounding Box: ${mob.boundingBox}") + } + } + + private fun EntityLivingBase.asString() = + this.entityId.toString() + " - " + this.javaClass.simpleName + " \"" + this.name + "\"" } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt index 21f731fc2..d37ddad09 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt @@ -139,4 +139,28 @@ object CollectionUtils { fun <K, V : Comparable<V>> Map<K, V>.sortedDesc(): Map<K, V> { return toList().sorted().reversed().toMap() } + + inline fun <reified T> ConcurrentLinkedQueue<T>.drainForEach(action: (T) -> Unit) { + while (true) { + val value = this.poll() ?: break + action(value) + } + } + + fun <T> Sequence<T>.takeWhileInclusive(predicate: (T) -> Boolean) = sequence { + with(iterator()) { + while (hasNext()) { + val next = next() + yield(next) + if (!predicate(next)) break + } + } + } + + /** Updates a value if it is present in the set (equals), useful if the newValue is not reference equal with the value in the set */ + inline fun <reified T> MutableSet<T>.refreshReference(newValue: T) = if (this.contains(newValue)) { + this.remove(newValue) + this.add(newValue) + true + } else false } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/EntityUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/EntityUtils.kt index b2ffc2826..c9b90d738 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/EntityUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/EntityUtils.kt @@ -1,10 +1,14 @@ package at.hannibal2.skyhanni.utils import at.hannibal2.skyhanni.events.SkyHanniRenderEntityEvent +import at.hannibal2.skyhanni.data.mob.MobFilter.isRealPlayer import at.hannibal2.skyhanni.utils.ItemUtils.getSkullTexture import at.hannibal2.skyhanni.utils.LocationUtils.canBeSeen import at.hannibal2.skyhanni.utils.LocationUtils.distanceTo +import at.hannibal2.skyhanni.utils.LocationUtils.distanceToIgnoreY import at.hannibal2.skyhanni.utils.LorenzUtils.baseMaxHealth +import at.hannibal2.skyhanni.utils.LorenzUtils.derpy +import at.hannibal2.skyhanni.utils.StringUtils.removeColor import net.minecraft.block.state.IBlockState import net.minecraft.client.Minecraft import net.minecraft.client.entity.EntityOtherPlayerMP @@ -147,6 +151,9 @@ object EntityUtils { inline fun <reified T : Entity> getEntitiesNearby(location: LorenzVec, radius: Double): Sequence<T> = getEntities<T>().filter { it.distanceTo(location) < radius } + inline fun <reified T : Entity> getEntitiesNearbyIgnoreY(location: LorenzVec, radius: Double): Sequence<T> = + getEntities<T>().filter { it.distanceToIgnoreY(location) < radius } + fun EntityLivingBase.isAtFullHealth() = baseMaxHealth == health.toInt() fun EntityArmorStand.hasSkullTexture(skin: String): Boolean { @@ -154,7 +161,7 @@ object EntityUtils { return inventory.any { it != null && it.getSkullTexture() == skin } } - fun EntityPlayer.isNPC() = uniqueID == null || uniqueID.version() != 4 + fun EntityPlayer.isNPC() = !isRealPlayer() fun EntityLivingBase.hasPotionEffect(potion: Potion) = getActivePotionEffect(potion) != null @@ -212,6 +219,12 @@ object EntityUtils { event.cancel() } } + + fun EntityLivingBase.isCorrupted() = baseMaxHealth == health.toInt().derpy() * 3 || isRunicAndCorrupt() + fun EntityLivingBase.isRunic() = baseMaxHealth == health.toInt().derpy() * 4 || isRunicAndCorrupt() + fun EntityLivingBase.isRunicAndCorrupt() = baseMaxHealth == health.toInt().derpy() * 3 * 4 + + fun Entity.cleanName() = this.name.removeColor() } private fun Event.cancel() { diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LocationUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/LocationUtils.kt index 29cc1d569..ca651ea08 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LocationUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LocationUtils.kt @@ -3,6 +3,9 @@ package at.hannibal2.skyhanni.utils import net.minecraft.client.Minecraft import net.minecraft.entity.Entity import net.minecraft.util.AxisAlignedBB +import net.minecraft.util.BlockPos +import kotlin.math.max +import kotlin.math.min object LocationUtils { @@ -22,6 +25,9 @@ object LocationUtils { fun Entity.distanceToPlayer() = getLorenzVec().distanceToPlayer() fun Entity.distanceTo(location: LorenzVec) = getLorenzVec().distance(location) + fun Entity.distanceTo(other: Entity) = getLorenzVec().distance(other.getLorenzVec()) + + fun Entity.distanceToIgnoreY(location: LorenzVec) = getLorenzVec().distanceIgnoreY(location) fun playerEyeLocation(): LorenzVec { val player = Minecraft.getMinecraft().thePlayer @@ -41,4 +47,53 @@ object LocationUtils { val inFov = true // TODO add Frustum "Frustum().isBoundingBoxInFrustum(entity.entityBoundingBox)" return noBlocks && notTooFar && inFov } + + fun AxisAlignedBB.minBox() = LorenzVec(minX, minY, minZ) + + fun AxisAlignedBB.maxBox() = LorenzVec(maxX, maxY, maxZ) + + fun AxisAlignedBB.rayIntersects(origin: LorenzVec, direction: LorenzVec): Boolean { + // Reference for Algorithm https://tavianator.com/2011/ray_box.html + val rayDirectionInverse = direction.inverse() + val t1 = (this.minBox().subtract(origin)).multiply(rayDirectionInverse) + val t2 = (this.maxBox().subtract(origin)).multiply(rayDirectionInverse) + + val tmin = max(t1.minOfEachElement(t2).max(), Double.NEGATIVE_INFINITY) + val tmax = min(t1.maxOfEachElement(t2).min(), Double.POSITIVE_INFINITY) + return tmax >= tmin && tmax >= 0.0 + } + + fun AxisAlignedBB.union(aabbs: List<AxisAlignedBB>?): AxisAlignedBB? { + if (aabbs.isNullOrEmpty()) { + return null + } + + var minX = this.minX + var minY = this.minY + var minZ = this.minZ + var maxX = this.maxX + var maxY = this.maxY + var maxZ = this.maxZ + + aabbs.forEach { aabb -> + if (aabb.minX < minX) minX = aabb.minX + if (aabb.minY < minY) minY = aabb.minY + if (aabb.minZ < minZ) minZ = aabb.minZ + if (aabb.maxX > maxX) maxX = aabb.maxX + if (aabb.maxY > maxY) maxY = aabb.maxY + if (aabb.maxZ > maxZ) maxZ = aabb.maxZ + } + + val combinedMin = BlockPos(minX, minY, minZ) + val combinedMax = BlockPos(maxX, maxY, maxZ) + + return AxisAlignedBB(minX, minY, minZ, maxX, maxY, maxZ) + } + + fun AxisAlignedBB.getEdgeLengths() = this.maxBox().subtract(this.minBox()) + + fun AxisAlignedBB.getCenter() = this.getEdgeLengths().multiply(0.5).add(this.minBox()) + + fun AxisAlignedBB.getTopCenter() = this.getCenter().add(y = (maxY - minY) / 2) } + diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt index 77021e977..534b54172 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt @@ -7,7 +7,11 @@ import net.minecraft.util.AxisAlignedBB import net.minecraft.util.BlockPos import net.minecraft.util.Rotations import net.minecraft.util.Vec3 +import kotlin.math.abs +import kotlin.math.acos import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min import kotlin.math.pow import kotlin.math.round import kotlin.math.sin @@ -18,6 +22,7 @@ data class LorenzVec( val y: Double, val z: Double, ) { + constructor() : this(0.0, 0.0, 0.0) constructor(x: Int, y: Int, z: Int) : this(x.toDouble(), y.toDouble(), z.toDouble()) @@ -35,6 +40,8 @@ data class LorenzVec( fun distance(x: Double, y: Double, z: Double): Double = distance(LorenzVec(x, y, z)) + fun distanceChebyshevIgnoreY(other: LorenzVec) = max(abs(this.x - other.x), abs(this.z - other.z)) + fun distanceSq(other: LorenzVec): Double { val dx = (other.x - x) val dy = (other.y - y) @@ -62,12 +69,31 @@ data class LorenzVec( fun divide(d : Double) = multiply(1.0/d) + fun multiply(v: LorenzVec) = LorenzVec(x multiplyZeroSave v.x, y multiplyZeroSave v.y, z multiplyZeroSave v.z) + + fun dotProduct(other: LorenzVec): Double = + x multiplyZeroSave other.x + y multiplyZeroSave other.y + z multiplyZeroSave other.z + + fun angleAsCos(other: LorenzVec) = this.normalize().dotProduct(other.normalize()) + + fun angleInRad(other: LorenzVec) = acos(this.angleAsCos(other)) + + fun angleInDeg(other: LorenzVec) = Math.toDegrees(this.angleInRad(other)) + fun add(other: LorenzVec) = LorenzVec(x + other.x, y + other.y, z + other.z) fun subtract(other: LorenzVec) = LorenzVec(x - other.x, y - other.y, z - other.z) fun normalize() = length().let { LorenzVec(x / it, y / it, z / it) } + fun inverse() = LorenzVec(1.0 / x, 1.0 / y, 1.0 / z) + + fun min() = min(x, min(y, z)) + fun max() = max(x, max(y, z)) + + fun minOfEachElement(other: LorenzVec) = LorenzVec(min(x, other.x), min(y, other.y), min(z, other.z)) + fun maxOfEachElement(other: LorenzVec) = LorenzVec(max(x, other.x), max(y, other.y), max(z, other.z)) + fun printWithAccuracy(accuracy: Int, splitChar: String = " "): String { return if (accuracy == 0) { val x = round(x).toInt() @@ -152,6 +178,10 @@ data class LorenzVec( return LorenzVec(x, y, z) } + fun rotateXY(theta: Double) = LorenzVec(x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta), z) + fun rotateXZ(theta: Double) = LorenzVec(x * cos(theta) + z * sin(theta), y, -x * sin(theta) + z * cos(theta)) + fun rotateYZ(theta: Double) = LorenzVec(x, y * cos(theta) - z * sin(theta), y * sin(theta) + z * cos(theta)) + companion object { fun getFromYawPitch(yaw: Double, pitch: Double): LorenzVec { @@ -183,6 +213,7 @@ fun BlockPos.toLorenzVec(): LorenzVec = LorenzVec(x, y, z) fun Entity.getLorenzVec(): LorenzVec = LorenzVec(posX, posY, posZ) fun Entity.getPrevLorenzVec(): LorenzVec = LorenzVec(prevPosX, prevPosY, prevPosZ) +fun Entity.getMotionLorenzVec(): LorenzVec = LorenzVec(motionX, motionY, motionZ) fun Vec3.toLorenzVec(): LorenzVec = LorenzVec(xCoord, yCoord, zCoord) diff --git a/src/main/java/at/hannibal2/skyhanni/utils/MobUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/MobUtils.kt new file mode 100644 index 000000000..e1b165d25 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/MobUtils.kt @@ -0,0 +1,80 @@ +package at.hannibal2.skyhanni.utils + +import at.hannibal2.skyhanni.data.mob.Mob +import at.hannibal2.skyhanni.data.mob.MobData +import at.hannibal2.skyhanni.utils.EntityUtils.cleanName +import at.hannibal2.skyhanni.utils.LocationUtils.distanceTo +import at.hannibal2.skyhanni.utils.LocationUtils.rayIntersects +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityLivingBase +import net.minecraft.entity.item.EntityArmorStand +import net.minecraft.entity.player.EntityPlayer + +object MobUtils { + private const val defaultArmorStandName = "Armor Stand" + + // The corresponding ArmorStand for a mob has always the ID + 1 (with some exceptions) + fun getArmorStand(entity: Entity, offset: Int = 1) = getNextEntity(entity, offset) as? EntityArmorStand + + fun getNextEntity(entity: Entity, offset: Int) = EntityUtils.getEntityByID(entity.entityId + offset) + + fun getArmorStandByRangeAll(entity: Entity, range: Double) = + EntityUtils.getEntitiesNearby<EntityArmorStand>(entity.getLorenzVec(), range) + + fun getClosedArmorStand(entity: Entity, range: Double) = + getArmorStandByRangeAll(entity, range).sortedBy { it.distanceTo(entity) }.firstOrNull() + + fun getClosedArmorStandWithName(entity: Entity, range: Double, name: String) = + getArmorStandByRangeAll(entity, range).filter { it.cleanName().startsWith(name) } + .sortedBy { it.distanceTo(entity) }.firstOrNull() + + fun EntityArmorStand.isDefaultValue() = this.name == defaultArmorStandName + + fun EntityArmorStand?.takeNonDefault() = this?.takeIf { !it.isDefaultValue() } + + class OwnerShip(val ownerName: String) { + val ownerPlayer = MobData.players.firstOrNull { it.name == ownerName } + override fun equals(other: Any?): Boolean { + if (other is EntityPlayer) return ownerPlayer == other || ownerName == other.name + if (other is String) return ownerName == other + return false + } + + override fun hashCode(): Int { + return ownerName.hashCode() + } + } + + fun rayTraceForMob(entity: Entity, distance: Double, partialTicks: Float, offset: LorenzVec = LorenzVec()) = + rayTraceForMob(entity, partialTicks, offset)?.takeIf { + it.baseEntity.distanceTo(entity.getLorenzVec()) <= distance + } + + fun rayTraceForMobs( + entity: Entity, + distance: Double, + partialTicks: Float, + offset: LorenzVec = LorenzVec() + ) = + rayTraceForMobs(entity, partialTicks, offset)?.filter { + it.baseEntity.distanceTo(entity.getLorenzVec()) <= distance + }.takeIf { it?.isNotEmpty() ?: false } + + fun rayTraceForMob(entity: Entity, partialTicks: Float, offset: LorenzVec = LorenzVec()) = + rayTraceForMobs(entity, partialTicks, offset)?.firstOrNull() + + fun rayTraceForMobs(entity: Entity, partialTicks: Float, offset: LorenzVec = LorenzVec()): List<Mob>? { + val pos = entity.getPositionEyes(partialTicks).toLorenzVec().add(offset) + val look = entity.getLook(partialTicks).toLorenzVec().normalize() + val possibleEntities = MobData.entityToMob.filterKeys { + it !is EntityArmorStand && it.entityBoundingBox.rayIntersects( + pos, look + ) + }.values + if (possibleEntities.isEmpty()) return null + return possibleEntities.distinct().sortedBy { it.baseEntity.distanceTo(pos) }.drop(1) // drop to remove player + } + + val EntityLivingBase.mob get() = MobData.entityToMob[this] + +} |