Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/main/java/com/lambda/mixin/MinecraftClientMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -189,6 +190,18 @@ void injectFastPlace(CallbackInfo ci) {
itemUseCooldown = Interact.getPlaceDelay();
}

@WrapMethod(method = "doItemUse")
void injectItemUse(Operation<Void> original) {
if (BetterFirework.INSTANCE.isDisabled() || !BetterFirework.onInteract())
original.call();
}

@WrapMethod(method = "doItemPick")
void injectItemPick(Operation<Void> original) {
if (BetterFirework.INSTANCE.isDisabled() || !BetterFirework.onPick())
original.call();
}

@WrapMethod(method = "getTargetMillisPerTick")
float getTargetMillisPerTick(float millis, Operation<Float> original) {
var length = TimerManager.INSTANCE.getLength();
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/com/lambda/config/groups/HotbarSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package com.lambda.config.groups

import com.lambda.config.Configurable
import com.lambda.event.Event
import com.lambda.event.events.TickEvent
import com.lambda.interaction.request.hotbar.HotbarConfig
import com.lambda.util.NamedEnum
Expand All @@ -31,5 +32,5 @@ class HotbarSettings(
override val swapDelay by c.setting("Swap Delay", 0, 0..3, 1, "The number of ticks delay before allowing another hotbar selection swap", " ticks", visibility = vis).group(baseGroup)
override val swapsPerTick by c.setting("Swaps Per Tick", 3, 1..10, 1, "The number of hotbar selection swaps that can take place each tick") { swapDelay <= 0 && vis() }.group(baseGroup)
override val swapPause by c.setting("Swap Pause", 0, 0..20, 1, "The delay in ticks to pause actions after switching to the slot", " ticks", visibility = vis).group(baseGroup)
override val sequenceStageMask by c.setting("Hotbar Stage Mask", setOf(TickEvent.Input.Post), description = "The sub-tick timing at which hotbar actions are performed", visibility = vis).group(baseGroup)
override val sequenceStageMask by c.setting("Hotbar Stage Mask", setOf<Event>(TickEvent.Input.Post), description = "The sub-tick timing at which hotbar actions are performed", visibility = vis).group(baseGroup)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
247 changes: 247 additions & 0 deletions src/main/kotlin/com/lambda/module/modules/movement/BetterFirework.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

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.HotbarManager
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.network.packet.c2s.play.PlayerInteractItemC2SPacket
import net.minecraft.screen.slot.SlotActionType
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 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 silentUse by setting("Silent", true, "Silent use fireworks from the inventory") { activateButton.key != KeyCode.Unbound.code }.group(Group.General)

override val hotbarConfig = HotbarSettings(this, Group.Hotbar).apply {
::sequenceStageMask.edit { immutableSet(setOf(TickEvent.Pre)) }
}

override val inventoryConfig = InventorySettings(this, Group.Inventory).apply {
::tickStageMask.edit { immutableSet(setOf(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<TickEvent.Pre> {
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(silentUse)
takeoffState = TakeoffState.None
}
}
}
listen<MouseEvent.Click> {
if (it.isPressed && 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
}
}
}
}
listen<KeyboardEvent.Press> {
if (it.isPressed && it.satisfies(activateButton)) {
if (activateButton.key == 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
}
}
}
}
}

/**
* Returns true if the mc item interaction should be canceled
*/
@JvmStatic
fun onInteract() =
runSafe {
if (!fireworkInteract) return false
if (player.inventory.selectedStack?.item != Items.FIREWORK_ROCKET) {
return false
}
if (player.isGliding) {
return false // No need to do special magic if we are already holding fireworks and flying
}
if (mc.crosshairTarget != null && mc.crosshairTarget!!.type != HitResult.Type.MISS && !fireworkInteractCancel) {
return false
}
mc.itemUseCooldown += 4
val cancelInteract = player.canTakeoff || fireworkInteractCancel
if (player.canTakeoff) {
takeoffState = TakeoffState.Jumping
} else if (player.canOpenElytra) {
takeoffState = TakeoffState.StartFlying
}
return cancelInteract
} ?: false

/**
* Returns true when the pick interaction should be canceled.
*/
@JvmStatic
fun onPick() =
runSafe {
if (mc.crosshairTarget?.type == HitResult.Type.BLOCK && !middleClickCancel) {
return false
}
if (!activateButton.isMouseBind || activateButton.mouse != mc.options.pickItemKey.boundKey.code) {
return false
}
if (takeoffState != TakeoffState.None) {
return false // 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
}
return 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 {
HotbarManager.request(HotbarRequest(player.hotbar.indexOf(it), this@BetterFirework, keepTicks = 0))
.done
.let {
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 {
HotbarManager.request(HotbarRequest(hotbarSlotToSwapWith, this@BetterFirework, keepTicks = 0, nowOrNothing = true))
.done
.let {
interaction.interactItem(player, Hand.MAIN_HAND)
sendSwing()
}
}
swap(swapSlotId, hotbarSlotToSwapWith)
}.submit()
}
}

enum class TakeoffState {
None,
Jumping,
StartFlying
}
}
Loading