diff --git a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt b/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt index 331086e9e..af9a22ef4 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/hotbar/HotbarRequest.kt @@ -20,6 +20,18 @@ package com.lambda.interaction.managers.hotbar import com.lambda.context.Automated import com.lambda.interaction.managers.Request +/** + * A request to change the hotbar slot. + * If you are having an issue with the slot not changing, try setting [nowOrNothing] to false. + * + * Example usage: + * + * val slot = 0 // 0-8, 0 = left-most slot + * // ModuleObjectName is the name of the module class basically, so for example, in CrystalAura it is this@CrystalAura + * // nowOrNothing is true by default, silent swapping requires it to be true, normal swapping does not + * // .done checks if the request is done so if it is false you don't want to continue execution + * if (!HotbarRequest(slot, this@ModuleObjectName, nowOrNothing = false).submit().done) return@runSafe + */ class HotbarRequest( val slot: Int, automated: Automated, diff --git a/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt similarity index 58% rename from src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt rename to src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt index 9bfa55a20..7ab76388f 100644 --- a/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt +++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAura.kt @@ -15,11 +15,13 @@ * along with this program. If not, see . */ -package com.lambda.module.modules.combat +package com.lambda.module.modules.combat.crystalaura import com.lambda.config.Tab import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig import com.lambda.config.blocks.TargetingSettings +import com.lambda.config.blocks.WorldLineSettings +import com.lambda.config.forEachSetting import com.lambda.config.hide import com.lambda.config.hideAllExcept import com.lambda.config.withEdits @@ -27,22 +29,15 @@ import com.lambda.context.SafeContext import com.lambda.event.events.EntityEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.interaction.handlers.ContainerHandler.transfer -import com.lambda.interaction.managers.hotbar.HotbarRequest -import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest -import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo -import com.lambda.interaction.managers.rotating.RotationManager -import com.lambda.interaction.material.StackSelection.Companion.selectStack -import com.lambda.interaction.material.container.containers.HotbarContainer -import com.lambda.interaction.material.container.containers.OffHandContainer +import com.lambda.graphics.mc.renderer.ImmediateRenderer.Companion.immediateRenderer import com.lambda.module.Module +import com.lambda.module.modules.combat.crystalaura.CrystalAuraExploding.explodeInternal +import com.lambda.module.modules.combat.crystalaura.CrystalAuraPlacing.placeInternal import com.lambda.module.tag.ModuleTag import com.lambda.threading.runSafe -import com.lambda.threading.runSafeAutomated import com.lambda.threading.runSafeGameScheduled import com.lambda.util.BlockUtils.blockState import com.lambda.util.CommunicationUtils.info -import com.lambda.util.PacketUtils.sendPacket import com.lambda.util.Timer import com.lambda.util.collections.LimitedDecayQueue import com.lambda.util.combat.CombatUtils.crystalDamage @@ -54,22 +49,17 @@ import com.lambda.util.math.flooredBlockPos import com.lambda.util.math.getHitVec import com.lambda.util.math.minus import com.lambda.util.math.plus -import com.lambda.util.player.RotationUtils.getVisibleSurfaces -import com.lambda.util.player.SlotUtils.hotbarStacks import com.lambda.util.world.fastEntitySearch import net.minecraft.block.Blocks import net.minecraft.entity.Entity import net.minecraft.entity.LivingEntity import net.minecraft.entity.decoration.EndCrystalEntity -import net.minecraft.item.Items -import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket -import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket import net.minecraft.util.Hand -import net.minecraft.util.hit.BlockHitResult import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Box import net.minecraft.util.math.Direction import net.minecraft.util.math.Vec3d +import java.awt.Color import kotlin.concurrent.fixedRateTimer import kotlin.math.max import kotlin.time.Duration.Companion.milliseconds @@ -85,19 +75,25 @@ object CrystalAura : Module( private const val EXPLODING_TAB = "Exploding" private const val PREDICTION_TAB = "Prediction" private const val TARGETING_TAB = "Targeting" + private const val RENDERING_TAB = "Rendering" - @Tab(GENERAL_TAB) private val rotate by setting("Rotate", true) - @Tab(GENERAL_TAB) private val updateMode by setting("Update Mode", UpdateMode.Async) + @Tab(GENERAL_TAB) + val rotate by setting("Rotate", true) + @Tab(GENERAL_TAB) private val updateMode by setting("Update Mode", UpdateMode.Ticked) @Tab(GENERAL_TAB) private val updateDelaySetting by setting("Update Delay", 25L, 5L..200L, 5L, unit = " ms") { updateMode == UpdateMode.Async } @Tab(GENERAL_TAB) private val maxUpdatesPerFrame by setting("Max Updates Per Frame", 5, 1..20, 1) { updateMode == UpdateMode.Async } @Tab(GENERAL_TAB) private val updateDelay get() = if (updateMode == UpdateMode.Async) updateDelaySetting else 0L @Tab(GENERAL_TAB) private val debug by setting("Debug", false) @Tab(PLACEMENT_TAB) private val placeRange by setting("Place Range", 4.6, 1.0..7.0, 0.1, "Range to place crystals", " blocks") - @Tab(PLACEMENT_TAB) private val placeDelay by setting("Place Delay", 50L, 0L..1000L, 1L, "Delay between placement attempts", " ms") - @Tab(PLACEMENT_TAB) private val swap by setting("Swap", true, "Swaps to crystals") - @Tab(PLACEMENT_TAB) private val swapHand by setting("Swap Hand", Hand.MAIN_HAND, "Which hand to swap the crystal to") { swap } - @Tab(PLACEMENT_TAB) private val priorityMode by setting("Crystal Priority", Priority.Damage) + @Tab(PLACEMENT_TAB) + val placeDelay by setting("Place Delay", 50L, 0L..1000L, 1L, "Delay between placement attempts", " ms") + @Tab(PLACEMENT_TAB) + val swap by setting("Swap", true, "Swaps to crystals") + @Tab(PLACEMENT_TAB) + val swapHand by setting("Swap Hand", Hand.MAIN_HAND, "Which hand to swap the crystal to") { swap } + @Tab(PLACEMENT_TAB) + val priorityMode by setting("Crystal Priority", Priority.Damage) @Tab(PLACEMENT_TAB) private val minDamageAdvantage by setting("Min Damage Advantage", 4.0, 1.0..10.0, 0.5) { priorityMode == Priority.Advantage } @Tab(PLACEMENT_TAB) private val minTargetDamage by setting("Min Target Damage", 8.0, 0.0..20.0, 0.5, "Minimum target damage to use crystals") @Tab(PLACEMENT_TAB) private val maxSelfDamage by setting("Max Self Damage", 8.0, 0.0..36.0, 0.5, "Maximum self damage to use crystals") @@ -106,17 +102,43 @@ object CrystalAura : Module( @Tab(PLACEMENT_TAB) private val oldPlace by setting("1.12 Placement", false) @Tab(EXPLODING_TAB) private val explodeRange by setting("Explode Range", 3.0, 1.0..7.0, 0.1, "Range to explode crystals", " blocks") - @Tab(EXPLODING_TAB) private val explodeDelay by setting("Explode Delay", 10L, 0L..1000L, 1L, "Delay between explosion attempts", " ms") - - @Tab(PREDICTION_TAB) private val prediction by setting("Prediction", PredictionMode.None) - @Tab(PREDICTION_TAB) private val packetPredictions by setting("Packet Predictions", 1, 0..20, 1) { prediction.onPacket } - @Tab(PREDICTION_TAB) private val placePostPause by setting("Place Post Pause", true) { prediction.onPacket } - @Tab(PREDICTION_TAB) private val placePredictions by setting("Place Predictions", 4, 1..20, 1) { prediction.onPlace } - @Tab(PREDICTION_TAB) private val packetLifetime by setting("Packet Lifetime", 500L, 50L..1000L) { prediction.onPlace } + @Tab(EXPLODING_TAB) + val explodeDelay by setting("Explode Delay", 10L, 0L..1000L, 1L, "Delay between explosion attempts", " ms") + + @Tab(PREDICTION_TAB) + val prediction by setting("Prediction", PredictionMode.None) + @Tab(PREDICTION_TAB) private val packetPredictions by setting("Packet Predictions", 1, 0..20, 1, "Flags grim") { prediction.onPacket } + @Tab(PREDICTION_TAB) private val explodeOnPacket by setting("Explode On Packet", false, "Explodes the received crystal on packet") { prediction != PredictionMode.None } + @Tab(PREDICTION_TAB) + val placePostPause by setting("Place Post Pause", false, "Resets the place delay timer after receiving a entity spawn packet (adds a delay)") { prediction != PredictionMode.None } + @Tab(PREDICTION_TAB) + val postPacketPlace by setting("Post Packet Place", true, "Places the crystal on the next tick from the entity spawn packet") { prediction == PredictionMode.Tick && !placePostPause } + @Tab(PREDICTION_TAB) + val placePredictions by setting("Place Predictions", 4, 1..20, 1) { prediction.onPlace } + @Tab(PREDICTION_TAB) + val packetLifetime by setting("Packet Lifetime", 500L, 50L..1000L) { prediction.onPlace } @Tab(PREDICTION_TAB) private val targetingSettings by configBlock(TargetingSettings.CombatSettings(this, 10.0)) + @Tab(RENDERING_TAB) private val render by setting("Rendering", true) + @Tab(RENDERING_TAB) private val renderPlacements by setting("Placement Rendering", true) + + @Tab(RENDERING_TAB) private val renderLineSettings by configBlock(WorldLineSettings(this)) + .withEdits { + hideAllExcept(::worldWidthSetting) + forEachSetting { + visibility { old -> { old() && render } } + } + } + @Tab(RENDERING_TAB) val primaryColorLine by setting("Primary Color (outline)", Color(130, 200, 255, 200), visibility = { render }) + @Tab(RENDERING_TAB) val secondaryColorLine by setting("Secondary Color (outline)", Color(130, 130, 255, 200), visibility = { render }) + @Tab(RENDERING_TAB) val primaryColor by setting("Primary Color", Color(225, 130, 225, 100), visibility = { render }) + @Tab(RENDERING_TAB) val secondaryColor by setting("Secondary Color", Color(170, 60, 170, 100), visibility = { render }) + private val blueprint = mutableMapOf() + var lastHit: BlockPos? = null + var blockingCrystal: EndCrystalEntity? = null + private var lastOpportunity: Opportunity? = null private var activeOpportunity: Opportunity? = null private var currentTarget: LivingEntity? = null @@ -127,14 +149,17 @@ object CrystalAura : Module( private val updateTimer = Timer() private var updatesThisFrame = 0 - private val placeTimer = Timer() - private val explodeTimer = Timer() + val placeTimer = Timer() + val explodeTimer = Timer() - private val predictionTimer = Timer() - private var lastEntityId = 0 + val predictionTimer = Timer() + var lastEntityId = 0 + var waitingForCrystal = false + var safeToPlaceInstantly = false private val decay = LimitedDecayQueue(10000, 3000L) + var lastPlace: Pair? = null private val collidingOffsets = mutableListOf().apply { for (x in -1..1) { for (z in -1..1) { @@ -150,46 +175,46 @@ object CrystalAura : Module( .withEdits { hideAllExcept(::buildConfig, ::rotationConfig, ::hotbarConfig, ::inventoryConfig) buildConfig.apply { - hide( - ::pathing, ::collectDrops, ::spleefEntities, - ::maxPendingActions, ::actionTimeout, ::maxBuildDependencies, ::breakBlocks, ::interactBlocks, ::placeBlocks - ) + hide( + ::pathing, ::collectDrops, ::spleefEntities, + ::maxPendingActions, ::actionTimeout, ::maxBuildDependencies, ::breakBlocks, ::interactBlocks, ::placeBlocks + ) } } // Async ticking - fixedRateTimer( - name = "Crystal Aura Thread", - daemon = true, - initialDelay = 0L, - period = 1L - ) { - if (isDisabled || updateMode != UpdateMode.Async) return@fixedRateTimer - - runSafe { - // timer may spam faster than main thread computes (game freezes completely at the beginning of the frame) - if (updatesThisFrame > maxUpdatesPerFrame) return@runSafe - updatesThisFrame++ - - // run this safely again to ensure that the context will stay safe at the next frame - runSafeGameScheduled { - tick() - } - } - } + fixedRateTimer( + name = "Crystal Aura Thread", + daemon = true, + initialDelay = 0L, + period = 1L + ) { + if (isDisabled || updateMode != UpdateMode.Async) return@fixedRateTimer + + runSafe { + // timer may spam faster than main thread computes (game freezes completely at the beginning of the frame) + if (updatesThisFrame > maxUpdatesPerFrame) return@runSafe + updatesThisFrame++ + + // run this safely again to ensure that the context will stay safe at the next frame + runSafeGameScheduled { + tick() + } + } + } - fixedRateTimer( - name = "CA Counter", - daemon = true, - initialDelay = 0L, - period = 1000L - ) { - if (isDisabled || !debug) return@fixedRateTimer + fixedRateTimer( + name = "CA Counter", + daemon = true, + initialDelay = 0L, + period = 1000L + ) { + if (isDisabled || !debug) return@fixedRateTimer - runSafeGameScheduled { - info((decay.size.toDouble() * 0.3333).roundToStep(0.1).toString()) - } - } + runSafeGameScheduled { + info((decay.size.toDouble() * 0.3333).roundToStep(0.1).toString()) + } + } // Ticking with alignment listen { @@ -216,18 +241,28 @@ object CrystalAura : Module( opportunity.crystal = crystal // Run packet prediction - if (!prediction.isActive || activeOpportunity != opportunity) return@listen + if (!prediction.isActive || (opportunity.priority < minDamageAdvantage && opportunity.blockPos.distSq(lastPlace?.first ?: pos) > 2.5)) { + return@listen + } - explodeInternal(lastEntityId) + lastEntityId = crystal.id - if (!prediction.onPacket) return@listen + if (explodeOnPacket) { + repeat(packetPredictions) { + explodeInternal(++lastEntityId) + } + } - repeat(packetPredictions) { - placeInternal(opportunity, swapHand) - explodeInternal(++lastEntityId) + if (!prediction.onPacket) { + // this only works in vanilla + repeat(packetPredictions) { + placeInternal(opportunity, swapHand) + explodeInternal(++lastEntityId) + } } if (placePostPause) placeTimer.reset() + if (postPacketPlace) safeToPlaceInstantly = true } listen { event -> @@ -240,8 +275,26 @@ object CrystalAura : Module( decay += crystal.id } + immediateRenderer("CrystalAura Immediate Renderer") { + runSafe { + val place = lastPlace ?: return@runSafe + if (place.second + 100 < System.currentTimeMillis()) { + return@runSafe + } + + box( + place.first, + renderLineSettings + ) { + outlineGradientY(secondaryColorLine, primaryColorLine) + fillGradientY(secondaryColor, primaryColor) + } + } + } + onEnable { currentTarget = null + lastHit = null resetBlueprint() } } @@ -262,9 +315,27 @@ object CrystalAura : Module( } private fun tickInteraction(best: Opportunity) { - if (!best.blocked) { - best.explode() - best.place() + fun handleExplosion(opportunity: Opportunity) { + opportunity.explode() + if (prediction.onTick) { + explodeTimer.runSafeIfPassed(explodeDelay.milliseconds) { + if (waitingForCrystal) { + CrystalAura.explodeInternal(++lastEntityId) + waitingForCrystal = false + lastHit = opportunity.blockPos + explodeTimer.reset() + } + } + } + } + if (best.blocked && blockingCrystal != null) { + handleExplosion(blueprint[blockingCrystal?.baseBlockPos?: return]?: return) + + blockingCrystal = null + return + } + if (!best.blocked && best.crystal != null) { + handleExplosion(best) return } @@ -279,32 +350,13 @@ object CrystalAura : Module( ) blueprint[mutableBlockPos] - }.filter { it.hasCrystal }.maxByOrNull { it.priority }?.explode() - + }.filter { it.hasCrystal }.maxByOrNull { it.priority }?.let { + handleExplosion(it) + return@tickInteraction + } best.place() } - private fun SafeContext.placeInternal(opportunity: Opportunity, hand: Hand) { - interaction.syncSelectedSlot() - connection.sendPacket { - PlayerInteractBlockC2SPacket( - hand, BlockHitResult(opportunity.crystalPosition, opportunity.side, opportunity.blockPos, false), 0 - ) - } - - player.swingHand(hand) - } - - private fun SafeContext.explodeInternal(id: Int) { - connection.sendPacket { - PlayerInteractEntityC2SPacket( - id, player.isSneaking, PlayerInteractEntityC2SPacket.ATTACK - ) - } - - player.swingHand(Hand.MAIN_HAND) - } - private fun SafeContext.updateBlueprint(target: LivingEntity) = updateTimer.runIfPassed(updateDelay.milliseconds) { resetBlueprint() @@ -313,10 +365,23 @@ object CrystalAura : Module( pos: BlockPos, target: LivingEntity, blocked: Boolean, - crystal: EndCrystalEntity? = null + crystal: EndCrystalEntity? = null, + _blockingCrystal: EndCrystalEntity? = null ): Opportunity? { val crystalPos = pos.crystalPosition + if (blockingCrystal != null && crystal == blockingCrystal) { + return Opportunity( + this@CrystalAura, + pos.toImmutable(), + Double.MAX_VALUE, // always prioritize the crystal that is blocking the placement + 0.0, + false, + crystal, + null + ) + } + val targetDamage = crystalDamage(crystalPos, target) if (targetDamage < minTargetDamage) return null @@ -333,18 +398,20 @@ object CrystalAura : Module( ) return null return Opportunity( + this@CrystalAura, pos.toImmutable(), targetDamage, selfDamage, blocked, - crystal + crystal, + _blockingCrystal ) } // Extra checks for placement, because you may explode but not place in special cases(crystal in the air) fun placeInfo( - pos: BlockPos, - target: LivingEntity + pos: BlockPos, + target: LivingEntity ): Opportunity? { // Check if crystals could be placed on the base block val state = blockState(pos) @@ -361,7 +428,8 @@ object CrystalAura : Module( val entitiesNearby = fastEntitySearch(3.5, pos) val crystals = entitiesNearby.filterIsInstance() - val otherEntities = entitiesNearby - crystals + player + // why does the player get removed from this? + val otherEntities = entitiesNearby - crystals.toSet() + player if (otherEntities.any { it.boundingBox.intersects(crystalBox) @@ -372,16 +440,18 @@ object CrystalAura : Module( it.baseBlockPos == pos } - val crystalPlaceBox = pos.crystalPlaceHitBox - val blocked = baseCrystal == null && crystals.any { - it.boundingBox.intersects(crystalPlaceBox) + val crystalPlaceBox = pos.up().crystalPlaceHitBox + blockingCrystal = crystals.firstOrNull { + it.blockPos.crystalBox.intersects(crystalPlaceBox) } + val blocked = baseCrystal == null && blockingCrystal != null return info( pos, target, blocked, - baseCrystal + baseCrystal, + blockingCrystal ) } @@ -392,6 +462,7 @@ object CrystalAura : Module( val crystalBase = BlockPos.Mutable() fastEntitySearch(range).forEach { crystal -> crystalBase.set(crystal.x, crystal.y - 0.5, crystal.z) + //if (crystalBase == lastHit) return@forEach damage += info(crystalBase, target, false, crystal) ?: return@forEach } @@ -411,16 +482,33 @@ object CrystalAura : Module( // Associate by actions blueprint.values.forEach { opportunity -> actionMap.getOrPut(opportunity.actionType, ::mutableListOf) += opportunity - if (opportunity.actionType.priority > actionType.priority) { actionType = opportunity.actionType } } - + // todo: optimize this + val blocked = mutableSetOf() // Select best action - activeOpportunity = actionMap[actionType]?.maxByOrNull { + activeOpportunity = actionMap[actionType]?.filter { + !it.blocked || lastHit == it.blockPos + }?.maxByOrNull { + it.priority + } + + if (activeOpportunity != null) { + return@runIfPassed + } + + actionMap[actionType]?.filter { + it.blocked + }?.toCollection(blocked) + + val best = blocked.maxByOrNull { it.priority } + activeOpportunity = actionMap[actionType]?.firstOrNull { + it.crystal == best?.blockingCrystal + } } private fun resetBlueprint() { @@ -430,128 +518,26 @@ object CrystalAura : Module( activeOpportunity = null } - /** - * Represents the damage information resulting from placing an end crystal on a given [blockPos] - * and causing an explosion that targets current target entity. - * - * @property blockPos The position of the base block where the crystal is placed. - * @property target The amount of damage inflicted on the target. - * @property self The amount of damage inflicted on the player. - * @property blocked Whether the placement on [blockPos] is blocked by other crystals. - * @property crystal A crystal that is placed on [blockPos]. - */ - private class Opportunity( - val blockPos: BlockPos, - val target: Double, - val self: Double, - var blocked: Boolean, - var crystal: EndCrystalEntity? // ToDo: packet-based update wisely - ) { - var actionType = ActionType.Normal - val priority = priorityMode.factor(target, self) - val hasCrystal get() = crystal != null - - val crystalPosition by lazy { - blockPos.crystalPosition - } - - val side by lazy { - runSafe { - val visibleSides = Box(blockPos).getVisibleSurfaces(player.eyePos) - if (visibleSides.contains(Direction.UP)) Direction.UP else visibleSides.minByOrNull { - blockPos.getHitVec(it) distSq player.eyePos - } - } ?: Direction.UP - } - - val placeRotation by lazy { - runSafe { - var vec = blockPos.getHitVec(side) - - // look at the top part of the side - if (side.axis != Direction.Axis.Y) vec += Vec3d(0.0, 0.45, 0.0) - - player.eyePos.rotationTo(vec) - } ?: RotationManager.activeRotation - } - - /** - * Places the crystal on [blockPos] - */ - fun place() = runSafe { - if (rotate && !rotationRequest { rotation(placeRotation) }.submit().done) - return@runSafe - - val selection = selectStack { isItem(Items.END_CRYSTAL) } - if ((swapHand == Hand.MAIN_HAND && player.mainHandStack.item != selection.item) || - (swapHand == Hand.OFF_HAND && player.offHandStack.item != selection.item) - ) runSafeAutomated { - if (!swap) return@runSafe - var crystalSlot = player.hotbarStacks.indexOfFirst { selection.filterStack(it) } - if (crystalSlot < 0) { - val swapTo = when (swapHand) { - Hand.MAIN_HAND -> HotbarContainer - Hand.OFF_HAND -> OffHandContainer - } - if (!selection.transfer(swapTo)) return@runSafe - crystalSlot = player.hotbarStacks.indexOfFirst { selection.filterStack(it) } - } - if (!HotbarRequest(crystalSlot, this).submit().done) return@runSafe - } - - placeTimer.runSafeIfPassed(placeDelay.milliseconds) { - placeInternal(this@Opportunity, swapHand) - - if (prediction.onPlace) - predictionTimer.runIfNotPassed(packetLifetime.milliseconds, false) { - val last = lastEntityId - - repeat(placePredictions) { - explodeInternal(++lastEntityId) - } - - lastEntityId = last + 1 - crystal = null - } - } - } - - /** - * Explodes a crystal that is on [blockPos] - * @return Whether the delay passed, null if the interaction failed or no crystal found - */ - fun explode() { - if (rotate && !rotationRequest { rotation(placeRotation) }.submit().done) return - - explodeTimer.runSafeIfPassed(explodeDelay.milliseconds) { - crystal?.let { crystal -> - explodeInternal(crystal.id) - explodeTimer.reset() - } - } - } - } - private val EndCrystalEntity.baseBlockPos get() = (pos - Vec3d(0.0, 0.5, 0.0)).flooredBlockPos - private val BlockPos.crystalPosition get() = + val BlockPos.crystalPosition get() = this.getHitVec(Direction.UP) private val BlockPos.crystalPlaceHitBox get() = crystalPosition.let { base -> - Box( - base - Vec3d(1.0, 0.0, 1.0), - base + Vec3d(1.0, 2.0, 1.0), - ) + Box( + base - Vec3d(1.0, 0.0, 1.0), + base + Vec3d(1.0, 2.0, 1.0), + ) } private val BlockPos.crystalBox get() = crystalPosition.let { base -> - Box( - base - Vec3d(0.5, 0.0, 0.5), - base + Vec3d(0.5, 2.0, 0.5), - ) + Box( + base - Vec3d(0.5, 0.0, 0.5), + base + Vec3d(0.5, 2.0, 0.5), + ) } private enum class UpdateMode { @@ -560,23 +546,30 @@ object CrystalAura : Module( } @Suppress("Unused") - private enum class PredictionMode(val onPacket: Boolean, val onPlace: Boolean) { + enum class PredictionMode(val onPacket: Boolean, val onPlace: Boolean, val onTick: Boolean) { // Prediction disable - None(false, false), + None(false, false, false), // Predict on packet receive - Packet(true, false), + Packet(true, false, false), // Predict on place - Deferred(false, true), + Deferred(false, true, false), + + // Predict on tick (functionality is in tickInteraction()) + // sends a burst of break packets every time the break cooldown has expired + // so it is kinda weird but may help with bypassing anticheats + // for grim on anarchy servers Packet is fine as BadPacketsW does not + // cancel the next packets even if we guess the crystal's id wrong + Tick(false, false, true), // Predict on both timings - Mixed(true, true); + Mixed(true, true, false); - val isActive = onPacket || onPlace + val isActive = onPacket || onPlace || onTick } - private enum class Priority(val factor: (targetDamage: Double, selfDamage: Double) -> Double) { + enum class Priority(val factor: (targetDamage: Double, selfDamage: Double) -> Double) { Damage({ target, _ -> target }), @@ -587,9 +580,9 @@ object CrystalAura : Module( // ToDo: implement actions @Suppress("Unused") - private enum class ActionType(val priority: Int) { + enum class ActionType(val priority: Int) { Normal(0), ForcePlace(1), SlowBreak(2) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraExploding.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraExploding.kt new file mode 100644 index 000000000..976670176 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraExploding.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 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.module.modules.combat.crystalaura + +import com.lambda.threading.runSafe +import com.lambda.util.PacketUtils.sendPacket +import net.minecraft.network.packet.c2s.play.PlayerInteractEntityC2SPacket +import net.minecraft.util.Hand + +object CrystalAuraExploding { + fun CrystalAura.explodeInternal(id: Int) = runSafe { + connection.sendPacket { + PlayerInteractEntityC2SPacket( + id, player.isSneaking, PlayerInteractEntityC2SPacket.ATTACK + ) + } + + player.swingHand(Hand.MAIN_HAND) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraPlacing.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraPlacing.kt new file mode 100644 index 000000000..533e81abb --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/CrystalAuraPlacing.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2026 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.module.modules.combat.crystalaura + +import com.lambda.threading.runSafe +import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket +import net.minecraft.util.Hand +import net.minecraft.util.hit.BlockHitResult + +object CrystalAuraPlacing { + fun CrystalAura.placeInternal(opportunity: Opportunity, hand: Hand) = runSafe { + lastPlace = Pair(opportunity.blockPos, System.currentTimeMillis()) + interaction.syncSelectedSlot() // TODO: when server only hotbar swap gets implemented, this will be removed + interaction.sendSequencedPacket(world) { sequence -> + PlayerInteractBlockC2SPacket( + hand, BlockHitResult(opportunity.crystalPosition, opportunity.side, opportunity.blockPos, false), sequence + ) + } + + player.swingHand(hand) + waitingForCrystal = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt new file mode 100644 index 000000000..a8827f757 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/combat/crystalaura/Opportunity.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2026 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.module.modules.combat.crystalaura + +import com.lambda.Lambda +import com.lambda.context.Automated +import com.lambda.interaction.handlers.ContainerHandler.transfer +import com.lambda.interaction.managers.hotbar.HotbarRequest +import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest +import com.lambda.interaction.managers.rotating.Rotation.Companion.rotationTo +import com.lambda.interaction.managers.rotating.RotationManager +import com.lambda.interaction.material.StackSelection +import com.lambda.interaction.material.container.containers.HotbarContainer +import com.lambda.interaction.material.container.containers.OffHandContainer +import com.lambda.module.modules.combat.crystalaura.CrystalAura.ActionType +import com.lambda.module.modules.combat.crystalaura.CrystalAura.PredictionMode +import com.lambda.module.modules.combat.crystalaura.CrystalAura.crystalPosition +import com.lambda.module.modules.combat.crystalaura.CrystalAura.explodeDelay +import com.lambda.module.modules.combat.crystalaura.CrystalAuraExploding.explodeInternal +import com.lambda.module.modules.combat.crystalaura.CrystalAura.explodeTimer +import com.lambda.module.modules.combat.crystalaura.CrystalAura.lastEntityId +import com.lambda.module.modules.combat.crystalaura.CrystalAura.lastHit +import com.lambda.module.modules.combat.crystalaura.CrystalAura.packetLifetime +import com.lambda.module.modules.combat.crystalaura.CrystalAura.placeDelay +import com.lambda.module.modules.combat.crystalaura.CrystalAura.placePostPause +import com.lambda.module.modules.combat.crystalaura.CrystalAura.placePredictions +import com.lambda.module.modules.combat.crystalaura.CrystalAura.placeTimer +import com.lambda.module.modules.combat.crystalaura.CrystalAura.postPacketPlace +import com.lambda.module.modules.combat.crystalaura.CrystalAura.prediction +import com.lambda.module.modules.combat.crystalaura.CrystalAura.predictionTimer +import com.lambda.module.modules.combat.crystalaura.CrystalAura.priorityMode +import com.lambda.module.modules.combat.crystalaura.CrystalAura.rotate +import com.lambda.module.modules.combat.crystalaura.CrystalAura.safeToPlaceInstantly +import com.lambda.module.modules.combat.crystalaura.CrystalAura.swap +import com.lambda.module.modules.combat.crystalaura.CrystalAura.swapHand +import com.lambda.module.modules.combat.crystalaura.CrystalAura.waitingForCrystal +import com.lambda.module.modules.combat.crystalaura.CrystalAuraPlacing.placeInternal +import com.lambda.threading.runSafe +import com.lambda.threading.runSafeAutomated +import com.lambda.util.math.distSq +import com.lambda.util.math.getHitVec +import com.lambda.util.math.plus +import com.lambda.util.player.RotationUtils.getVisibleSurfaces +import com.lambda.util.player.SlotUtils.hotbarStacks +import net.minecraft.entity.decoration.EndCrystalEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import net.minecraft.util.math.Direction +import net.minecraft.util.math.Vec3d +import kotlin.time.Duration.Companion.milliseconds + + +/** + * Represents the damage information resulting from placing an end crystal on a given [blockPos] + * and causing an explosion that targets current target entity. + * + * @property blockPos The position of the base block where the crystal is placed. + * @property target The amount of damage inflicted on the target. + * @property self The amount of damage inflicted on the player. + * @property blocked Whether the placement on [blockPos] is blocked by other crystals. + * @property crystal A crystal that is placed on [blockPos]. + */ +class Opportunity( + private val automated: Automated, + val blockPos: BlockPos, + val target: Double, + val self: Double, + var blocked: Boolean, + var crystal: EndCrystalEntity?, // ToDo: packet-based update wisely + var blockingCrystal: EndCrystalEntity? = null +) { + var actionType = ActionType.Normal + val priority = priorityMode.factor(target, self) + val hasCrystal get() = crystal != null + + val crystalPosition by lazy { + blockPos.crystalPosition + } + + val side by lazy { + runSafe { + val visibleSides = Box(blockPos).getVisibleSurfaces(player.eyePos) + if (visibleSides.contains(Direction.UP)) Direction.UP else visibleSides.minByOrNull { + blockPos.getHitVec(it) distSq player.eyePos + } + } ?: Direction.UP + } + + val placeRotation by lazy { + runSafe { + var vec = blockPos.getHitVec(side) + + // look at the top part of the side + if (side.axis != Direction.Axis.Y) vec + Vec3d(0.0, 0.45, 0.0) + + player.eyePos.rotationTo(vec) + } ?: RotationManager.activeRotation + } + + /** + * Places the crystal on [blockPos] + */ + fun place() = runSafe { + if (rotate && !automated.rotationRequest { rotation(placeRotation) }.submit().done) { + Lambda.LOG.info("Not yet rotated") + return@runSafe + } + var crystalHand: Hand? = null + val selection = StackSelection.selectStack { isItem(Items.END_CRYSTAL) } + if ((swapHand == Hand.MAIN_HAND && player.mainHandStack.item != Items.END_CRYSTAL) || + (swapHand == Hand.OFF_HAND && player.offHandStack.item != Items.END_CRYSTAL) + ) automated.runSafeAutomated { + if (!swap) return@runSafe + val itemToUse: ItemStack? = if (player.mainHandStack.item == Items.END_CRYSTAL) { + crystalHand = Hand.MAIN_HAND + player.mainHandStack + } else if (player.offHandStack.item == Items.END_CRYSTAL) { + crystalHand = Hand.OFF_HAND + player.offHandStack + } else null + + val swapTo = when (swapHand) { + Hand.MAIN_HAND -> HotbarContainer + Hand.OFF_HAND -> OffHandContainer + } + + if (itemToUse == null || swapHand == Hand.MAIN_HAND) { + if (swapHand == Hand.MAIN_HAND) { + val itemStack = selection.bestItemMatch(player.hotbarStacks) + if (itemStack == null) { // retrieve to hotbar + if (!selection.transfer(swapTo)) return@runSafe + } + val s = player.hotbarStacks.indexOf(itemStack) + if ((!HotbarRequest(s, automated, nowOrNothing = false).submit().done) && itemToUse == null) return@runSafe + } else { // retrieve to offhand + if (!selection.transfer(swapTo)) return@runSafe + } + } + } + + if (placeTimer.timePassed(placeDelay.milliseconds) || (postPacketPlace && safeToPlaceInstantly && !placePostPause)) { + CrystalAura.placeInternal(this@Opportunity, crystalHand ?: swapHand) // we should not be here without a crystal in hand but ig better to check than not to + safeToPlaceInstantly = false + lastHit = null + if (prediction.onPlace) { + predictionTimer.runIfNotPassed(packetLifetime.milliseconds, false) { + val last = lastEntityId + + repeat(placePredictions) { + CrystalAura.explodeInternal(++lastEntityId) + } + lastHit = blockPos + lastEntityId = last + 1 + crystal = null + } + } + placeTimer.reset() + } + } + + /** + * Explodes a crystal that is on [blockPos] + * @return Whether the delay passed, null if the interaction failed or no crystal found + */ + fun explode() { + if (rotate && !automated.rotationRequest { rotation(placeRotation) }.submit().done) return + + explodeTimer.runSafeIfPassed(explodeDelay.milliseconds) { + crystal?.let { crystal -> + CrystalAura.explodeInternal(crystal.id) + lastEntityId = crystal.id + lastHit = blockPos + explodeTimer.reset() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt b/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt index ffb65f330..cb334170d 100644 --- a/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt +++ b/src/main/kotlin/com/lambda/module/modules/movement/TargetStrafe.kt @@ -17,6 +17,8 @@ package com.lambda.module.modules.movement +import com.lambda.config.forEachSetting +import com.lambda.config.hideAllExcept import com.lambda.config.blocks.WorldLineSettings import com.lambda.config.forEachSetting import com.lambda.config.hideAllExcept