diff --git a/utils/build.gradle.kts b/utils/build.gradle.kts index 44f375c..327ac38 100644 --- a/utils/build.gradle.kts +++ b/utils/build.gradle.kts @@ -14,4 +14,4 @@ dependencies { } description = "AlpsLib-Utils" -version = "1.4.5" +version = "1.4.6" diff --git a/utils/src/main/java/com/alpsbte/alpslib/utils/GeneratorUtils.java b/utils/src/main/java/com/alpsbte/alpslib/utils/GeneratorUtils.java index 0b6e687..9001239 100644 --- a/utils/src/main/java/com/alpsbte/alpslib/utils/GeneratorUtils.java +++ b/utils/src/main/java/com/alpsbte/alpslib/utils/GeneratorUtils.java @@ -8,6 +8,7 @@ import clipper2.offset.JoinType; import com.cryptomorin.xseries.XMaterial; import com.fastasyncworldedit.core.limit.FaweLimit; +import com.fastasyncworldedit.core.registry.state.PropertyKey; import com.sk89q.worldedit.*; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.extension.factory.MaskFactory; @@ -36,6 +37,7 @@ import com.sk89q.worldedit.regions.selector.Polygonal2DRegionSelector; import com.sk89q.worldedit.session.ClipboardHolder; import com.sk89q.worldedit.session.SessionManager; +import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; @@ -113,6 +115,7 @@ *
• {@link #drawLineWithMasks(LocalSession, Actor, com.sk89q.worldedit.world.World, Block[][][], List, Vector, Vector, BlockState[], boolean)} *
• {@link #pasteSchematicWithMasks(LocalSession, Actor, com.sk89q.worldedit.world.World, Block[][][], List, String, Location, double)} *
• {@link #pasteSchematic(LocalSession, Actor, com.sk89q.worldedit.world.World, Block[][][], String, Location, double)} + *
• {@link #setBlockStatesAtPositions(LocalSession, Actor, com.sk89q.worldedit.world.World, List, List)} *
• {@link #expandSelection(LocalSession, Vector)} *
• {@link #clearHistory(LocalSession)} *
• {@link #undo(LocalSession, Player, Actor, int)} @@ -122,6 +125,12 @@ * *

Vector Helper Functions: *
• {@link #adjustHeight(List, Block[][][])} + *
• {@link #toBlockVector(Vector)} + *
• {@link #copyToBlockVectors(List)} + *
• {@link #createShortestBlockPath(List)} + *
• {@link #appendShortestBlockLine(List, Vector, Vector)} + *
• {@link #removeOrthogonalCorners(List)} + *
• {@link #createBoundsSelectionPoints(List, int)} *
• {@link #populatePoints(List, int)} *
• {@link #reducePoints(List, int, int)} *
• {@link #extendPolyLine(List)} @@ -217,11 +226,11 @@ private static void sendWikiLink(Player p) { - + /*=============================================** - + SCRIPT HELPER FUNCTIONS - + **=============================================*/ /** @@ -291,7 +300,54 @@ public static BlockState[] getBlockState(XMaterial[] xMaterial){ return blockStates; } - + + /** + * Returns the default block state of a directional block with the facing property set. + * + * @param blockType The block type to create the state from + * @param facing The facing direction to apply + * @return The block state with the facing property set + */ + public static BlockState getBlockStateWithFacing(BlockType blockType, Direction facing) { + if (blockType == null || facing == null) return null; + + return blockType.getDefaultState().with(PropertyKey.FACING, facing); + } + + /** + * Returns the cardinal facing from one vector to another. + * + * @param from The start vector + * @param to The target vector + * @param fallbackFacing The direction to use when the vectors have no clear horizontal direction + * @return The best matching cardinal direction + */ + public static Direction getFacing(Vector from, Vector to, Direction fallbackFacing) { + if (from == null || to == null) return fallbackFacing; + + return getFacing(to.getBlockX() - from.getBlockX(), to.getBlockZ() - from.getBlockZ(), fallbackFacing); + } + + /** + * Returns the cardinal facing for a horizontal delta. + * + * @param deltaX The X delta + * @param deltaZ The Z delta + * @param fallbackFacing The direction to use when the delta has no dominant horizontal axis + * @return The best matching cardinal direction + */ + public static Direction getFacing(int deltaX, int deltaZ, Direction fallbackFacing) { + if (deltaX == 0 && deltaZ == 0) return fallbackFacing; + + if (Math.abs(deltaX) > Math.abs(deltaZ)) return deltaX >= 0 ? Direction.EAST : Direction.WEST; + + if (Math.abs(deltaZ) > Math.abs(deltaX)) return deltaZ >= 0 ? Direction.SOUTH : Direction.NORTH; + + if (fallbackFacing != null) return fallbackFacing; + + return deltaZ >= 0 ? Direction.SOUTH : Direction.NORTH; + } + /*=============================================** @@ -316,17 +372,17 @@ public static String getWorldEditSchematicsFolderPath(){ } - - - + + + /*=============================================** - + WORLDEDIT REGION FUNCTIONS - + **=============================================*/ - - - + + + /** * Returns the WorldEdit selection Vector from a player no matter which type of selection the player made. * @@ -418,9 +474,9 @@ public static List getSelectionPointsFromRegion(Region region) { /** * Returns the minimum and maximum points of a region as a Vector array. - * + * * @param region The region to get the minimum and maximum points from - * @return A Vector array with the minimum vector at index 0 and the maximum vector at index 1 + * @return A Vector array with the minimum vector at index 0 and the maximum vector at index 1 */ public static Vector[] getMinMaxPoints(Region region){ Vector[] minMax = new Vector[2]; @@ -520,7 +576,7 @@ public static boolean containsBlock(Block[][][] blocks, XMaterial xMaterial, int for (Block block : block1D) { ItemStack item = xMaterial.parseItem(); if (block != null && item != null && block.getType() == item.getType()) - amountFound++; + amountFound++; } return amountFound >= requiredAmount; @@ -664,19 +720,19 @@ public static Region changeRegionWorld(Region region, com.sk89q.worldedit.world. return null; } - + /*=============================================** - + WORLDEDIT OPERATION FUNCTIONS - + **=============================================*/ - - + + /** * Prepares a script session by expanding the selection, removing non-solid blocks and ignored materials. @@ -705,7 +761,7 @@ public static Block[][][] prepareScriptSession(LocalSession localSession, Actor if(removeNonSolidBlocks) replaceBlocksWithMasks(localSession, actor, world, Collections.singletonList("!#solid"), null, new BlockState[]{air.getDefaultState()}, 1) - .join(); + .join(); if(removeIgnoredMaterials) { Material[] materials = getIgnoredMaterials(); @@ -718,7 +774,7 @@ public static Block[][][] prepareScriptSession(LocalSession localSession, Actor List blockStates = blockType.getAllStates(); BlockState[] blockStatesArray = blockStates.toArray(new BlockState[0]); replaceBlocks(localSession, actor, world, blockStatesArray, new BlockState[]{air.getDefaultState()}) - .join(); + .join(); } } @@ -730,7 +786,7 @@ public static Block[][][] prepareScriptSession(LocalSession localSession, Actor return regionBlocks; } - + /** Analyzes a region and returns a three-dimensional array of all blocks in the region. * The size of the array is defined by the width, height and length of the region from WorldEdit of the player. @@ -889,7 +945,7 @@ public static CompletableFuture copyRegion(LocalSession localSession, Acto */ public static CompletableFuture replaceBlocksWithMasks(LocalSession localSession, Actor actor, com.sk89q.worldedit.world.World weWorld, List masks, BlockState from, BlockState[] to, int iterations) { if(to == null || to.length == 0) - throw new IllegalArgumentException("BlockState[] to is empty"); + throw new IllegalArgumentException("BlockState[] to is empty"); CompletableFuture future = new CompletableFuture<>(); Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> { @@ -1378,11 +1434,11 @@ public static void undo(LocalSession session, Player player, Actor actor, int am } /** - * Redoes the last action of a LocalSession. - * @param session The local session to redo the last action of - * @param player The player who created the structure - * @param amount The amount of actions to redo - */ + * Redoes the last action of a LocalSession. + * @param session The local session to redo the last action of + * @param player The player who created the structure + * @param amount The amount of actions to redo + */ public static void redo(LocalSession session, Player player, Actor actor, int amount){ com.sk89q.worldedit.entity.Player wePlayer = BukkitAdapter.adapt(player); @@ -1444,6 +1500,53 @@ public static void saveEditSession(EditSession editSession, LocalSession localSe editSession.close(); } + /** + * Sets individual block states at exact positions and stores the operation in the player's WorldEdit history. + * + * @param localSession The WorldEdit local session + * @param actor The WorldEdit actor + * @param weWorld The WorldEdit world + * @param positions The block positions to change + * @param blockStates The block states to place at the matching positions + * @return A CompletableFuture that completes when the operation is finished + */ + public static CompletableFuture setBlockStatesAtPositions( + LocalSession localSession, + Actor actor, + com.sk89q.worldedit.world.World weWorld, + List positions, + List blockStates + ) { + if (positions.size() != blockStates.size()) + throw new IllegalArgumentException("Position and BlockState lists must have the same size"); + + CompletableFuture future = new CompletableFuture<>(); + + Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> { + try (EditSession editSession = WorldEdit.getInstance().newEditSession(weWorld)) { + for (int index = 0; index < positions.size(); index++) { + BlockState blockState = blockStates.get(index); + + if (blockState == null) + continue; + + Vector position = positions.get(index); + editSession.setBlock( + BlockVector3.at(position.getBlockX(), position.getBlockY(), position.getBlockZ()), + (Pattern) blockState + ); + } + + saveEditSession(editSession, localSession, actor); + future.complete(null); + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + + return future; + } + @@ -1469,6 +1572,166 @@ public static void adjustHeight(List points, Block[][][] blocks){ point.setY(getMaxHeight(blocks, point.getBlockX(), point.getBlockZ(), getIgnoredMaterials())); } + /** + * Returns a new vector snapped to integer block coordinates. + * + * @param vector The vector to snap + * @return A vector using the input's block coordinates + */ + public static Vector toBlockVector(Vector vector) { + return new Vector(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + } + + /** + * Copies the given vectors and snaps every copy to integer block coordinates. + * + * @param vectors The vectors to copy + * @return A copied list using block coordinates + */ + public static List copyToBlockVectors(List vectors) { + List copiedVectors = new ArrayList<>(); + + for (Vector vector : vectors) + copiedVectors.add(toBlockVector(vector)); + + return copiedVectors; + } + + /** + * Checks whether two vectors point to the same block coordinate. + * + * @param first The first vector + * @param second The second vector + * @return Whether both vectors have the same block coordinates + */ + public static boolean isSameBlock(Vector first, Vector second) { + return first.getBlockX() == second.getBlockX() + && first.getBlockY() == second.getBlockY() + && first.getBlockZ() == second.getBlockZ(); + } + + /** + * Creates a block-by-block path through the given control points. + * + * @param points The control points to connect + * @return The shortest block path connecting the points + */ + public static List createShortestBlockPath(List points) { + List path = new ArrayList<>(); + + if (points.isEmpty()) + return path; + + path.add(toBlockVector(points.getFirst())); + + for (int index = 0; index < points.size() - 1; index++) + appendShortestBlockLine(path, points.get(index), points.get(index + 1)); + + return path; + } + + /** + * Appends the shortest horizontal block line between two points to an existing path. + * + * @param path The path to append to + * @param start The start point + * @param end The end point + */ + public static void appendShortestBlockLine(List path, Vector start, Vector end) { + int deltaX = end.getBlockX() - start.getBlockX(); + int deltaY = end.getBlockY() - start.getBlockY(); + int deltaZ = end.getBlockZ() - start.getBlockZ(); + int horizontalSteps = Math.max(Math.abs(deltaX), Math.abs(deltaZ)); + + if (horizontalSteps == 0) { + Vector blockPoint = toBlockVector(end); + + if (path.isEmpty()) + path.add(blockPoint); + else + path.set(path.size() - 1, blockPoint); + + return; + } + + for (int step = 1; step <= horizontalSteps; step++) { + double progress = step / (double) horizontalSteps; + path.add(new Vector( + start.getBlockX() + (int) Math.round(deltaX * progress), + start.getBlockY() + (int) Math.round(deltaY * progress), + start.getBlockZ() + (int) Math.round(deltaZ * progress) + )); + } + } + + /** + * Removes orthogonal corner points from a block path. + * + * @param path The path to smooth + * @return A path without blocky right-angle corner duplicates + */ + public static List removeOrthogonalCorners(List path) { + List result = new ArrayList<>(); + + for (int index = 0; index < path.size(); index++) { + if (index > 0 && index < path.size() - 1 && isOrthogonalCorner(path.get(index - 1), path.get(index), path.get(index + 1))) + continue; + + result.add(path.get(index)); + } + + return result; + } + + /** + * Checks whether a point is a right-angle corner between its neighbors. + * + * @param previous The previous point + * @param current The current point + * @param next The next point + * @return Whether the current point is an orthogonal corner + */ + public static boolean isOrthogonalCorner(Vector previous, Vector current, Vector next) { + int previousDx = Integer.compare(current.getBlockX() - previous.getBlockX(), 0); + int previousDz = Integer.compare(current.getBlockZ() - previous.getBlockZ(), 0); + int nextDx = Integer.compare(next.getBlockX() - current.getBlockX(), 0); + int nextDz = Integer.compare(next.getBlockZ() - current.getBlockZ(), 0); + + return (previousDx != 0 || previousDz != 0) + && (nextDx != 0 || nextDz != 0) + && previousDx * nextDx + previousDz * nextDz == 0 + && previousDx != nextDx + && previousDz != nextDz; + } + + /** + * Creates a flat rectangular polygon that bounds the given points with padding. + * + * @param points The points to bound + * @param padding The horizontal padding around the bounds + * @return Four points describing the padded bounds + */ + public static List createBoundsSelectionPoints(List points, int padding) { + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + + for (Vector point : points) { + minX = Math.min(minX, point.getBlockX() - padding); + minZ = Math.min(minZ, point.getBlockZ() - padding); + maxX = Math.max(maxX, point.getBlockX() + padding); + maxZ = Math.max(maxZ, point.getBlockZ() + padding); + } + + return List.of( + new Vector(minX, 0, minZ), + new Vector(maxX, 0, minZ), + new Vector(maxX, 0, maxZ), + new Vector(minX, 0, maxZ) + ); + } + /** As long as two neighboring vectors are further than a given distance of blocks apart, add a new vector in between them * * @param points The points to populate @@ -1730,7 +1993,7 @@ public static List shiftPoints(List vectors, double shift, boole return resultVectors.get(longestPathIndex); - // Otherwise, return all paths combined into one + // Otherwise, return all paths combined into one }else{ List result = new ArrayList<>(); for(List vectorList : resultVectors) @@ -1783,7 +2046,7 @@ public static int getMinHeight(List vectors){ * @param vectors The list of vectors to get the maximum height of * @return The maximum height */ - private static int getMaxHeight(List vectors){ + public static int getMaxHeight(List vectors){ int maxHeight = Integer.MIN_VALUE; for(Vector vector : vectors) maxHeight = Math.max(maxHeight, vector.getBlockY()); @@ -1882,4 +2145,4 @@ public static boolean checkForWoolBlock(Block[][][] blocks, Player p, WikiDocume } return true; } -} \ No newline at end of file +}