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
+}