diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java index 6faea1334..46b8f9daf 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/Costs.java @@ -37,6 +37,7 @@ public class Costs { public static final double STRAIGHT = 1; public static final double DIAGONAL = 1.4142135623730951; public static final double JUMP = 0.3; + public static final double ONE_GAP_JUMP = STRAIGHT + JUMP; public static final double FALL_1 = 0.1; public static final double FALL_2 = 0.2; public static final double FALL_3 = 0.3; diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java new file mode 100644 index 000000000..cc9962def --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/execution/GapJumpAction.java @@ -0,0 +1,105 @@ +/* + * 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.RotationOrigin; +import lombok.RequiredArgsConstructor; +import lombok.ToString; +import net.pistonmaster.serverwrecker.protocol.BotConnection; +import net.pistonmaster.serverwrecker.util.BlockTypeHelper; +import org.cloudburstmc.math.vector.Vector3d; + +@ToString +@RequiredArgsConstructor +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) { + var movementManager = connection.sessionDataManager().getBotMovementManager(); + var botPosition = movementManager.getPlayerPos(); + var levelState = connection.sessionDataManager().getCurrentLevel(); + if (levelState == null) { + return false; + } + + var blockMeta = levelState.getBlockStateAt(position.toInt()); + var insideBlock = blockMeta.isPresent() && !BlockTypeHelper.isEmpty(blockMeta.get()); + + if (insideBlock) { + // We are inside a block, so being close is good enough + var distance = botPosition.distance(position); + return distance <= 1; + } else if (botPosition.getY() != position.getY()) { + // We want to be on the same Y level + return false; + } else { + var distance = botPosition.distance(position); + return distance <= 0.3; + } + } + + @Override + public void tick(BotConnection connection) { + var movementManager = connection.sessionDataManager().getBotMovementManager(); + var botPosition = movementManager.getPlayerPos(); + + var previousYaw = movementManager.getYaw(); + movementManager.lookAt(RotationOrigin.EYES, position); + movementManager.setPitch(0); + var newYaw = movementManager.getYaw(); + + var yawDifference = Math.abs(previousYaw - newYaw); + + // We should only set the yaw once to the server to prevent the bot looking weird due to inaccuracy + if (didLook && yawDifference > 5) { + movementManager.setLastSentYaw(movementManager.getYaw()); + } else { + didLook = true; + } + + // Don't let the bot look up or down (makes it look weird) + movementManager.getControlState().resetAll(); + movementManager.getControlState().setForward(true); + + if (shouldJump()) { + movementManager.getControlState().setJumping(true); + } + } + + private boolean shouldJump() { + if (noJumpTicks < 1) { + noJumpTicks++; + return false; + } else { + noJumpTicks = 0; + return true; + } + } + + @Override + public int getAllowedTicks() { + // 5-seconds max to walk to a block + return 5 * 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 b331a03ab..0a959ce7d 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/MinecraftGraph.java @@ -25,7 +25,10 @@ import net.pistonmaster.serverwrecker.pathfinding.BotEntityState; import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphAction; import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphInstructions; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.PlayerMovement; import net.pistonmaster.serverwrecker.pathfinding.graph.actions.movement.*; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.parkour.ParkourDirection; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.parkour.ParkourMovement; import net.pistonmaster.serverwrecker.protocol.bot.BotActionManager; import net.pistonmaster.serverwrecker.protocol.bot.block.BlockStateMeta; import net.pistonmaster.serverwrecker.protocol.bot.state.tag.TagsState; @@ -65,6 +68,14 @@ public record MinecraftGraph(TagsState tagsState) { } } + for (var direction : ParkourDirection.VALUES) { + actions.add(registerParkourMovement( + blockSubscribers, + new ParkourMovement(direction), + actions.size() + )); + } + ACTIONS_TEMPLATE = actions.toArray(new GraphAction[0]); SUBSCRIPTION_KEYS = new Vector3i[blockSubscribers.size()]; SUBSCRIPTION_VALUES = new BlockSubscription[blockSubscribers.size()][]; @@ -111,119 +122,145 @@ public GraphInstructions[] getActions(BotEntityState node) { .orElseThrow(OutOfLevelException::new); } - var movement = (PlayerMovement) action; - 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) { - if (movement.isAllowBlockActions()) { - movement.getNoNeedToBreak()[subscriber.blockArrayIndex] = true; + if (action instanceof PlayerMovement movement) { + 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; } - continue; - } + if (isFree) { + if (movement.isAllowBlockActions()) { + movement.getNoNeedToBreak()[subscriber.blockArrayIndex] = true; + } - // Search for a way to break this block - if (movement.isAllowBlockActions() - // Narrow this down to blocks that can be broken - && blockState.blockType().diggable() - // Check if we previously found out this block is unsafe to break - && !movement.getUnsafeToBreak()[subscriber.blockArrayIndex] - // Narrows the list down to a reasonable size - && BlockItems.hasItemType(blockState.blockType())) { - var cacheableMiningCost = node.inventory() - .getMiningCosts(tagsState, blockState); - // We can mine this block, lets add costs and continue - movement.getBlockBreakCosts()[subscriber.blockArrayIndex] = new MovementMiningCost( - absolutePositionBlock, - cacheableMiningCost.miningCost(), - cacheableMiningCost.willDrop() - ); - } else { - // No way to break this block - movement.setImpossible(true); - } - } - case MOVEMENT_BREAK_SAFETY_CHECK -> { - // There is no need to break this block, so there is no need for safety checks - if (movement.getNoNeedToBreak()[subscriber.blockArrayIndex]) { - continue; - } + continue; + } - // The block was already marked as unsafe - if (movement.getUnsafeToBreak()[subscriber.blockArrayIndex]) { - continue; + // Search for a way to break this block + if (movement.isAllowBlockActions() + // Narrow this down to blocks that can be broken + && blockState.blockType().diggable() + // Check if we previously found out this block is unsafe to break + && !movement.getUnsafeToBreak()[subscriber.blockArrayIndex] + // Narrows the list down to a reasonable size + && BlockItems.hasItemType(blockState.blockType())) { + var cacheableMiningCost = node.inventory() + .getMiningCosts(tagsState, blockState); + // We can mine this block, lets add costs and continue + movement.getBlockBreakCosts()[subscriber.blockArrayIndex] = new MovementMiningCost( + absolutePositionBlock, + cacheableMiningCost.miningCost(), + cacheableMiningCost.willDrop() + ); + } else { + // No way to break this block + movement.setImpossible(true); + } } + case MOVEMENT_BREAK_SAFETY_CHECK -> { + // There is no need to break this block, so there is no need for safety checks + if (movement.getNoNeedToBreak()[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()); - }; + // The block was already marked as unsafe + if (movement.getUnsafeToBreak()[subscriber.blockArrayIndex]) { + continue; + } - if (unsafe) { - var currentValue = movement.getBlockBreakCosts()[subscriber.blockArrayIndex]; + 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 = movement.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 + movement.getUnsafeToBreak()[subscriber.blockArrayIndex] = true; + } else { + // We learned that this block needs to be broken, so we need to set it as impossible + movement.setImpossible(true); + } + } + } + case MOVEMENT_SOLID -> { + // Block is safe to walk on, no need to check for more + if (blockState.blockShapeType().isFullBlock()) { + continue; + } - if (currentValue == null) { - // Store for a later time that this is unsafe, - // so if we check this block, - // we know it's unsafe - movement.getUnsafeToBreak()[subscriber.blockArrayIndex] = true; + if (movement.isAllowBlockActions() + && node.inventory().hasBlockToPlace() + && BlockTypeHelper.isReplaceable(blockState.blockType())) { + // We can place a block here, but we need to find a block to place against + movement.setRequiresAgainstBlock(true); } else { - // We learned that this block needs to be broken, so we need to set it as impossible movement.setImpossible(true); } } - } - case MOVEMENT_SOLID -> { - // Block is safe to walk on, no need to check for more - if (blockState.blockShapeType().isFullBlock()) { - continue; + case MOVEMENT_AGAINST_PLACE_SOLID -> { + // We already found one, no need to check for more + if (movement.getBlockPlaceData() != null) { + continue; + } + + // This block should not be placed against + if (!blockState.blockShapeType().isFullBlock()) { + continue; + } + + // Fixup the position to be the block we are placing against instead of relative + movement.setBlockPlaceData(new BotActionManager.BlockPlaceData( + absolutePositionBlock, + subscriber.blockToPlaceAgainst.blockFace() + )); } + case MOVEMENT_ADD_CORNER_COST_IF_SOLID -> { + // No need to apply the cost multiple times. + if (movement.isAppliedCornerCost()) { + continue; + } - if (movement.isAllowBlockActions() - && node.inventory().hasBlockToPlace() - && BlockTypeHelper.isReplaceable(blockState.blockType())) { - // We can place a block here, but we need to find a block to place against - movement.setRequiresAgainstBlock(true); - } else { - movement.setImpossible(true); + if (blockState.blockShapeType().isFullBlock()) { + movement.addCornerCost(); + } else if (BlockTypeHelper.isHurtOnTouch(blockState.blockType())) { + // Since this is a corner, we can also avoid touching blocks that hurt us, e.g., cacti + movement.setImpossible(true); + } } } - case MOVEMENT_AGAINST_PLACE_SOLID -> { - // We already found one, no need to check for more - if (movement.getBlockPlaceData() != null) { - continue; - } + } else if (action instanceof ParkourMovement parkourMovement) { + 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; + } - // This block should not be placed against - if (!blockState.blockShapeType().isFullBlock()) { - continue; - } + if (isFree) { + continue; + } - // Fixup the position to be the block we are placing against instead of relative - movement.setBlockPlaceData(new BotActionManager.BlockPlaceData( - absolutePositionBlock, - subscriber.blockToPlaceAgainst.blockFace() - )); - } - case MOVEMENT_ADD_CORNER_COST_IF_SOLID -> { - // No need to apply the cost multiple times. - if (movement.isAppliedCornerCost()) { - continue; + parkourMovement.setImpossible(true); } + case MOVEMENT_SOLID -> { + // Block is safe to walk on, no need to check for more + if (blockState.blockShapeType().isFullBlock()) { + continue; + } - if (blockState.blockShapeType().isFullBlock()) { - movement.addCornerCost(); - } else if (BlockTypeHelper.isHurtOnTouch(blockState.blockType())) { - // Since this is a corner, we can also avoid touching blocks that hurt us, e.g., cacti - movement.setImpossible(true); + parkourMovement.setImpossible(true); } } } @@ -294,6 +331,24 @@ private static PlayerMovement registerMovement(Object2ObjectMap> blockSubscribers, + ParkourMovement 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++)); + } + } + + { + blockSubscribers.computeIfAbsent(movement.requiredSolidBlock(), CREATE_MISSING_FUNCTION) + .add(new BlockSubscription(movementIndex, SubscriptionType.MOVEMENT_SOLID)); + } + + return movement; + } + enum SubscriptionType { MOVEMENT_FREE, MOVEMENT_BREAK_SAFETY_CHECK, diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/movement/PlayerMovement.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/PlayerMovement.java similarity index 99% rename from src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/movement/PlayerMovement.java rename to src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/PlayerMovement.java index d019b22dc..b8f0b6071 100644 --- a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/movement/PlayerMovement.java +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/PlayerMovement.java @@ -17,7 +17,7 @@ * License along with this program. If not, see * . */ -package net.pistonmaster.serverwrecker.pathfinding.graph.actions.movement; +package net.pistonmaster.serverwrecker.pathfinding.graph.actions; import com.github.steveice10.mc.protocol.data.game.entity.object.Direction; import it.unimi.dsi.fastutil.objects.ObjectArrayList; @@ -30,8 +30,7 @@ import net.pistonmaster.serverwrecker.pathfinding.execution.BlockPlaceAction; import net.pistonmaster.serverwrecker.pathfinding.execution.MovementAction; 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.*; import net.pistonmaster.serverwrecker.protocol.bot.BotActionManager; import net.pistonmaster.serverwrecker.util.VectorHelper; import org.cloudburstmc.math.vector.Vector3i; diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourDirection.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourDirection.java new file mode 100644 index 000000000..de8f9772a --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourDirection.java @@ -0,0 +1,41 @@ +/* + * 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.parkour; + +import org.cloudburstmc.math.vector.Vector3i; + +public enum ParkourDirection { + NORTH, + SOUTH, + EAST, + WEST; + + public static final ParkourDirection[] VALUES = values(); + + @SuppressWarnings("DuplicatedCode") + public Vector3i offset(Vector3i vector) { + return switch (this) { + case NORTH -> vector.add(0, 0, -1); + case SOUTH -> vector.add(0, 0, 1); + case EAST -> vector.add(1, 0, 0); + case WEST -> vector.add(-1, 0, 0); + }; + } +} diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourMovement.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourMovement.java new file mode 100644 index 000000000..c6b74ff7d --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/parkour/ParkourMovement.java @@ -0,0 +1,110 @@ +/* + * 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.parkour; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import lombok.Getter; +import lombok.Setter; +import net.pistonmaster.serverwrecker.pathfinding.BotEntityState; +import net.pistonmaster.serverwrecker.pathfinding.Costs; +import net.pistonmaster.serverwrecker.pathfinding.execution.GapJumpAction; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphAction; +import net.pistonmaster.serverwrecker.pathfinding.graph.actions.GraphInstructions; +import net.pistonmaster.serverwrecker.util.VectorHelper; +import org.cloudburstmc.math.vector.Vector3i; + +import java.util.List; + +public class ParkourMovement implements GraphAction { + private static final Vector3i FEET_POSITION_RELATIVE_BLOCK = Vector3i.ZERO; + private final ParkourDirection direction; + private final Vector3i targetFeetBlock; + @Setter + @Getter + private boolean isImpossible = false; + + public ParkourMovement(ParkourDirection direction) { + this.direction = direction; + this.targetFeetBlock = direction.offset(direction.offset(FEET_POSITION_RELATIVE_BLOCK)); + } + + private ParkourMovement(ParkourMovement other) { + this.direction = other.direction; + this.targetFeetBlock = other.targetFeetBlock; + this.isImpossible = other.isImpossible; + } + + public List listRequiredFreeBlocks() { + List requiredFreeBlocks = new ObjectArrayList<>(); + + // Make head block free (maybe head block is a slab) + requiredFreeBlocks.add(FEET_POSITION_RELATIVE_BLOCK.add(0, 1, 0)); + + // Make block above the head block free for jump + requiredFreeBlocks.add(FEET_POSITION_RELATIVE_BLOCK.add(0, 2, 0)); + + var oneFurther = direction.offset(FEET_POSITION_RELATIVE_BLOCK); + + // The gap to jump over + requiredFreeBlocks.add(oneFurther.sub(0, 1, 0)); + + // Room for jumping + requiredFreeBlocks.add(oneFurther); + requiredFreeBlocks.add(oneFurther.add(0, 1, 0)); + requiredFreeBlocks.add(oneFurther.add(0, 2, 0)); + + var twoFurther = direction.offset(oneFurther); + + // Room for jumping + requiredFreeBlocks.add(twoFurther); + requiredFreeBlocks.add(twoFurther.add(0, 1, 0)); + requiredFreeBlocks.add(twoFurther.add(0, 2, 0)); + + return requiredFreeBlocks; + } + + public Vector3i requiredSolidBlock() { + // Floor block + return targetFeetBlock.sub(0, 1, 0); + } + + @Override + public boolean isImpossibleToComplete() { + return isImpossible; + } + + @Override + public GraphInstructions getInstructions(BotEntityState previousEntityState) { + var absoluteTargetFeetBlock = previousEntityState.positionBlock().add(targetFeetBlock); + var targetFeetDoublePosition = VectorHelper.middleOfBlockNormalize(absoluteTargetFeetBlock.toDouble()); + + return new GraphInstructions(new BotEntityState( + targetFeetDoublePosition, + absoluteTargetFeetBlock, + previousEntityState.levelState(), + previousEntityState.inventory() + ), Costs.ONE_GAP_JUMP, List.of(new GapJumpAction(targetFeetDoublePosition))); + } + + @Override + public ParkourMovement copy(BotEntityState previousEntityState) { + return new ParkourMovement(this); + } +} diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownDirection.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownDirection.java new file mode 100644 index 000000000..b5b5efdfb --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownDirection.java @@ -0,0 +1,25 @@ +/* + * 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; + +public enum UpDownDirection { + UP, + DOWN +} diff --git a/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownMovement.java b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownMovement.java new file mode 100644 index 000000000..b33d116e3 --- /dev/null +++ b/src/main/java/net/pistonmaster/serverwrecker/pathfinding/graph/actions/updown/UpDownMovement.java @@ -0,0 +1,23 @@ +/* + * 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; + +public class UpDownMovement { +}