Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(AutoArmor): Save Armor with low remaining durability #5600

Closed
wants to merge 12 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ object ArmorEvaluation {
private const val EXPECTED_DAMAGE: Float = 6.0F

fun findBestArmorPieces(
slots: List<ItemSlot> = Slots.All
slots: List<ItemSlot> = Slots.All,
durabilityThreshold: Int = Int.MIN_VALUE
): Map<EquipmentSlot, ArmorPiece?> {
val armorPiecesGroupedByType = groupArmorByType(slots)

Expand All @@ -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) }
}
Expand Down Expand Up @@ -56,12 +57,21 @@ object ArmorEvaluation {
return armorPiecesGroupedByType
}

fun getArmorComparatorFor(currentKit: Map<EquipmentSlot, ArmorPiece?>): ArmorComparator {
return getArmorComparatorForParameters(ArmorKitParameters.getParametersForSlots(currentKit))
fun getArmorComparatorFor(
currentKit: Map<EquipmentSlot, ArmorPiece?>,
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)
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.ccbluex.liquidbounce.features.module.modules.combat.autoarmor

import net.ccbluex.liquidbounce.config.types.ToggleableConfigurable

object AutoArmorSaveArmor : ToggleableConfigurable(ModuleAutoArmor, "SaveArmor", true) {
val durabilityThreshold by int("DurabilityThreshold", 24, 0..100)
val autoOpen by boolean("AutoOpenInventory", true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@

import net.ccbluex.liquidbounce.event.events.ScheduleInventoryActionEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.event.tickHandler
import net.ccbluex.liquidbounce.features.module.Category
import net.ccbluex.liquidbounce.features.module.ClientModule
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.durability
import net.ccbluex.liquidbounce.utils.item.isNothing
import net.ccbluex.liquidbounce.utils.item.type
import net.ccbluex.liquidbounce.utils.kotlin.Priority
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.gui.screen.ingame.InventoryScreen
import net.minecraft.item.ArmorItem
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) {

Expand All @@ -45,12 +51,106 @@
* If disabled, it will only use inventory moves.
*/
private val useHotbar by boolean("Hotbar", true)
private var hasOpenedInventory = false

init {
tree(AutoArmorSaveArmor)
}

/**
* 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 (!AutoArmorSaveArmor.enabled) {
return@tickHandler
}

// the module will save armor automatically if open inventory isn't required
if (!inventoryConstraints.requiresOpenInventory || !AutoArmorSaveArmor.autoOpen) {
return@tickHandler
}

val armorToEquipWithSlots = ArmorEvaluation
.findBestArmorPieces(durabilityThreshold = AutoArmorSaveArmor.durabilityThreshold)
.values
.filterNotNull()
.filter { !it.isAlreadyEquipped && it.itemSlot.itemStack.item is ArmorItem }

val hasAnyHotBarReplacement = useHotbar && isNewerThanOrEqual1_19_4 && armorToEquipWithSlots.any { it.itemSlot is HotbarItemSlot }
if (hasAnyHotBarReplacement) {
// 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
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 <= AutoArmorSaveArmor.durabilityThreshold &&
armorToEquip.any { it.type() == (armorStack.item as ArmorItem).type() }
}

// closes the inventory after the armor is replaced
if (hasOpenedInventory && armorToEquip.isEmpty()) {
hasOpenedInventory = false
waitTicks(inventoryConstraints.closeDelay.random())

// the current screen might change while the module is waiting
if (mc.currentScreen is InventoryScreen) {
player.closeHandledScreen()
}
}

// tries to close the previous screen and open the inventory
while (hasArmorToReplace && mc.currentScreen !is InventoryScreen) {

if (mc.currentScreen is HandledScreen<*>) {
// closes chests/crating tables/etc.
// TODO: well, it doesn't... :(
// When the player is in a chest/anvil/crafting table/etc.,
// hasArmorToReplace is always false...
// The server simply doesn't let the player know anything new about his armor :/
// the client knows only the state of the armor before opening the screen,
// the client doesn't receive any updates on the armor slots until the screen is closed.
// However, the client still gets updates on the armor of other players :/

// TODO: since the client get no updates on the armor while a chest/crating table/etc. is open,
// try to approximately track the durability of the player's armor manually
// when the player receives damage and chest/crating table/etc. is open :)
player.closeHandledScreen()
} else if (mc.currentScreen != null) {
// closes ClickGUI, game chat, etc. to save some armor :)
mc.currentScreen!!.close()
}

waitTicks(1) // TODO: custom delay?

// again, the current screen might change while the module is waiting
if (mc.currentScreen == null) {
mc.setScreen(InventoryScreen(player))
hasOpenedInventory = true
}
}
}

private val scheduleHandler = handler<ScheduleInventoryActionEvent> { event ->
// Filter out already equipped armor pieces
val armorToEquip = ArmorEvaluation.findBestArmorPieces().values.filterNotNull().filter {
!it.isAlreadyEquipped
}
val durabilityThreshold = if (AutoArmorSaveArmor.enabled) { AutoArmorSaveArmor.durabilityThreshold } else Int.MIN_VALUE

val armorToEquip = ArmorEvaluation
.findBestArmorPieces(durabilityThreshold = durabilityThreshold)
.values.filterNotNull().filter {
!it.isAlreadyEquipped
}

for (armorPiece in armorToEquip) {
event.schedule(
Expand All @@ -75,15 +175,12 @@
return null
}

val inventorySlot = armorPiece.itemSlot
val armorPieceSlot = ArmorItemSlot(armorPiece.entitySlotId)

return if (!stackInArmor.isNothing()) {
// Clear current armor
performMoveOrHotbarClick(armorPieceSlot, isInArmorSlot = true)
performMoveOrHotbarClick(armorPiece, isInArmorSlot = true)
} else {
// Equip new armor
performMoveOrHotbarClick(inventorySlot, isInArmorSlot = false)
performMoveOrHotbarClick(armorPiece, isInArmorSlot = false)
}
}

Expand All @@ -98,21 +195,24 @@
* @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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArmorPiece> {
companion object {
private val DAMAGE_REDUCTION_ENCHANTMENTS: Array<RegistryKey<Enchantment>> = arrayOf(
Expand All @@ -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() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<EntityAttribute>): Float {
val attribInstance = EntityAttributeInstance(attribute) {}

Expand Down
Loading