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