diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java index 46b8f9daf..a2ae542c5 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java @@ -47,6 +47,7 @@ public class Costs { // We don't want a bot that tries to break blocks instead of walking around them public static final double BREAK_BLOCK_ADDITION = 2; public static final double PLACE_BLOCK = 5; + public static final double JUMP_UP_AND_PLACE_BELOW = JUMP + PLACE_BLOCK; // Sliding around a corner is roughly like walking two blocks public static final double CORNER_SLIDE = 2 - DIAGONAL; /** diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/BlockPlaceAction.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/BlockPlaceAction.java index 8fe897bf8..5336e4159 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/BlockPlaceAction.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/BlockPlaceAction.java @@ -39,6 +39,7 @@ public class BlockPlaceAction implements WorldAction { private final Vector3i blockPosition; private final BotActionManager.BlockPlaceData blockPlaceData; private boolean putOnHotbar = false; + private boolean finishedPlacing = false; @Override public boolean isCompleted(BotConnection connection) { @@ -141,7 +142,12 @@ public void tick(BotConnection connection) { throw new IllegalStateException("Failed to find item stack"); } + if (finishedPlacing) { + return; + } + connection.sessionDataManager().getBotActionManager().placeBlock(Hand.MAIN_HAND, blockPlaceData); + finishedPlacing = true; } @Override diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java index cc9962def..040fda3b3 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java @@ -32,7 +32,6 @@ public class GapJumpAction implements WorldAction { private final Vector3d position; private boolean didLook = false; private int noJumpTicks = 0; - private boolean didJump = false; @Override public boolean isCompleted(BotConnection connection) { diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/JumpAndPlaceBelowAction.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/JumpAndPlaceBelowAction.java new file mode 100644 index 000000000..efc8feef8 --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/JumpAndPlaceBelowAction.java @@ -0,0 +1,166 @@ +/* + * ServerWrecker + * + * Copyright (C) 2023 ServerWrecker + * + * 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 net.pistonmaster.serverwrecker.pathfinding.execution; + +import com.github.steveice10.mc.protocol.data.game.entity.player.Hand; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import net.pistonmaster.serverwrecker.data.BlockItems; +import net.pistonmaster.serverwrecker.protocol.BotConnection; +import net.pistonmaster.serverwrecker.protocol.bot.BotActionManager; +import net.pistonmaster.serverwrecker.protocol.bot.container.SWItemStack; +import net.pistonmaster.serverwrecker.util.BlockTypeHelper; +import net.pistonmaster.serverwrecker.util.ItemTypeHelper; +import net.pistonmaster.serverwrecker.util.TimeUtil; +import org.cloudburstmc.math.vector.Vector3i; + +import java.util.concurrent.TimeUnit; + +@ToString +@RequiredArgsConstructor +public class JumpAndPlaceBelowAction implements WorldAction { + private final Vector3i blockPosition; + private final BotActionManager.BlockPlaceData blockPlaceData; + private boolean putOnHotbar = false; + private boolean finishedPlacing = false; + + @Override + public boolean isCompleted(BotConnection connection) { + var levelState = connection.sessionDataManager().getCurrentLevel(); + if (levelState == null) { + return false; + } + + return levelState.getBlockStateAt(blockPosition) + .map(BlockTypeHelper::isFullBlock) + .orElse(false); + } + + @Override + public void tick(BotConnection connection) { + var sessionDataManager = connection.sessionDataManager(); + var movementManager = sessionDataManager.getBotMovementManager(); + movementManager.getControlState().resetAll(); + + if (!putOnHotbar) { + var inventoryManager = sessionDataManager.getInventoryManager(); + var playerInventory = inventoryManager.getPlayerInventory(); + + SWItemStack leastHardItem = null; + var leastHardness = 0F; + for (var slot : playerInventory.getStorage()) { + if (slot.item() == null) { + continue; + } + + var item = slot.item(); + var blockType = BlockItems.getBlockType(item.getType()); + if (blockType.isEmpty()) { + continue; + } + + if (leastHardItem == null || blockType.get().hardness() < leastHardness) { + leastHardItem = item; + leastHardness = blockType.get().hardness(); + } + } + + var heldSlot = playerInventory.getHotbarSlot(inventoryManager.getHeldItemSlot()); + if (heldSlot.item() != null) { + var item = heldSlot.item(); + if (ItemTypeHelper.isSafeFullBlockItem(item.getType())) { + putOnHotbar = true; + return; + } + } + + for (var hotbarSlot : playerInventory.getHotbar()) { + if (hotbarSlot.item() == null) { + continue; + } + + var item = hotbarSlot.item(); + if (!ItemTypeHelper.isSafeFullBlockItem(item.getType())) { + continue; + } + + inventoryManager.setHeldItemSlot(playerInventory.toHotbarIndex(hotbarSlot)); + inventoryManager.sendHeldItemChange(); + putOnHotbar = true; + return; + } + + for (var slot : playerInventory.getMainInventory()) { + if (slot.item() == null) { + continue; + } + + var item = slot.item(); + if (!ItemTypeHelper.isSafeFullBlockItem(item.getType())) { + continue; + } + + if (!inventoryManager.tryInventoryControl()) { + return; + } + + try { + inventoryManager.leftClickSlot(slot.slot()); + TimeUtil.waitTime(50, TimeUnit.MILLISECONDS); + inventoryManager.leftClickSlot(playerInventory.getHotbarSlot(inventoryManager.getHeldItemSlot()).slot()); + TimeUtil.waitTime(50, TimeUnit.MILLISECONDS); + + if (inventoryManager.getCursorItem() != null) { + inventoryManager.leftClickSlot(slot.slot()); + TimeUtil.waitTime(50, TimeUnit.MILLISECONDS); + } + } finally { + inventoryManager.unlockInventoryControl(); + } + + putOnHotbar = true; + return; + } + + throw new IllegalStateException("Failed to find item stack"); + } + + if (finishedPlacing) { + return; + } + + if (movementManager.getY() <= blockPosition.getY() + 1) { + // Make sure we are so high that we can place the block + movementManager.getControlState().setJumping(true); + return; + } else { + movementManager.getControlState().setJumping(false); + } + + connection.sessionDataManager().getBotActionManager().placeBlock(Hand.MAIN_HAND, blockPlaceData); + finishedPlacing = true; + } + + @Override + public int getAllowedTicks() { + // 3-seconds max to place a block + return 3 * 20; + } +} diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java index ce646c3f1..fb06b8b7f 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java @@ -29,6 +29,7 @@ import net.pistonmaster.serverwrecker.pathfinding.graph.actions.parkour.ParkourDirection; import net.pistonmaster.serverwrecker.pathfinding.graph.actions.parkour.ParkourMovement; import net.pistonmaster.serverwrecker.pathfinding.graph.actions.updown.DownMovement; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.updown.UpMovement; import net.pistonmaster.serverwrecker.protocol.bot.BotActionManager; import net.pistonmaster.serverwrecker.protocol.bot.block.BlockStateMeta; import net.pistonmaster.serverwrecker.protocol.bot.state.tag.TagsState; @@ -82,6 +83,12 @@ public record MinecraftGraph(TagsState tagsState) { actions.size() )); + actions.add(registerUpMovement( + blockSubscribers, + new UpMovement(), + actions.size() + )); + ACTIONS_TEMPLATE = actions.toArray(new GraphAction[0]); SUBSCRIPTION_KEYS = new Vector3i[blockSubscribers.size()]; SUBSCRIPTION_VALUES = new BlockSubscription[blockSubscribers.size()][]; @@ -310,6 +317,70 @@ public GraphInstructions[] getActions(BotEntityState node) { } } } + } else if (action instanceof UpMovement upMovement) { + switch (subscriber.type) { + case MOVEMENT_FREE -> { + if (!calculatedFree) { + // We can walk through blocks like air or grass + isFree = blockState.blockShapeType().hasNoCollisions() + && !BlockTypeHelper.isFluid(blockState.blockType()); + calculatedFree = true; + } + + if (isFree) { + upMovement.getNoNeedToBreak()[subscriber.blockArrayIndex] = true; + continue; + } + + // Search for a way to break this block + if (blockState.blockType().diggable() + && !upMovement.getUnsafeToBreak()[subscriber.blockArrayIndex] + && BlockItems.hasItemType(blockState.blockType())) { + var cacheableMiningCost = node.inventory() + .getMiningCosts(tagsState, blockState); + // We can mine this block, lets add costs and continue + upMovement.getBlockBreakCosts()[subscriber.blockArrayIndex] = new MovementMiningCost( + absolutePositionBlock, + cacheableMiningCost.miningCost(), + cacheableMiningCost.willDrop() + ); + } else { + // No way to break this block + upMovement.setImpossible(true); + } + } + case MOVEMENT_BREAK_SAFETY_CHECK -> { + // There is no need to break this block, so there is no need for safety checks + if (upMovement.getNoNeedToBreak()[subscriber.blockArrayIndex]) { + continue; + } + + // The block was already marked as unsafe + if (upMovement.getUnsafeToBreak()[subscriber.blockArrayIndex]) { + continue; + } + + var unsafe = switch (subscriber.safetyType) { + case FALLING_AND_FLUIDS -> BlockTypeHelper.isFluid(blockState.blockType()) + || BlockTypeHelper.isFallingAroundMinedBlock(blockState.blockType()); + case FLUIDS -> BlockTypeHelper.isFluid(blockState.blockType()); + }; + + if (unsafe) { + var currentValue = upMovement.getBlockBreakCosts()[subscriber.blockArrayIndex]; + + if (currentValue == null) { + // Store for a later time that this is unsafe, + // so if we check this block, + // we know it's unsafe + upMovement.getUnsafeToBreak()[subscriber.blockArrayIndex] = true; + } else { + // We learned that this block needs to be broken, so we need to set it as impossible + upMovement.setImpossible(true); + } + } + } + } } } } @@ -418,6 +489,34 @@ private static DownMovement registerDownMovement(Object2ObjectMap> blockSubscribers, + UpMovement movement, int movementIndex) { + { + var blockId = 0; + for (var freeBlock : movement.listRequiredFreeBlocks()) { + blockSubscribers.computeIfAbsent(freeBlock, CREATE_MISSING_FUNCTION) + .add(new BlockSubscription(movementIndex, SubscriptionType.MOVEMENT_FREE, blockId++)); + } + } + + { + var safeBlocks = movement.listCheckSafeMineBlocks(); + for (var i = 0; i < safeBlocks.length; i++) { + var savedBlock = safeBlocks[i]; + if (savedBlock == null) { + continue; + } + + for (var block : savedBlock) { + blockSubscribers.computeIfAbsent(block.position(), CREATE_MISSING_FUNCTION) + .add(new BlockSubscription(movementIndex, SubscriptionType.MOVEMENT_BREAK_SAFETY_CHECK, i, block.type())); + } + } + } + + return movement; + } + enum SubscriptionType { MOVEMENT_FREE, MOVEMENT_BREAK_SAFETY_CHECK, diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpMovement.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpMovement.java new file mode 100644 index 000000000..02301e5b4 --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpMovement.java @@ -0,0 +1,159 @@ +/* + * ServerWrecker + * + * Copyright (C) 2023 ServerWrecker + * + * 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 net.pistonmaster.serverwrecker.pathfinding.graph.actions.updown; + +import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.pistonmaster.serverwrecker.pathfinding.BotEntityState; +import net.pistonmaster.serverwrecker.pathfinding.Costs; +import net.pistonmaster.serverwrecker.pathfinding.execution.BlockBreakAction; +import net.pistonmaster.serverwrecker.pathfinding.execution.JumpAndPlaceBelowAction; +import net.pistonmaster.serverwrecker.pathfinding.execution.WorldAction; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphAction; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphInstructions; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.movement.BlockDirection; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.movement.BlockSafetyData; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.movement.MovementMiningCost; +import net.pistonmaster.serverwrecker.protocol.bot.BotActionManager; +import net.pistonmaster.serverwrecker.util.VectorHelper; +import org.cloudburstmc.math.vector.Vector3i; + +import java.util.List; + +@Slf4j +public final class UpMovement implements GraphAction { + private static final Vector3i FEET_POSITION_RELATIVE_BLOCK = Vector3i.ZERO; + private final Vector3i targetFeetBlock; + @Getter + private final MovementMiningCost[] blockBreakCosts; + @Getter + private final boolean[] unsafeToBreak; + @Getter + private final boolean[] noNeedToBreak; + private double cost; + @Setter + @Getter + private boolean isImpossible = false; + @Setter + private boolean requiresAgainstBlock = false; + + public UpMovement() { + this.cost = Costs.JUMP_UP_AND_PLACE_BELOW; + + this.targetFeetBlock = FEET_POSITION_RELATIVE_BLOCK.add(0, 1, 0); + + blockBreakCosts = new MovementMiningCost[freeCapacity()]; + unsafeToBreak = new boolean[freeCapacity()]; + noNeedToBreak = new boolean[freeCapacity()]; + } + + private UpMovement(UpMovement other) { + this.targetFeetBlock = other.targetFeetBlock; + this.cost = other.cost; + this.isImpossible = other.isImpossible; + this.blockBreakCosts = new MovementMiningCost[other.blockBreakCosts.length]; + this.unsafeToBreak = new boolean[other.unsafeToBreak.length]; + this.noNeedToBreak = new boolean[other.noNeedToBreak.length]; + this.requiresAgainstBlock = other.requiresAgainstBlock; + } + + private int freeCapacity() { + return 1; + } + + public List listRequiredFreeBlocks() { + List requiredFreeBlocks = new ObjectArrayList<>(freeCapacity()); + + // The one above the head to jump + requiredFreeBlocks.add(FEET_POSITION_RELATIVE_BLOCK.add(0, 2, 0)); + + return requiredFreeBlocks; + } + + public BlockSafetyData[][] listCheckSafeMineBlocks() { + var requiredFreeBlocks = listRequiredFreeBlocks(); + var results = new BlockSafetyData[requiredFreeBlocks.size()][]; + + var firstDirection = BlockDirection.NORTH; + var oppositeDirection = firstDirection.opposite(); + var leftDirectionSide = firstDirection.leftSide(); + var rightDirectionSide = firstDirection.rightSide(); + + var aboveHead = FEET_POSITION_RELATIVE_BLOCK.add(0, 2, 0); + results[requiredFreeBlocks.indexOf(aboveHead)] = new BlockSafetyData[]{ + new BlockSafetyData(aboveHead.add(0, 1, 0), BlockSafetyData.BlockSafetyType.FALLING_AND_FLUIDS), + new BlockSafetyData(oppositeDirection.offset(aboveHead), BlockSafetyData.BlockSafetyType.FLUIDS), + new BlockSafetyData(leftDirectionSide.offset(aboveHead), BlockSafetyData.BlockSafetyType.FLUIDS), + new BlockSafetyData(rightDirectionSide.offset(aboveHead), BlockSafetyData.BlockSafetyType.FLUIDS) + }; + + return results; + } + + @Override + public boolean isImpossibleToComplete() { + return isImpossible; + } + + @Override + public GraphInstructions getInstructions(BotEntityState previousEntityState) { + var actions = new ObjectArrayList(); + var inventory = previousEntityState.inventory(); + var levelState = previousEntityState.levelState(); + var cost = this.cost; + for (var breakCost : blockBreakCosts) { + if (breakCost == null) { + continue; + } + + cost += breakCost.miningCost(); + actions.add(new BlockBreakAction(breakCost.block())); + if (breakCost.willDrop()) { + inventory = inventory.withOneMoreBlock(); + } + + levelState = levelState.withChangeToAir(breakCost.block()); + } + + var absoluteTargetFeetBlock = previousEntityState.positionBlock().add(targetFeetBlock); + var targetFeetDoublePosition = VectorHelper.middleOfBlockNormalize(absoluteTargetFeetBlock.toDouble()); + + // Where we are standing right now, we'll place the target block below us after jumping + actions.add(new JumpAndPlaceBelowAction(previousEntityState.positionBlock(), new BotActionManager.BlockPlaceData( + previousEntityState.positionBlock().sub(0, 1, 0), + Direction.UP + ))); + + return new GraphInstructions(new BotEntityState( + targetFeetDoublePosition, + absoluteTargetFeetBlock, + levelState, + inventory + ), cost, actions); + } + + @Override + public UpMovement copy(BotEntityState previousEntityState) { + return new UpMovement(this); + } +}