From 535d06d6941ed6bda675dac845c2c26cf99a8969 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Sun, 26 Apr 2026 17:42:04 +0200 Subject: [PATCH 1/6] feature: added a rail generator command including a custom undo and redo feature: added a first version for the convex rail generator --- .../generator/commands/GeneratorCommand.java | 27 +- .../generator/components/rail/Rail.java | 28 +- .../components/rail/RailScripts.java | 594 +++++++++++++++--- .../modules/generator/menu/GeneratorMenu.java | 171 +++-- .../modules/generator/model/History.java | 148 ++++- 5 files changed, 779 insertions(+), 189 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java index b3fe253c..c14bba39 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/commands/GeneratorCommand.java @@ -6,8 +6,6 @@ import net.buildtheearth.buildteamtools.modules.generator.model.History; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.utils.Utils; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -16,54 +14,44 @@ public class GeneratorCommand implements CommandExecutor { - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String cmdLabel, String @NotNull [] args) { if (!(sender instanceof Player p)) { sender.sendMessage("§cOnly players can execute this command."); return true; } - if(!p.hasPermission(Permissions.GENERATOR_USE)) { + if (!p.hasPermission(Permissions.GENERATOR_USE)) { p.sendMessage(ChatHelper.getErrorString("You don't have permission to use this command!")); return true; } - // Command Usage: /gen if (args.length == 0) { new GeneratorMenu(p, true); return true; } - - // Command Usage: /gen house ... - switch (args[0]) { + switch (args[0].toLowerCase()) { case "house": GeneratorModule.getInstance().getHouse().analyzeCommand(p, args); return true; - // Command Usage: /gen road ... case "road": GeneratorModule.getInstance().getRoad().analyzeCommand(p, args); return true; - // Command Usage: /gen rail ... case "rail": - p.sendMessage(Component.text("This generator have some serious issues and is currently disabled.", NamedTextColor.DARK_RED)); - //GeneratorModule.getInstance().getRail().analyzeCommand(p, args); + case "railway": + GeneratorModule.getInstance().getRail().analyzeCommand(p, args); return true; - // Command Usage: /gen tree ... case "tree": GeneratorModule.getInstance().getTree().analyzeCommand(p, args); return true; - // Command Usage: /gen field ... case "field": - p.sendMessage(Component.text("This generator have some serious issues and is currently disabled.", NamedTextColor.DARK_RED)); - //GeneratorModule.getInstance().getField().analyzeCommand(p, args); + p.sendMessage("§cThis generator has serious issues and is currently disabled."); return true; - // Command Usage: /gen history case "history": if (GeneratorModule.getInstance().getPlayerHistory(p).getHistoryEntries().isEmpty()) { p.sendMessage("§cYou didn't generate any structures yet. Use /gen to create one."); @@ -85,6 +73,7 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N case "redo": GeneratorModule.getInstance().getPlayerHistory(p).redoCommand(p); return true; + default: sendHelp(p); return true; @@ -93,7 +82,6 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @N public static void sendHelp(CommandSender sender) { ChatHelper.sendMessageBox(sender, "Generator Command", () -> { - sender.sendMessage("§eHouse Generator:§7 /gen house help"); sender.sendMessage("§eRoad Generator:§7 /gen road help"); sender.sendMessage("§eRail Generator:§7 /gen rail help"); @@ -103,7 +91,6 @@ public static void sendHelp(CommandSender sender) { sender.sendMessage("§eGenerator History:§7 /gen history"); sender.sendMessage("§eUndo last command:§7 /gen undo"); sender.sendMessage("§eRedo last command:§7 /gen redo"); - }); } -} +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java index b0a7b913..90265618 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java @@ -1,6 +1,10 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; import com.alpsbte.alpslib.utils.GeneratorUtils; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.regions.Polygonal2DRegion; +import com.sk89q.worldedit.regions.Region; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; @@ -13,15 +17,29 @@ public Rail() { } @Override - public boolean checkForPlayer(Player p) { - return !GeneratorUtils.checkForNoWorldEditSelection(p); + public boolean checkForPlayer(Player player) { + if (GeneratorUtils.checkForNoWorldEditSelection(player)) { + player.sendMessage("§cRail Generator requires an active WorldEdit selection."); + return false; + } + + Region region = GeneratorUtils.getWorldEditSelection(player); + + if (!(region instanceof CuboidRegion) + && !(region instanceof Polygonal2DRegion) + && !(region instanceof ConvexPolyhedralRegion)) { + player.sendMessage("§cRail Generator only supports cuboid, polygonal and convex WorldEdit selections."); + return false; + } + + return true; } @Override - public void generate(Player p) { - if (!GeneratorModule.getInstance().getRail().checkForPlayer(p)) + public void generate(Player player) { + if (!GeneratorModule.getInstance().getRail().checkForPlayer(player)) return; - new RailScripts(p, this); + new RailScripts(player, this); } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java index ab1156d2..4f84409e 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java @@ -1,127 +1,571 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; import com.alpsbte.alpslib.utils.GeneratorUtils; +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; +import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; +import net.buildtheearth.buildteamtools.modules.generator.model.History; import net.buildtheearth.buildteamtools.modules.generator.model.Script; -import org.bukkit.block.Block; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; import org.bukkit.entity.Player; import org.bukkit.util.Vector; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; public class RailScripts extends Script { + private static final Material[] CENTER_MATERIALS = new Material[]{ + Material.DEAD_FIRE_CORAL_BLOCK, + Material.STONE, + Material.COBBLESTONE + }; + + private static final Material SIDE_MATERIAL = Material.ANVIL; + public RailScripts(Player player, GeneratorComponent generatorComponent) { super(player, generatorComponent); - throw new UnsupportedOperationException("RailScripts is currently broken."); - //getPlayer().chat("/clearhistory"); - //Bukkit.getScheduler().runTaskAsynchronously(BuildTeamTools.getInstance(), this::railScript_v_1_3); + Thread thread = new Thread(this::generateRail); + thread.start(); } - public void railScript_v_1_3() { - List commands = new ArrayList<>(); - //HashMap flags = rail.getPlayerSettings().get(p.getUniqueId()).getValues(); + private void generateRail() { + try { + List controlPoints = getControlPoints(); - int xPos = getPlayer().getLocation().getBlockX(); - int zPos = getPlayer().getLocation().getBlockZ(); + if (controlPoints.size() < 2) { + getPlayer().sendMessage("§cRail Generator needs at least two usable points in the selection."); + return; + } - int operations = 0; + List centerPath = createCenterPath(controlPoints); - int railWidth = 5; + if (centerPath.size() < 2) { + getPlayer().sendMessage("§cRail Generator could not derive a valid path from this selection."); + return; + } + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeSampleTrack(centerPath)); + } catch (Exception exception) { + getPlayer().sendMessage("§cRail Generator failed while reading the WorldEdit selection."); + exception.printStackTrace(); + } + } - // TODO START TEMP + private List getControlPoints() { + if (getRegion() instanceof CuboidRegion cuboidRegion) + return getCuboidCenterLine(cuboidRegion); - Block[][][] regionBlocks = GeneratorUtils.analyzeRegion(getPlayer(), getPlayer().getWorld()); List points = GeneratorUtils.getSelectionPointsFromRegion(getRegion()); - createCuboidSelection(points.get(0), points.get(1)); - createCommand("//set redstone_block"); - createBreakPoint(); - createCommand("//set gold_block"); - finish(regionBlocks, points); + if (points == null || points.size() < 2) + return new ArrayList<>(); + List blockPoints = new ArrayList<>(); - // TODO END TEMP + for (Vector point : points) + blockPoints.add(toBlockVector(point)); - /* - // Get the points of the region - List points = GeneratorUtils.getSelectionPointsFromRegion(getRegion()); - points = GeneratorUtils.populatePoints(points, 5); + return orderPointsAsPath(blockPoints); + } + + private List getCuboidCenterLine(CuboidRegion cuboidRegion) { + int minX = cuboidRegion.getMinimumPoint().x(); + int maxX = cuboidRegion.getMaximumPoint().x(); + int minY = cuboidRegion.getMinimumPoint().y(); + int minZ = cuboidRegion.getMinimumPoint().z(); + int maxZ = cuboidRegion.getMaximumPoint().z(); + + int centerX = (minX + maxX) / 2; + int centerZ = (minZ + maxZ) / 2; + + int widthX = Math.abs(maxX - minX); + int widthZ = Math.abs(maxZ - minZ); + + List points = new ArrayList<>(); + + if (widthX >= widthZ) { + points.add(new Vector(minX, minY, centerZ)); + points.add(new Vector(maxX, minY, centerZ)); + } else { + points.add(new Vector(centerX, minY, minZ)); + points.add(new Vector(centerX, minY, maxZ)); + } + + return points; + } + + private List orderPointsAsPath(List points) { + List remaining = new ArrayList<>(); + List ordered = new ArrayList<>(); + + for (Vector point : points) { + if (!containsBlock(remaining, point)) + remaining.add(point); + } + + if (remaining.isEmpty()) + return ordered; - // ----------- PREPARATION 01 ---------- - // Replace all unnecessary blocks with air + Vector current = findStartPoint(remaining); + ordered.add(current); + remaining.remove(current); - List polyRegionLine = new ArrayList<>(points); - polyRegionLine = GeneratorUtils.extendPolyLine(polyRegionLine); - List polyRegionPoints = GeneratorUtils.shiftPoints(polyRegionLine, railWidth + 2, true); + while (!remaining.isEmpty()) { + Vector next = findNearestPoint(current, remaining); + ordered.add(next); + remaining.remove(next); + current = next; + } + + return ordered; + } + + private Vector findStartPoint(List points) { + Vector best = points.get(0); + + for (Vector point : points) { + if (point.getBlockX() < best.getBlockX()) { + best = point; + continue; + } + + if (point.getBlockX() == best.getBlockX() && point.getBlockZ() < best.getBlockZ()) + best = point; + } + + return best; + } - // Create a region from the points - GeneratorUtils.createPolySelection(getPlayer(), polyRegionPoints, null); + private Vector findNearestPoint(Vector from, List points) { + Vector best = points.get(0); + double bestDistance = distanceSquared2D(from, best); - getPlayer().chat("//expand 30 up"); - getPlayer().chat("//expand 10 down"); + for (Vector point : points) { + double distance = distanceSquared2D(from, point); - // Remove non-solid blocks - getPlayer().chat("//gmask !#solid"); - getPlayer().chat("//replace 0"); - operations++; + if (distance < bestDistance) { + best = point; + bestDistance = distance; + } + } - // Remove all trees and pumpkins - getPlayer().chat("//gmask"); - getPlayer().chat("//replace leaves,log,pumpkin 0"); - operations++; + return best; + } - getPlayer().chat("//gmask"); + private double distanceSquared2D(Vector a, Vector b) { + double dx = a.getBlockX() - b.getBlockX(); + double dz = a.getBlockZ() - b.getBlockZ(); + return dx * dx + dz * dz; + } - Block[][][] regionBlocks = GeneratorUtils.analyzeRegion(getPlayer(), getPlayer().getWorld()); - GeneratorUtils.adjustHeight(points, regionBlocks); + private boolean containsBlock(List points, Vector target) { + for (Vector point : points) { + if (sameBlock(point, target)) + return true; + } + return false; + } - // ----------- RAILWAY ---------- + private List createCenterPath(List controlPoints) { + List centerPath = new ArrayList<>(); - // Draw the railway curve + for (int i = 0; i < controlPoints.size() - 1; i++) { + Vector from = controlPoints.get(i); + Vector to = controlPoints.get(i + 1); - GeneratorUtils.createConvexSelection(commands, points); - commands.add("//gmask !solid"); - commands.add("//curve 42"); - operations++; - commands.add("//gmask"); + appendEightDirectionalLine(centerPath, from, to); + } + return removeOnlyConsecutiveDuplicates(repairGaps(centerPath)); + } - // Create the railway - GeneratorUtils.createPolySelection(commands, polyRegionPoints); + private void appendEightDirectionalLine(List path, Vector from, Vector to) { + int startX = from.getBlockX(); + int startY = from.getBlockY(); + int startZ = from.getBlockZ(); - commands.add("//replace \"0 !>42 =queryRel(0,-1,-1,42,-1)||queryRel(0,-1,1,42,-1)\" 145:1"); - operations++; - commands.add("//replace \"0 !>42 =queryRel(-1,-1,0,42,-1)||queryRel(1,-1,0,42,-1)\" 145:0"); - operations++; + int endX = to.getBlockX(); + int endY = to.getBlockY(); + int endZ = to.getBlockZ(); - commands.add("//gmask =(sqrt((x-(" + xPos + "))^2+(z-(" + zPos + "))^2)%3)-2"); - commands.add("//replace \"0 =queryRel(0,0,1,145,-1)||queryRel(0,0,-1,145,-1)||queryRel(1,0,0,145,-1)||queryRel(-1,0,0,145,-1)\" 44:0"); - operations++; - commands.add("//replace \"!145 !0 <145\" 43:0"); - operations++; - commands.add("//gmask"); - commands.add("//replace 42 2"); - operations++; + int dx = endX - startX; + int dy = endY - startY; + int dz = endZ - startZ; - commands.add("//gmask"); + int steps = Math.max(Math.abs(dx), Math.abs(dz)); - // Depending on the selection type, the selection needs to be restored correctly - if(getRegion() instanceof Polygonal2DRegion || getRegion() instanceof ConvexPolyhedralRegion) - GeneratorUtils.createConvexSelection(commands, points); - else if(getRegion() instanceof CuboidRegion){ - CuboidRegion cuboidRegion = (CuboidRegion) getRegion(); - Vector pos1 = new Vector(cuboidRegion.getPos1().getX(), cuboidRegion.getPos1().getY(), cuboidRegion.getPos1().getZ()); - Vector pos2 = new Vector(cuboidRegion.getPos2().getX(), cuboidRegion.getPos2().getY(), cuboidRegion.getPos2().getZ()); - GeneratorUtils.createCuboidSelection(commands, pos1, pos2); + if (steps == 0) { + addPointIfNew(path, new Vector(startX, startY, startZ)); + return; } - GeneratorModule.getInstance().getGeneratorCommands().add(new Command(getPlayer(), getGeneratorComponent(), commands, operations, regionBlocks)); - GeneratorModule.getInstance().getPlayerHistory(getPlayer()).addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, operations));*/ + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + + int x = (int) Math.round(startX + dx * t); + int y = (int) Math.round(startY + dy * t); + int z = (int) Math.round(startZ + dz * t); + + addPointIfNew(path, new Vector(x, y, z)); + } + } + + private List repairGaps(List path) { + if (path.size() < 2) + return path; + + List repaired = new ArrayList<>(); + repaired.add(path.get(0)); + + for (int i = 1; i < path.size(); i++) { + Vector previous = repaired.get(repaired.size() - 1); + Vector current = path.get(i); + + if (getChebyshevDistance(previous, current) <= 1) { + addPointIfNew(repaired, current); + continue; + } + + appendEightDirectionalLine(repaired, previous, current); + } + + return repaired; + } + + private int getChebyshevDistance(Vector a, Vector b) { + int dx = Math.abs(a.getBlockX() - b.getBlockX()); + int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); + + return Math.max(dx, dz); + } + + private void addPointIfNew(List path, Vector point) { + if (path.isEmpty()) { + path.add(point); + return; + } + + Vector last = path.get(path.size() - 1); + + if (!sameBlock(last, point)) + path.add(point); + } + + private List removeOnlyConsecutiveDuplicates(List path) { + List result = new ArrayList<>(); + + for (Vector point : path) + addPointIfNew(result, point); + + return result; + } + + private Vector toBlockVector(Vector vector) { + return new Vector( + Math.round(vector.getX()), + Math.round(vector.getY()), + Math.round(vector.getZ()) + ); + } + + private boolean sameBlock(Vector a, Vector b) { + return a.getBlockX() == b.getBlockX() + && a.getBlockY() == b.getBlockY() + && a.getBlockZ() == b.getBlockZ(); + } + + private void placeSampleTrack(List centerPath) { + Map blockMap = new LinkedHashMap<>(); + LinkedHashSet centerPositions = new LinkedHashSet<>(); + Map sideBlocks = new LinkedHashMap<>(); + + for (Vector center : centerPath) + centerPositions.add(toBlockVector3(center)); + + createSideBlocksFromCenterEdges(centerPath, centerPositions, sideBlocks); + placeSideBlocks(blockMap, sideBlocks); + placeCenterBlocks(blockMap, centerPositions); + + boolean placedWithWorldEdit = tryPlaceWithWorldEdit(blockMap); + + if (!placedWithWorldEdit) { + getPlayer().sendMessage("§eWorldEdit history is unavailable. Falling back to Bukkit placement."); + getPlayer().sendMessage("§eUse §6/gen undo§e instead of §6//undo§e for this generation."); + placeWithBukkitFallback(blockMap); + return; + } + + GeneratorModule.getInstance() + .getPlayerHistory(getPlayer()) + .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, 1)); + + getGeneratorComponent().sendSuccessMessage(getPlayer()); + } + + private void createSideBlocksFromCenterEdges( + List centerPath, + LinkedHashSet centerPositions, + Map sideBlocks + ) { + for (int i = 0; i < centerPath.size() - 1; i++) { + Vector from = centerPath.get(i); + Vector to = centerPath.get(i + 1); + Vector direction = getDirection(from, to); + + if (direction == null) + continue; + + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + if (dx != 0 && dz != 0) { + placeDiagonalEdgeSideBlocks(sideBlocks, centerPositions, from, dx, dz); + } else { + placeStraightEdgeSideBlocks(sideBlocks, centerPositions, from, to, direction); + } + } + } + + private void placeStraightEdgeSideBlocks( + Map sideBlocks, + LinkedHashSet centerPositions, + Vector from, + Vector to, + Vector direction + ) { + Vector leftOffset = getLeftOffset(direction); + Vector rightOffset = getRightOffset(direction); + + addSideBlock(sideBlocks, centerPositions, offset(from, leftOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(to, leftOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(from, rightOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(to, rightOffset), direction); + } + + private void placeDiagonalEdgeSideBlocks( + Map sideBlocks, + LinkedHashSet centerPositions, + Vector from, + int dx, + int dz + ) { + Vector direction = new Vector(dx, 0, dz); + + BlockVector3 horizontalSide = BlockVector3.at( + from.getBlockX() + dx, + from.getBlockY(), + from.getBlockZ() + ); + + BlockVector3 verticalSide = BlockVector3.at( + from.getBlockX(), + from.getBlockY(), + from.getBlockZ() + dz + ); + + addSideBlock(sideBlocks, centerPositions, horizontalSide, direction); + addSideBlock(sideBlocks, centerPositions, verticalSide, direction); + } + + private BlockVector3 offset(Vector center, Vector offset) { + return BlockVector3.at( + center.getBlockX() + offset.getBlockX(), + center.getBlockY(), + center.getBlockZ() + offset.getBlockZ() + ); + } + + private void addSideBlock( + Map sideBlocks, + LinkedHashSet centerPositions, + BlockVector3 position, + Vector direction + ) { + if (centerPositions.contains(position)) + return; + + SideBlock sideBlock = sideBlocks.computeIfAbsent(position, SideBlock::new); + sideBlock.addDirection(direction); + } + + private Vector getLeftOffset(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + return new Vector(-dz, 0, dx); + } + + private Vector getRightOffset(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + return new Vector(dz, 0, -dx); + } + + private void placeSideBlocks(Map blockMap, Map sideBlocks) { + for (SideBlock sideBlock : sideBlocks.values()) { + Vector direction = getBestAnvilDirection(sideBlock, sideBlocks); + blockMap.put(sideBlock.position, getAnvilBlockData(direction)); + } + } + + private Vector getBestAnvilDirection(SideBlock sideBlock, Map sideBlocks) { + boolean eastWest = sideBlocks.containsKey(sideBlock.position.add(1, 0, 0)) + || sideBlocks.containsKey(sideBlock.position.add(-1, 0, 0)); + + boolean northSouth = sideBlocks.containsKey(sideBlock.position.add(0, 0, 1)) + || sideBlocks.containsKey(sideBlock.position.add(0, 0, -1)); + + if (eastWest && !northSouth) + return new Vector(1, 0, 0); + + if (northSouth && !eastWest) + return new Vector(0, 0, 1); + + return sideBlock.getAverageDirection(); + } + + private Vector getDirection(Vector from, Vector to) { + int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); + int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); + + if (dx == 0 && dz == 0) + return null; + + return new Vector(dx, 0, dz); + } + + private void placeCenterBlocks(Map blockMap, LinkedHashSet centerPositions) { + for (BlockVector3 center : centerPositions) + blockMap.put(center, getCenterBlockData(center)); + } + + private boolean tryPlaceWithWorldEdit(Map blockMap) { + try (EditSession editSession = WorldEdit.getInstance() + .newEditSessionBuilder() + .world(getWeWorld()) + .actor(getActor()) + .build()) { + + for (Map.Entry entry : blockMap.entrySet()) + editSession.setBlock(entry.getKey(), BukkitAdapter.adapt(entry.getValue())); + + editSession.flushQueue(); + getLocalSession().remember(editSession); + return true; + } catch (Throwable throwable) { + throwable.printStackTrace(); + return false; + } + } + + private void placeWithBukkitFallback(Map blockMap) { + List changes = new ArrayList<>(); + + for (Map.Entry entry : blockMap.entrySet()) { + BlockVector3 position = entry.getKey(); + BlockData newData = entry.getValue(); + + org.bukkit.block.Block block = getPlayer().getWorld().getBlockAt(position.x(), position.y(), position.z()); + + changes.add(new History.BlockChange( + getPlayer().getWorld().getName(), + position.x(), + position.y(), + position.z(), + block.getBlockData().getAsString(), + newData.getAsString() + )); + + block.setBlockData(newData, false); + } + + GeneratorModule.getInstance() + .getPlayerHistory(getPlayer()) + .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, changes)); + + getGeneratorComponent().sendSuccessMessage(getPlayer()); + } + + private BlockData getCenterBlockData(BlockVector3 position) { + int index = Math.floorMod(position.x() * 31 + position.z() * 17, CENTER_MATERIALS.length); + return CENTER_MATERIALS[index].createBlockData(); + } + + private BlockData getAnvilBlockData(Vector direction) { + BlockData data = SIDE_MATERIAL.createBlockData(); + + if (data instanceof Directional directional) + directional.setFacing(toBlockFace(direction)); + + return data; + } + + private BlockFace toBlockFace(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + if (Math.abs(dx) >= Math.abs(dz)) { + if (dx > 0) + return BlockFace.EAST; + + if (dx < 0) + return BlockFace.WEST; + } + + if (dz > 0) + return BlockFace.SOUTH; + + if (dz < 0) + return BlockFace.NORTH; + + return BlockFace.EAST; + } + + private BlockVector3 toBlockVector3(Vector vector) { + return BlockVector3.at(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + } + + private static class SideBlock { + + private final BlockVector3 position; + private int directionX; + private int directionZ; + + private SideBlock(BlockVector3 position) { + this.position = position; + } + + private void addDirection(Vector direction) { + directionX += direction.getBlockX(); + directionZ += direction.getBlockZ(); + } + + private Vector getAverageDirection() { + int dx = Integer.compare(directionX, 0); + int dz = Integer.compare(directionZ, 0); + + if (dx == 0 && dz == 0) + return new Vector(1, 0, 0); + + return new Vector(dx, 0, dz); + } } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java index ef738425..0c07f34f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java @@ -8,6 +8,8 @@ import net.buildtheearth.buildteamtools.modules.generator.components.house.HouseSettings; import net.buildtheearth.buildteamtools.modules.generator.components.house.RoofType; import net.buildtheearth.buildteamtools.modules.generator.components.house.menu.WallColorMenu; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; import net.buildtheearth.buildteamtools.modules.generator.components.road.Road; import net.buildtheearth.buildteamtools.modules.generator.components.road.RoadSettings; import net.buildtheearth.buildteamtools.modules.generator.components.road.menu.RoadColorMenu; @@ -45,81 +47,126 @@ public class GeneratorMenu extends AbstractMenu { public static final int FIELD_ITEM_SLOT = 17; - public GeneratorMenu(Player player, boolean autoLoad) { super(3, GENERATOR_INV_NAME, player, autoLoad); } @Override protected void setPreviewItems() { - // HOUSE ITEM - ArrayList houseLore = ListUtil.createList("", "§eDescription:", "Generate basic building shells", "with multiple floors, windows and roofs", "", "§eFeatures:", "- " + RoofType.values().length + " Roof Types", "- Custom Wall, Base and Roof Color", "- Custom Floor and Window Sizes", "", "§8Left-click to generate", "§8Right-click for Tutorial"); + ArrayList houseLore = ListUtil.createList( + "", + "§eDescription:", + "Generate basic building shells", + "with multiple floors, windows and roofs", + "", + "§eFeatures:", + "- " + RoofType.values().length + " Roof Types", + "- Custom Wall, Base and Roof Color", + "- Custom Floor and Window Sizes", + "", + "§8Left-click to generate", + "§8Right-click for Tutorial" + ); ItemStack houseItem = Item.create(XMaterial.BIRCH_DOOR.get(), "§cGenerate House", houseLore); - - // Set navigator item getMenu().getSlot(HOUSE_ITEM_SLOT).setItem(houseItem); + ArrayList roadLore = ListUtil.createList( + "", + "§eDescription:", + "Generate roads and highways", + "with multiple lanes and sidewalks", + "", + "§eFeatures:", + "- Custom Road Width and Color", + "- Custom Sidewalk Width and Color", + "- Custom Lane Count", + "", + "§8Left-click to generate", + "§8Right-click for Tutorial" + ); + + ItemStack roadItem = new Item(XMaterial.SMOOTH_STONE_SLAB.parseItem()) + .setDisplayName("§bGenerate Road") + .setLore(roadLore) + .build(); - // ROAD ITEM - ArrayList roadLore = ListUtil.createList("", "§eDescription:", "Generate roads and highways", "with multiple lanes and sidewalks", "", "§eFeatures:", "- Custom Road Width and Color", "- Custom Sidewalk Width and Color", "- Custom Lane Count", "", "§8Left-click to generate", "§8Right-click for Tutorial"); - - - ItemStack roadItem = new Item(XMaterial.SMOOTH_STONE_SLAB.parseItem()).setDisplayName("§bGenerate Road").setLore(roadLore).build(); - - // Set navigator item getMenu().getSlot(ROAD_ITEM_SLOT).setItem(roadItem); - - // RAILWAY ITEM - ArrayList railwayLore = ListUtil.createList("", "§eDescription:", "Generate railways with multiple tracks", "and many different designs", "", "§eFeatures:", "- Custom Railway Width and Color (TODO)", "- Custom Track Count (TODO)", "", "§8Left-click to generate", "§8Right-click for Tutorial"); - - railwayLore = ListUtil.createList("", "§cThis §eGenerator §cis currently broken", "§cRailway Generator is disabled", "", "§8If you want to help fixing ask on Dev Hub!"); - - ItemStack railwayItem = Item.create(XMaterial.RAIL.get(), "§9Generate Railway §c(DISABLED)", railwayLore); - - // Set navigator item + ArrayList railwayLore = ListUtil.createList( + "", + "§eDescription:", + "Generate a predefined railway", + "from your active WorldEdit selection", + "", + "§eSupported selections:", + "- Cuboid", + "- Polygonal", + "- Convex", + "", + "§eFeatures:", + "- Straight rail sections", + "- Curves and corners", + "- Automatic rail orientation", + "", + "§8Left-click to generate", + "§8Right-click for Tutorial" + ); + + ItemStack railwayItem = Item.create(XMaterial.RAIL.get(), "§9Generate Railway", railwayLore); getMenu().getSlot(RAILWAY_ITEM_SLOT).setItem(railwayItem); - if (!CommonModule.getInstance().getDependencyComponent().isSchematicBrushEnabled()) { - // TREE ITEM - ArrayList treeLore = ListUtil.createList("", "§cPlugin §eSchematicBrush §cis not installed", "§cTree Generator is disabled", "", "§8Leftclick for Installation Instructions"); + ArrayList treeLore = ListUtil.createList( + "", + "§cPlugin §eSchematicBrush §cis not installed", + "§cTree Generator is disabled", + "", + "§8Leftclick for Installation Instructions" + ); ItemStack treeItem = Item.create(XMaterial.OAK_SAPLING.get(), "§aGenerate Tree & Forest §c(DISABLED)", treeLore); - - // Set navigator item getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } else if (!GeneratorCollections.hasUpdatedGeneratorCollections(getMenuPlayer())) { - // TREE ITEM - ArrayList treeLore = ListUtil.createList("", "§cThe §eTree Pack " + Tree.TREE_PACK_VERSION + " §cis not installed", "§cTree Generator is disabled", "", "§8Leftclick for Installation Instructions"); + ArrayList treeLore = ListUtil.createList( + "", + "§cThe §eTree Pack " + Tree.TREE_PACK_VERSION + " §cis not installed", + "§cTree Generator is disabled", + "", + "§8Leftclick for Installation Instructions" + ); ItemStack treeItem = Item.create(XMaterial.OAK_SAPLING.get(), "§aGenerate Tree & Forest §c(DISABLED)", treeLore); - - // Set navigator item getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } else { - // TREE ITEM - ArrayList treeLore = ListUtil.createList("", "§eDescription:", "Generate trees from a set of", "hundreds of different types", "", "§eFeatures:", "- Custom Tree Type", "", "§8Left-click to generate", "§8Right-click for Tutorial"); + ArrayList treeLore = ListUtil.createList( + "", + "§eDescription:", + "Generate trees from a set of", + "hundreds of different types", + "", + "§eFeatures:", + "- Custom Tree Type", + "", + "§8Left-click to generate", + "§8Right-click for Tutorial" + ); ItemStack treeItem = Item.create(XMaterial.OAK_SAPLING.get(), "§aGenerate Tree & Forest", treeLore); - - // Set navigator item getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } - - // FIELD ITEM - ArrayList fieldLore = ListUtil.createList("", "§eDescription:", "Generate fields with different", "crops and plants", "", "§eFeatures:", "- Custom Crop Type", "- Custom Crop Size", "", "§8Left-click to generate", "§8Right-click for Tutorial"); - - fieldLore = ListUtil.createList("", "§cThis §eGenerator §cis currently broken", "§cField Generator is disabled", "", "§8If you want to help fixing ask on Dev Hub!"); + ArrayList fieldLore = ListUtil.createList( + "", + "§cThis §eGenerator §cis currently broken", + "§cField Generator is disabled", + "", + "§8If you want to help fixing ask on Dev Hub!" + ); ItemStack fieldItem = Item.create(XMaterial.WHEAT.get(), "§6Generate Field §c(DISABLED)", fieldLore); - - // Set navigator item getMenu().getSlot(FIELD_ITEM_SLOT).setItem(fieldItem); - super.setPreviewItems(); } @@ -130,7 +177,6 @@ protected void setMenuItemsAsync() { @Override protected void setItemClickEventsAsync() { - // Set click event for house item getMenu().getSlot(HOUSE_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { if (clickInformation.getClickType().equals(ClickType.RIGHT)) { sendMoreInformation(clickPlayer, GeneratorType.HOUSE); @@ -140,14 +186,14 @@ protected void setItemClickEventsAsync() { House house = GeneratorModule.getInstance().getHouse(); house.getPlayerSettings().put(clickPlayer.getUniqueId(), new HouseSettings(clickPlayer)); - if (!house.checkForPlayer(clickPlayer)) return; + if (!house.checkForPlayer(clickPlayer)) + return; clickPlayer.closeInventory(); clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); new WallColorMenu(clickPlayer, true); })); - // Set click event for road item getMenu().getSlot(ROAD_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { if (clickInformation.getClickType().equals(ClickType.RIGHT)) { sendMoreInformation(clickPlayer, GeneratorType.ROAD); @@ -157,34 +203,31 @@ protected void setItemClickEventsAsync() { Road road = GeneratorModule.getInstance().getRoad(); road.getPlayerSettings().put(clickPlayer.getUniqueId(), new RoadSettings(clickPlayer)); - if (!road.checkForPlayer(clickPlayer)) return; + if (!road.checkForPlayer(clickPlayer)) + return; clickPlayer.closeInventory(); clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); new RoadColorMenu(clickPlayer, true); })); - // Set click event for railway item getMenu().getSlot(RAILWAY_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { if (clickInformation.getClickType().equals(ClickType.RIGHT)) { sendMoreInformation(clickPlayer, GeneratorType.RAILWAY); return; } - sendMoreInformation(clickPlayer, GeneratorType.RAILWAY); - /*Rail rail = GeneratorModule.getInstance().getRail(); + Rail rail = GeneratorModule.getInstance().getRail(); rail.getPlayerSettings().put(clickPlayer.getUniqueId(), new RailSettings(clickPlayer)); - if(!rail.checkForPlayer(clickPlayer)) + if (!rail.checkForPlayer(clickPlayer)) return; clickPlayer.closeInventory(); clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - - GeneratorModule.getInstance().getRail().generate(clickPlayer);*/ + rail.generate(clickPlayer); })); - // Set click event for tree item getMenu().getSlot(TREE_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { if (clickInformation.getClickType().equals(ClickType.RIGHT)) { sendMoreInformation(clickPlayer, GeneratorType.TREE); @@ -194,30 +237,21 @@ protected void setItemClickEventsAsync() { Tree tree = GeneratorModule.getInstance().getTree(); tree.getPlayerSettings().put(clickPlayer.getUniqueId(), new TreeSettings(clickPlayer)); - if (!tree.checkForPlayer(clickPlayer)) return; + if (!tree.checkForPlayer(clickPlayer)) + return; clickPlayer.closeInventory(); clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); new TreeTypeMenu(clickPlayer, true); })); - // Set click event for field item getMenu().getSlot(FIELD_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { if (clickInformation.getClickType().equals(ClickType.RIGHT)) { sendMoreInformation(clickPlayer, GeneratorType.FIELD); return; } - sendMoreInformation(clickPlayer, GeneratorType.FIELD); - - /*Field field = GeneratorModule.getInstance().getField(); - field.getPlayerSettings().put(clickPlayer.getUniqueId(), new FieldSettings(clickPlayer)); - - if(!field.checkForPlayer(clickPlayer)) - return; - clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - new CropTypeMenu(clickPlayer, true);*/ + sendMoreInformation(clickPlayer, GeneratorType.FIELD); })); } @@ -227,6 +261,11 @@ private void sendMoreInformation(@NonNull Player clickPlayer, @NonNull Generator @Override protected Mask getMask() { - return BinaryMask.builder(getMenu()).item(MenuItems.ITEM_BACKGROUND).pattern("111111111").pattern("010101010").pattern("111111111").build(); + return BinaryMask.builder(getMenu()) + .item(MenuItems.ITEM_BACKGROUND) + .pattern("111111111") + .pattern("010101010") + .pattern("111111111") + .build(); } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java index 02e8473f..207dfab7 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/History.java @@ -5,6 +5,7 @@ import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.extension.platform.Actor; import lombok.Getter; +import lombok.Setter; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; @@ -12,9 +13,13 @@ import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; import java.util.ArrayList; +import java.util.List; public class History { @@ -27,36 +32,43 @@ public class History { @Getter private final ArrayList undoHistoryEntries; - public History(Player p){ + public History(Player p) { this.p = p; this.historyEntries = new ArrayList<>(); this.undoHistoryEntries = new ArrayList<>(); } - public void addHistoryEntry(HistoryEntry entry){ + public void addHistoryEntry(HistoryEntry entry) { historyEntries.add(entry); + undoHistoryEntries.clear(); } - public void undoCommand(Player p){ - if(getHistoryEntries().isEmpty()){ + public void undoCommand(Player p) { + if (getHistoryEntries().isEmpty()) { p.sendMessage("§cYou didn't generate any structures yet. Use /gen to create one. You can only undo the last structure."); return; } - LocalSession session = getHistoryEntries().get(0).getScript().getLocalSession(); - Actor actor = getHistoryEntries().get(0).getScript().getActor(); - int worldEditCommandCount = getHistoryEntries().get(0).getWorldEditCommandCount(); + HistoryEntry entry = getHistoryEntries().get(getHistoryEntries().size() - 1); - GeneratorUtils.undo(session, p, actor, worldEditCommandCount); + if (entry.hasBlockChanges()) { + entry.applyUndo(); + } else { + LocalSession session = entry.getScript().getLocalSession(); + Actor actor = entry.getScript().getActor(); + int worldEditCommandCount = entry.getWorldEditCommandCount(); - getUndoHistoryEntries().add(getHistoryEntries().get(0)); - getHistoryEntries().clear(); + GeneratorUtils.undo(session, p, actor, worldEditCommandCount); + } + + getUndoHistoryEntries().add(entry); + getHistoryEntries().remove(entry); p.playSound(p.getLocation(), Sound.ENTITY_ZOMBIE_DESTROY_EGG, 1.0F, 1.0F); Bukkit.getScheduler().scheduleSyncDelayedTask(BuildTeamTools.getInstance(), () -> { ChatHelper.sendSuccessfulMessage(p, "Successfully %s the last structure.", "undid"); - p.sendMessage(ChatHelper.getStandardComponent(true,"Use %s to undo it.", "/gen redo") + p.sendMessage(ChatHelper.getStandardComponent(true, "Use %s to redo it.", "/gen redo") .clickEvent(ClickEvent.runCommand("/gen redo")) .hoverEvent(HoverEvent.showText(Component.text("Click to redo the last structure.", NamedTextColor.GRAY))) ); @@ -64,27 +76,32 @@ public void undoCommand(Player p){ }, 20L); } - public void redoCommand(Player p){ - if(getUndoHistoryEntries().isEmpty()){ + public void redoCommand(Player p) { + if (getUndoHistoryEntries().isEmpty()) { p.sendMessage("§cYou didn't undo any structures yet. Use /gen undo to undo one. You can only redo the last structure."); return; } - LocalSession session = getUndoHistoryEntries().get(0).getScript().getLocalSession(); - Actor actor = getUndoHistoryEntries().get(0).getScript().getActor(); - int worldEditCommandCount = getUndoHistoryEntries().get(0).getWorldEditCommandCount(); + HistoryEntry entry = getUndoHistoryEntries().get(getUndoHistoryEntries().size() - 1); + + if (entry.hasBlockChanges()) { + entry.applyRedo(); + } else { + LocalSession session = entry.getScript().getLocalSession(); + Actor actor = entry.getScript().getActor(); + int worldEditCommandCount = entry.getWorldEditCommandCount(); - GeneratorUtils.redo(session, p, actor, worldEditCommandCount); + GeneratorUtils.redo(session, p, actor, worldEditCommandCount); + } - getHistoryEntries().add(getUndoHistoryEntries().get(0)); - getUndoHistoryEntries().clear(); + getHistoryEntries().add(entry); + getUndoHistoryEntries().remove(entry); p.playSound(p.getLocation(), Sound.ENTITY_ZOMBIE_DESTROY_EGG, 1.0F, 1.0F); - Bukkit.getScheduler().scheduleSyncDelayedTask(BuildTeamTools.getInstance(), () -> { - ChatHelper.sendSuccessfulMessage(p,"Successfully %s the last structure.", "redid"); - p.sendMessage(ChatHelper.getStandardComponent(true,"Use %s to undo it.", "/gen undo") + ChatHelper.sendSuccessfulMessage(p, "Successfully %s the last structure.", "redid"); + p.sendMessage(ChatHelper.getStandardComponent(true, "Use %s to undo it.", "/gen undo") .clickEvent(ClickEvent.runCommand("/gen undo")) .hoverEvent(HoverEvent.showText(Component.text("Click to undo the last structure.", NamedTextColor.GRAY))) ); @@ -96,20 +113,105 @@ public static class HistoryEntry { @Getter private final GeneratorType generatorType; + @Getter private final long timeCreated; + @Getter private final Script script; + @Getter private final int worldEditCommandCount; + @Getter + private final List blockChanges; + public HistoryEntry(GeneratorType generatorType, Script script) { this.generatorType = generatorType; this.timeCreated = System.currentTimeMillis(); this.worldEditCommandCount = script.getChanges(); this.script = script; + this.blockChanges = new ArrayList<>(); + } + + public HistoryEntry(GeneratorType generatorType, Script script, int worldEditCommandCount) { + this.generatorType = generatorType; + this.timeCreated = System.currentTimeMillis(); + this.worldEditCommandCount = worldEditCommandCount; + this.script = script; + this.blockChanges = new ArrayList<>(); + } + + public HistoryEntry(GeneratorType generatorType, Script script, List blockChanges) { + this.generatorType = generatorType; + this.timeCreated = System.currentTimeMillis(); + this.worldEditCommandCount = 0; + this.script = script; + this.blockChanges = blockChanges == null ? new ArrayList<>() : blockChanges; + } + + public boolean hasBlockChanges() { + return blockChanges != null && !blockChanges.isEmpty(); + } + + public void applyUndo() { + for (int i = blockChanges.size() - 1; i >= 0; i--) + blockChanges.get(i).applyOld(); + } + + public void applyRedo() { + for (BlockChange blockChange : blockChanges) + blockChange.applyNew(); } } -} + public static class BlockChange { + + @Getter + private final String worldName; + + @Getter + private final int x; + + @Getter + private final int y; + + @Getter + private final int z; + + @Getter + private final String oldBlockData; + @Getter + @Setter + private String newBlockData; + + public BlockChange(String worldName, int x, int y, int z, String oldBlockData, String newBlockData) { + this.worldName = worldName; + this.x = x; + this.y = y; + this.z = z; + this.oldBlockData = oldBlockData; + this.newBlockData = newBlockData; + } + + public void applyOld() { + apply(oldBlockData); + } + + public void applyNew() { + apply(newBlockData); + } + + private void apply(String blockDataString) { + World world = Bukkit.getWorld(worldName); + + if (world == null) + return; + + Block block = world.getBlockAt(x, y, z); + BlockData blockData = Bukkit.createBlockData(blockDataString); + block.setBlockData(blockData, false); + } + } +} \ No newline at end of file From c0c98ceea6f41ba62085088ce7478845abe4cb76 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Mon, 27 Apr 2026 19:38:06 +0200 Subject: [PATCH 2/6] feature: added a first version for the cuboid and poly rail generator --- .../components/rail/RailScripts.java | 77 ++++++++++++++----- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java index 4f84409e..33158d76 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java @@ -4,8 +4,10 @@ import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.regions.Polygonal2DRegion; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; @@ -68,7 +70,10 @@ private void generateRail() { private List getControlPoints() { if (getRegion() instanceof CuboidRegion cuboidRegion) - return getCuboidCenterLine(cuboidRegion); + return getCuboidControlPoints(cuboidRegion); + + if (getRegion() instanceof Polygonal2DRegion polygonalRegion) + return getPolygonalControlPoints(polygonalRegion); List points = GeneratorUtils.getSelectionPointsFromRegion(getRegion()); @@ -83,32 +88,68 @@ private List getControlPoints() { return orderPointsAsPath(blockPoints); } - private List getCuboidCenterLine(CuboidRegion cuboidRegion) { - int minX = cuboidRegion.getMinimumPoint().x(); - int maxX = cuboidRegion.getMaximumPoint().x(); - int minY = cuboidRegion.getMinimumPoint().y(); - int minZ = cuboidRegion.getMinimumPoint().z(); - int maxZ = cuboidRegion.getMaximumPoint().z(); + private List getCuboidControlPoints(CuboidRegion cuboidRegion) { + List points = new ArrayList<>(); - int centerX = (minX + maxX) / 2; - int centerZ = (minZ + maxZ) / 2; + BlockVector3 pos1 = cuboidRegion.getPos1(); + BlockVector3 pos2 = cuboidRegion.getPos2(); - int widthX = Math.abs(maxX - minX); - int widthZ = Math.abs(maxZ - minZ); + Vector start = new Vector(pos1.x(), pos1.y(), pos1.z()); + Vector end = new Vector(pos2.x(), pos2.y(), pos2.z()); - List points = new ArrayList<>(); + if (sameBlock(start, end)) { + start = new Vector( + cuboidRegion.getMinimumPoint().x(), + cuboidRegion.getMinimumPoint().y(), + cuboidRegion.getMinimumPoint().z() + ); - if (widthX >= widthZ) { - points.add(new Vector(minX, minY, centerZ)); - points.add(new Vector(maxX, minY, centerZ)); - } else { - points.add(new Vector(centerX, minY, minZ)); - points.add(new Vector(centerX, minY, maxZ)); + end = new Vector( + cuboidRegion.getMaximumPoint().x(), + cuboidRegion.getMinimumPoint().y(), + cuboidRegion.getMaximumPoint().z() + ); } + points.add(start); + points.add(end); + return points; } + private List getPolygonalControlPoints(Polygonal2DRegion polygonalRegion) { + List points = new ArrayList<>(); + + int minY = polygonalRegion.getMinimumY(); + int maxY = polygonalRegion.getMaximumY(); + + for (BlockVector2 point : polygonalRegion.getPoints()) { + int y = findBestYForPolygonPoint(point.x(), point.z(), minY, maxY); + points.add(new Vector(point.x(), y, point.z())); + } + + if (points.size() < 2) + return points; + + return removeOnlyConsecutiveDuplicates(points); + } + + private int findBestYForPolygonPoint(int x, int z, int minY, int maxY) { + org.bukkit.World world = getPlayer().getWorld(); + + int safeMinY = Math.max(world.getMinHeight(), Math.min(minY, maxY)); + int safeMaxY = Math.min(world.getMaxHeight() - 1, Math.max(minY, maxY)); + + for (int y = safeMaxY; y >= safeMinY; y--) { + Material material = world.getBlockAt(x, y, z).getType(); + + if (material.isSolid()) + return y; + } + + return safeMaxY; + } + private List orderPointsAsPath(List points) { List remaining = new ArrayList<>(); List ordered = new ArrayList<>(); From 738a7f5d2a85e8c93009b25a793ec090bb1e4750 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Wed, 29 Apr 2026 21:00:57 +0200 Subject: [PATCH 3/6] feature: - Added a railway generator entry to the generator menu - Made railway documentation links clickable in chat - Added RailSelectionPointReader for cuboid, polygonal, and convex selections - Validated supported rail selections before generation - Added RailPath and RailPathBuilder for center path generation - Moved center path generation out of RailScripts - Kept RailScripts focused on generation flow and placement --- .../generator/components/rail/Rail.java | 19 +- .../components/rail/RailScripts.java | 280 +----------------- .../components/rail/path/RailPath.java | 28 ++ .../components/rail/path/RailPathBuilder.java | 115 +++++++ .../selection/RailSelectionPointReader.java | 230 ++++++++++++++ .../modules/generator/menu/GeneratorMenu.java | 25 +- 6 files changed, 413 insertions(+), 284 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPath.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/selection/RailSelectionPointReader.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java index 90265618..4d244bd4 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java @@ -1,14 +1,15 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; import com.alpsbte.alpslib.utils.GeneratorUtils; -import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; -import com.sk89q.worldedit.regions.CuboidRegion; -import com.sk89q.worldedit.regions.Polygonal2DRegion; import com.sk89q.worldedit.regions.Region; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.selection.RailSelectionPointReader; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.List; public class Rail extends GeneratorComponent { @@ -24,14 +25,20 @@ public boolean checkForPlayer(Player player) { } Region region = GeneratorUtils.getWorldEditSelection(player); + RailSelectionPointReader reader = new RailSelectionPointReader(player, region); - if (!(region instanceof CuboidRegion) - && !(region instanceof Polygonal2DRegion) - && !(region instanceof ConvexPolyhedralRegion)) { + if (!reader.isSupportedSelection()) { player.sendMessage("§cRail Generator only supports cuboid, polygonal and convex WorldEdit selections."); return false; } + List controlPoints = reader.readControlPoints(); + + if (controlPoints.size() < 2) { + player.sendMessage("§cRail Generator could not read enough points from this selection."); + return false; + } + return true; } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java index 33158d76..a0717797 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java @@ -1,15 +1,14 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; -import com.alpsbte.alpslib.utils.GeneratorUtils; import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.regions.CuboidRegion; -import com.sk89q.worldedit.regions.Polygonal2DRegion; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPath; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPathBuilder; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.selection.RailSelectionPointReader; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; import net.buildtheearth.buildteamtools.modules.generator.model.History; @@ -54,285 +53,28 @@ private void generateRail() { return; } - List centerPath = createCenterPath(controlPoints); + RailPath railPath = new RailPathBuilder().build(controlPoints); - if (centerPath.size() < 2) { + if (!railPath.isValid()) { getPlayer().sendMessage("§cRail Generator could not derive a valid path from this selection."); return; } - Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeSampleTrack(centerPath)); + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeSampleTrack(railPath)); } catch (Exception exception) { - getPlayer().sendMessage("§cRail Generator failed while reading the WorldEdit selection."); + getPlayer().sendMessage("§cRail Generator failed while generating from the WorldEdit selection."); exception.printStackTrace(); } } private List getControlPoints() { - if (getRegion() instanceof CuboidRegion cuboidRegion) - return getCuboidControlPoints(cuboidRegion); - - if (getRegion() instanceof Polygonal2DRegion polygonalRegion) - return getPolygonalControlPoints(polygonalRegion); - - List points = GeneratorUtils.getSelectionPointsFromRegion(getRegion()); - - if (points == null || points.size() < 2) - return new ArrayList<>(); - - List blockPoints = new ArrayList<>(); - - for (Vector point : points) - blockPoints.add(toBlockVector(point)); - - return orderPointsAsPath(blockPoints); - } - - private List getCuboidControlPoints(CuboidRegion cuboidRegion) { - List points = new ArrayList<>(); - - BlockVector3 pos1 = cuboidRegion.getPos1(); - BlockVector3 pos2 = cuboidRegion.getPos2(); - - Vector start = new Vector(pos1.x(), pos1.y(), pos1.z()); - Vector end = new Vector(pos2.x(), pos2.y(), pos2.z()); - - if (sameBlock(start, end)) { - start = new Vector( - cuboidRegion.getMinimumPoint().x(), - cuboidRegion.getMinimumPoint().y(), - cuboidRegion.getMinimumPoint().z() - ); - - end = new Vector( - cuboidRegion.getMaximumPoint().x(), - cuboidRegion.getMinimumPoint().y(), - cuboidRegion.getMaximumPoint().z() - ); - } - - points.add(start); - points.add(end); - - return points; - } - - private List getPolygonalControlPoints(Polygonal2DRegion polygonalRegion) { - List points = new ArrayList<>(); - - int minY = polygonalRegion.getMinimumY(); - int maxY = polygonalRegion.getMaximumY(); - - for (BlockVector2 point : polygonalRegion.getPoints()) { - int y = findBestYForPolygonPoint(point.x(), point.z(), minY, maxY); - points.add(new Vector(point.x(), y, point.z())); - } - - if (points.size() < 2) - return points; - - return removeOnlyConsecutiveDuplicates(points); - } - - private int findBestYForPolygonPoint(int x, int z, int minY, int maxY) { - org.bukkit.World world = getPlayer().getWorld(); - - int safeMinY = Math.max(world.getMinHeight(), Math.min(minY, maxY)); - int safeMaxY = Math.min(world.getMaxHeight() - 1, Math.max(minY, maxY)); - - for (int y = safeMaxY; y >= safeMinY; y--) { - Material material = world.getBlockAt(x, y, z).getType(); - - if (material.isSolid()) - return y; - } - - return safeMaxY; - } - - private List orderPointsAsPath(List points) { - List remaining = new ArrayList<>(); - List ordered = new ArrayList<>(); - - for (Vector point : points) { - if (!containsBlock(remaining, point)) - remaining.add(point); - } - - if (remaining.isEmpty()) - return ordered; - - Vector current = findStartPoint(remaining); - ordered.add(current); - remaining.remove(current); - - while (!remaining.isEmpty()) { - Vector next = findNearestPoint(current, remaining); - ordered.add(next); - remaining.remove(next); - current = next; - } - - return ordered; - } - - private Vector findStartPoint(List points) { - Vector best = points.get(0); - - for (Vector point : points) { - if (point.getBlockX() < best.getBlockX()) { - best = point; - continue; - } - - if (point.getBlockX() == best.getBlockX() && point.getBlockZ() < best.getBlockZ()) - best = point; - } - - return best; - } - - private Vector findNearestPoint(Vector from, List points) { - Vector best = points.get(0); - double bestDistance = distanceSquared2D(from, best); - - for (Vector point : points) { - double distance = distanceSquared2D(from, point); - - if (distance < bestDistance) { - best = point; - bestDistance = distance; - } - } - - return best; + RailSelectionPointReader reader = new RailSelectionPointReader(getPlayer(), getRegion()); + return reader.readControlPoints(); } - private double distanceSquared2D(Vector a, Vector b) { - double dx = a.getBlockX() - b.getBlockX(); - double dz = a.getBlockZ() - b.getBlockZ(); - - return dx * dx + dz * dz; - } - - private boolean containsBlock(List points, Vector target) { - for (Vector point : points) { - if (sameBlock(point, target)) - return true; - } - - return false; - } - - private List createCenterPath(List controlPoints) { - List centerPath = new ArrayList<>(); - - for (int i = 0; i < controlPoints.size() - 1; i++) { - Vector from = controlPoints.get(i); - Vector to = controlPoints.get(i + 1); - - appendEightDirectionalLine(centerPath, from, to); - } - - return removeOnlyConsecutiveDuplicates(repairGaps(centerPath)); - } - - private void appendEightDirectionalLine(List path, Vector from, Vector to) { - int startX = from.getBlockX(); - int startY = from.getBlockY(); - int startZ = from.getBlockZ(); - - int endX = to.getBlockX(); - int endY = to.getBlockY(); - int endZ = to.getBlockZ(); - - int dx = endX - startX; - int dy = endY - startY; - int dz = endZ - startZ; - - int steps = Math.max(Math.abs(dx), Math.abs(dz)); - - if (steps == 0) { - addPointIfNew(path, new Vector(startX, startY, startZ)); - return; - } - - for (int step = 0; step <= steps; step++) { - double t = step / (double) steps; - - int x = (int) Math.round(startX + dx * t); - int y = (int) Math.round(startY + dy * t); - int z = (int) Math.round(startZ + dz * t); - - addPointIfNew(path, new Vector(x, y, z)); - } - } - - private List repairGaps(List path) { - if (path.size() < 2) - return path; - - List repaired = new ArrayList<>(); - repaired.add(path.get(0)); - - for (int i = 1; i < path.size(); i++) { - Vector previous = repaired.get(repaired.size() - 1); - Vector current = path.get(i); - - if (getChebyshevDistance(previous, current) <= 1) { - addPointIfNew(repaired, current); - continue; - } - - appendEightDirectionalLine(repaired, previous, current); - } - - return repaired; - } - - private int getChebyshevDistance(Vector a, Vector b) { - int dx = Math.abs(a.getBlockX() - b.getBlockX()); - int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); - - return Math.max(dx, dz); - } - - private void addPointIfNew(List path, Vector point) { - if (path.isEmpty()) { - path.add(point); - return; - } - - Vector last = path.get(path.size() - 1); - - if (!sameBlock(last, point)) - path.add(point); - } - - private List removeOnlyConsecutiveDuplicates(List path) { - List result = new ArrayList<>(); - - for (Vector point : path) - addPointIfNew(result, point); - - return result; - } - - private Vector toBlockVector(Vector vector) { - return new Vector( - Math.round(vector.getX()), - Math.round(vector.getY()), - Math.round(vector.getZ()) - ); - } - - private boolean sameBlock(Vector a, Vector b) { - return a.getBlockX() == b.getBlockX() - && a.getBlockY() == b.getBlockY() - && a.getBlockZ() == b.getBlockZ(); - } + private void placeSampleTrack(RailPath railPath) { + List centerPath = railPath.getCenterPath(); - private void placeSampleTrack(List centerPath) { Map blockMap = new LinkedHashMap<>(); LinkedHashSet centerPositions = new LinkedHashSet<>(); Map sideBlocks = new LinkedHashMap<>(); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPath.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPath.java new file mode 100644 index 00000000..5ffec594 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPath.java @@ -0,0 +1,28 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.path; + +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RailPath { + + private final List centerPath; + + public RailPath(List centerPath) { + this.centerPath = new ArrayList<>(centerPath); + } + + public List getCenterPath() { + return Collections.unmodifiableList(centerPath); + } + + public int size() { + return centerPath.size(); + } + + public boolean isValid() { + return centerPath.size() >= 2; + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java new file mode 100644 index 00000000..74f42180 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java @@ -0,0 +1,115 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.path; + +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +public class RailPathBuilder { + + public RailPath build(List controlPoints) { + if (controlPoints == null || controlPoints.size() < 2) + return new RailPath(new ArrayList<>()); + + List centerPath = new ArrayList<>(); + + for (int i = 0; i < controlPoints.size() - 1; i++) { + Vector from = controlPoints.get(i); + Vector to = controlPoints.get(i + 1); + + appendEightDirectionalLine(centerPath, from, to); + } + + centerPath = repairGaps(centerPath); + centerPath = removeOnlyConsecutiveDuplicates(centerPath); + + return new RailPath(centerPath); + } + + private void appendEightDirectionalLine(List path, Vector from, Vector to) { + int startX = from.getBlockX(); + int startY = from.getBlockY(); + int startZ = from.getBlockZ(); + + int endX = to.getBlockX(); + int endY = to.getBlockY(); + int endZ = to.getBlockZ(); + + int dx = endX - startX; + int dy = endY - startY; + int dz = endZ - startZ; + + int steps = Math.max(Math.abs(dx), Math.abs(dz)); + + if (steps == 0) { + addPointIfNew(path, new Vector(startX, startY, startZ)); + return; + } + + for (int step = 0; step <= steps; step++) { + double t = step / (double) steps; + + int x = (int) Math.round(startX + dx * t); + int y = (int) Math.round(startY + dy * t); + int z = (int) Math.round(startZ + dz * t); + + addPointIfNew(path, new Vector(x, y, z)); + } + } + + private List repairGaps(List path) { + if (path.size() < 2) + return path; + + List repaired = new ArrayList<>(); + repaired.add(path.get(0)); + + for (int i = 1; i < path.size(); i++) { + Vector previous = repaired.get(repaired.size() - 1); + Vector current = path.get(i); + + if (getChebyshevDistance(previous, current) <= 1) { + addPointIfNew(repaired, current); + continue; + } + + appendEightDirectionalLine(repaired, previous, current); + } + + return repaired; + } + + private int getChebyshevDistance(Vector a, Vector b) { + int dx = Math.abs(a.getBlockX() - b.getBlockX()); + int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); + + return Math.max(dx, dz); + } + + private void addPointIfNew(List path, Vector point) { + if (path.isEmpty()) { + path.add(point); + return; + } + + Vector last = path.get(path.size() - 1); + + if (!sameBlock(last, point)) + path.add(point); + } + + private List removeOnlyConsecutiveDuplicates(List path) { + List result = new ArrayList<>(); + + for (Vector point : path) + addPointIfNew(result, point); + + return result; + } + + private boolean sameBlock(Vector a, Vector b) { + return a.getBlockX() == b.getBlockX() + && a.getBlockY() == b.getBlockY() + && a.getBlockZ() == b.getBlockZ(); + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/selection/RailSelectionPointReader.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/selection/RailSelectionPointReader.java new file mode 100644 index 00000000..3559228f --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/selection/RailSelectionPointReader.java @@ -0,0 +1,230 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.selection; + +import com.alpsbte.alpslib.utils.GeneratorUtils; +import com.sk89q.worldedit.math.BlockVector2; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.ConvexPolyhedralRegion; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.regions.Polygonal2DRegion; +import com.sk89q.worldedit.regions.Region; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +public class RailSelectionPointReader { + + private final Player player; + private final Region region; + + public RailSelectionPointReader(Player player, Region region) { + this.player = player; + this.region = region; + } + + public boolean isSupportedSelection() { + return region instanceof CuboidRegion + || region instanceof Polygonal2DRegion + || region instanceof ConvexPolyhedralRegion; + } + + public List readControlPoints() { + if (region instanceof CuboidRegion cuboidRegion) + return readCuboidPoints(cuboidRegion); + + if (region instanceof Polygonal2DRegion polygonalRegion) + return readPolygonalPoints(polygonalRegion); + + if (region instanceof ConvexPolyhedralRegion convexRegion) + return readConvexPoints(convexRegion); + + return new ArrayList<>(); + } + + private List readCuboidPoints(CuboidRegion cuboidRegion) { + List points = new ArrayList<>(); + + BlockVector3 pos1 = cuboidRegion.getPos1(); + BlockVector3 pos2 = cuboidRegion.getPos2(); + + Vector start = new Vector(pos1.x(), pos1.y(), pos1.z()); + Vector end = new Vector(pos2.x(), pos2.y(), pos2.z()); + + if (sameBlock(start, end)) { + start = new Vector( + cuboidRegion.getMinimumPoint().x(), + cuboidRegion.getMinimumPoint().y(), + cuboidRegion.getMinimumPoint().z() + ); + + end = new Vector( + cuboidRegion.getMaximumPoint().x(), + cuboidRegion.getMinimumPoint().y(), + cuboidRegion.getMaximumPoint().z() + ); + } + + points.add(start); + points.add(end); + + return points; + } + + private List readPolygonalPoints(Polygonal2DRegion polygonalRegion) { + List points = new ArrayList<>(); + + int minY = polygonalRegion.getMinimumY(); + int maxY = polygonalRegion.getMaximumY(); + + for (BlockVector2 point : polygonalRegion.getPoints()) { + int y = findBestY(point.x(), point.z(), minY, maxY); + points.add(new Vector(point.x(), y, point.z())); + } + + return removeOnlyConsecutiveDuplicates(points); + } + + private List readConvexPoints(ConvexPolyhedralRegion convexRegion) { + List points = new ArrayList<>(); + + for (BlockVector3 point : convexRegion.getVertices()) { + Vector vector = new Vector(point.x(), point.y(), point.z()); + + if (!containsBlock(points, vector)) + points.add(vector); + } + + if (points.size() < 2) { + List fallbackPoints = GeneratorUtils.getSelectionPointsFromRegion(region); + + if (fallbackPoints != null) { + for (Vector point : fallbackPoints) { + Vector blockPoint = toBlockVector(point); + + if (!containsBlock(points, blockPoint)) + points.add(blockPoint); + } + } + } + + return orderPointsAsPath(points); + } + + private int findBestY(int x, int z, int minY, int maxY) { + World world = player.getWorld(); + + int safeMinY = Math.max(world.getMinHeight(), Math.min(minY, maxY)); + int safeMaxY = Math.min(world.getMaxHeight() - 1, Math.max(minY, maxY)); + + for (int y = safeMaxY; y >= safeMinY; y--) { + Material material = world.getBlockAt(x, y, z).getType(); + + if (material.isSolid()) + return y; + } + + return safeMaxY; + } + + private List orderPointsAsPath(List points) { + List remaining = new ArrayList<>(); + List ordered = new ArrayList<>(); + + for (Vector point : points) { + if (!containsBlock(remaining, point)) + remaining.add(point); + } + + if (remaining.isEmpty()) + return ordered; + + Vector current = findStartPoint(remaining); + ordered.add(current); + remaining.remove(current); + + while (!remaining.isEmpty()) { + Vector next = findNearestPoint(current, remaining); + ordered.add(next); + remaining.remove(next); + current = next; + } + + return ordered; + } + + private Vector findStartPoint(List points) { + Vector best = points.get(0); + + for (Vector point : points) { + if (point.getBlockX() < best.getBlockX()) { + best = point; + continue; + } + + if (point.getBlockX() == best.getBlockX() && point.getBlockZ() < best.getBlockZ()) + best = point; + } + + return best; + } + + private Vector findNearestPoint(Vector from, List points) { + Vector best = points.get(0); + double bestDistance = distanceSquared2D(from, best); + + for (Vector point : points) { + double distance = distanceSquared2D(from, point); + + if (distance < bestDistance) { + best = point; + bestDistance = distance; + } + } + + return best; + } + + private double distanceSquared2D(Vector a, Vector b) { + double dx = a.getBlockX() - b.getBlockX(); + double dz = a.getBlockZ() - b.getBlockZ(); + + return dx * dx + dz * dz; + } + + private List removeOnlyConsecutiveDuplicates(List points) { + List result = new ArrayList<>(); + + for (Vector point : points) { + if (result.isEmpty() || !sameBlock(result.get(result.size() - 1), point)) + result.add(point); + } + + return result; + } + + private Vector toBlockVector(Vector vector) { + return new Vector( + Math.round(vector.getX()), + Math.round(vector.getY()), + Math.round(vector.getZ()) + ); + } + + private boolean containsBlock(List points, Vector target) { + for (Vector point : points) { + if (sameBlock(point, target)) + return true; + } + + return false; + } + + private boolean sameBlock(Vector a, Vector b) { + return a.getBlockX() == b.getBlockX() + && a.getBlockY() == b.getBlockY() + && a.getBlockZ() == b.getBlockZ(); + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java index 0c07f34f..61069e1d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/menu/GeneratorMenu.java @@ -22,6 +22,8 @@ import net.buildtheearth.buildteamtools.utils.MenuItems; import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -38,13 +40,9 @@ public class GeneratorMenu extends AbstractMenu { public static final String GENERATOR_INV_NAME = "What do you want to generate?"; public static final int HOUSE_ITEM_SLOT = 9; - public static final int ROAD_ITEM_SLOT = 11; - public static final int RAILWAY_ITEM_SLOT = 13; - public static final int TREE_ITEM_SLOT = 15; - public static final int FIELD_ITEM_SLOT = 17; public GeneratorMenu(Player player, boolean autoLoad) { @@ -105,9 +103,9 @@ protected void setPreviewItems() { "- Convex", "", "§eFeatures:", - "- Straight rail sections", - "- Curves and corners", - "- Automatic rail orientation", + "- Straight sections", + "- Direction changes", + "- Automatic side block orientation", "", "§8Left-click to generate", "§8Right-click for Tutorial" @@ -172,7 +170,7 @@ protected void setPreviewItems() { @Override protected void setMenuItemsAsync() { - // No Async / DB Items + // No async or database items. } @Override @@ -256,7 +254,16 @@ protected void setItemClickEventsAsync() { } private void sendMoreInformation(@NonNull Player clickPlayer, @NonNull GeneratorType generator) { - clickPlayer.sendMessage(Component.text(generator.getWikiPage(), NamedTextColor.RED)); + String wikiPage = generator.getWikiPage(); + + clickPlayer.sendMessage( + Component.text("Open generator documentation: ", NamedTextColor.GRAY) + .append( + Component.text(wikiPage, NamedTextColor.RED) + .clickEvent(ClickEvent.openUrl(wikiPage)) + .hoverEvent(HoverEvent.showText(Component.text("Click to open this page", NamedTextColor.GRAY))) + ) + ); } @Override From 003504c594946e03b5ca3c89ce53430ebbcbf7df Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 30 Apr 2026 21:51:36 +0200 Subject: [PATCH 4/6] feature: - Improved path handling for curves, diagonals, gaps, and direction changes - Added side block builder for railway side/anvil placement - Added orientation resolver for side block facing - Added RailBlockPlacement and RailBlockRole placement model - Added RailType interface and SampleRailType implementation - Moved block data creation into the rail type system - Added WorldEdit/Bukkit placement handler for rail generation - Refactored RailScripts into an orchestration flow --- .../components/rail/RailScripts.java | 310 ++---------------- .../components/rail/path/RailPathBuilder.java | 246 ++++++++++++-- .../rail/placement/RailBlockPlacement.java | 29 ++ .../rail/placement/RailBlockRole.java | 6 + .../rail/placement/RailPlacementBuilder.java | 58 ++++ .../rail/placement/RailWorldEditPlacer.java | 79 +++++ .../rail/side/RailOrientationResolver.java | 95 ++++++ .../components/rail/side/RailSideBlock.java | 34 ++ .../components/rail/side/RailSideBuilder.java | 178 ++++++++++ .../components/rail/types/RailType.java | 11 + .../components/rail/types/SampleRailType.java | 69 ++++ 11 files changed, 807 insertions(+), 308 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockRole.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailPlacementBuilder.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBlock.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBuilder.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/RailType.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/SampleRailType.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java index a0717797..c642da81 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java @@ -1,41 +1,32 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; -import com.sk89q.worldedit.EditSession; -import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.math.BlockVector3; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPath; import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPathBuilder; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailBlockPlacement; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailPlacementBuilder; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailWorldEditPlacer; import net.buildtheearth.buildteamtools.modules.generator.components.rail.selection.RailSelectionPointReader; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.types.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.types.SampleRailType; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; import net.buildtheearth.buildteamtools.modules.generator.model.History; import net.buildtheearth.buildteamtools.modules.generator.model.Script; import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Directional; import org.bukkit.entity.Player; import org.bukkit.util.Vector; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; public class RailScripts extends Script { - private static final Material[] CENTER_MATERIALS = new Material[]{ - Material.DEAD_FIRE_CORAL_BLOCK, - Material.STONE, - Material.COBBLESTONE - }; + private final RailPathBuilder pathBuilder = new RailPathBuilder(); + private final RailPlacementBuilder placementBuilder = new RailPlacementBuilder(); + private final RailWorldEditPlacer placer = new RailWorldEditPlacer(); - private static final Material SIDE_MATERIAL = Material.ANVIL; + private final RailType railType = new SampleRailType(); public RailScripts(Player player, GeneratorComponent generatorComponent) { super(player, generatorComponent); @@ -53,14 +44,21 @@ private void generateRail() { return; } - RailPath railPath = new RailPathBuilder().build(controlPoints); + RailPath railPath = pathBuilder.build(controlPoints); if (!railPath.isValid()) { getPlayer().sendMessage("§cRail Generator could not derive a valid path from this selection."); return; } - Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeSampleTrack(railPath)); + List placements = placementBuilder.buildPlacements(railPath); + + if (placements.isEmpty()) { + getPlayer().sendMessage("§cRail Generator did not create any block placements."); + return; + } + + Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeRail(placements)); } catch (Exception exception) { getPlayer().sendMessage("§cRail Generator failed while generating from the WorldEdit selection."); exception.printStackTrace(); @@ -72,283 +70,27 @@ private List getControlPoints() { return reader.readControlPoints(); } - private void placeSampleTrack(RailPath railPath) { - List centerPath = railPath.getCenterPath(); - - Map blockMap = new LinkedHashMap<>(); - LinkedHashSet centerPositions = new LinkedHashSet<>(); - Map sideBlocks = new LinkedHashMap<>(); - - for (Vector center : centerPath) - centerPositions.add(toBlockVector3(center)); - - createSideBlocksFromCenterEdges(centerPath, centerPositions, sideBlocks); - placeSideBlocks(blockMap, sideBlocks); - placeCenterBlocks(blockMap, centerPositions); - - boolean placedWithWorldEdit = tryPlaceWithWorldEdit(blockMap); + private void placeRail(List placements) { + boolean placedWithWorldEdit = placer.placeWithWorldEdit(this, placements, railType); if (!placedWithWorldEdit) { getPlayer().sendMessage("§eWorldEdit history is unavailable. Falling back to Bukkit placement."); getPlayer().sendMessage("§eUse §6/gen undo§e instead of §6//undo§e for this generation."); - placeWithBukkitFallback(blockMap); - return; - } - - GeneratorModule.getInstance() - .getPlayerHistory(getPlayer()) - .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, 1)); - - getGeneratorComponent().sendSuccessMessage(getPlayer()); - } - - private void createSideBlocksFromCenterEdges( - List centerPath, - LinkedHashSet centerPositions, - Map sideBlocks - ) { - for (int i = 0; i < centerPath.size() - 1; i++) { - Vector from = centerPath.get(i); - Vector to = centerPath.get(i + 1); - Vector direction = getDirection(from, to); - if (direction == null) - continue; + List changes = placer.placeWithBukkitFallback(this, placements, railType); - int dx = direction.getBlockX(); - int dz = direction.getBlockZ(); + GeneratorModule.getInstance() + .getPlayerHistory(getPlayer()) + .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, changes)); - if (dx != 0 && dz != 0) { - placeDiagonalEdgeSideBlocks(sideBlocks, centerPositions, from, dx, dz); - } else { - placeStraightEdgeSideBlocks(sideBlocks, centerPositions, from, to, direction); - } - } - } - - private void placeStraightEdgeSideBlocks( - Map sideBlocks, - LinkedHashSet centerPositions, - Vector from, - Vector to, - Vector direction - ) { - Vector leftOffset = getLeftOffset(direction); - Vector rightOffset = getRightOffset(direction); - - addSideBlock(sideBlocks, centerPositions, offset(from, leftOffset), direction); - addSideBlock(sideBlocks, centerPositions, offset(to, leftOffset), direction); - addSideBlock(sideBlocks, centerPositions, offset(from, rightOffset), direction); - addSideBlock(sideBlocks, centerPositions, offset(to, rightOffset), direction); - } - - private void placeDiagonalEdgeSideBlocks( - Map sideBlocks, - LinkedHashSet centerPositions, - Vector from, - int dx, - int dz - ) { - Vector direction = new Vector(dx, 0, dz); - - BlockVector3 horizontalSide = BlockVector3.at( - from.getBlockX() + dx, - from.getBlockY(), - from.getBlockZ() - ); - - BlockVector3 verticalSide = BlockVector3.at( - from.getBlockX(), - from.getBlockY(), - from.getBlockZ() + dz - ); - - addSideBlock(sideBlocks, centerPositions, horizontalSide, direction); - addSideBlock(sideBlocks, centerPositions, verticalSide, direction); - } - - private BlockVector3 offset(Vector center, Vector offset) { - return BlockVector3.at( - center.getBlockX() + offset.getBlockX(), - center.getBlockY(), - center.getBlockZ() + offset.getBlockZ() - ); - } - - private void addSideBlock( - Map sideBlocks, - LinkedHashSet centerPositions, - BlockVector3 position, - Vector direction - ) { - if (centerPositions.contains(position)) + getGeneratorComponent().sendSuccessMessage(getPlayer()); return; - - SideBlock sideBlock = sideBlocks.computeIfAbsent(position, SideBlock::new); - sideBlock.addDirection(direction); - } - - private Vector getLeftOffset(Vector direction) { - int dx = direction.getBlockX(); - int dz = direction.getBlockZ(); - - return new Vector(-dz, 0, dx); - } - - private Vector getRightOffset(Vector direction) { - int dx = direction.getBlockX(); - int dz = direction.getBlockZ(); - - return new Vector(dz, 0, -dx); - } - - private void placeSideBlocks(Map blockMap, Map sideBlocks) { - for (SideBlock sideBlock : sideBlocks.values()) { - Vector direction = getBestAnvilDirection(sideBlock, sideBlocks); - blockMap.put(sideBlock.position, getAnvilBlockData(direction)); - } - } - - private Vector getBestAnvilDirection(SideBlock sideBlock, Map sideBlocks) { - boolean eastWest = sideBlocks.containsKey(sideBlock.position.add(1, 0, 0)) - || sideBlocks.containsKey(sideBlock.position.add(-1, 0, 0)); - - boolean northSouth = sideBlocks.containsKey(sideBlock.position.add(0, 0, 1)) - || sideBlocks.containsKey(sideBlock.position.add(0, 0, -1)); - - if (eastWest && !northSouth) - return new Vector(1, 0, 0); - - if (northSouth && !eastWest) - return new Vector(0, 0, 1); - - return sideBlock.getAverageDirection(); - } - - private Vector getDirection(Vector from, Vector to) { - int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); - int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); - - if (dx == 0 && dz == 0) - return null; - - return new Vector(dx, 0, dz); - } - - private void placeCenterBlocks(Map blockMap, LinkedHashSet centerPositions) { - for (BlockVector3 center : centerPositions) - blockMap.put(center, getCenterBlockData(center)); - } - - private boolean tryPlaceWithWorldEdit(Map blockMap) { - try (EditSession editSession = WorldEdit.getInstance() - .newEditSessionBuilder() - .world(getWeWorld()) - .actor(getActor()) - .build()) { - - for (Map.Entry entry : blockMap.entrySet()) - editSession.setBlock(entry.getKey(), BukkitAdapter.adapt(entry.getValue())); - - editSession.flushQueue(); - getLocalSession().remember(editSession); - return true; - } catch (Throwable throwable) { - throwable.printStackTrace(); - return false; - } - } - - private void placeWithBukkitFallback(Map blockMap) { - List changes = new ArrayList<>(); - - for (Map.Entry entry : blockMap.entrySet()) { - BlockVector3 position = entry.getKey(); - BlockData newData = entry.getValue(); - - org.bukkit.block.Block block = getPlayer().getWorld().getBlockAt(position.x(), position.y(), position.z()); - - changes.add(new History.BlockChange( - getPlayer().getWorld().getName(), - position.x(), - position.y(), - position.z(), - block.getBlockData().getAsString(), - newData.getAsString() - )); - - block.setBlockData(newData, false); } GeneratorModule.getInstance() .getPlayerHistory(getPlayer()) - .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, changes)); + .addHistoryEntry(new History.HistoryEntry(GeneratorType.RAILWAY, this, 1)); getGeneratorComponent().sendSuccessMessage(getPlayer()); } - - private BlockData getCenterBlockData(BlockVector3 position) { - int index = Math.floorMod(position.x() * 31 + position.z() * 17, CENTER_MATERIALS.length); - return CENTER_MATERIALS[index].createBlockData(); - } - - private BlockData getAnvilBlockData(Vector direction) { - BlockData data = SIDE_MATERIAL.createBlockData(); - - if (data instanceof Directional directional) - directional.setFacing(toBlockFace(direction)); - - return data; - } - - private BlockFace toBlockFace(Vector direction) { - int dx = direction.getBlockX(); - int dz = direction.getBlockZ(); - - if (Math.abs(dx) >= Math.abs(dz)) { - if (dx > 0) - return BlockFace.EAST; - - if (dx < 0) - return BlockFace.WEST; - } - - if (dz > 0) - return BlockFace.SOUTH; - - if (dz < 0) - return BlockFace.NORTH; - - return BlockFace.EAST; - } - - private BlockVector3 toBlockVector3(Vector vector) { - return BlockVector3.at(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); - } - - private static class SideBlock { - - private final BlockVector3 position; - private int directionX; - private int directionZ; - - private SideBlock(BlockVector3 position) { - this.position = position; - } - - private void addDirection(Vector direction) { - directionX += direction.getBlockX(); - directionZ += direction.getBlockZ(); - } - - private Vector getAverageDirection() { - int dx = Integer.compare(directionX, 0); - int dz = Integer.compare(directionZ, 0); - - if (dx == 0 && dz == 0) - return new Vector(1, 0, 0); - - return new Vector(dx, 0, dz); - } - } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java index 74f42180..34b4caa0 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java @@ -7,33 +7,174 @@ public class RailPathBuilder { + private static final int MAX_CORNER_TRIM = 4; + private static final int MIN_CORNER_DISTANCE = 4; + public RailPath build(List controlPoints) { if (controlPoints == null || controlPoints.size() < 2) return new RailPath(new ArrayList<>()); - List centerPath = new ArrayList<>(); + List normalizedControlPoints = removeOnlyConsecutiveDuplicates(controlPoints); - for (int i = 0; i < controlPoints.size() - 1; i++) { - Vector from = controlPoints.get(i); - Vector to = controlPoints.get(i + 1); + if (normalizedControlPoints.size() < 2) + return new RailPath(new ArrayList<>()); - appendEightDirectionalLine(centerPath, from, to); - } + List centerPath = createCurvedCenterPath(normalizedControlPoints); centerPath = repairGaps(centerPath); + centerPath = removeImmediateBacktracking(centerPath); centerPath = removeOnlyConsecutiveDuplicates(centerPath); return new RailPath(centerPath); } + private List createCurvedCenterPath(List controlPoints) { + List path = new ArrayList<>(); + + addPointIfNew(path, toBlockVector(controlPoints.get(0))); + + if (controlPoints.size() == 2) { + appendEightDirectionalLine(path, controlPoints.get(0), controlPoints.get(1)); + return path; + } + + for (int i = 1; i < controlPoints.size() - 1; i++) { + Vector previous = toBlockVector(controlPoints.get(i - 1)); + Vector current = toBlockVector(controlPoints.get(i)); + Vector next = toBlockVector(controlPoints.get(i + 1)); + + if (!shouldCurveCorner(previous, current, next)) { + appendEightDirectionalLine(path, path.get(path.size() - 1), current); + continue; + } + + int trim = getCornerTrim(previous, current, next); + + if (trim <= 0) { + appendEightDirectionalLine(path, path.get(path.size() - 1), current); + continue; + } + + Vector beforeCorner = getPointBeforeCorner(previous, current, trim); + Vector afterCorner = getPointAfterCorner(current, next, trim); + + if (sameBlock(beforeCorner, current) || sameBlock(afterCorner, current)) { + appendEightDirectionalLine(path, path.get(path.size() - 1), current); + continue; + } + + appendEightDirectionalLine(path, path.get(path.size() - 1), beforeCorner); + appendQuadraticCurve(path, beforeCorner, current, afterCorner); + } + + appendEightDirectionalLine(path, path.get(path.size() - 1), controlPoints.get(controlPoints.size() - 1)); + + return path; + } + + private boolean shouldCurveCorner(Vector previous, Vector current, Vector next) { + Vector incoming = getDirection(previous, current); + Vector outgoing = getDirection(current, next); + + if (incoming == null || outgoing == null) + return false; + + if (sameDirection(incoming, outgoing)) + return false; + + if (oppositeDirection(incoming, outgoing)) + return false; + + int incomingLength = getChebyshevDistance(previous, current); + int outgoingLength = getChebyshevDistance(current, next); + + return incomingLength >= MIN_CORNER_DISTANCE && outgoingLength >= MIN_CORNER_DISTANCE; + } + + private int getCornerTrim(Vector previous, Vector current, Vector next) { + int incomingLength = getChebyshevDistance(previous, current); + int outgoingLength = getChebyshevDistance(current, next); + + int shortest = Math.min(incomingLength, outgoingLength); + + if (shortest < MIN_CORNER_DISTANCE) + return 0; + + return Math.max(1, Math.min(MAX_CORNER_TRIM, shortest / 3)); + } + + private Vector getPointBeforeCorner(Vector previous, Vector current, int trim) { + List incomingLine = createEightDirectionalLine(previous, current); + + int index = Math.max(0, incomingLine.size() - 1 - trim); + + return incomingLine.get(index); + } + + private Vector getPointAfterCorner(Vector current, Vector next, int trim) { + List outgoingLine = createEightDirectionalLine(current, next); + + int index = Math.min(outgoingLine.size() - 1, trim); + + return outgoingLine.get(index); + } + + private void appendQuadraticCurve(List path, Vector start, Vector control, Vector end) { + int firstDistance = getChebyshevDistance(start, control); + int secondDistance = getChebyshevDistance(control, end); + int samples = Math.max(6, (firstDistance + secondDistance) * 3); + + for (int sample = 1; sample <= samples; sample++) { + double t = sample / (double) samples; + + double inverse = 1.0 - t; + + double x = inverse * inverse * start.getX() + + 2.0 * inverse * t * control.getX() + + t * t * end.getX(); + + double y = inverse * inverse * start.getY() + + 2.0 * inverse * t * control.getY() + + t * t * end.getY(); + + double z = inverse * inverse * start.getZ() + + 2.0 * inverse * t * control.getZ() + + t * t * end.getZ(); + + Vector point = toBlockVector(new Vector(x, y, z)); + + if (path.isEmpty()) { + addPointIfNew(path, point); + continue; + } + + Vector last = path.get(path.size() - 1); + + if (getChebyshevDistance(last, point) <= 1) { + addPointIfNew(path, point); + } else { + appendEightDirectionalLine(path, last, point); + } + } + } + + private List createEightDirectionalLine(Vector from, Vector to) { + List line = new ArrayList<>(); + appendEightDirectionalLine(line, from, to); + return line; + } + private void appendEightDirectionalLine(List path, Vector from, Vector to) { - int startX = from.getBlockX(); - int startY = from.getBlockY(); - int startZ = from.getBlockZ(); + Vector start = toBlockVector(from); + Vector end = toBlockVector(to); - int endX = to.getBlockX(); - int endY = to.getBlockY(); - int endZ = to.getBlockZ(); + int startX = start.getBlockX(); + int startY = start.getBlockY(); + int startZ = start.getBlockZ(); + + int endX = end.getBlockX(); + int endY = end.getBlockY(); + int endZ = end.getBlockZ(); int dx = endX - startX; int dy = endY - startY; @@ -79,32 +220,89 @@ private List repairGaps(List path) { return repaired; } - private int getChebyshevDistance(Vector a, Vector b) { - int dx = Math.abs(a.getBlockX() - b.getBlockX()); - int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); + private List removeImmediateBacktracking(List path) { + if (path.size() < 3) + return path; - return Math.max(dx, dz); + List cleaned = new ArrayList<>(); + + for (Vector point : path) { + addPointIfNew(cleaned, point); + + while (cleaned.size() >= 3) { + Vector a = cleaned.get(cleaned.size() - 3); + Vector b = cleaned.get(cleaned.size() - 2); + Vector c = cleaned.get(cleaned.size() - 1); + + if (sameBlock(a, c)) { + cleaned.remove(cleaned.size() - 1); + cleaned.remove(cleaned.size() - 1); + continue; + } + + break; + } + } + + return cleaned; + } + + private List removeOnlyConsecutiveDuplicates(List path) { + List result = new ArrayList<>(); + + for (Vector point : path) + addPointIfNew(result, toBlockVector(point)); + + return result; } private void addPointIfNew(List path, Vector point) { + Vector blockPoint = toBlockVector(point); + if (path.isEmpty()) { - path.add(point); + path.add(blockPoint); return; } Vector last = path.get(path.size() - 1); - if (!sameBlock(last, point)) - path.add(point); + if (!sameBlock(last, blockPoint)) + path.add(blockPoint); } - private List removeOnlyConsecutiveDuplicates(List path) { - List result = new ArrayList<>(); + private Vector getDirection(Vector from, Vector to) { + int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); + int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); - for (Vector point : path) - addPointIfNew(result, point); + if (dx == 0 && dz == 0) + return null; - return result; + return new Vector(dx, 0, dz); + } + + private boolean sameDirection(Vector a, Vector b) { + return a.getBlockX() == b.getBlockX() + && a.getBlockZ() == b.getBlockZ(); + } + + private boolean oppositeDirection(Vector a, Vector b) { + return a.getBlockX() == -b.getBlockX() + && a.getBlockZ() == -b.getBlockZ(); + } + + private int getChebyshevDistance(Vector a, Vector b) { + int dx = Math.abs(a.getBlockX() - b.getBlockX()); + int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); + + return Math.max(dx, dz); + } + + private Vector toBlockVector(Vector vector) { + return new Vector( + Math.round(vector.getX()), + Math.round(vector.getY()), + Math.round(vector.getZ()) + ); } private boolean sameBlock(Vector a, Vector b) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java new file mode 100644 index 00000000..e33015c2 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java @@ -0,0 +1,29 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.placement; + +import com.sk89q.worldedit.math.BlockVector3; +import org.bukkit.util.Vector; + +public class RailBlockPlacement { + + private final BlockVector3 position; + private final RailBlockRole role; + private final Vector direction; + + public RailBlockPlacement(BlockVector3 position, RailBlockRole role, Vector direction) { + this.position = position; + this.role = role; + this.direction = direction == null ? new Vector(1, 0, 0) : direction; + } + + public BlockVector3 getPosition() { + return position; + } + + public RailBlockRole getRole() { + return role; + } + + public Vector getDirection() { + return direction; + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockRole.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockRole.java new file mode 100644 index 00000000..97f161a7 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockRole.java @@ -0,0 +1,6 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.placement; + +public enum RailBlockRole { + CENTER, + SIDE +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailPlacementBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailPlacementBuilder.java new file mode 100644 index 00000000..588e1bc7 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailPlacementBuilder.java @@ -0,0 +1,58 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.placement; + +import com.sk89q.worldedit.math.BlockVector3; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPath; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.side.RailOrientationResolver; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.side.RailSideBlock; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.side.RailSideBuilder; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +public class RailPlacementBuilder { + + private final RailSideBuilder sideBuilder; + private final RailOrientationResolver orientationResolver; + + public RailPlacementBuilder() { + this.sideBuilder = new RailSideBuilder(); + this.orientationResolver = new RailOrientationResolver(); + } + + public List buildPlacements(RailPath railPath) { + List placements = new ArrayList<>(); + + LinkedHashSet centerPositions = sideBuilder.getCenterPositions(railPath); + Map sideBlocks = sideBuilder.buildSideBlocks(railPath); + + addSidePlacements(placements, sideBlocks); + addCenterPlacements(placements, centerPositions); + + return placements; + } + + private void addSidePlacements(List placements, Map sideBlocks) { + for (RailSideBlock sideBlock : sideBlocks.values()) { + Vector direction = orientationResolver.resolveDirection(sideBlock, sideBlocks); + + placements.add(new RailBlockPlacement( + sideBlock.getPosition(), + RailBlockRole.SIDE, + direction + )); + } + } + + private void addCenterPlacements(List placements, LinkedHashSet centerPositions) { + for (BlockVector3 centerPosition : centerPositions) { + placements.add(new RailBlockPlacement( + centerPosition, + RailBlockRole.CENTER, + new Vector(1, 0, 0) + )); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java new file mode 100644 index 00000000..5019dbca --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java @@ -0,0 +1,79 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.placement; + +import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.types.RailType; +import net.buildtheearth.buildteamtools.modules.generator.model.History; +import net.buildtheearth.buildteamtools.modules.generator.model.Script; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class RailWorldEditPlacer { + + public boolean placeWithWorldEdit(Script script, List placements, RailType railType) { + Map blockMap = createBlockMap(placements, railType); + + try (EditSession editSession = WorldEdit.getInstance() + .newEditSessionBuilder() + .world(script.getWeWorld()) + .actor(script.getActor()) + .build()) { + + for (Map.Entry entry : blockMap.entrySet()) + editSession.setBlock(entry.getKey(), BukkitAdapter.adapt(entry.getValue())); + + editSession.flushQueue(); + script.getLocalSession().remember(editSession); + + return true; + } catch (Throwable throwable) { + throwable.printStackTrace(); + return false; + } + } + + public List placeWithBukkitFallback( + Script script, + List placements, + RailType railType + ) { + Map blockMap = createBlockMap(placements, railType); + List changes = new ArrayList<>(); + + for (Map.Entry entry : blockMap.entrySet()) { + BlockVector3 position = entry.getKey(); + BlockData newData = entry.getValue(); + + Block block = script.getPlayer().getWorld().getBlockAt(position.x(), position.y(), position.z()); + + changes.add(new History.BlockChange( + script.getPlayer().getWorld().getName(), + position.x(), + position.y(), + position.z(), + block.getBlockData().getAsString(), + newData.getAsString() + )); + + block.setBlockData(newData, false); + } + + return changes; + } + + private Map createBlockMap(List placements, RailType railType) { + Map blockMap = new LinkedHashMap<>(); + + for (RailBlockPlacement placement : placements) + blockMap.put(placement.getPosition(), railType.createBlockData(placement)); + + return blockMap; + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java new file mode 100644 index 00000000..7f68c291 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java @@ -0,0 +1,95 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.side; + +import com.sk89q.worldedit.math.BlockVector3; +import org.bukkit.util.Vector; + +import java.util.Map; + +public class RailOrientationResolver { + + public Vector resolveDirection(RailSideBlock sideBlock, Map sideBlocks) { + BlockVector3 position = sideBlock.getPosition(); + + boolean east = sideBlocks.containsKey(position.add(1, 0, 0)); + boolean west = sideBlocks.containsKey(position.add(-1, 0, 0)); + boolean south = sideBlocks.containsKey(position.add(0, 0, 1)); + boolean north = sideBlocks.containsKey(position.add(0, 0, -1)); + + boolean eastWest = east || west; + boolean northSouth = north || south; + + if (eastWest && !northSouth) + return getHorizontalDirection(east, west); + + if (northSouth && !eastWest) + return getVerticalDirection(south, north); + + if (eastWest && northSouth) + return resolveCornerDirection(sideBlock, east, west, south, north); + + return sideBlock.getAverageDirection(); + } + + private Vector getHorizontalDirection(boolean east, boolean west) { + if (east && !west) + return new Vector(1, 0, 0); + + if (west && !east) + return new Vector(-1, 0, 0); + + return new Vector(1, 0, 0); + } + + private Vector getVerticalDirection(boolean south, boolean north) { + if (south && !north) + return new Vector(0, 0, 1); + + if (north && !south) + return new Vector(0, 0, -1); + + return new Vector(0, 0, 1); + } + + private Vector resolveCornerDirection( + RailSideBlock sideBlock, + boolean east, + boolean west, + boolean south, + boolean north + ) { + Vector average = sideBlock.getAverageDirection(); + + int avgX = average.getBlockX(); + int avgZ = average.getBlockZ(); + + if (Math.abs(avgX) > Math.abs(avgZ)) { + if (avgX > 0 && east) + return new Vector(1, 0, 0); + + if (avgX < 0 && west) + return new Vector(-1, 0, 0); + } + + if (Math.abs(avgZ) > Math.abs(avgX)) { + if (avgZ > 0 && south) + return new Vector(0, 0, 1); + + if (avgZ < 0 && north) + return new Vector(0, 0, -1); + } + + if (east) + return new Vector(1, 0, 0); + + if (west) + return new Vector(-1, 0, 0); + + if (south) + return new Vector(0, 0, 1); + + if (north) + return new Vector(0, 0, -1); + + return average; + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBlock.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBlock.java new file mode 100644 index 00000000..b07b892e --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBlock.java @@ -0,0 +1,34 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.side; + +import com.sk89q.worldedit.math.BlockVector3; +import org.bukkit.util.Vector; + +public class RailSideBlock { + + private final BlockVector3 position; + private int directionX; + private int directionZ; + + public RailSideBlock(BlockVector3 position) { + this.position = position; + } + + public BlockVector3 getPosition() { + return position; + } + + public void addDirection(Vector direction) { + directionX += direction.getBlockX(); + directionZ += direction.getBlockZ(); + } + + public Vector getAverageDirection() { + int dx = Integer.compare(directionX, 0); + int dz = Integer.compare(directionZ, 0); + + if (dx == 0 && dz == 0) + return new Vector(1, 0, 0); + + return new Vector(dx, 0, dz); + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBuilder.java new file mode 100644 index 00000000..7169ec98 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailSideBuilder.java @@ -0,0 +1,178 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.side; + +import com.sk89q.worldedit.math.BlockVector3; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.path.RailPath; +import org.bukkit.util.Vector; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +public class RailSideBuilder { + + public Map buildSideBlocks(RailPath railPath) { + Map sideBlocks = new LinkedHashMap<>(); + LinkedHashSet centerPositions = getCenterPositions(railPath); + + List centerPath = railPath.getCenterPath(); + + for (int i = 0; i < centerPath.size() - 1; i++) { + Vector from = centerPath.get(i); + Vector to = centerPath.get(i + 1); + Vector direction = getDirection(from, to); + + if (direction == null) + continue; + + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + if (dx != 0 && dz != 0) { + placeDiagonalEdgeSideBlocks(sideBlocks, centerPositions, from, dx, dz); + } else { + placeStraightEdgeSideBlocks(sideBlocks, centerPositions, from, to, direction); + } + } + + removeFloatingSideBlocks(sideBlocks); + + return sideBlocks; + } + + public LinkedHashSet getCenterPositions(RailPath railPath) { + LinkedHashSet centerPositions = new LinkedHashSet<>(); + + for (Vector center : railPath.getCenterPath()) + centerPositions.add(toBlockVector3(center)); + + return centerPositions; + } + + private void placeStraightEdgeSideBlocks( + Map sideBlocks, + LinkedHashSet centerPositions, + Vector from, + Vector to, + Vector direction + ) { + Vector leftOffset = getLeftOffset(direction); + Vector rightOffset = getRightOffset(direction); + + addSideBlock(sideBlocks, centerPositions, offset(from, leftOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(to, leftOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(from, rightOffset), direction); + addSideBlock(sideBlocks, centerPositions, offset(to, rightOffset), direction); + } + + private void placeDiagonalEdgeSideBlocks( + Map sideBlocks, + LinkedHashSet centerPositions, + Vector from, + int dx, + int dz + ) { + Vector direction = new Vector(dx, 0, dz); + + BlockVector3 horizontalSide = BlockVector3.at( + from.getBlockX() + dx, + from.getBlockY(), + from.getBlockZ() + ); + + BlockVector3 verticalSide = BlockVector3.at( + from.getBlockX(), + from.getBlockY(), + from.getBlockZ() + dz + ); + + addSideBlock(sideBlocks, centerPositions, horizontalSide, direction); + addSideBlock(sideBlocks, centerPositions, verticalSide, direction); + } + + private void addSideBlock( + Map sideBlocks, + LinkedHashSet centerPositions, + BlockVector3 position, + Vector direction + ) { + if (centerPositions.contains(position)) + return; + + RailSideBlock sideBlock = sideBlocks.computeIfAbsent(position, RailSideBlock::new); + sideBlock.addDirection(direction); + } + + private void removeFloatingSideBlocks(Map sideBlocks) { + if (sideBlocks.size() <= 2) + return; + + sideBlocks.entrySet().removeIf(entry -> countSideNeighbours(entry.getKey(), sideBlocks) == 0); + } + + private int countSideNeighbours(BlockVector3 position, Map sideBlocks) { + int count = 0; + + if (sideBlocks.containsKey(position.add(1, 0, 0))) + count++; + + if (sideBlocks.containsKey(position.add(-1, 0, 0))) + count++; + + if (sideBlocks.containsKey(position.add(0, 0, 1))) + count++; + + if (sideBlocks.containsKey(position.add(0, 0, -1))) + count++; + + if (sideBlocks.containsKey(position.add(1, 0, 1))) + count++; + + if (sideBlocks.containsKey(position.add(1, 0, -1))) + count++; + + if (sideBlocks.containsKey(position.add(-1, 0, 1))) + count++; + + if (sideBlocks.containsKey(position.add(-1, 0, -1))) + count++; + + return count; + } + + private Vector getLeftOffset(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + return new Vector(-dz, 0, dx); + } + + private Vector getRightOffset(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + return new Vector(dz, 0, -dx); + } + + private BlockVector3 offset(Vector center, Vector offset) { + return BlockVector3.at( + center.getBlockX() + offset.getBlockX(), + center.getBlockY(), + center.getBlockZ() + offset.getBlockZ() + ); + } + + private Vector getDirection(Vector from, Vector to) { + int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); + int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); + + if (dx == 0 && dz == 0) + return null; + + return new Vector(dx, 0, dz); + } + + private BlockVector3 toBlockVector3(Vector vector) { + return BlockVector3.at(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + } +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/RailType.java new file mode 100644 index 00000000..e7c9b358 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/RailType.java @@ -0,0 +1,11 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.types; + +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailBlockPlacement; +import org.bukkit.block.data.BlockData; + +public interface RailType { + + String getName(); + + BlockData createBlockData(RailBlockPlacement placement); +} \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/SampleRailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/SampleRailType.java new file mode 100644 index 00000000..6114369e --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/types/SampleRailType.java @@ -0,0 +1,69 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.types; + +import com.sk89q.worldedit.math.BlockVector3; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailBlockPlacement; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.placement.RailBlockRole; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.util.Vector; + +public class SampleRailType implements RailType { + + private static final Material[] CENTER_MATERIALS = new Material[]{ + Material.DEAD_FIRE_CORAL_BLOCK, + Material.STONE, + Material.COBBLESTONE + }; + + private static final Material SIDE_MATERIAL = Material.ANVIL; + + @Override + public String getName() { + return "Sample Railway"; + } + + @Override + public BlockData createBlockData(RailBlockPlacement placement) { + if (placement.getRole() == RailBlockRole.CENTER) + return createCenterBlockData(placement.getPosition()); + + return createSideBlockData(placement.getDirection()); + } + + private BlockData createCenterBlockData(BlockVector3 position) { + int index = Math.floorMod(position.x() * 31 + position.z() * 17, CENTER_MATERIALS.length); + return CENTER_MATERIALS[index].createBlockData(); + } + + private BlockData createSideBlockData(Vector direction) { + BlockData data = SIDE_MATERIAL.createBlockData(); + + if (data instanceof Directional directional) + directional.setFacing(toBlockFace(direction)); + + return data; + } + + private BlockFace toBlockFace(Vector direction) { + int dx = direction.getBlockX(); + int dz = direction.getBlockZ(); + + if (Math.abs(dx) >= Math.abs(dz)) { + if (dx > 0) + return BlockFace.EAST; + + if (dx < 0) + return BlockFace.WEST; + } + + if (dz > 0) + return BlockFace.SOUTH; + + if (dz < 0) + return BlockFace.NORTH; + + return BlockFace.EAST; + } +} \ No newline at end of file From 6bd86153d0c23488f2f99ce2d81a1b1004b34416 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Fri, 1 May 2026 14:35:18 +0200 Subject: [PATCH 5/6] refactor: - Normalised vectors before comparing block positions - Used Vector#getBlockX/Y/Z instead of manual rounding - Moved rail validation logic into hasValidRailSelection - Kept checkForPlayer aligned with the generator component contract - Improved readability of rail path and selection validation code --- .../generator/components/rail/Rail.java | 4 +++ .../components/rail/path/RailPathBuilder.java | 26 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java index 4d244bd4..3a2363a3 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java @@ -19,6 +19,10 @@ public Rail() { @Override public boolean checkForPlayer(Player player) { + return hasValidRailSelection(player); + } + + private boolean hasValidRailSelection(Player player) { if (GeneratorUtils.checkForNoWorldEditSelection(player)) { player.sendMessage("§cRail Generator requires an active WorldEdit selection."); return false; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java index 34b4caa0..a7663baf 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/path/RailPathBuilder.java @@ -126,7 +126,6 @@ private void appendQuadraticCurve(List path, Vector start, Vector contro for (int sample = 1; sample <= samples; sample++) { double t = sample / (double) samples; - double inverse = 1.0 - t; double x = inverse * inverse * start.getX() @@ -231,7 +230,6 @@ private List removeImmediateBacktracking(List path) { while (cleaned.size() >= 3) { Vector a = cleaned.get(cleaned.size() - 3); - Vector b = cleaned.get(cleaned.size() - 2); Vector c = cleaned.get(cleaned.size() - 1); if (sameBlock(a, c)) { @@ -271,8 +269,11 @@ private void addPointIfNew(List path, Vector point) { } private Vector getDirection(Vector from, Vector to) { - int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); - int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); + Vector blockFrom = toBlockVector(from); + Vector blockTo = toBlockVector(to); + + int dx = Integer.compare(blockTo.getBlockX() - blockFrom.getBlockX(), 0); + int dz = Integer.compare(blockTo.getBlockZ() - blockFrom.getBlockZ(), 0); if (dx == 0 && dz == 0) return null; @@ -291,23 +292,24 @@ private boolean oppositeDirection(Vector a, Vector b) { } private int getChebyshevDistance(Vector a, Vector b) { - int dx = Math.abs(a.getBlockX() - b.getBlockX()); - int dz = Math.abs(a.getBlockZ() - b.getBlockZ()); + Vector blockA = toBlockVector(a); + Vector blockB = toBlockVector(b); + + int dx = Math.abs(blockA.getBlockX() - blockB.getBlockX()); + int dz = Math.abs(blockA.getBlockZ() - blockB.getBlockZ()); return Math.max(dx, dz); } private Vector toBlockVector(Vector vector) { return new Vector( - Math.round(vector.getX()), - Math.round(vector.getY()), - Math.round(vector.getZ()) + vector.getBlockX(), + vector.getBlockY(), + vector.getBlockZ() ); } private boolean sameBlock(Vector a, Vector b) { - return a.getBlockX() == b.getBlockX() - && a.getBlockY() == b.getBlockY() - && a.getBlockZ() == b.getBlockZ(); + return toBlockVector(a).equals(toBlockVector(b)); } } \ No newline at end of file From f3315c422354fd039a4cd567a3fa1bcd028fa66e Mon Sep 17 00:00:00 2001 From: Jasupa Date: Tue, 5 May 2026 20:04:49 +0200 Subject: [PATCH 6/6] refactor: - Fixed the Qodana issues --- .../generator/components/rail/RailScripts.java | 8 +++++++- .../rail/placement/RailBlockPlacement.java | 14 ++------------ .../rail/placement/RailWorldEditPlacer.java | 17 ++++++++++++++--- .../rail/side/RailOrientationResolver.java | 4 ++-- .../components/rail/side/RailSideBlock.java | 6 ++---- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java index c642da81..6cab068b 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java @@ -19,6 +19,7 @@ import org.bukkit.util.Vector; import java.util.List; +import java.util.logging.Level; public class RailScripts extends Script { @@ -61,7 +62,12 @@ private void generateRail() { Bukkit.getScheduler().runTask(BuildTeamTools.getInstance(), () -> placeRail(placements)); } catch (Exception exception) { getPlayer().sendMessage("§cRail Generator failed while generating from the WorldEdit selection."); - exception.printStackTrace(); + + BuildTeamTools.getInstance().getLogger().log( + Level.SEVERE, + "Rail Generator failed while generating from the WorldEdit selection.", + exception + ); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java index e33015c2..54080feb 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailBlockPlacement.java @@ -1,8 +1,10 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail.placement; import com.sk89q.worldedit.math.BlockVector3; +import lombok.Getter; import org.bukkit.util.Vector; +@Getter public class RailBlockPlacement { private final BlockVector3 position; @@ -14,16 +16,4 @@ public RailBlockPlacement(BlockVector3 position, RailBlockRole role, Vector dire this.role = role; this.direction = direction == null ? new Vector(1, 0, 0) : direction; } - - public BlockVector3 getPosition() { - return position; - } - - public RailBlockRole getRole() { - return role; - } - - public Vector getDirection() { - return direction; - } } \ No newline at end of file diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java index 5019dbca..029f1525 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/placement/RailWorldEditPlacer.java @@ -4,6 +4,8 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.block.BlockState; +import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.components.rail.types.RailType; import net.buildtheearth.buildteamtools.modules.generator.model.History; import net.buildtheearth.buildteamtools.modules.generator.model.Script; @@ -14,6 +16,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; public class RailWorldEditPlacer { @@ -26,15 +29,23 @@ public boolean placeWithWorldEdit(Script script, List placem .actor(script.getActor()) .build()) { - for (Map.Entry entry : blockMap.entrySet()) - editSession.setBlock(entry.getKey(), BukkitAdapter.adapt(entry.getValue())); + for (Map.Entry entry : blockMap.entrySet()) { + BlockVector3 position = entry.getKey(); + BlockState blockState = BukkitAdapter.adapt(entry.getValue()); + + editSession.setBlock(position.x(), position.y(), position.z(), blockState); + } editSession.flushQueue(); script.getLocalSession().remember(editSession); return true; } catch (Throwable throwable) { - throwable.printStackTrace(); + BuildTeamTools.getInstance().getLogger().log( + Level.SEVERE, + "Failed to place railway with WorldEdit.", + throwable + ); return false; } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java index 7f68c291..072f2b90 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/side/RailOrientationResolver.java @@ -21,10 +21,10 @@ public Vector resolveDirection(RailSideBlock sideBlock, Map