diff --git a/src/main/java/net/ccbluex/liquidbounce/utils/client/vfp/VfpCompatibility.java b/src/main/java/net/ccbluex/liquidbounce/utils/client/vfp/VfpCompatibility.java index 302da17a248..ba5b0a4030b 100644 --- a/src/main/java/net/ccbluex/liquidbounce/utils/client/vfp/VfpCompatibility.java +++ b/src/main/java/net/ccbluex/liquidbounce/utils/client/vfp/VfpCompatibility.java @@ -151,4 +151,16 @@ public boolean isOlderThanOrEqual1_11_1() { } } + public boolean isNewerThanOrEqual1_19_4() { + try { + var version = ViaFabricPlus.getImpl().getTargetVersion(); + + // Check if the version is older or equal than 1.19.4 + return version.newerThanOrEqualTo(ProtocolVersion.v1_19_4); + } catch (Throwable throwable) { + LiquidBounce.INSTANCE.getLogger().error("Failed to check if 1.19.4", throwable); + return false; + } + } + } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ArmorEvaluation.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ArmorEvaluation.kt index 5d92e30ffc8..2bb2fcf2877 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ArmorEvaluation.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ArmorEvaluation.kt @@ -15,7 +15,8 @@ object ArmorEvaluation { private const val EXPECTED_DAMAGE: Float = 6.0F fun findBestArmorPieces( - slots: List = Slots.All + slots: List = Slots.All, + durabilityThreshold: Int = Int.MIN_VALUE ): Map { val armorPiecesGroupedByType = groupArmorByType(slots) @@ -26,7 +27,7 @@ object ArmorEvaluation { // Run some passes in which we try to find best armor pieces based on the parameters of the last pass for (ignored in 0 until 2) { - val comparator = getArmorComparatorFor(currentBestPieces) + val comparator = getArmorComparatorFor(currentBestPieces, durabilityThreshold) currentBestPieces = armorPiecesGroupedByType.mapValues { it.value.maxWithOrNull(comparator) } } @@ -56,12 +57,21 @@ object ArmorEvaluation { return armorPiecesGroupedByType } - fun getArmorComparatorFor(currentKit: Map): ArmorComparator { - return getArmorComparatorForParameters(ArmorKitParameters.getParametersForSlots(currentKit)) + fun getArmorComparatorFor( + currentKit: Map, + durabilityThreshold: Int = Int.MIN_VALUE + ): ArmorComparator { + return getArmorComparatorForParameters( + ArmorKitParameters.getParametersForSlots(currentKit), + durabilityThreshold + ) } - fun getArmorComparatorForParameters(currentParameters: ArmorKitParameters): ArmorComparator { - return ArmorComparator(EXPECTED_DAMAGE, currentParameters) + fun getArmorComparatorForParameters( + currentParameters: ArmorKitParameters, + durabilityThreshold: Int = Int.MIN_VALUE + ): ArmorComparator { + return ArmorComparator(EXPECTED_DAMAGE, currentParameters, durabilityThreshold) } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/AutoArmorSaveArmor.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/AutoArmorSaveArmor.kt new file mode 100644 index 00000000000..ee1e1952072 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/AutoArmorSaveArmor.kt @@ -0,0 +1,148 @@ +package net.ccbluex.liquidbounce.features.module.modules.combat.autoarmor + +import net.ccbluex.liquidbounce.config.types.ToggleableConfigurable +import net.ccbluex.liquidbounce.event.Sequence +import net.ccbluex.liquidbounce.event.tickHandler +import net.ccbluex.liquidbounce.utils.client.isNewerThanOrEqual1_19_4 +import net.ccbluex.liquidbounce.utils.inventory.HotbarItemSlot +import net.ccbluex.liquidbounce.utils.item.durability +import net.ccbluex.liquidbounce.utils.item.type +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.screen.ingame.InventoryScreen +import net.minecraft.item.ArmorItem + +object AutoArmorSaveArmor : ToggleableConfigurable(ModuleAutoArmor, "SaveArmor", true) { + val durabilityThreshold by int("DurabilityThreshold", 24, 0..100) + private val autoOpen by boolean("AutoOpenInventory", true) + + private var hasOpenedInventory = false + private var prevArmor = 0 + + /** + * Opens the inventory to save armor (as if the player has opened it manually) if the following conditions are met: + * - The module is told to save armor and there is a replacement :) + * - The inventory constraints require open inventory + * (Otherwise, the inventory will be open automatically in a silent way and the armor will be saved) + * - There is no replacement from the hotbar + * (If there are some pieces that can be replaced by the pieces from the hotbar, + * they will be used first, without opening the inventory) + */ + @Suppress("unused") + private val armorAutoSaveHandler = tickHandler { + if (player.isCreative || player.isSpectator) { + return@tickHandler + } + + if (!ModuleAutoArmor.running || !AutoArmorSaveArmor.enabled) { + return@tickHandler + } + + // the module will save armor automatically if open inventory isn't required + if (!ModuleAutoArmor.inventoryConstraints.requiresOpenInventory || !autoOpen) { + return@tickHandler + } + + /** + * The server doesn't let the client know about the state of its armor items + * when the player in a handled screen⁽¹⁾ like a chest, crafting table, anvil, etc., + * making the armor saving process not work at all when a handled screen is open. + * In other words, `{player.inventory.armor}` doesn't get updated in such case. + * + * So, it's necessary to track the armor items and update their state (e.g. durability) + * on the client side when the player receives damage, isn't it? :) + * Well, unfortunately, it's not possible to do this accurately, not even close. + * The server doesn't provide the client with enough data + * to calculate the armor durability on the client side. :( + * + * Nonetheless, if the player is still in a handled screen⁽¹⁾ + * and gets an armor piece broken, his armor attribute, `{player.armor}`, gets updated. + * This update lets the module know exactly when it should close the handled screen⁽¹⁾ + * so that the player equips a new armor piece. + * Yes, this won't save armor pieces but might save the player's life. + * + * (1) - not including the player's own inventory which is also a handled screen. + */ + val hasLostArmorPiece = shouldTrackArmor && player.armor < prevArmor + prevArmor = player.armor + + // closes the current screen so that the armor slots are synced again + if (hasLostArmorPiece) { + player.closeHandledScreen() + return@tickHandler + } + + val armorToEquipWithSlots = ArmorEvaluation + .findBestArmorPieces(durabilityThreshold = durabilityThreshold) + .values + .filterNotNull() + .filter { !it.isAlreadyEquipped && it.itemSlot.itemStack.item is ArmorItem } + + val hasAnyHotBarReplacement = ModuleAutoArmor.useHotbar && isNewerThanOrEqual1_19_4 && + armorToEquipWithSlots.any { it.itemSlot is HotbarItemSlot } + + // the new pieces from the hotbar have a higher priority + // due to the replacement speed (it's much faster, it makes sense to replace them first), + // so it waits until all pieces from hotbar are replaced + if (hasAnyHotBarReplacement) { + return@tickHandler + } + + val playerArmor = player.inventory.armor.filter { it.item is ArmorItem } + val armorToEquip = armorToEquipWithSlots.map { it.itemSlot.itemStack.item as ArmorItem } + + val hasArmorToReplace = playerArmor.any { armorStack -> + armorStack.durability <= durabilityThreshold && + armorToEquip.any { it.type() == (armorStack.item as ArmorItem).type() } + } + + // closes the inventory if the armor is replaced. + closeInventory(hasArmorToEquip = armorToEquip.isNotEmpty()) + + // tries to close the previous screen and open the inventory + openInventory(hasArmorToReplace = hasArmorToReplace) + } + + /** + * Waits and closes the inventory after the armor is replaced. + */ + private suspend fun Sequence.closeInventory(hasArmorToEquip: Boolean) { + if (!hasOpenedInventory || hasArmorToEquip) { + return + } + + this@AutoArmorSaveArmor.hasOpenedInventory = false + waitTicks(ModuleAutoArmor.inventoryConstraints.closeDelay.random()) + + // the current screen might change while the module is waiting + if (mc.currentScreen is InventoryScreen) { + player.closeHandledScreen() + } + } + + /** + * Closes the previous game screen and opens the inventory. + */ + private suspend fun Sequence.openInventory(hasArmorToReplace : Boolean) { + while (hasArmorToReplace && mc.currentScreen !is InventoryScreen) { + + if (mc.currentScreen is HandledScreen<*>) { + // closes chests/crating tables/etc. (it never happens) + player.closeHandledScreen() + } else if (mc.currentScreen != null) { + // closes ClickGUI, game chat, etc. to save some armor :) + mc.currentScreen!!.close() + } + + waitTicks(1) + + // again, the current screen might change while the module is waiting + if (mc.currentScreen == null) { + mc.setScreen(InventoryScreen(player)) + hasOpenedInventory = true + } + } + } + + private val shouldTrackArmor : Boolean + get() = mc.currentScreen !is InventoryScreen && mc.currentScreen is HandledScreen<*> +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ModuleAutoArmor.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ModuleAutoArmor.kt index 6a91cc52b5d..e14134527ca 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ModuleAutoArmor.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/combat/autoarmor/ModuleAutoArmor.kt @@ -22,9 +22,10 @@ import net.ccbluex.liquidbounce.event.events.ScheduleInventoryActionEvent import net.ccbluex.liquidbounce.event.handler import net.ccbluex.liquidbounce.features.module.Category import net.ccbluex.liquidbounce.features.module.ClientModule +import net.ccbluex.liquidbounce.features.module.modules.combat.autoarmor.AutoArmorSaveArmor.durabilityThreshold +import net.ccbluex.liquidbounce.utils.client.isNewerThanOrEqual1_19_4 import net.ccbluex.liquidbounce.utils.inventory.ArmorItemSlot import net.ccbluex.liquidbounce.utils.inventory.HotbarItemSlot -import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.inventory.* import net.ccbluex.liquidbounce.utils.item.ArmorPiece import net.ccbluex.liquidbounce.utils.item.isNothing @@ -34,24 +35,35 @@ import net.minecraft.item.Items /** * AutoArmor module * - * Automatically put on the best armor. + * Automatically puts on the best armor. */ object ModuleAutoArmor : ClientModule("AutoArmor", Category.COMBAT) { - private val inventoryConstraints = tree(PlayerInventoryConstraints()) + val inventoryConstraints = tree(PlayerInventoryConstraints()) /** - * Should the module use the hotbar to equip armor pieces. + * Should the module use the hotbar to equip armor pieces? * If disabled, it will only use inventory moves. */ - private val useHotbar by boolean("Hotbar", true) + val useHotbar by boolean("Hotbar", true) + init { + tree(AutoArmorSaveArmor) + } + + @Suppress("unused") private val scheduleHandler = handler { event -> - // Filter out already equipped armor pieces - val armorToEquip = ArmorEvaluation.findBestArmorPieces().values.filterNotNull().filter { - !it.isAlreadyEquipped + if (player.isCreative || player.isSpectator) { + return@handler } + // Filter out already equipped armor pieces + val durabilityThreshold = if (AutoArmorSaveArmor.enabled) durabilityThreshold else Int.MIN_VALUE + + val armorToEquip = ArmorEvaluation + .findBestArmorPieces(durabilityThreshold = durabilityThreshold) + .values.filterNotNull().filter { !it.isAlreadyEquipped } + for (armorPiece in armorToEquip) { event.schedule( inventoryConstraints, @@ -75,16 +87,7 @@ object ModuleAutoArmor : ClientModule("AutoArmor", Category.COMBAT) { return null } - val inventorySlot = armorPiece.itemSlot - val armorPieceSlot = ArmorItemSlot(armorPiece.entitySlotId) - - return if (!stackInArmor.isNothing()) { - // Clear current armor - performMoveOrHotbarClick(armorPieceSlot, isInArmorSlot = true) - } else { - // Equip new armor - performMoveOrHotbarClick(inventorySlot, isInArmorSlot = false) - } + return performMoveOrHotbarClick(armorPiece, isInArmorSlot = !stackInArmor.isNothing()) } /** @@ -98,21 +101,26 @@ object ModuleAutoArmor : ClientModule("AutoArmor", Category.COMBAT) { * @return True if a move occurred. */ private fun performMoveOrHotbarClick( - slot: ItemSlot, + armorPiece: ArmorPiece, isInArmorSlot: Boolean ): InventoryAction { - val canTryHotbarMove = !isInArmorSlot && useHotbar && !InventoryManager.isInventoryOpen - if (slot is HotbarItemSlot && canTryHotbarMove) { - return UseInventoryAction(slot) + val inventorySlot = armorPiece.itemSlot + val armorPieceSlot = if (isInArmorSlot) ArmorItemSlot(armorPiece.entitySlotId) else inventorySlot + + val canTryHotbarMove = (!isInArmorSlot || isNewerThanOrEqual1_19_4) + && useHotbar && !InventoryManager.isInventoryOpen + + if (inventorySlot is HotbarItemSlot && canTryHotbarMove) { + return UseInventoryAction(inventorySlot) } // Should the item be just thrown out of the inventory val shouldThrow = isInArmorSlot && !hasInventorySpace() return if (shouldThrow) { - ClickInventoryAction.performThrow(screen = null, slot) + ClickInventoryAction.performThrow(screen = null, armorPieceSlot) } else { - ClickInventoryAction.performQuickMove(screen = null, slot) + ClickInventoryAction.performQuickMove(screen = null, armorPieceSlot) } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/client/ProtocolUtil.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/client/ProtocolUtil.kt index 1412e1ee4f1..0f04e7cad7e 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/client/ProtocolUtil.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/client/ProtocolUtil.kt @@ -115,6 +115,14 @@ val isOlderThanOrEqual1_11_1: Boolean logger.error("Failed to check if the server is using 1.11.1", it) }.getOrDefault(false) +val isNewerThanOrEqual1_19_4: Boolean + get() = runCatching { + // Check if the ViaFabricPlus mod is loaded - prevents from causing too many exceptions + usesViaFabricPlus && VfpCompatibility.INSTANCE.isNewerThanOrEqual1_19_4 + }.onFailure { + logger.error("Failed to check if the server is using 1.19.4", it) + }.getOrDefault(false) + fun selectProtocolVersion(protocolId: Int) { // Check if the ViaFabricPlus mod is loaded - prevents from causing too many exceptions if (usesViaFabricPlus) { diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/InventoryUtils.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/InventoryUtils.kt index 6a9aed4da88..c1301bb6612 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/InventoryUtils.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/InventoryUtils.kt @@ -85,7 +85,7 @@ class PlayerInventoryConstraints : InventoryConstraints() { * Sad. * :( */ - private val requiresOpenInventory by boolean("RequiresInventoryOpen", false) + val requiresOpenInventory by boolean("RequiresInventoryOpen", false) override fun passesRequirements(action: InventoryAction) = super.passesRequirements(action) && diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorComparator.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorComparator.kt index 37770a4e965..b9c2cdb1ced 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorComparator.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorComparator.kt @@ -81,10 +81,15 @@ class ArmorKitParameters( * @property armorKitParametersForSlot armor (i.e. iron with Protection II vs plain diamond) behaves differently based * on the other armor pieces. Thus, the expected defense points and toughness have to be provided. Since those are * dependent on the other armor pieces, the armor parameters have to be provided slot-wise. + * @property durabilityThreshold the minimum durability an armor piece must have to be prioritized for use. + * If an armor piece's remaining durability is lower than this threshold, + * the piece is not prioritized anymore, and it can be replaced with another piece + * so that this piece can be preserved. */ class ArmorComparator( private val expectedDamage: Float, - private val armorKitParametersForSlot: ArmorKitParameters + private val armorKitParametersForSlot: ArmorKitParameters, + private val durabilityThreshold : Int = Int.MIN_VALUE ) : Comparator { companion object { private val DAMAGE_REDUCTION_ENCHANTMENTS: Array> = arrayOf( @@ -106,6 +111,7 @@ class ArmorComparator( } private val comparator = ComparatorChain( + compareBy { it.itemSlot.itemStack.durability > durabilityThreshold }, compareByDescending { round(getThresholdedDamageReduction(it.itemSlot.itemStack).toDouble(), 3) }, compareBy { round(getEnchantmentThreshold(it.itemSlot.itemStack).toDouble(), 3) }, compareBy { it.itemSlot.itemStack.getEnchantmentCount() }, diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ItemExtensions.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ItemExtensions.kt index 2596771c9b9..38825614247 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ItemExtensions.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ItemExtensions.kt @@ -149,6 +149,9 @@ fun ItemStack.getSharpnessDamage(level: Int = sharpnessLevel) = if (level == 0) val ItemStack.attackSpeed: Float get() = item.getAttributeValue(EntityAttributes.ATTACK_SPEED) +val ItemStack.durability + get() = this.maxDamage - this.damage + private fun Item.getAttributeValue(attribute: RegistryEntry): Float { val attribInstance = EntityAttributeInstance(attribute) {}