diff --git a/src/main/kotlin/com/lambda/config/groups/Targeting.kt b/src/main/kotlin/com/lambda/config/groups/Targeting.kt index a50753e8b..0470f7aca 100644 --- a/src/main/kotlin/com/lambda/config/groups/Targeting.kt +++ b/src/main/kotlin/com/lambda/config/groups/Targeting.kt @@ -29,11 +29,17 @@ import com.lambda.util.NamedEnum import com.lambda.util.extension.fullHealth import com.lambda.util.math.distSq import com.lambda.util.world.fastEntitySearch +import com.lambda.world.LambdaAngerManagement import net.minecraft.client.network.ClientPlayerEntity import net.minecraft.client.network.OtherClientPlayerEntity import net.minecraft.entity.LivingEntity import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.entity.mob.AmbientEntity +import net.minecraft.entity.mob.Angerable import net.minecraft.entity.mob.HostileEntity +import net.minecraft.entity.mob.ShulkerEntity +import net.minecraft.entity.passive.AnimalEntity +import net.minecraft.entity.passive.GolemEntity import net.minecraft.entity.passive.PassiveEntity import java.util.* @@ -50,166 +56,229 @@ import java.util.* * @param maxRange The maximum range within which entities can be targeted. */ abstract class Targeting( - private val c: Configurable, - baseGroup: NamedEnum, - private val defaultRange: Double, - private val maxRange: Double, + private val c: Configurable, + baseGroup: NamedEnum, + private val defaultRange: Double, + private val maxRange: Double, ) : SettingGroup(c), TargetingConfig { - /** - * The range within which entities can be targeted. This value is configurable and constrained - * between 1.0 and [maxRange]. - */ - override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup) - - /** - * Whether players are included in the targeting scope. - */ - override val players by c.setting("Players", true).group(baseGroup) - - /** - * Whether friends are included in the targeting scope. - * Requires [players] to be true. - */ - override val friends by c.setting("Friends", false) { players }.group(baseGroup) - - /** - * Whether mobs are included in the targeting scope. - */ - private val mobs by c.setting("Mobs", true).group(baseGroup) - - /** - * Whether hostile mobs are included in the targeting scope - */ - private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup) - - /** - * Whether passive animals are included in the targeting scope - */ - private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup) - - /** - * Indicates whether hostile entities are included in the targeting scope. - */ - override val hostiles get() = mobs && hostilesSetting - - /** - * Indicates whether passive animals are included in the targeting scope. - */ - override val animals get() = mobs && animalsSetting - - /** - * Whether invisible entities are included in the targeting scope. - */ - override val invisible by c.setting("Invisible", true).group(baseGroup) - - /** - * Whether dead entities are included in the targeting scope. - */ - override val dead by c.setting("Dead", false).group(baseGroup) - - /** - * Validates whether a given entity is targetable by the player based on current settings. - * - * @param player The [ClientPlayerEntity] performing the targeting. - * @param entity The [LivingEntity] being evaluated. - * @return `true` if the entity is valid for targeting, `false` otherwise. - */ - open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when { - !players && entity is OtherClientPlayerEntity -> false - players && entity is OtherClientPlayerEntity && entity.isFriend -> false - !animals && entity is PassiveEntity -> false - !hostiles && entity is HostileEntity -> false - entity is ArmorStandEntity -> false - - !invisible && entity.isInvisibleTo(player) -> false - !dead && entity.isDead -> false - - else -> true - } - - /** - * Subclass for targeting entities specifically for combat purposes. - * - * @property fov The field of view limit within which entities are considered for targeting. Configurable. - * @property priority The priority used to determine which entity is targeted when multiple candidates are available. - */ - class Combat( - c: Configurable, - baseGroup: NamedEnum, - defaultRange: Double = 5.0, - maxRange: Double = 16.0, - ) : Targeting(c, baseGroup, defaultRange, maxRange) { - - /** - * The field of view limit for targeting entities. Configurable between 5 and 180 degrees. - */ - val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup) - - /** - * The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance]. - */ - val priority by c.setting("Priority", Priority.Distance).group(baseGroup) - - /** - * Validates whether a given entity is targetable for combat based on the field of view limit and other settings. - * - * @param player The [ClientPlayerEntity] performing the targeting. - * @param entity The [LivingEntity] being evaluated. - * @return `true` if the entity is valid for targeting, `false` otherwise. - */ - override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean { - if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false - if (entity.uuid in illegalTargets) return false - return super.validate(player, entity) - } - - /** - * Gets the best target for combat based on the current settings and priority. - * - * @return The best [LivingEntity] target, or `null` if no valid target is found. - */ - fun target(): LivingEntity? = runSafe { - return@runSafe fastEntitySearch(targetingRange) { - validate(player, it) - }.minByOrNull { - priority.factor(this, it) - } - } - - private val illegalTargets = setOf( - UUID(5706954458220675710, -6736729783554821869), - UUID(-2945922493004570036, -7599209072395336449) - ) - } - - /** - * Subclass for targeting entities for ESP (Extrasensory Perception) purposes. - */ - class ESP( - c: Configurable, - baseGroup: NamedEnum, - ) : Targeting(c, baseGroup, 128.0, 1024.0) - - /** - * Enum representing the different priority factors used for determining the best target. - * - * @property factor A lambda function that calculates the priority factor for a given [LivingEntity]. - */ - @Suppress("Unused") - enum class Priority(val factor: SafeContext.(LivingEntity) -> Double) { - /** - * Prioritizes entities based on their distance from the player. - */ - Distance({ player.pos distSq it.pos }), - - /** - * Prioritizes entities based on their health. - */ - Health({ it.fullHealth }), - - /** - * Prioritizes entities based on their angle relative to the player's field of view. - */ - Fov({ player.rotation dist player.eyePos.rotationTo(it.pos) }) - } + /** + * The range within which entities can be targeted. This value is configurable and constrained + * between 1.0 and [maxRange]. + */ + override val targetingRange by c.setting("Targeting Range", defaultRange, 1.0..maxRange, 0.05).group(baseGroup) + + /** + * Whether players are included in the targeting scope. + */ + override val players by c.setting("Players", true).group(baseGroup) + + /** + * Whether friends are included in the targeting scope. + * Requires [players] to be true. + */ + override val friends by c.setting("Friends", false) { players }.group(baseGroup) + + /** + * Whether mobs are included in the targeting scope. + */ + private val mobs by c.setting("Mobs", true).group(baseGroup) + + /** + * Whether hostile mobs are included in the targeting scope + */ + private val hostilesSetting by c.setting("Hostiles", true) { mobs }.group(baseGroup) + + /** + * Whether hostile entities should be only attacked when they are angry + */ + private val hostileOnlyAngrySetting by c.setting( + "Hostiles Only Angry", true, + "Only attacks angerable entities if they are angered. This does not affect for example zombies but does affect endermen." + ) { hostilesSetting }.group(baseGroup) + + /** + * Whether passive entities are included in the targeting scope + */ + private val passivesSetting by c.setting("Passives", false) { mobs }.group(baseGroup) + + /** + * Whether animals are included in the targeting scope + */ + private val animalsSetting by c.setting("Animals", true) { mobs }.group(baseGroup) + + private val animalsOnlyAngrySetting by c.setting( + "Animals Only Angry", false, + "Only attacks Animals if they are angry. For example Bees and Wolves." + ) { animalsSetting }.group(baseGroup) + + /** + * Whether golems are included in the targeting scope + */ + private val golemsSetting by c.setting("Golems", true) { mobs }.group(baseGroup) + + private val golemsOnlyAngrySetting by c.setting( + "Golems Only Angry", + true, + "Iron and Copper golems." + ) { golemsSetting }.group(baseGroup) + + /** + * Indicates whether hostile entities are included in the targeting scope. + */ + override val hostiles get() = mobs && hostilesSetting + + override val hostilesOnlyAngry: Boolean + get() = mobs && hostilesSetting && hostileOnlyAngrySetting + + /** + * Indicates whether passive entities are included in the targeting scope. + */ + override val passives get() = mobs && passivesSetting + + /** + * Indicates whether animals are included in the targeting scope. + */ + override val animals get() = mobs && animalsSetting + override val animalsOnlyAngry get() = mobs && animalsSetting && animalsOnlyAngrySetting + + override val golems get() = mobs && golemsSetting + override val golemsOnlyAngry get() = mobs && golemsSetting && golemsOnlyAngrySetting + + /** + * Whether invisible entities are included in the targeting scope. + */ + override val invisible by c.setting("Invisible", true).group(baseGroup) + + /** + * Whether dead entities are included in the targeting scope. + */ + override val dead by c.setting("Dead", false).group(baseGroup) + + /** + * Validates whether a given entity is targetable by the player based on current settings. + * + * @param player The [ClientPlayerEntity] performing the targeting. + * @param entity The [LivingEntity] being evaluated. + * @return `true` if the entity is valid for targeting, `false` otherwise. + */ + open fun validate(player: ClientPlayerEntity, entity: LivingEntity) = when { + players && entity is OtherClientPlayerEntity -> true + !players && entity is OtherClientPlayerEntity && entity.isFriend -> true + animals && (entity is AnimalEntity || entity is AmbientEntity) -> { + if (animalsOnlyAngry) { + LambdaAngerManagement.isEntityAngry(entity.uuid) + } else { + true + } + } + passives && entity is PassiveEntity -> true + hostiles && entity is HostileEntity -> { + if (hostilesOnlyAngry) { + LambdaAngerManagement.isEntityAngry(entity.uuid) + } else { + true + } + } + + /* For some reason Shulkers are golems, I decided to exclude them from the selection as what does it matter for the player */ + golems && entity is GolemEntity && entity !is ShulkerEntity -> { + if (golemsOnlyAngry) { + LambdaAngerManagement.isEntityAngry(entity.uuid) + } else { + true + } + } + entity is ArmorStandEntity -> false + + invisible && entity.isInvisibleTo(player) -> true + dead && entity.isDead -> true + + else -> false + } + + /** + * Subclass for targeting entities specifically for combat purposes. + * + * @property fov The field of view limit within which entities are considered for targeting. Configurable. + * @property priority The priority used to determine which entity is targeted when multiple candidates are available. + */ + class Combat( + c: Configurable, + baseGroup: NamedEnum, + defaultRange: Double = 5.0, + maxRange: Double = 16.0, + ) : Targeting(c, baseGroup, defaultRange, maxRange) { + + /** + * The field of view limit for targeting entities. Configurable between 5 and 180 degrees. + */ + val fov by c.setting("FOV Limit", 180, 5..180, 1) { priority == Priority.Fov }.group(baseGroup) + + /** + * The priority used to determine which entity is targeted. Configurable with default set to [Priority.Distance]. + */ + val priority by c.setting("Priority", Priority.Distance).group(baseGroup) + + /** + * Validates whether a given entity is targetable for combat based on the field of view limit and other settings. + * + * @param player The [ClientPlayerEntity] performing the targeting. + * @param entity The [LivingEntity] being evaluated. + * @return `true` if the entity is valid for targeting, `false` otherwise. + */ + override fun validate(player: ClientPlayerEntity, entity: LivingEntity): Boolean { + if (fov < 180 && player.rotation dist player.eyePos.rotationTo(entity.pos) > fov) return false + if (entity.uuid in illegalTargets) return false + return super.validate(player, entity) + } + + /** + * Gets the best target for combat based on the current settings and priority. + * + * @return The best [LivingEntity] target, or `null` if no valid target is found. + */ + fun target(): LivingEntity? = runSafe { + return@runSafe fastEntitySearch(targetingRange) { + validate(player, it) + }.minByOrNull { + priority.factor(this, it) + } + } + + private val illegalTargets = setOf( + UUID(5706954458220675710, -6736729783554821869), + UUID(-2945922493004570036, -7599209072395336449) + ) + } + + /** + * Subclass for targeting entities for ESP (Extrasensory Perception) purposes. + */ + class ESP( + c: Configurable, + baseGroup: NamedEnum, + ) : Targeting(c, baseGroup, 128.0, 1024.0) + + /** + * Enum representing the different priority factors used for determining the best target. + * + * @property factor A lambda function that calculates the priority factor for a given [LivingEntity]. + */ + @Suppress("Unused") + enum class Priority(val factor: SafeContext.(LivingEntity) -> Double) { + /** + * Prioritizes entities based on their distance from the player. + */ + Distance({ player.pos distSq it.pos }), + + /** + * Prioritizes entities based on their health. + */ + Health({ it.fullHealth }), + + /** + * Prioritizes entities based on their angle relative to the player's field of view. + */ + Fov({ player.rotation dist player.eyePos.rotationTo(it.pos) }) + } } diff --git a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt index a54df2231..cb7c1dcfa 100644 --- a/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt +++ b/src/main/kotlin/com/lambda/config/groups/TargetingConfig.kt @@ -23,7 +23,12 @@ interface TargetingConfig { val players: Boolean val friends: Boolean val hostiles: Boolean + val hostilesOnlyAngry: Boolean + val passives: Boolean val animals: Boolean + val animalsOnlyAngry: Boolean + val golems: Boolean + val golemsOnlyAngry: Boolean val invisible: Boolean val dead: Boolean diff --git a/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt b/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt new file mode 100644 index 000000000..bde46ecee --- /dev/null +++ b/src/main/kotlin/com/lambda/world/LambdaAngerManagement.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.world + +import com.lambda.Lambda.mc +import com.lambda.core.Loadable +import com.lambda.event.events.ConnectionEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.threading.runSafeConcurrent +import com.lambda.util.extension.tickDelta +import com.lambda.util.math.distSq +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import net.minecraft.entity.mob.PiglinActivity +import net.minecraft.entity.mob.PiglinEntity +import net.minecraft.entity.mob.ZombifiedPiglinEntity +import net.minecraft.entity.passive.BeeEntity +import net.minecraft.entity.passive.PolarBearEntity +import net.minecraft.entity.passive.WolfEntity +import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket +import net.minecraft.util.math.Vec3d +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class LambdaAngerManagement: Loadable { + + /* + for future reference, these are the entities that use this + enderman + zombified piglin + bee + iron golem + wolf + polar bear + + private static final UniformIntProvider ANGER_TIME_RANGE = TimeHelper.betweenSeconds(20, 39); + + though because zombified piglins tell their friends with a delay and the anger time is counted + for each entity separately they will not stop attacking, this seems to be a bug in the game + + */ + + private var i = 0 + + init { + listen { event -> + if (event.packet is PlaySoundS2CPacket) { + val uuid = findClosestEntity(event.packet) ?: return@listen // this was not a sound that entities emit when angry + registerAngryEntity(uuid) + } + } + listen { + if (i++ % 10 != 0) return@listen + i = 0 + runSafeConcurrent { + mc.world?.entities?.forEach { entity -> + when (entity) { + is BeeEntity -> { + if (entity.hasAngerTime()) { + registerAngryEntity(entity.uuid) + } + } + is WolfEntity -> { + if (entity.hasAngerTime()) { + registerAngryEntity(entity.uuid) + } + } + is ZombifiedPiglinEntity -> { + if (entity.isAttacking) { + registerAngryEntity(entity.uuid) + } + } + is PiglinEntity -> { + if (entity.activity == PiglinActivity.ATTACKING_WITH_MELEE_WEAPON || + entity.activity == PiglinActivity.CROSSBOW_CHARGE + ) { + registerAngryEntity(entity.uuid) + } + } + is PolarBearEntity -> { + if (entity.getWarningAnimationProgress(mc.tickDelta) > 0f) { + registerAngryEntity(entity.uuid) + } + } + } + } ?: return@runSafeConcurrent + cleanupUnloadedEntities() + } + } + listen { + angryEntities.clear() + } + // todo: task every 5 minutes or something to clean up old angry entities + } + + companion object { + const val ANGER_DURATION_MS = 30000L + private val angryEntities = ConcurrentHashMap() + + @JvmStatic + private fun matchLocations(vec3d: Vec3d, radius: Double = 3.0): UUID? { + val maxDistSq = radius * radius + return mc.world?.entities + ?.asSequence() + ?.map { it to it.distSq(vec3d) } + ?.filter { it.second <= maxDistSq } + ?.minByOrNull { it.second } + ?.first?.uuid + } + + @JvmStatic + private fun findClosestEntity(packet: PlaySoundS2CPacket): UUID? { + val vec3 = Vec3d(packet.x, packet.y, packet.z) + val soundId = packet.sound.value().id.path + return when { + soundId.contains(".growl") || // (wolf) randomly while angry + soundId.contains(".stare") || // when it mad when u look at it + soundId.contains(".scream") || // randomly when hostile + soundId.contains(".angry") // (zombie pigmen) randomly when angry and when getting angry + -> matchLocations(vec3) + else -> null + } + } + /* + todo: spiders + + things that give them away when they are aggro: + - head directly towards the player + - the "jump attack", can be easily detected as normally they don't have + that much positive y velocity when climbing + - spiders could be tracked separately and the "aggro score" could be + decreased if they are not moving as their normal pathing behavior is to + path randomly around ~5-10 blocks, no jumps, and then stop for a while + */ + + @JvmStatic + fun registerAngryEntity(entityUuid: UUID) { + angryEntities[entityUuid] = System.currentTimeMillis() + } + + @JvmStatic + fun isEntityAngry(entityUuid: UUID): Boolean { + val timestamp = angryEntities[entityUuid] ?: return false + val isAngry = System.currentTimeMillis() - timestamp < ANGER_DURATION_MS + if (!isAngry) { + angryEntities.remove(entityUuid) + } + return isAngry + } + + + @JvmStatic + suspend fun cleanupUnloadedEntities() = coroutineScope { + async { + angryEntities.keys.removeIf { uuid -> + !(mc.world?.entities?.any { it.uuid == uuid } ?: false) + } + } + } + + @JvmStatic + fun removeAngryEntity(entityUuid: UUID) { + angryEntities.remove(entityUuid) + } + } +} \ No newline at end of file