diff --git a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index cd94a75d8..0526bab37 100644 --- a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -24,6 +24,7 @@ import com.lambda.event.events.TickEvent; import com.lambda.gui.DearImGui; import com.lambda.gui.components.ClickGuiLayout; +import com.lambda.module.modules.movement.BetterFirework; import com.lambda.module.modules.player.Interact; import com.lambda.module.modules.player.InventoryMove; import com.lambda.module.modules.player.PacketMine; @@ -189,6 +190,18 @@ void injectFastPlace(CallbackInfo ci) { itemUseCooldown = Interact.getPlaceDelay(); } + @WrapMethod(method = "doItemUse") + void injectItemUse(Operation original) { + if (BetterFirework.INSTANCE.isDisabled() || !BetterFirework.onInteract()) + original.call(); + } + + @WrapMethod(method = "doItemPick") + void injectItemPick(Operation original) { + if (BetterFirework.INSTANCE.isDisabled() || !BetterFirework.onPick()) + original.call(); + } + @WrapMethod(method = "getTargetMillisPerTick") float getTargetMillisPerTick(float millis, Operation original) { var length = TimerManager.INSTANCE.getLength(); diff --git a/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt b/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt index 39b22e11d..ddfc1cf6a 100644 --- a/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt +++ b/src/main/kotlin/com/lambda/config/settings/complex/KeybindSetting.kt @@ -176,6 +176,12 @@ data class Bind( if (modifiers and GLFW_MOD_NUM_LOCK != 0) add(KeyCode.NumLock) } + val isMouseBind: Boolean + get() = mouse >= 0 + + val isKeyBind: Boolean + get() = key > 0 + val name: String get() { if (mouse < 0 && modifiers <= 0 && key <= 0) return "Unbound" diff --git a/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt new file mode 100644 index 000000000..797be0e2d --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt @@ -0,0 +1,257 @@ +/* + * 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.module.modules.movement + +import com.lambda.config.groups.HotbarSettings +import com.lambda.config.groups.InventorySettings +import com.lambda.config.settings.collections.SetSetting.Companion.immutableSet +import com.lambda.config.settings.complex.Bind +import com.lambda.context.SafeContext +import com.lambda.event.events.KeyboardEvent +import com.lambda.event.events.MouseEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.material.StackSelection.Companion.selectStack +import com.lambda.interaction.request.hotbar.HotbarRequest +import com.lambda.interaction.request.inventory.InventoryRequest.Companion.inventoryRequest +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.KeyCode +import com.lambda.util.Mouse +import com.lambda.util.NamedEnum +import com.lambda.util.player.SlotUtils.hotbar +import com.lambda.util.player.SlotUtils.hotbarAndStorage +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.entity.effect.StatusEffects +import net.minecraft.item.Items +import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket +import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket +import net.minecraft.util.Hand +import net.minecraft.util.hit.HitResult + +object BetterFirework : Module( + name = "BetterFirework", + description = "Automatic takeoff with fireworks", + tag = ModuleTag.MOVEMENT, +) { + private var activateButton by setting("Activate Key", Bind(0, 0, Mouse.Middle.ordinal), "Button to activate Firework").group(Group.General) + private var midFlightActivationKey by setting("Mid-Flight Activation Key", Bind(0, 0, KeyCode.Unbound.code), "Firework use key for mid flight activation").group(Group.General) + private var middleClickCancel by setting("Middle Click Cancel", false, description = "Cancel pick block action on middle mouse click") { activateButton.key != KeyCode.Unbound.code }.group(Group.General) + private var fireworkInteract by setting("Right Click Fly", true, "Automatically start flying when right clicking fireworks") + private var fireworkInteractCancel by setting("Right Click Cancel", false, "Cancel block interactions while holding fireworks") { fireworkInteract } + + private var clientSwing by setting("Swing", true, "Swing hand client side").group(Group.General) + private var invUse by setting("Inventory", true, "Use fireworks from inventory") { activateButton.key != KeyCode.Unbound.code }.group(Group.General) + + override val hotbarConfig = HotbarSettings(this, Group.Hotbar, vis = { false }).apply { + ::sequenceStageMask.edit { immutableSet(setOf(TickEvent.Pre)); defaultValue(mutableSetOf(TickEvent.Pre)) } + } + + override val inventoryConfig = InventorySettings(this, Group.Inventory, vis = { false }).apply { + ::tickStageMask.edit { immutableSet(setOf(TickEvent.Pre)); defaultValue(mutableSetOf(TickEvent.Pre)) } + } + + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + Hotbar("Hotbar"), + Inventory("Inventory") + } + + private var takeoffState = TakeoffState.None + + val ClientPlayerEntity.canTakeoff: Boolean + get() = isOnGround || canOpenElytra + + val ClientPlayerEntity.canOpenElytra: Boolean + get() = !abilities.flying && !isClimbing && !isGliding && !isTouchingWater && !isOnGround && !hasVehicle() && !hasStatusEffect(StatusEffects.LEVITATION) + + init { + listen { + when (takeoffState) { + TakeoffState.None -> {} + + TakeoffState.Jumping -> { + player.jump() + takeoffState = TakeoffState.StartFlying + } + + TakeoffState.StartFlying -> { + if (player.canOpenElytra) { + player.startGliding() + connection.sendPacket(ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.START_FALL_FLYING)) + } + startFirework(invUse) + takeoffState = TakeoffState.None + } + } + } + listen { + if (!it.isPressed) { + return@listen + } + if (it.satisfies(activateButton)) { + if (activateButton.mouse == mc.options.pickItemKey.boundKey.code) { + return@listen + } + runSafe { + if (takeoffState != TakeoffState.None) { + return@listen // Prevent using multiple times + } + if (player.canOpenElytra || player.isGliding) { + // If already gliding use another firework + takeoffState = TakeoffState.StartFlying + } else if (player.canTakeoff) { + takeoffState = TakeoffState.Jumping + } + } + } + if (it.satisfies(midFlightActivationKey)) { + runSafe { + if (player.isGliding) + takeoffState = TakeoffState.StartFlying + } + } + } + listen { + if (!it.isPressed) { + return@listen + } + if (it.satisfies(activateButton)) { + if (activateButton.key != mc.options.pickItemKey.boundKey.code) { + runSafe { + if (takeoffState == TakeoffState.None) { + if (player.canOpenElytra || player.isGliding) { + // If already gliding use another firework + takeoffState = TakeoffState.StartFlying + } else if (player.canTakeoff) { + takeoffState = TakeoffState.Jumping + } + } + } + } + } + if (it.satisfies(midFlightActivationKey)) { + runSafe { + if (player.isGliding) + takeoffState = TakeoffState.StartFlying + } + } + } + } + + /** + * Returns true if the mc item interaction should be canceled + */ + @JvmStatic + fun onInteract() = + runSafe { + when { + !fireworkInteract || + player.inventory.selectedStack?.item != Items.FIREWORK_ROCKET || + player.isGliding || // No need to do special magic if we are already holding fireworks and flying + (mc.crosshairTarget != null && mc.crosshairTarget!!.type != HitResult.Type.MISS && !fireworkInteractCancel) -> false + else -> { + mc.itemUseCooldown += 4 + val cancelInteract = player.canTakeoff || fireworkInteractCancel + if (player.canTakeoff) { + takeoffState = TakeoffState.Jumping + } else if (player.canOpenElytra) { + takeoffState = TakeoffState.StartFlying + } + cancelInteract + } + } + } ?: false + + /** + * Returns true when the pick interaction should be canceled. + */ + @JvmStatic + fun onPick() = + runSafe { + when { + (mc.crosshairTarget?.type == HitResult.Type.BLOCK && !middleClickCancel) || + (!activateButton.isMouseBind || activateButton.mouse != mc.options.pickItemKey.boundKey.code) || + takeoffState != TakeoffState.None -> false // Prevent using multiple times + else -> { + if (player.canOpenElytra || player.isGliding) { + // If already gliding use another firework + takeoffState = TakeoffState.StartFlying + } else if (player.canTakeoff) { + takeoffState = TakeoffState.Jumping + } + middleClickCancel + } + } + } ?: false + + fun SafeContext.sendSwing() { + if (clientSwing) { + player.swingHand(Hand.MAIN_HAND) + } else { + connection.sendPacket(HandSwingC2SPacket(Hand.MAIN_HAND)) + } + } + + /** + * Use a firework from the hotbar or inventory if possible. + * Return true if a firework has been used + */ + fun SafeContext.startFirework(silent: Boolean) { + val stack = selectStack(count = 1) { isItem(Items.FIREWORK_ROCKET) } + + stack.bestItemMatch(player.hotbar) + ?.let { + val request = HotbarRequest(player.hotbar.indexOf(it), this@BetterFirework, keepTicks = 0) + .submit(queueIfClosed = false) + if (request.done) { + interaction.interactItem(player, Hand.MAIN_HAND) + sendSwing() + } + return + } + + if (!silent) return + + stack.bestItemMatch(player.hotbarAndStorage) + ?.let { + val swapSlotId = player.hotbarAndStorage.indexOf(it) + val hotbarSlotToSwapWith = player.hotbar.find { slot -> slot.isEmpty }?.let { slot -> player.hotbar.indexOf(slot) } ?: 8 + + inventoryRequest { + swap(swapSlotId, hotbarSlotToSwapWith) + action { + val request = HotbarRequest(hotbarSlotToSwapWith, this@BetterFirework, keepTicks = 0, nowOrNothing = true) + .submit(queueIfClosed = false) + if (request.done) { + interaction.interactItem(player, Hand.MAIN_HAND) + sendSwing() + } + } + swap(swapSlotId, hotbarSlotToSwapWith) + }.submit() + } + } + + enum class TakeoffState { + None, + Jumping, + StartFlying + } +}