|
| 1 | +/* |
| 2 | + * Copyright 2025 Lambda |
| 3 | + * |
| 4 | + * This program is free software: you can redistribute it and/or modify |
| 5 | + * it under the terms of the GNU General Public License as published by |
| 6 | + * the Free Software Foundation, either version 3 of the License, or |
| 7 | + * (at your option) any later version. |
| 8 | + * |
| 9 | + * This program is distributed in the hope that it will be useful, |
| 10 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | + * GNU General Public License for more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU General Public License |
| 15 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + */ |
| 17 | + |
| 18 | +package com.lambda.module.modules.movement |
| 19 | + |
| 20 | +import com.lambda.config.groups.HotbarSettings |
| 21 | +import com.lambda.config.groups.InventorySettings |
| 22 | +import com.lambda.config.settings.collections.SetSetting.Companion.immutableSet |
| 23 | +import com.lambda.config.settings.complex.Bind |
| 24 | +import com.lambda.context.SafeContext |
| 25 | +import com.lambda.event.events.KeyboardEvent |
| 26 | +import com.lambda.event.events.MouseEvent |
| 27 | +import com.lambda.event.events.TickEvent |
| 28 | +import com.lambda.event.listener.SafeListener.Companion.listen |
| 29 | +import com.lambda.interaction.material.StackSelection.Companion.selectStack |
| 30 | +import com.lambda.interaction.request.hotbar.HotbarRequest |
| 31 | +import com.lambda.interaction.request.inventory.InventoryRequest.Companion.inventoryRequest |
| 32 | +import com.lambda.module.Module |
| 33 | +import com.lambda.module.tag.ModuleTag |
| 34 | +import com.lambda.threading.runSafe |
| 35 | +import com.lambda.util.KeyCode |
| 36 | +import com.lambda.util.Mouse |
| 37 | +import com.lambda.util.NamedEnum |
| 38 | +import com.lambda.util.player.SlotUtils.hotbar |
| 39 | +import com.lambda.util.player.SlotUtils.hotbarAndStorage |
| 40 | +import net.minecraft.client.network.ClientPlayerEntity |
| 41 | +import net.minecraft.entity.effect.StatusEffects |
| 42 | +import net.minecraft.item.Items |
| 43 | +import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket |
| 44 | +import net.minecraft.network.packet.c2s.play.HandSwingC2SPacket |
| 45 | +import net.minecraft.util.Hand |
| 46 | +import net.minecraft.util.hit.HitResult |
| 47 | + |
| 48 | +object BetterFirework : Module( |
| 49 | + name = "BetterFirework", |
| 50 | + description = "Automatic takeoff with fireworks", |
| 51 | + tag = ModuleTag.MOVEMENT, |
| 52 | +) { |
| 53 | + private var activateButton by setting("Activate Key", Bind(0, 0, Mouse.Middle.ordinal), "Button to activate Firework").group(Group.General) |
| 54 | + 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) |
| 55 | + 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) |
| 56 | + private var fireworkInteract by setting("Right Click Fly", true, "Automatically start flying when right clicking fireworks") |
| 57 | + private var fireworkInteractCancel by setting("Right Click Cancel", false, "Cancel block interactions while holding fireworks") { fireworkInteract } |
| 58 | + |
| 59 | + private var clientSwing by setting("Swing", true, "Swing hand client side").group(Group.General) |
| 60 | + private var invUse by setting("Inventory", true, "Use fireworks from inventory") { activateButton.key != KeyCode.Unbound.code }.group(Group.General) |
| 61 | + |
| 62 | + override val hotbarConfig = HotbarSettings(this, Group.Hotbar, vis = { false }).apply { |
| 63 | + ::sequenceStageMask.edit { immutableSet(setOf(TickEvent.Pre)); defaultValue(mutableSetOf(TickEvent.Pre)) } |
| 64 | + } |
| 65 | + |
| 66 | + override val inventoryConfig = InventorySettings(this, Group.Inventory, vis = { false }).apply { |
| 67 | + ::tickStageMask.edit { immutableSet(setOf(TickEvent.Pre)); defaultValue(mutableSetOf(TickEvent.Pre)) } |
| 68 | + } |
| 69 | + |
| 70 | + private enum class Group(override val displayName: String) : NamedEnum { |
| 71 | + General("General"), |
| 72 | + Hotbar("Hotbar"), |
| 73 | + Inventory("Inventory") |
| 74 | + } |
| 75 | + |
| 76 | + private var takeoffState = TakeoffState.None |
| 77 | + |
| 78 | + val ClientPlayerEntity.canTakeoff: Boolean |
| 79 | + get() = isOnGround || canOpenElytra |
| 80 | + |
| 81 | + val ClientPlayerEntity.canOpenElytra: Boolean |
| 82 | + get() = !abilities.flying && !isClimbing && !isGliding && !isTouchingWater && !isOnGround && !hasVehicle() && !hasStatusEffect(StatusEffects.LEVITATION) |
| 83 | + |
| 84 | + init { |
| 85 | + listen<TickEvent.Pre> { |
| 86 | + when (takeoffState) { |
| 87 | + TakeoffState.None -> {} |
| 88 | + |
| 89 | + TakeoffState.Jumping -> { |
| 90 | + player.jump() |
| 91 | + takeoffState = TakeoffState.StartFlying |
| 92 | + } |
| 93 | + |
| 94 | + TakeoffState.StartFlying -> { |
| 95 | + if (player.canOpenElytra) { |
| 96 | + player.startGliding() |
| 97 | + connection.sendPacket(ClientCommandC2SPacket(player, ClientCommandC2SPacket.Mode.START_FALL_FLYING)) |
| 98 | + } |
| 99 | + startFirework(invUse) |
| 100 | + takeoffState = TakeoffState.None |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + listen<MouseEvent.Click> { |
| 105 | + if (!it.isPressed) { |
| 106 | + return@listen |
| 107 | + } |
| 108 | + if (it.satisfies(activateButton)) { |
| 109 | + if (activateButton.mouse == mc.options.pickItemKey.boundKey.code) { |
| 110 | + return@listen |
| 111 | + } |
| 112 | + runSafe { |
| 113 | + if (takeoffState != TakeoffState.None) { |
| 114 | + return@listen // Prevent using multiple times |
| 115 | + } |
| 116 | + if (player.canOpenElytra || player.isGliding) { |
| 117 | + // If already gliding use another firework |
| 118 | + takeoffState = TakeoffState.StartFlying |
| 119 | + } else if (player.canTakeoff) { |
| 120 | + takeoffState = TakeoffState.Jumping |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + if (it.satisfies(midFlightActivationKey)) { |
| 125 | + runSafe { |
| 126 | + if (player.isGliding) |
| 127 | + takeoffState = TakeoffState.StartFlying |
| 128 | + } |
| 129 | + } |
| 130 | + } |
| 131 | + listen<KeyboardEvent.Press> { |
| 132 | + if (!it.isPressed) { |
| 133 | + return@listen |
| 134 | + } |
| 135 | + if (it.satisfies(activateButton)) { |
| 136 | + if (activateButton.key != mc.options.pickItemKey.boundKey.code) { |
| 137 | + runSafe { |
| 138 | + if (takeoffState == TakeoffState.None) { |
| 139 | + if (player.canOpenElytra || player.isGliding) { |
| 140 | + // If already gliding use another firework |
| 141 | + takeoffState = TakeoffState.StartFlying |
| 142 | + } else if (player.canTakeoff) { |
| 143 | + takeoffState = TakeoffState.Jumping |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | + if (it.satisfies(midFlightActivationKey)) { |
| 150 | + runSafe { |
| 151 | + if (player.isGliding) |
| 152 | + takeoffState = TakeoffState.StartFlying |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + /** |
| 159 | + * Returns true if the mc item interaction should be canceled |
| 160 | + */ |
| 161 | + @JvmStatic |
| 162 | + fun onInteract() = |
| 163 | + runSafe { |
| 164 | + when { |
| 165 | + !fireworkInteract || |
| 166 | + player.inventory.selectedStack?.item != Items.FIREWORK_ROCKET || |
| 167 | + player.isGliding || // No need to do special magic if we are already holding fireworks and flying |
| 168 | + (mc.crosshairTarget != null && mc.crosshairTarget!!.type != HitResult.Type.MISS && !fireworkInteractCancel) -> false |
| 169 | + else -> { |
| 170 | + mc.itemUseCooldown += 4 |
| 171 | + val cancelInteract = player.canTakeoff || fireworkInteractCancel |
| 172 | + if (player.canTakeoff) { |
| 173 | + takeoffState = TakeoffState.Jumping |
| 174 | + } else if (player.canOpenElytra) { |
| 175 | + takeoffState = TakeoffState.StartFlying |
| 176 | + } |
| 177 | + cancelInteract |
| 178 | + } |
| 179 | + } |
| 180 | + } ?: false |
| 181 | + |
| 182 | + /** |
| 183 | + * Returns true when the pick interaction should be canceled. |
| 184 | + */ |
| 185 | + @JvmStatic |
| 186 | + fun onPick() = |
| 187 | + runSafe { |
| 188 | + when { |
| 189 | + (mc.crosshairTarget?.type == HitResult.Type.BLOCK && !middleClickCancel) || |
| 190 | + (!activateButton.isMouseBind || activateButton.mouse != mc.options.pickItemKey.boundKey.code) || |
| 191 | + takeoffState != TakeoffState.None -> false // Prevent using multiple times |
| 192 | + else -> { |
| 193 | + if (player.canOpenElytra || player.isGliding) { |
| 194 | + // If already gliding use another firework |
| 195 | + takeoffState = TakeoffState.StartFlying |
| 196 | + } else if (player.canTakeoff) { |
| 197 | + takeoffState = TakeoffState.Jumping |
| 198 | + } |
| 199 | + middleClickCancel |
| 200 | + } |
| 201 | + } |
| 202 | + } ?: false |
| 203 | + |
| 204 | + fun SafeContext.sendSwing() { |
| 205 | + if (clientSwing) { |
| 206 | + player.swingHand(Hand.MAIN_HAND) |
| 207 | + } else { |
| 208 | + connection.sendPacket(HandSwingC2SPacket(Hand.MAIN_HAND)) |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + /** |
| 213 | + * Use a firework from the hotbar or inventory if possible. |
| 214 | + * Return true if a firework has been used |
| 215 | + */ |
| 216 | + fun SafeContext.startFirework(silent: Boolean) { |
| 217 | + val stack = selectStack(count = 1) { isItem(Items.FIREWORK_ROCKET) } |
| 218 | + |
| 219 | + stack.bestItemMatch(player.hotbar) |
| 220 | + ?.let { |
| 221 | + val request = HotbarRequest(player.hotbar.indexOf(it), this@BetterFirework, keepTicks = 0) |
| 222 | + .submit(queueIfClosed = false) |
| 223 | + if (request.done) { |
| 224 | + interaction.interactItem(player, Hand.MAIN_HAND) |
| 225 | + sendSwing() |
| 226 | + } |
| 227 | + return |
| 228 | + } |
| 229 | + |
| 230 | + if (!silent) return |
| 231 | + |
| 232 | + stack.bestItemMatch(player.hotbarAndStorage) |
| 233 | + ?.let { |
| 234 | + val swapSlotId = player.hotbarAndStorage.indexOf(it) |
| 235 | + val hotbarSlotToSwapWith = player.hotbar.find { slot -> slot.isEmpty }?.let { slot -> player.hotbar.indexOf(slot) } ?: 8 |
| 236 | + |
| 237 | + inventoryRequest { |
| 238 | + swap(swapSlotId, hotbarSlotToSwapWith) |
| 239 | + action { |
| 240 | + val request = HotbarRequest(hotbarSlotToSwapWith, this@BetterFirework, keepTicks = 0, nowOrNothing = true) |
| 241 | + .submit(queueIfClosed = false) |
| 242 | + if (request.done) { |
| 243 | + interaction.interactItem(player, Hand.MAIN_HAND) |
| 244 | + sendSwing() |
| 245 | + } |
| 246 | + } |
| 247 | + swap(swapSlotId, hotbarSlotToSwapWith) |
| 248 | + }.submit() |
| 249 | + } |
| 250 | + } |
| 251 | + |
| 252 | + enum class TakeoffState { |
| 253 | + None, |
| 254 | + Jumping, |
| 255 | + StartFlying |
| 256 | + } |
| 257 | +} |
0 commit comments