aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThunderblade73 <85900443+Thunderblade73@users.noreply.github.com>2024-04-03 20:50:31 +0200
committerGitHub <noreply@github.com>2024-04-03 20:50:31 +0200
commit2f85351bacddb9ab3704a53c778d558a755bcc06 (patch)
treef479045f271f04a66a1ba69a61cb1b9893261eb5
parent76be6ad6de39c7078550394e8ec24a494ddb3bcc (diff)
downloadskyhanni-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>
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt6
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugMobConfig.java81
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/dev/DevConfig.java4
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/ClickType.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/Mob.kt177
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/MobData.kt140
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/MobDebug.kt97
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/MobDetection.kt361
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/MobFactories.kt133
-rw-r--r--src/main/java/at/hannibal2/skyhanni/data/mob/MobFilter.kt565
-rw-r--r--src/main/java/at/hannibal2/skyhanni/events/MobEvent.kt23
-rw-r--r--src/main/java/at/hannibal2/skyhanni/test/command/CopyNearbyEntitiesCommand.kt97
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/CollectionUtils.kt24
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/EntityUtils.kt15
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/LocationUtils.kt55
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/LorenzVec.kt31
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/MobUtils.kt80
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]
+
+}