From 3cf603b2ae00c6a3ea01bc8c3190c78b3d26265f Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 11:46:09 +0200 Subject: [PATCH 1/8] feat(rail): add configurable parallel rail generation --- .../generator/components/rail/Rail.java | 18 +- .../components/rail/RailBlockBuilder.java | 107 +++-- .../generator/components/rail/RailFlag.java | 4 +- .../components/rail/RailLanePathBuilder.java | 24 +- .../generator/components/rail/RailLimits.java | 4 +- .../components/rail/RailPermissionGuard.java | 20 + .../rail/RailPreparationProgress.java | 58 +-- .../components/rail/RailScripts.java | 81 +++- .../components/rail/RailSettings.java | 2 +- .../generator/components/rail/RailType.java | 284 ++++++++++++- .../components/rail/RailTypeManager.java | 249 +++++++++++ .../rail/menu/RailBlockPickerMenu.java | 75 ++++ .../components/rail/menu/RailBlockRole.java | 89 ++++ .../components/rail/menu/RailMenuText.java | 13 + .../components/rail/menu/RailTypeDraft.java | 43 ++ .../rail/menu/RailTypeEditorMenu.java | 311 ++++++++++++++ .../components/rail/menu/RailTypeMenu.java | 401 +++++++++++++++++- .../modules/generator/menu/GeneratorMenu.java | 259 ++++++----- .../modules/network/model/Permissions.java | 6 + src/main/resources/plugin.yml | 25 ++ 20 files changed, 1855 insertions(+), 218 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPermissionGuard.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.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 83809e8c..badec2bd 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 @@ -6,9 +6,12 @@ import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.Polygonal2DRegion; import com.sk89q.worldedit.regions.Region; +import lombok.Getter; +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.network.model.Permissions; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -20,8 +23,12 @@ public class Rail extends GeneratorComponent { private final Set preparingPlayers = ConcurrentHashMap.newKeySet(); + @Getter + private final RailTypeManager railTypeManager; + public Rail() { super(GeneratorType.RAIL); + railTypeManager = new RailTypeManager(BuildTeamTools.getInstance().getDataFolder()); } @Override @@ -38,7 +45,7 @@ public boolean checkForPlayer(Player player) { "Rail Generator supports cuboid, polygonal and convex WorldEdit selections." ))); player.closeInventory(); - player.playSound(player.getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0F, 1.0F); + playSound(player, Sound.ENTITY_ITEM_BREAK); return false; } @@ -50,6 +57,9 @@ private boolean isSupportedRailSelection(Region region) { @Override public void generate(Player player) { + if (!RailPermissionGuard.check(player, Permissions.RAIL_GENERATOR_USE)) + return; + if (GeneratorModule.getInstance().isGenerating(player) || !preparingPlayers.add(player.getUniqueId())) { sendAlreadyGeneratingMessage(player); return; @@ -67,6 +77,10 @@ private void sendAlreadyGeneratingMessage(Player player) { player.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( "Rail Generator is already running. Please wait until the current generation is finished." ))); - player.playSound(player.getLocation(), Sound.ENTITY_ITEM_BREAK, 1.0F, 1.0F); + playSound(player, Sound.ENTITY_ITEM_BREAK); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java index b2e130e8..78e7a878 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java @@ -1,9 +1,11 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; import com.alpsbte.alpslib.utils.GeneratorUtils; +import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; import org.bukkit.util.Vector; @@ -17,45 +19,34 @@ final class RailBlockBuilder { private static final Direction DEFAULT_FACING = Direction.EAST; - private static final XMaterial[] CENTER_MATERIALS = new XMaterial[]{ - XMaterial.DEAD_FIRE_CORAL_BLOCK, - XMaterial.STONE, - XMaterial.COBBLESTONE - }; + private static final String FACING_PROPERTY = "facing"; + // Sleeper ends stick out one block beyond the rails, which sit at offset 1 from the track center. + private static final int SLEEPER_SIDE_OFFSET = 2; - private final List controlPoints; private final RailTerrainResolver terrainResolver; private final RailType railType; + private final BlockType railBlockType; private final RailPreparationProgress preparationProgress; - private final int railLaneCount; - private final int railLaneSpacing; private final long terrainAdjustedPercentage; private final long buildFinishedPercentage; RailBlockBuilder( - List controlPoints, RailTerrainResolver terrainResolver, RailType railType, RailPreparationProgress preparationProgress, - int railLaneCount, - int railLaneSpacing, long terrainAdjustedPercentage, long buildFinishedPercentage ) { - this.controlPoints = controlPoints; this.terrainResolver = terrainResolver; this.railType = railType; + this.railBlockType = getRailBlockType(railType); this.preparationProgress = preparationProgress; - this.railLaneCount = railLaneCount; - this.railLaneSpacing = railLaneSpacing; this.terrainAdjustedPercentage = terrainAdjustedPercentage; this.buildFinishedPercentage = buildFinishedPercentage; } - Map build(List path) { + Map build(List> railCenterPaths) { Map railBlocks = new LinkedHashMap<>(); - List> railCenterPaths = new RailLanePathBuilder(controlPoints, terrainResolver, railLaneCount, railLaneSpacing) - .createRailCenterPaths(path); Set centerPositions = getCenterPositions(railCenterPaths); Map sideBlocks = new LinkedHashMap<>(); int totalPathPoints = getTotalPathPointCount(railCenterPaths); @@ -76,16 +67,21 @@ Map build(List path) { int processedSideBlocks = 0; for (RailSideBlock sideBlock : sideBlocks.values()) { - railBlocks.put(sideBlock.key(), createAnvilBlockState(resolveSideBlockFacing(sideBlock, sideBlocks))); + railBlocks.put(sideBlock.key(), createRailBlockState(resolveSideBlockFacing(sideBlock, sideBlocks))); processedSideBlocks++; preparationProgress.update(preparationProgress.scale(processedSideBlocks, sideBlocks.size(), 86L, 89L)); } + if (railType.hasSleepers()) + for (List railCenterPath : railCenterPaths) + addSleeperBlocks(railBlocks, railCenterPath, centerPositions); + int processedCenterPoints = 0; for (List railCenterPath : railCenterPaths) { - for (Vector center : railCenterPath) { - railBlocks.put(PositionKey.from(center), createCenterBlockState(center)); + for (int index = 0; index < railCenterPath.size(); index++) { + Vector center = railCenterPath.get(index); + railBlocks.put(PositionKey.from(center), createCenterBlockState(center, isSleeperPoint(index))); processedCenterPoints++; preparationProgress.update(preparationProgress.scale(processedCenterPoints, totalPathPoints, 89L, buildFinishedPercentage)); } @@ -94,6 +90,50 @@ Map build(List path) { return railBlocks; } + private boolean isSleeperPoint(int index) { + return railType.hasSleepers() && index % railType.getSleeperSpacing() == 0; + } + + private void addSleeperBlocks(Map railBlocks, List path, Set centerPositions) { + BlockState sleeperBlockState = GeneratorUtils.getBlockState(railType.getSleeperBlock()); + + for (int index = 0; index < path.size(); index++) { + if (!isSleeperPoint(index)) + continue; + + Vector center = path.get(index); + RailStep step = getRailStep(path, index, new RailStep(1, 0)); + RailStep perpendicularStep = new RailStep(-step.dz(), step.dx()); + + addSleeperBlock(railBlocks, center, perpendicularStep, SLEEPER_SIDE_OFFSET, sleeperBlockState, centerPositions); + addSleeperBlock(railBlocks, center, perpendicularStep, -SLEEPER_SIDE_OFFSET, sleeperBlockState, centerPositions); + } + } + + private void addSleeperBlock( + Map railBlocks, + Vector center, + RailStep perpendicularStep, + int offset, + BlockState sleeperBlockState, + Set centerPositions + ) { + if (perpendicularStep.dx() == 0 && perpendicularStep.dz() == 0) + return; + + int x = center.getBlockX() + perpendicularStep.dx() * offset; + int z = center.getBlockZ() + perpendicularStep.dz() * offset; + int y = terrainResolver.getNearestRailSurfaceY(x, z, center.getBlockY()); + + PositionKey key = PositionKey.of(x, y, z); + + // Rails and track centers take precedence over sleeper ends. + if (centerPositions.contains(key) || railBlocks.containsKey(key)) + return; + + railBlocks.put(key, sleeperBlockState); + } + private int getTotalPathPointCount(List> railCenterPaths) { int totalPathPoints = 0; @@ -247,22 +287,29 @@ private RailStep getStep(Vector from, Vector to) { return new RailStep(dx, dz); } - private BlockState createCenterBlockState(Vector position) { - return switch (railType) { - case STANDARD -> createStandardCenterBlockState(position); - }; - } + private BlockState createCenterBlockState(Vector position, boolean sleeperPoint) { + if (sleeperPoint) + return GeneratorUtils.getBlockState(railType.getSleeperBlock()); - private BlockState createStandardCenterBlockState(Vector position) { + List blocksBelow = railType.getBlocksBelow(); int index = Math.floorMod( position.getBlockX() * 31 + position.getBlockY() * 23 + position.getBlockZ() * 17, - CENTER_MATERIALS.length + blocksBelow.size() ); - return GeneratorUtils.getBlockState(CENTER_MATERIALS[index]); + return GeneratorUtils.getBlockState(blocksBelow.get(index)); } - private BlockState createAnvilBlockState(Direction direction) { - return GeneratorUtils.getBlockStateWithFacing(BlockTypes.ANVIL, direction); + private BlockState createRailBlockState(Direction direction) { + if (!railBlockType.getPropertyMap().containsKey(FACING_PROPERTY)) + return railBlockType.getDefaultState(); + + return GeneratorUtils.getBlockStateWithFacing(railBlockType, direction); + } + + private BlockType getRailBlockType(RailType railType) { + BlockType blockType = Item.convertXMaterialToWEBlockType(railType.getRailBlock()); + + return blockType == null ? BlockTypes.ANVIL : blockType; } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailFlag.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailFlag.java index a60a1ccf..e3e0d1ad 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailFlag.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailFlag.java @@ -7,7 +7,9 @@ public enum RailFlag implements Flag { - RAIL_TYPE("t", FlagType.RAIL_TYPE); + RAIL_TYPE("t", FlagType.RAIL_TYPE), + TRACK_COUNT("c", FlagType.INTEGER), + TRACK_SPACING("s", FlagType.INTEGER); @Getter private final String flag; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java index 62f2ae9d..bd24386b 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java @@ -26,22 +26,22 @@ List> createRailCenterPaths(List path) { return List.of(path); List> railCenterPaths = new ArrayList<>(); - int sideLaneCount = (railLaneCount - 1) / 2; + double centerLaneIndex = (railLaneCount - 1) / 2.0; - for (int laneIndex = sideLaneCount; laneIndex >= 1; laneIndex--) { - List leftLane = createShiftedRailLane(laneIndex * railLaneSpacing, 1); + // Lanes are spread symmetrically around the selected path. Even track counts + // have no lane on the path itself, only shifted lanes on both sides. + for (int laneIndex = 0; laneIndex < railLaneCount; laneIndex++) { + int offset = (int) Math.round((laneIndex - centerLaneIndex) * railLaneSpacing); - if (leftLane.size() >= 2) - railCenterPaths.add(leftLane); - } - - railCenterPaths.add(path); + if (offset == 0) { + railCenterPaths.add(path); + continue; + } - for (int laneIndex = 1; laneIndex <= sideLaneCount; laneIndex++) { - List rightLane = createShiftedRailLane(laneIndex * railLaneSpacing, -1); + List shiftedLane = createShiftedRailLane(Math.abs(offset), offset > 0 ? 1 : -1); - if (rightLane.size() >= 2) - railCenterPaths.add(rightLane); + if (shiftedLane.size() >= 2) + railCenterPaths.add(shiftedLane); } return railCenterPaths; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java index 860fd0ae..e287fecb 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java @@ -54,10 +54,10 @@ static RailLimits fromConfig() { } private static int getBoundedInt(FileConfiguration config, String path, int fallback, int minimum, int maximum) { - return Math.max(minimum, Math.min(maximum, config.getInt(path, fallback))); + return Math.clamp(config.getInt(path, fallback), minimum, maximum); } private static long getBoundedLong(FileConfiguration config, String path, long fallback, long minimum, long maximum) { - return Math.max(minimum, Math.min(maximum, config.getLong(path, fallback))); + return Math.clamp(config.getLong(path, fallback), minimum, maximum); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPermissionGuard.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPermissionGuard.java new file mode 100644 index 00000000..1f3411ae --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPermissionGuard.java @@ -0,0 +1,20 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail; + +import com.alpsbte.alpslib.utils.ChatHelper; +import lombok.experimental.UtilityClass; +import org.bukkit.entity.Player; + +@UtilityClass +public class RailPermissionGuard { + + public static boolean check(Player player, String permission) { + if (player.hasPermission(permission)) + return true; + + player.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "You don't have permission to do this. Required permission: %s", + permission + ))); + return false; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java index f653b082..b744170d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java @@ -6,18 +6,21 @@ import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitTask; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + final class RailPreparationProgress implements Runnable { private final Player player; private final long maxPercentage; private final long updateIntervalTicks; - private volatile BukkitTask task; - private volatile long stageStartPercentage; - private volatile long stageEndPercentage; - private volatile long stageStartedAtMillis = System.currentTimeMillis(); - private volatile long stageEstimatedDurationMillis = 1L; - private volatile long queuedPercentage = -1L; - private volatile long lastSentPercentage = -1L; + private final AtomicReference task = new AtomicReference<>(); + private final AtomicLong stageStartPercentage = new AtomicLong(); + private final AtomicLong stageEndPercentage = new AtomicLong(); + private final AtomicLong stageStartedAtMillis = new AtomicLong(System.currentTimeMillis()); + private final AtomicLong stageEstimatedDurationMillis = new AtomicLong(1L); + private final AtomicLong queuedPercentage = new AtomicLong(-1L); + private final AtomicLong lastSentPercentage = new AtomicLong(-1L); RailPreparationProgress(Player player, long maxPercentage, long updateIntervalTicks) { this.player = player; @@ -29,36 +32,39 @@ void start() { if (!canContinue()) return; - if (task != null) + if (task.get() != null) return; - task = Bukkit.getScheduler().runTaskTimerAsynchronously( + BukkitTask newTask = Bukkit.getScheduler().runTaskTimerAsynchronously( BuildTeamTools.getInstance(), this, 0L, updateIntervalTicks ); + + if (!task.compareAndSet(null, newTask)) + newTask.cancel(); } void stop() { - BukkitTask currentTask = task; + BukkitTask currentTask = task.getAndSet(null); if (currentTask == null) return; currentTask.cancel(); - task = null; } void startStage(long startPercentage, long endPercentage, long estimatedDurationMillis) { if (!canContinue()) return; - stageStartPercentage = clamp(startPercentage); - stageEndPercentage = clamp(endPercentage); - stageStartedAtMillis = System.currentTimeMillis(); - stageEstimatedDurationMillis = Math.max(1L, estimatedDurationMillis); - update(stageStartPercentage); + long clampedStartPercentage = clamp(startPercentage); + stageStartPercentage.set(clampedStartPercentage); + stageEndPercentage.set(clamp(endPercentage)); + stageStartedAtMillis.set(System.currentTimeMillis()); + stageEstimatedDurationMillis.set(Math.max(1L, estimatedDurationMillis)); + update(clampedStartPercentage); } void completeStage(long percentage) { @@ -71,14 +77,14 @@ void update(long percentage) { long clampedPercentage = clamp(percentage); - if (clampedPercentage <= queuedPercentage) + if (clampedPercentage <= queuedPercentage.get()) return; - queuedPercentage = clampedPercentage; - if (clampedPercentage <= lastSentPercentage) + queuedPercentage.set(clampedPercentage); + if (clampedPercentage <= lastSentPercentage.get()) return; - lastSentPercentage = clampedPercentage; + lastSentPercentage.set(clampedPercentage); player.sendActionBar(ChatHelper.getStandardComponent(false, "Generator Progress: %s", clampedPercentage + "%")); } @@ -86,7 +92,7 @@ long scale(int completed, int total, long startPercentage, long endPercentage) { if (total <= 0) return endPercentage; - double progress = Math.max(0D, Math.min(1D, (double) completed / (double) total)); + double progress = Math.clamp((double) completed / (double) total, 0D, 1D); return startPercentage + Math.round(progress * (endPercentage - startPercentage)); } @@ -97,14 +103,14 @@ public void run() { return; } - long currentStageStart = stageStartPercentage; - long currentStageEnd = stageEndPercentage; + long currentStageStart = stageStartPercentage.get(); + long currentStageEnd = stageEndPercentage.get(); if (currentStageEnd <= currentStageStart) return; - long elapsedMillis = Math.max(0L, System.currentTimeMillis() - stageStartedAtMillis); - double progress = Math.min(0.98D, (double) elapsedMillis / (double) stageEstimatedDurationMillis); + long elapsedMillis = Math.max(0L, System.currentTimeMillis() - stageStartedAtMillis.get()); + double progress = Math.clamp((double) elapsedMillis / (double) stageEstimatedDurationMillis.get(), 0D, 0.98D); long estimatedPercentage = currentStageStart + (long) Math.floor(progress * (currentStageEnd - currentStageStart)); if (estimatedPercentage >= currentStageEnd) @@ -114,7 +120,7 @@ public void run() { } private long clamp(long percentage) { - return Math.max(0L, Math.min(maxPercentage, percentage)); + return Math.clamp(percentage, 0L, maxPercentage); } private boolean canContinue() { 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 1d055b74..64d6172e 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 @@ -7,6 +7,7 @@ import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.Script; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -42,14 +43,13 @@ public class RailScripts extends Script { private static final long RAIL_BLOCK_BUILD_ESTIMATED_MILLIS = 1_800L; private static final long QUEUE_OPERATIONS_ESTIMATED_MILLIS = 300L; - private static final int DEFAULT_RAIL_LANE_COUNT = 1; - private static final int DEFAULT_RAIL_LANE_SPACING = 5; - private Block[][][] blocks; private List controlPoints = new ArrayList<>(); private List centerPath = new ArrayList<>(); private RailTerrainResolver terrainResolver; - private RailType railType = RailType.STANDARD; + private RailType railType = RailType.getDefault(); + private int trackCount = RailType.DEFAULT_TRACK_COUNT; + private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; private final RailLimits limits; private final RailPreparationProgress preparationProgress; private final Runnable preparationFinishedCallback; @@ -90,6 +90,10 @@ public RailScripts(Player player, GeneratorComponent generatorComponent, Runnabl private boolean prepareSession() { if (!canContinue()) return false; + railType = getRailType(); + + if (!resolveTrackLayout()) return false; + controlPoints = getControlPoints(); railReferenceY = getRailReferenceY(controlPoints); preparationProgress.completeStage(CONTROL_POINTS_PROGRESS); @@ -141,7 +145,6 @@ private boolean prepareSession() { snapMissingControlPointHeightsToTerrain(controlPoints); centerPath = createCenterPath(controlPoints); adjustCenterPathToTerrain(); - railType = getRailType(); preparationProgress.completeStage(TERRAIN_ADJUST_PROGRESS); return true; @@ -154,7 +157,20 @@ private boolean queueRailGeneration() { return false; preparationProgress.startStage(TERRAIN_ADJUST_PROGRESS, RAIL_BLOCK_BUILD_PROGRESS, RAIL_BLOCK_BUILD_ESTIMATED_MILLIS); - Map railBlocks = buildRailBlocks(centerPath); + List> railCenterPaths = new RailLanePathBuilder(controlPoints, terrainResolver, trackCount, trackSpacing) + .createRailCenterPaths(centerPath); + + if (railCenterPaths.size() < trackCount) { + sendRailError( + "Rail Generator could not fit %s parallel tracks with %s blocks of spacing in this selection. " + + "Reduce the track count or spacing, or make the selection longer and less curved.", + trackCount, + trackSpacing + ); + return false; + } + + Map railBlocks = buildRailBlocks(railCenterPaths); preparationProgress.completeStage(RAIL_BLOCK_BUILD_PROGRESS); if (railBlocks.size() > limits.maxBlockPlacements()) { @@ -256,7 +272,7 @@ private boolean hasValidCenterPath() { } private boolean hasSafeEstimatedBlockCount(List path) { - long estimatedBlocks = (long) path.size() * getRailLaneCount() * 5L; + long estimatedBlocks = (long) path.size() * trackCount * 5L; if (estimatedBlocks <= limits.maxBlockPlacements()) return true; @@ -358,33 +374,58 @@ private List createRailSelectionPoints(List points) { } private int getSelectionPadding() { - int sideLaneCount = (getRailLaneCount() - 1) / 2; - return SELECTION_PADDING + (getRailLaneSpacing() * sideLaneCount) + 2; + int maxLaneOffset = (int) Math.round(((trackCount - 1) / 2.0) * trackSpacing); + return SELECTION_PADDING + maxLaneOffset + 2; } private List createCenterPath(List points) { return GeneratorUtils.removeOrthogonalCorners(GeneratorUtils.createShortestBlockPath(points)); } - private Map buildRailBlocks(List path) { + private Map buildRailBlocks(List> railCenterPaths) { return new RailBlockBuilder( - controlPoints, terrainResolver, railType, preparationProgress, - getRailLaneCount(), - getRailLaneSpacing(), TERRAIN_ADJUST_PROGRESS, RAIL_BLOCK_BUILD_PROGRESS - ).build(path); + ).build(railCenterPaths); } - private int getRailLaneCount() { - return DEFAULT_RAIL_LANE_COUNT; + private boolean resolveTrackLayout() { + trackCount = railType.getTrackCount(); + trackSpacing = railType.getTrackSpacing(); + + Integer trackCountFlag = getIntegerSetting(RailFlag.TRACK_COUNT); + Integer trackSpacingFlag = getIntegerSetting(RailFlag.TRACK_SPACING); + + if (trackCountFlag != null) + trackCount = trackCountFlag; + + if (trackSpacingFlag != null) + trackSpacing = trackSpacingFlag; + + if (trackCount < RailType.MIN_TRACK_COUNT || trackCount > RailType.MAX_TRACK_COUNT) { + sendRailError("Track count must be between %s and %s.", RailType.MIN_TRACK_COUNT, RailType.MAX_TRACK_COUNT); + return false; + } + + if (trackSpacing < RailType.MIN_TRACK_SPACING || trackSpacing > RailType.MAX_TRACK_SPACING) { + sendRailError("Track spacing must be between %s and %s.", RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING); + return false; + } + + return trackCount <= 1 || RailPermissionGuard.check(getPlayer(), Permissions.RAIL_MULTIPLE_TRACKS); } - private int getRailLaneSpacing() { - return DEFAULT_RAIL_LANE_SPACING; + private Integer getIntegerSetting(RailFlag flag) { + Settings settings = getGeneratorComponent().getPlayerSettings().get(getPlayer().getUniqueId()); + + if (!(settings instanceof RailSettings railSettings)) + return null; + + Object value = railSettings.getValues().get(flag); + return value instanceof Integer intValue ? intValue : null; } private void snapMissingControlPointHeightsToTerrain(List points) { @@ -429,10 +470,10 @@ private RailType getRailType() { Settings settings = getGeneratorComponent().getPlayerSettings().get(getPlayer().getUniqueId()); if (!(settings instanceof RailSettings railSettings)) - return RailType.STANDARD; + return RailType.getDefault(); Object value = railSettings.getValues().get(RailFlag.RAIL_TYPE); - return value instanceof RailType selectedRailType ? selectedRailType : RailType.STANDARD; + return value instanceof RailType selectedRailType ? selectedRailType : RailType.getDefault(); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java index ae244c3d..09629afa 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java @@ -11,6 +11,6 @@ public RailSettings(Player player) { @Override public void setDefaultValues() { - setValue(RailFlag.RAIL_TYPE, RailType.STANDARD); + setValue(RailFlag.RAIL_TYPE, RailType.getDefault()); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java index 6c641532..5e5e9b39 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java @@ -2,31 +2,289 @@ import com.cryptomorin.xseries.XMaterial; import lombok.Getter; +import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import org.bukkit.Material; import org.jspecify.annotations.Nullable; -public enum RailType { +import java.util.List; - STANDARD("standard", "Standard", XMaterial.RAIL); +@Getter +public final class RailType { + + public static final String STANDARD_IDENTIFIER = "standard"; + + public static final int MIN_TRACK_COUNT = 1; + public static final int MAX_TRACK_COUNT = 8; + public static final int MIN_TRACK_SPACING = 3; + public static final int MAX_TRACK_SPACING = 32; + // A sleeper spacing of 0 disables sleepers entirely. + public static final int MIN_SLEEPER_SPACING = 0; + public static final int MAX_SLEEPER_SPACING = 16; + public static final int MAX_IDENTIFIER_LENGTH = 32; + public static final int MAX_DISPLAY_NAME_LENGTH = 48; + + public static final int DEFAULT_TRACK_COUNT = 1; + public static final int DEFAULT_TRACK_SPACING = 5; + + private static final String IDENTIFIER_PATTERN = "[a-z0-9_-]+"; - @Getter private final String identifier; - @Getter private final String displayName; - @Getter private final XMaterial icon; + private final XMaterial railBlock; + private final List blocksBelow; + private final @Nullable XMaterial sleeperBlock; + private final int sleeperSpacing; + private final int trackCount; + private final int trackSpacing; + private final boolean builtIn; + + RailType(Configuration configuration, boolean builtIn) { + List configuredBlocksBelow = configuration.blocksBelow(); + + this.identifier = configuration.identifier(); + this.displayName = configuration.displayName(); + this.icon = configuration.icon(); + this.railBlock = configuration.railBlock(); + this.blocksBelow = configuredBlocksBelow == null ? List.of() : List.copyOf(configuredBlocksBelow); + this.sleeperBlock = configuration.sleeperBlock(); + this.sleeperSpacing = configuration.sleeperSpacing(); + this.trackCount = configuration.trackCount(); + this.trackSpacing = configuration.trackSpacing(); + this.builtIn = builtIn; + } + + public boolean hasSleepers() { + return sleeperSpacing > 0 && sleeperBlock != null; + } + + /** + * Creates a custom rail type with the rail block as menu icon. + * Call {@link RailTypeManager#saveRailType(RailType)} to validate and persist it. + */ + public static RailType createCustom(Configuration configuration) { + return new RailType(configuration, false); + } + + static RailType createStandard() { + return new RailType(new Configuration() + .identifier(STANDARD_IDENTIFIER) + .displayName("Standard") + .icon(XMaterial.RAIL) + .railBlock(XMaterial.ANVIL) + .blocksBelow(List.of(XMaterial.DEAD_FIRE_CORAL_BLOCK, XMaterial.STONE, XMaterial.COBBLESTONE)) + .sleeperBlock(XMaterial.SPRUCE_PLANKS) + .sleeperSpacing(0) + .trackCount(DEFAULT_TRACK_COUNT) + .trackSpacing(DEFAULT_TRACK_SPACING), true); + } + + /** + * Validates all configurable rail type values. + * + * @return A human-readable error message, or null if all values are valid. + */ + public static @Nullable String validate(Configuration configuration) { + String error = validateIdentifier(configuration.identifier()); + + if (error != null) + return error; + + error = validateDisplayName(configuration.displayName()); + + if (error != null) + return error; + + error = validateBlocks(configuration); + + if (error != null) + return error; + + error = validateSleeper(configuration); + + if (error != null) + return error; + + return validateTracks(configuration); + } + + private static @Nullable String validateIdentifier(@Nullable String identifier) { + if (!hasInvalidIdentifier(identifier)) + return null; + + return "Rail type name must be %s characters or less and may only contain letters, numbers, '-' and '_'." + .formatted(MAX_IDENTIFIER_LENGTH); + } + + private static @Nullable String validateDisplayName(@Nullable String displayName) { + if (displayName != null && !displayName.isBlank() && displayName.length() <= MAX_DISPLAY_NAME_LENGTH) + return null; + + return "Rail type display name must not be empty and must be %s characters or less." + .formatted(MAX_DISPLAY_NAME_LENGTH); + } + + private static @Nullable String validateBlocks(Configuration configuration) { + if (!isPlaceableBlock(configuration.railBlock())) + return "Rail block must be a valid placeable Minecraft block."; + + List blocksBelow = configuration.blocksBelow(); + + if (blocksBelow == null || blocksBelow.isEmpty()) + return "Rail type needs at least one block below the rails."; + + for (XMaterial blockBelow : blocksBelow) + if (!isPlaceableBlock(blockBelow)) + return "Blocks below the rails must be valid placeable Minecraft blocks."; + + return null; + } + + private static @Nullable String validateSleeper(Configuration configuration) { + int sleeperSpacing = configuration.sleeperSpacing(); + + if (sleeperSpacing < MIN_SLEEPER_SPACING || sleeperSpacing > MAX_SLEEPER_SPACING) + return "Sleeper spacing must be between %s and %s. Use 0 to disable sleepers." + .formatted(MIN_SLEEPER_SPACING, MAX_SLEEPER_SPACING); + + if (sleeperSpacing > 0 && !isPlaceableBlock(configuration.sleeperBlock())) + return "Sleeper block must be a valid placeable Minecraft block."; + + return null; + } + + private static @Nullable String validateTracks(Configuration configuration) { + int trackCount = configuration.trackCount(); + + if (trackCount < MIN_TRACK_COUNT || trackCount > MAX_TRACK_COUNT) + return "Track count must be between %s and %s.".formatted(MIN_TRACK_COUNT, MAX_TRACK_COUNT); + + int trackSpacing = configuration.trackSpacing(); + + if (trackSpacing < MIN_TRACK_SPACING || trackSpacing > MAX_TRACK_SPACING) + return "Track spacing must be between %s and %s.".formatted(MIN_TRACK_SPACING, MAX_TRACK_SPACING); + + return null; + } + + private static boolean hasInvalidIdentifier(@Nullable String identifier) { + return identifier == null + || identifier.isBlank() + || identifier.length() > MAX_IDENTIFIER_LENGTH + || !identifier.toLowerCase().matches(IDENTIFIER_PATTERN); + } + + private static boolean isPlaceableBlock(@Nullable XMaterial material) { + if (material == null) + return false; - RailType(String identifier, String displayName, XMaterial icon) { - this.identifier = identifier; - this.displayName = displayName; - this.icon = icon; + Material bukkitMaterial = material.get(); + return bukkitMaterial != null && bukkitMaterial.isBlock(); } public static @Nullable RailType byString(String value) { - for (RailType railType : RailType.values()) { - if (railType.getIdentifier().equalsIgnoreCase(value) || railType.name().equalsIgnoreCase(value)) - return railType; + Rail rail = GeneratorModule.getInstance().getRail(); + + return rail == null ? null : rail.getRailTypeManager().byString(value); + } + + public static RailType getDefault() { + Rail rail = GeneratorModule.getInstance().getRail(); + + return rail == null ? createStandard() : rail.getRailTypeManager().getDefault(); + } + + public static final class Configuration { + + private @Nullable String identifier; + private @Nullable String displayName; + private @Nullable XMaterial icon; + private @Nullable XMaterial railBlock; + private @Nullable List blocksBelow; + private @Nullable XMaterial sleeperBlock; + private int sleeperSpacing; + private int trackCount; + private int trackSpacing; + + public @Nullable String identifier() { + return identifier; } - return null; + public Configuration identifier(@Nullable String identifier) { + this.identifier = identifier; + return this; + } + + public @Nullable String displayName() { + return displayName; + } + + public Configuration displayName(@Nullable String displayName) { + this.displayName = displayName; + return this; + } + + public @Nullable XMaterial icon() { + return icon; + } + + public Configuration icon(@Nullable XMaterial icon) { + this.icon = icon; + return this; + } + + public @Nullable XMaterial railBlock() { + return railBlock; + } + + public Configuration railBlock(@Nullable XMaterial railBlock) { + this.railBlock = railBlock; + return this; + } + + public @Nullable List blocksBelow() { + return blocksBelow; + } + + public Configuration blocksBelow(@Nullable List blocksBelow) { + this.blocksBelow = blocksBelow; + return this; + } + + public @Nullable XMaterial sleeperBlock() { + return sleeperBlock; + } + + public Configuration sleeperBlock(@Nullable XMaterial sleeperBlock) { + this.sleeperBlock = sleeperBlock; + return this; + } + + public int sleeperSpacing() { + return sleeperSpacing; + } + + public Configuration sleeperSpacing(int sleeperSpacing) { + this.sleeperSpacing = sleeperSpacing; + return this; + } + + public int trackCount() { + return trackCount; + } + + public Configuration trackCount(int trackCount) { + this.trackCount = trackCount; + return this; + } + + public int trackSpacing() { + return trackSpacing; + } + + public Configuration trackSpacing(int trackSpacing) { + this.trackSpacing = trackSpacing; + return this; + } } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java new file mode 100644 index 00000000..402530c3 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java @@ -0,0 +1,249 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail; + +import com.alpsbte.alpslib.utils.ChatHelper; +import com.cryptomorin.xseries.XMaterial; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jspecify.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Holds all available rail types and persists player-created rail types to disk. + * The built-in standard rail type is always available and cannot be overwritten or deleted. + */ +public class RailTypeManager { + + private static final String FILE_PATH = "modules/generator/rail-types.yml"; + private static final String TYPES_SECTION = "rail-types"; + + private static final String KEY_DISPLAY_NAME = "display-name"; + private static final String KEY_ICON = "icon"; + private static final String KEY_RAIL_BLOCK = "rail-block"; + private static final String KEY_BLOCKS_BELOW = "blocks-below"; + private static final String KEY_SLEEPER_BLOCK = "sleeper-block"; + private static final String KEY_SLEEPER_SPACING = "sleeper-spacing"; + private static final String KEY_TRACK_COUNT = "track-count"; + private static final String KEY_TRACK_SPACING = "track-spacing"; + + private final Map railTypes = new LinkedHashMap<>(); + private final File file; + + public RailTypeManager(File dataFolder) { + this.file = new File(dataFolder, FILE_PATH); + reload(); + } + + public void reload() { + railTypes.clear(); + + RailType standard = RailType.createStandard(); + railTypes.put(standard.getIdentifier(), standard); + + if (!file.exists()) + return; + + ConfigurationSection typesSection = YamlConfiguration.loadConfiguration(file).getConfigurationSection(TYPES_SECTION); + + if (typesSection == null) + return; + + for (String identifier : typesSection.getKeys(false)) + loadRailType(typesSection.getConfigurationSection(identifier), identifier); + } + + public Collection getRailTypes() { + return Collections.unmodifiableCollection(railTypes.values()); + } + + public RailType getDefault() { + return railTypes.get(RailType.STANDARD_IDENTIFIER); + } + + /** + * @return The first free "custom-N" identifier for a new player-created rail type. + */ + public String getNextCustomIdentifier() { + int index = 1; + + while (railTypes.containsKey("custom-" + index)) + index++; + + return "custom-" + index; + } + + public @Nullable RailType byString(@Nullable String value) { + if (value == null) + return null; + + RailType byIdentifier = railTypes.get(value.toLowerCase(Locale.ROOT)); + + if (byIdentifier != null) + return byIdentifier; + + for (RailType railType : railTypes.values()) + if (railType.getDisplayName().equalsIgnoreCase(value)) + return railType; + + return null; + } + + /** + * Validates and stores a custom rail type and saves it to disk. + * + * @return A human-readable error message, or null if the rail type was saved successfully. + */ + public @Nullable String saveRailType(RailType railType) { + String error = RailType.validate(new RailType.Configuration() + .identifier(railType.getIdentifier()) + .displayName(railType.getDisplayName()) + .icon(railType.getIcon()) + .railBlock(railType.getRailBlock()) + .blocksBelow(railType.getBlocksBelow()) + .sleeperBlock(railType.getSleeperBlock()) + .sleeperSpacing(railType.getSleeperSpacing()) + .trackCount(railType.getTrackCount()) + .trackSpacing(railType.getTrackSpacing())); + + if (error != null) + return error; + + RailType existingRailType = railTypes.get(railType.getIdentifier()); + + if (existingRailType != null && existingRailType.isBuiltIn()) + return "The rail type '%s' is built-in and cannot be overwritten.".formatted(railType.getIdentifier()); + + railTypes.put(railType.getIdentifier(), railType); + return save(); + } + + /** + * Deletes a custom rail type and saves the change to disk. + * + * @return A human-readable error message, or null if the rail type was deleted successfully. + */ + public @Nullable String deleteRailType(String identifier) { + RailType railType = railTypes.get(identifier.toLowerCase(Locale.ROOT)); + + if (railType == null) + return "The rail type '%s' does not exist.".formatted(identifier); + + if (railType.isBuiltIn()) + return "The rail type '%s' is built-in and cannot be deleted.".formatted(identifier); + + railTypes.remove(railType.getIdentifier()); + return save(); + } + + private void loadRailType(@Nullable ConfigurationSection section, String identifier) { + if (section == null) + return; + + String normalizedIdentifier = identifier.toLowerCase(Locale.ROOT); + String displayName = section.getString(KEY_DISPLAY_NAME, identifier); + XMaterial railBlock = parseMaterial(section.getString(KEY_RAIL_BLOCK)); + List blocksBelow = parseMaterials(section.getStringList(KEY_BLOCKS_BELOW)); + XMaterial sleeperBlock = parseMaterial(section.getString(KEY_SLEEPER_BLOCK)); + int sleeperSpacing = section.getInt(KEY_SLEEPER_SPACING, RailType.MIN_SLEEPER_SPACING); + int trackCount = section.getInt(KEY_TRACK_COUNT, RailType.DEFAULT_TRACK_COUNT); + int trackSpacing = section.getInt(KEY_TRACK_SPACING, RailType.DEFAULT_TRACK_SPACING); + + String error = RailType.validate(new RailType.Configuration() + .identifier(normalizedIdentifier) + .displayName(displayName) + .icon(railBlock) + .railBlock(railBlock) + .blocksBelow(blocksBelow) + .sleeperBlock(sleeperBlock) + .sleeperSpacing(sleeperSpacing) + .trackCount(trackCount) + .trackSpacing(trackSpacing)); + + if (error != null) { + ChatHelper.logError("Skipping invalid rail type '%s' in %s: %s", identifier, FILE_PATH, error); + return; + } + + if (railTypes.containsKey(normalizedIdentifier)) { + ChatHelper.logError("Skipping duplicate rail type '%s' in %s.", identifier, FILE_PATH); + return; + } + + XMaterial icon = parseMaterial(section.getString(KEY_ICON)); + + railTypes.put(normalizedIdentifier, new RailType(new RailType.Configuration() + .identifier(normalizedIdentifier) + .displayName(displayName) + .icon(icon == null ? railBlock : icon) + .railBlock(railBlock) + .blocksBelow(blocksBelow) + .sleeperBlock(sleeperBlock) + .sleeperSpacing(sleeperSpacing) + .trackCount(trackCount) + .trackSpacing(trackSpacing), false)); + } + + private @Nullable String save() { + YamlConfiguration config = new YamlConfiguration(); + + for (RailType railType : railTypes.values()) { + if (railType.isBuiltIn()) + continue; + + String path = TYPES_SECTION + "." + railType.getIdentifier(); + + config.set(path + "." + KEY_DISPLAY_NAME, railType.getDisplayName()); + config.set(path + "." + KEY_ICON, railType.getIcon().name()); + config.set(path + "." + KEY_RAIL_BLOCK, railType.getRailBlock().name()); + config.set(path + "." + KEY_BLOCKS_BELOW, railType.getBlocksBelow().stream().map(XMaterial::name).toList()); + + XMaterial sleeperBlock = railType.getSleeperBlock(); + + if (sleeperBlock != null) + config.set(path + "." + KEY_SLEEPER_BLOCK, sleeperBlock.name()); + + config.set(path + "." + KEY_SLEEPER_SPACING, railType.getSleeperSpacing()); + config.set(path + "." + KEY_TRACK_COUNT, railType.getTrackCount()); + config.set(path + "." + KEY_TRACK_SPACING, railType.getTrackSpacing()); + } + + try { + if (file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) + throw new IOException("Could not create directory " + file.getParentFile()); + + config.save(file); + return null; + } catch (IOException exception) { + ChatHelper.logError("Could not save rail types to %s.", exception, FILE_PATH); + return "Rail types could not be saved. Please check the server console."; + } + } + + private @Nullable XMaterial parseMaterial(@Nullable String value) { + if (value == null || value.isBlank()) + return null; + + return XMaterial.matchXMaterial(value).orElse(null); + } + + private List parseMaterials(List values) { + List materials = new ArrayList<>(); + + for (String value : values) { + XMaterial material = parseMaterial(value); + + // Keep invalid entries as null so validation reports them instead of silently dropping them. + materials.add(material); + } + + return materials; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java new file mode 100644 index 00000000..f2d90ece --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java @@ -0,0 +1,75 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.utils.menus.BlockListMenu; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.List; + +/** + * Lets the player pick a single block for one of the {@link RailBlockRole} slots + * of a rail type draft, then returns to the editor menu. + */ +public class RailBlockPickerMenu extends BlockListMenu { + + private final RailTypeDraft draft; + private final RailBlockRole role; + + RailBlockPickerMenu(Player player, RailTypeDraft draft, RailBlockRole role, boolean autoLoad) { + super(player, role.getMenuTitle(), role.createChoices(), new RailTypeEditorMenu(player, draft, false), autoLoad); + + this.draft = draft; + this.role = role; + } + + @Override + protected void setPaginatedItemClickEventsAsync(List source) { + List itemStacks = source.stream().map(l -> (ItemStack) l).toList(); + int slot = 0; + + // Only one block can be picked per role, so selecting a block deselects the previous one. + for (ItemStack ignored : itemStacks) { + final int _slot = slot; + getMenu().getSlot(_slot).setClickHandler((clickPlayer, clickInformation) -> { + String type = Item.getUppercaseMaterialString(getMenu().getSlot(_slot).getItem(getMenuPlayer())); + + if (selectedMaterials.contains(type)) { + selectedMaterials.remove(type); + } else { + selectedMaterials.clear(); + selectedMaterials.add(type); + } + + reloadMenuAsync(); + }); + slot++; + } + } + + @Override + protected void setItemClickEventsAsync() { + super.setItemClickEventsAsync(); + + if (canProceed()) + getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + XMaterial material = Item.convertStringToXMaterial(selectedMaterials.getFirst()); + + if (material == null) + return; + + role.apply(draft, material); + + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new RailTypeEditorMenu(clickPlayer, draft, true); + }); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java new file mode 100644 index 00000000..349cd6c2 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java @@ -0,0 +1,89 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.cryptomorin.xseries.XMaterial; +import lombok.Getter; +import net.buildtheearth.buildteamtools.utils.MenuItems; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +/** + * The block slots of a rail type that can be configured in the editor menu. + * Each role provides its own predefined list of valid blocks to choose from. + */ +enum RailBlockRole { + + RAIL_BLOCK("Choose a Rail Block") { + @Override + List createChoices() { + return createItems(List.of( + XMaterial.ANVIL, + XMaterial.CHIPPED_ANVIL, + XMaterial.DAMAGED_ANVIL, + XMaterial.RAIL, + XMaterial.POWERED_RAIL, + XMaterial.DETECTOR_RAIL, + XMaterial.ACTIVATOR_RAIL, + XMaterial.IRON_BARS, + XMaterial.SMOOTH_STONE_SLAB, + XMaterial.STONE_SLAB, + XMaterial.ANDESITE_WALL, + XMaterial.POLISHED_BLACKSTONE_WALL + )); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setRailBlock(material); + } + }, + + BLOCK_BELOW("Choose a Block Below the Rails") { + @Override + List createChoices() { + return MenuItems.getSolidBlocks(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setBlockBelow(material); + } + }, + + SLEEPER_BLOCK("Choose a Sleeper Block") { + @Override + List createChoices() { + return MenuItems.getSolidBlocks(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setSleeperBlock(material); + } + }; + + @Getter + private final String menuTitle; + + RailBlockRole(String menuTitle) { + this.menuTitle = menuTitle; + } + + abstract List createChoices(); + + abstract void apply(RailTypeDraft draft, XMaterial material); + + private static List createItems(List materials) { + List items = new ArrayList<>(); + + for (XMaterial material : materials) { + ItemStack item = material.parseItem(); + + if (item != null) + items.add(item); + } + + return items; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java new file mode 100644 index 00000000..6f364ae8 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java @@ -0,0 +1,13 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.alpsbte.alpslib.utils.ChatHelper; +import lombok.experimental.UtilityClass; +import net.kyori.adventure.text.format.NamedTextColor; + +@UtilityClass +final class RailMenuText { + + static String color(NamedTextColor color, String text) { + return ChatHelper.getColorizedString(color, text, false); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java new file mode 100644 index 00000000..86b93a43 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -0,0 +1,43 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.cryptomorin.xseries.XMaterial; +import lombok.Getter; +import lombok.Setter; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; + +import java.util.List; + +/** + * Mutable state of a rail type that is being created in the editor menu. + */ +@Getter +@Setter +class RailTypeDraft { + + private String identifier; + private String displayName; + private XMaterial railBlock = XMaterial.ANVIL; + private XMaterial blockBelow = XMaterial.GRAVEL; + private XMaterial sleeperBlock = XMaterial.SPRUCE_PLANKS; + private int sleeperSpacing = RailType.MIN_SLEEPER_SPACING; + private int trackCount = RailType.DEFAULT_TRACK_COUNT; + private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; + + static RailTypeDraft from(RailType railType, boolean keepIdentity) { + RailTypeDraft draft = new RailTypeDraft(); + List blocksBelow = railType.getBlocksBelow(); + + if (keepIdentity) { + draft.identifier = railType.getIdentifier(); + draft.displayName = railType.getDisplayName(); + } + + draft.railBlock = railType.getRailBlock(); + draft.blockBelow = blocksBelow.isEmpty() ? XMaterial.GRAVEL : blocksBelow.getFirst(); + draft.sleeperBlock = railType.getSleeperBlock() == null ? XMaterial.SPRUCE_PLANKS : railType.getSleeperBlock(); + draft.sleeperSpacing = railType.getSleeperSpacing(); + draft.trackCount = railType.getTrackCount(); + draft.trackSpacing = railType.getTrackSpacing(); + return draft; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java new file mode 100644 index 00000000..09747f30 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -0,0 +1,311 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.alpsbte.alpslib.utils.ChatHelper; +import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailPermissionGuard; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailTypeManager; +import net.buildtheearth.buildteamtools.modules.generator.model.Settings; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.buildtheearth.buildteamtools.utils.MenuItems; +import net.buildtheearth.buildteamtools.utils.heads.HeadColor; +import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; +import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; +import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; +import net.wesjd.anvilgui.AnvilGUI; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.ipvp.canvas.mask.BinaryMask; +import org.ipvp.canvas.mask.Mask; + +import java.util.List; +import java.util.Objects; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +/** + * Editor for creating a custom rail type in-game. The configured draft is + * validated and saved persistently through the {@link RailTypeManager}. + */ +public class RailTypeEditorMenu extends AbstractMenu { + + public static final String EDITOR_INV_NAME = "Create a Rail Type"; + private static final String FILLED_MASK_ROW = "111111111"; + + private static final int NAME_SLOT = 14; + + private static final int TRACK_COUNT_SLOT = 2; + private static final int TRACK_SPACING_SLOT = 11; + private static final int SLEEPER_SPACING_SLOT = 20; + + private static final int RAIL_BLOCK_SLOT = 7; + private static final int BLOCK_BELOW_SLOT = 16; + private static final int SLEEPER_BLOCK_SLOT = 25; + + private static final int BACK_ITEM_SLOT = 36; + private static final int SAVE_ITEM_SLOT = 44; + + private final RailTypeDraft draft; + + RailTypeEditorMenu(Player player, RailTypeDraft draft, boolean autoLoad) { + // The parent constructor renders immediately, so load manually after the draft is set. + super(5, EDITOR_INV_NAME, player, false); + + this.draft = draft; + + if (autoLoad) + reloadMenuAsync(); + } + + @Override + protected void setPreviewItems() { + if (getMask() != null) + getMask().apply(getMenu()); + + getMenu().getSlot(NAME_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.NAME_TAG.get()), + yellow("Rail Type Name: ") + white(getDisplayName()), + List.of(gray("Click to change the display name.")) + )); + + createCounter(HeadColor.WHITE, TRACK_COUNT_SLOT, "Track Count", draft.getTrackCount(), + RailType.MIN_TRACK_COUNT, RailType.MAX_TRACK_COUNT, "Tracks"); + createCounter(HeadColor.LIGHT_GRAY, TRACK_SPACING_SLOT, "Track Spacing", draft.getTrackSpacing(), + RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING, "Blocks"); + createCounter(HeadColor.WHITE, SLEEPER_SPACING_SLOT, "Sleeper Spacing", draft.getSleeperSpacing(), + RailType.MIN_SLEEPER_SPACING, RailType.MAX_SLEEPER_SPACING, "Blocks"); + + getMenu().getSlot(RAIL_BLOCK_SLOT).setItem(createBlockItem( + "Rail Block", + draft.getRailBlock(), + "The block the rails are made of." + )); + getMenu().getSlot(BLOCK_BELOW_SLOT).setItem(createBlockItem( + "Block Below the Rails", + draft.getBlockBelow(), + "The block placed between and below the rails." + )); + getMenu().getSlot(SLEEPER_BLOCK_SLOT).setItem(createBlockItem( + "Sleeper Block", + draft.getSleeperBlock(), + "Sleeper Spacing 0 disables sleepers." + )); + + setBackItem(BACK_ITEM_SLOT, new RailTypeMenu(getMenuPlayer(), false)); + getMenu().getSlot(SAVE_ITEM_SLOT).setItem(HeadFactory.head( + HeadTexture.CHECKMARK, + green(draft.getIdentifier() == null ? "Save Rail Type" : "Update Rail Type") + )); + + getMenu().open(getMenuPlayer()); + } + + @Override + protected void setMenuItemsAsync() { + // All editor items are rendered synchronously because they depend only on the local draft state. + } + + @Override + protected void setItemClickEventsAsync() { + getMenu().getSlot(NAME_SLOT).setClickHandler((clickPlayer, clickInformation) -> openNameEditor(clickPlayer)); + + setCounterClickEvents(TRACK_COUNT_SLOT, RailType.MIN_TRACK_COUNT, RailType.MAX_TRACK_COUNT, + draft::getTrackCount, draft::setTrackCount); + setCounterClickEvents(TRACK_SPACING_SLOT, RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING, + draft::getTrackSpacing, draft::setTrackSpacing); + setCounterClickEvents(SLEEPER_SPACING_SLOT, RailType.MIN_SLEEPER_SPACING, RailType.MAX_SLEEPER_SPACING, + draft::getSleeperSpacing, draft::setSleeperSpacing); + + setBlockPickerClickEvents(RAIL_BLOCK_SLOT, RailBlockRole.RAIL_BLOCK); + setBlockPickerClickEvents(BLOCK_BELOW_SLOT, RailBlockRole.BLOCK_BELOW); + setBlockPickerClickEvents(SLEEPER_BLOCK_SLOT, RailBlockRole.SLEEPER_BLOCK); + + getMenu().getSlot(SAVE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> saveRailType(clickPlayer)); + } + + @Override + protected Mask getMask() { + return BinaryMask.builder(getMenu()) + .item(MenuItems.ITEM_BACKGROUND) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .build(); + } + + private void openNameEditor(Player clickPlayer) { + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new AnvilGUI.Builder() + .onClick((slot, stateSnapshot) -> { + if (slot != AnvilGUI.Slot.OUTPUT) + return List.of(); + + String displayName = stateSnapshot.getText().trim(); + + if (displayName.isEmpty()) { + stateSnapshot.getPlayer().sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Rail type name cannot be empty." + ))); + return List.of(AnvilGUI.ResponseAction.replaceInputText(getDisplayName())); + } + + draft.setDisplayName(displayName); + playSound(stateSnapshot.getPlayer(), Sound.UI_BUTTON_CLICK); + stateSnapshot.getPlayer().sendMessage(ChatHelper.getStandardComponent( + true, + "Rail type name set to '%s'.", + displayName + )); + return List.of( + AnvilGUI.ResponseAction.close(), + AnvilGUI.ResponseAction.run(() -> new RailTypeEditorMenu(clickPlayer, draft, true)) + ); + }) + .text(getDisplayName()) + .itemLeft(Item.create(Objects.requireNonNull(XMaterial.NAME_TAG.get()), yellow("Change Rail Type Name"))) + .title(darkGray("Change rail type name")) + .plugin(BuildTeamTools.getInstance()) + .open(clickPlayer); + } + + private void saveRailType(Player clickPlayer) { + String permission = draft.getIdentifier() == null ? Permissions.RAIL_TYPE_CREATE : Permissions.RAIL_TYPE_EDIT; + + if (!RailPermissionGuard.check(clickPlayer, permission)) + return; + + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + RailTypeManager railTypeManager = rail.getRailTypeManager(); + String identifier = draft.getIdentifier() == null ? railTypeManager.getNextCustomIdentifier() : draft.getIdentifier(); + String displayName = draft.getDisplayName() == null + ? "Custom Rail " + identifier.substring("custom-".length()) + : draft.getDisplayName(); + + RailType railType = RailType.createCustom(new RailType.Configuration() + .identifier(identifier) + .displayName(displayName) + .icon(draft.getRailBlock()) + .railBlock(draft.getRailBlock()) + .blocksBelow(List.of(draft.getBlockBelow())) + .sleeperBlock(draft.getSleeperBlock()) + .sleeperSpacing(draft.getSleeperSpacing()) + .trackCount(draft.getTrackCount()) + .trackSpacing(draft.getTrackSpacing())); + + String error = railTypeManager.saveRailType(railType); + + if (error != null) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent(error))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + Settings settings = rail.getPlayerSettings().get(clickPlayer.getUniqueId()); + + if (settings instanceof RailSettings railSettings) + railSettings.setValue(RailFlag.RAIL_TYPE, railType); + + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Saved rail type %s as '%s'.", + displayName, + identifier + )); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new RailTypeMenu(clickPlayer, true); + } + + private void setBlockPickerClickEvents(int slot, RailBlockRole role) { + getMenu().getSlot(slot).setClickHandler((clickPlayer, clickInformation) -> { + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new RailBlockPickerMenu(clickPlayer, draft, role, true); + }); + } + + private void setCounterClickEvents(int slot, int minValue, int maxValue, IntSupplier getter, IntConsumer setter) { + getMenu().getSlot(slot - 1).setClickHandler((clickPlayer, clickInformation) -> { + if (getter.getAsInt() <= minValue) { + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + setter.accept(getter.getAsInt() - 1); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + }); + + getMenu().getSlot(slot + 1).setClickHandler((clickPlayer, clickInformation) -> { + if (getter.getAsInt() >= maxValue) { + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + setter.accept(getter.getAsInt() + 1); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + }); + } + + private ItemStack createBlockItem(String name, XMaterial material, String description) { + Material bukkitMaterial = material.get(); + + if (bukkitMaterial == null) + bukkitMaterial = Objects.requireNonNull(XMaterial.BARRIER.get()); + + return Item.create( + bukkitMaterial, + yellow(name + ": ") + white(RailTypeMenu.formatMaterial(material)), + List.of(gray(description), gray("Click to choose a different block.")) + ); + } + + private String getDisplayName() { + if (draft.getDisplayName() != null) + return draft.getDisplayName(); + + return draft.getIdentifier() == null ? "Custom Rail" : draft.getIdentifier(); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); + } + + private static String darkGray(String text) { + return RailMenuText.color(NamedTextColor.DARK_GRAY, text); + } + + private static String gray(String text) { + return RailMenuText.color(NamedTextColor.GRAY, text); + } + + private static String white(String text) { + return RailMenuText.color(NamedTextColor.WHITE, text); + } + + private static String yellow(String text) { + return RailMenuText.color(NamedTextColor.YELLOW, text); + } + + private static String green(String text) { + return RailMenuText.color(NamedTextColor.GREEN, text); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index 8bb5f6ad..d35483f6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -1,48 +1,339 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; +import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailPermissionGuard; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; import net.buildtheearth.buildteamtools.modules.generator.menu.GeneratorMenu; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; +import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; +import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; import net.buildtheearth.buildteamtools.utils.menus.NameListMenu; +import net.kyori.adventure.text.format.NamedTextColor; import org.apache.commons.lang3.tuple.MutablePair; +import org.bukkit.Material; import org.bukkit.Sound; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; import org.bukkit.inventory.ItemStack; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Objects; public class RailTypeMenu extends NameListMenu { public static final String RAIL_TYPE_INV_NAME = "Choose a Rail Type"; + // Slots 30-32 belong to the page switcher, so the extra buttons sit next to it. + private static final int CREATE_ITEM_SLOT = 29; + private static final int RELOAD_ITEM_SLOT = 33; + public RailTypeMenu(Player player, boolean autoLoad) { super(player, RAIL_TYPE_INV_NAME, getRailTypes(), new GeneratorMenu(player, false), autoLoad); + preselectCurrentRailType(player); + } + + private void preselectCurrentRailType(Player player) { + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + Settings settings = rail.getPlayerSettings().get(player.getUniqueId()); + + if (!(settings instanceof RailSettings railSettings)) + return; + + Object value = railSettings.getValues().get(RailFlag.RAIL_TYPE); + + if (value instanceof RailType railType && RailType.byString(railType.getIdentifier()) != null) + selectedNames.add(railType.getIdentifier()); } private static @NonNull List> getRailTypes() { List> railTypes = new ArrayList<>(); + Rail rail = GeneratorModule.getInstance().getRail(); - for (RailType railType : RailType.values()) { - railTypes.add(new MutablePair<>( - Item.create(Objects.requireNonNull(railType.getIcon().get()), railType.getDisplayName()), - railType.getIdentifier() - )); - } + if (rail == null) + return railTypes; + + for (RailType railType : rail.getRailTypeManager().getRailTypes()) + railTypes.add(new MutablePair<>(createRailTypeItem(railType), railType.getIdentifier())); return railTypes; } + private static ItemStack createRailTypeItem(RailType railType) { + Material icon = railType.getIcon().get(); + + if (icon == null) + icon = Objects.requireNonNull(XMaterial.RAIL.get()); + + List lore = new ArrayList<>(); + lore.add(gray("Rail Block: ") + white(formatMaterial(railType.getRailBlock()))); + lore.add(gray("Blocks Below: ") + formatMaterials(railType.getBlocksBelow())); + + if (railType.hasSleepers()) + lore.add(gray("Sleepers: ") + + white(formatMaterial(railType.getSleeperBlock())) + + gray(" every ") + + white(String.valueOf(railType.getSleeperSpacing())) + + gray(" blocks")); + else + lore.add(gray("Sleepers: ") + white("None")); + + if (railType.getTrackCount() > 1) + lore.add(gray("Tracks: ") + + white(String.valueOf(railType.getTrackCount())) + + gray(" with spacing ") + + white(String.valueOf(railType.getTrackSpacing()))); + else + lore.add(gray("Tracks: ") + white("1")); + + if (railType.isBuiltIn()) + lore.add(darkGray("Right-Click to create an editable copy")); + else + lore.add(darkGray("Right-Click to edit ") + gray("- ") + darkGray("Shift+Right-Click to delete")); + + return Item.create(icon, yellow(railType.getDisplayName()), lore); + } + + static String formatMaterials(List materials) { + List names = new ArrayList<>(); + + for (XMaterial material : materials) + names.add(white(formatMaterial(material))); + + return String.join(gray(", "), names); + } + + static String formatMaterial(@Nullable XMaterial material) { + if (material == null) + return "None"; + + String[] words = material.name().toLowerCase(Locale.ROOT).split("_"); + List capitalizedWords = new ArrayList<>(); + + for (String word : words) + capitalizedWords.add(word.isEmpty() ? word : Character.toUpperCase(word.charAt(0)) + word.substring(1)); + + return String.join(" ", capitalizedWords); + } + + @Override + protected void setPreviewItems() { + super.setPreviewItems(); + + setRailTypePageItems(); + + getMenu().getSlot(CREATE_ITEM_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.NETHER_STAR.get()), + green("Create a Rail Type"), + List.of(gray("Configure and save a custom rail type.")) + )); + + getMenu().getSlot(RELOAD_ITEM_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.CLOCK.get()), + yellow("Reload Rail Types"), + List.of( + gray("Re-reads rail-types.yml from disk,"), + gray("so you can test changes without a restart.") + ) + )); + } + + @Override + protected void setPaginatedPreviewItems(@NonNull List source) { + List> pagItems = getPageItems(source); + int slot = 0; + + // Render the selection glow on a copy so deselected items lose their glow again. + for (MutablePair item : pagItems) { + ItemStack displayedItem = item.getLeft(); + + if (selectedNames.contains(item.getRight())) + displayedItem = new Item(displayedItem.clone()) + .setAmount(1) + .addEnchantment(Enchantment.LUCK_OF_THE_SEA, 1) + .hideEnchantments(true) + .build(); + + getMenu().getSlot(slot).setItem(displayedItem); + slot++; + } + } + + @Override + protected void setPaginatedItemClickEventsAsync(@NonNull List source) { + List> pagItems = getPageItems(source); + int slot = 0; + + // Rail types are mutually exclusive, so selecting one deselects the previous one. + for (MutablePair item : pagItems) { + final int _slot = slot; + getMenu().getSlot(_slot).setClickHandler((clickPlayer, clickInformation) -> { + String type = item.getRight().toLowerCase(); + + if (clickInformation.getClickType() == ClickType.SHIFT_RIGHT) { + deleteCustomRailType(clickPlayer, type); + return; + } + + if (clickInformation.getClickType() == ClickType.RIGHT) { + openEditorForRailType(clickPlayer, type); + return; + } + + if (selectedNames.contains(type)) { + selectedNames.remove(type); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Deselected rail type '%s'.", item.getRight())); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + } else { + selectedNames.clear(); + selectedNames.add(type); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Selected rail type '%s'.", item.getRight())); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + } + + reloadMenuAsync(); + }); + slot++; + } + } + + private List> getPageItems(List source) { + List> items = new ArrayList<>(); + + for (Object item : source) { + if (item instanceof MutablePair pair + && pair.getLeft() instanceof ItemStack itemStack + && pair.getRight() instanceof String identifier) + items.add(new MutablePair<>(itemStack, identifier)); + } + + return items; + } + + private void openEditorForRailType(Player clickPlayer, String identifier) { + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + RailType railType = rail.getRailTypeManager().byString(identifier); + + if (railType == null) { + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + String permission = railType.isBuiltIn() ? Permissions.RAIL_TYPE_CREATE : Permissions.RAIL_TYPE_EDIT; + + if (!RailPermissionGuard.check(clickPlayer, permission)) + return; + + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + railType.isBuiltIn() + ? "Opening an editable copy of rail type '%s'." + : "Editing rail type '%s'.", + railType.getIdentifier() + )); + + // Built-in types cannot be overwritten, so editing one saves an editable copy instead. + new RailTypeEditorMenu(clickPlayer, RailTypeDraft.from(railType, !railType.isBuiltIn()), true); + } + + private void deleteCustomRailType(Player clickPlayer, String identifier) { + if (!RailPermissionGuard.check(clickPlayer, Permissions.RAIL_TYPE_DELETE)) + return; + + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + RailType railType = rail.getRailTypeManager().byString(identifier); + + if (railType == null) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "This rail type no longer exists." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + if (railType.isBuiltIn()) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Built-in rail types cannot be deleted. Right-click to create an editable copy." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + String error = rail.getRailTypeManager().deleteRailType(railType.getIdentifier()); + + if (error != null) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent(error))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + selectedNames.remove(railType.getIdentifier()); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Deleted rail type '%s'.", railType.getIdentifier())); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new RailTypeMenu(clickPlayer, true); + } + @Override protected void setItemClickEventsAsync() { super.setItemClickEventsAsync(); + setRailTypePageClickEvents(); + + getMenu().getSlot(CREATE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + if (!RailPermissionGuard.check(clickPlayer, Permissions.RAIL_TYPE_CREATE)) + return; + + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Creating a new rail type.")); + + new RailTypeEditorMenu(clickPlayer, new RailTypeDraft(), true); + }); + + getMenu().getSlot(RELOAD_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + rail.getRailTypeManager().reload(); + + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Reloaded %s rail types from rail-types.yml.", + rail.getRailTypeManager().getRailTypes().size() + )); + + new RailTypeMenu(clickPlayer, true); + }); + if (canProceed()) getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { Settings settings = GeneratorModule.getInstance().getRail().getPlayerSettings().get(clickPlayer.getUniqueId()); @@ -53,9 +344,105 @@ protected void setItemClickEventsAsync() { railSettings.setValue(RailFlag.RAIL_TYPE, selectedNames.getFirst()); clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); GeneratorModule.getInstance().getRail().generate(clickPlayer); }); } + + private void setRailTypePageItems() { + getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT - 1).setItem(createPreviousPageItem()); + + getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.PAPER.get()), + yellow("Current Page ") + gray("- ") + white(String.valueOf(getPage())), + List.of(gray("Use the arrows next to this item to browse rail types.")) + )); + + getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT + 1).setItem(createNextPageItem()); + } + + private ItemStack createPreviousPageItem() { + if (!hasPreviousPage()) + return Item.create( + Objects.requireNonNull(XMaterial.GRAY_STAINED_GLASS_PANE.get()), + gray("No Previous Page"), + List.of(darkGray("You are already on the first page.")) + ); + + return HeadFactory.head( + HeadTexture.WHITE_ARROW_LEFT, + yellow("Previous Page ") + gray("- ") + white(String.valueOf(getPage() - 1)), + new ArrayList<>(List.of(gray("Click to show earlier rail types."))) + ); + } + + private ItemStack createNextPageItem() { + if (!hasNextPage()) + return Item.create( + Objects.requireNonNull(XMaterial.GRAY_STAINED_GLASS_PANE.get()), + gray("No Next Page"), + List.of(darkGray("There are no more rail types.")) + ); + + return HeadFactory.head( + HeadTexture.WHITE_ARROW_RIGHT, + yellow("Next Page ") + gray("- ") + white(String.valueOf(getPage() + 1)), + new ArrayList<>(List.of(gray("Click to show more rail types."))) + ); + } + + private void setRailTypePageClickEvents() { + getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT - 1).setClickHandler((clickPlayer, clickInformation) -> { + if (!hasPreviousPage()) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "You are already on the first rail type page." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + previousPage(); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Opened rail type page %s.", getPage())); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + }); + + getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT + 1).setClickHandler((clickPlayer, clickInformation) -> { + if (!hasNextPage()) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "There is no next rail type page." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + nextPage(); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Opened rail type page %s.", getPage())); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + }); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); + } + + private static String darkGray(String text) { + return RailMenuText.color(NamedTextColor.DARK_GRAY, text); + } + + private static String gray(String text) { + return RailMenuText.color(NamedTextColor.GRAY, text); + } + + private static String white(String text) { + return RailMenuText.color(NamedTextColor.WHITE, text); + } + + private static String yellow(String text) { + return RailMenuText.color(NamedTextColor.YELLOW, text); + } + + private static String green(String text) { + return RailMenuText.color(NamedTextColor.GREEN, text); + } } 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 9cba7e1b..3ea1cbf0 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 @@ -1,5 +1,6 @@ package net.buildtheearth.buildteamtools.modules.generator.menu; +import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; import net.buildtheearth.buildteamtools.modules.common.CommonModule; @@ -9,6 +10,7 @@ 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.RailPermissionGuard; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; import net.buildtheearth.buildteamtools.modules.generator.components.rail.menu.RailTypeMenu; import net.buildtheearth.buildteamtools.modules.generator.components.road.Road; @@ -20,9 +22,11 @@ import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorCollections; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.utils.ListUtil; import net.buildtheearth.buildteamtools.utils.MenuItems; import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Sound; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; @@ -37,6 +41,10 @@ public class GeneratorMenu extends AbstractMenu { public static final String GENERATOR_INV_NAME = "What do you want to generate?"; + private static final String DESCRIPTION_LABEL = color(NamedTextColor.YELLOW, "Description:"); + private static final String FEATURES_LABEL = color(NamedTextColor.YELLOW, "Features:"); + private static final String LEFT_CLICK_TO_GENERATE = color(NamedTextColor.DARK_GRAY, "Left-click to generate"); + private static final String RIGHT_CLICK_FOR_TUTORIAL = color(NamedTextColor.DARK_GRAY, "Right-click for Tutorial"); public static final int HOUSE_ITEM_SLOT = 9; public static final int ROAD_ITEM_SLOT = 11; @@ -52,113 +60,135 @@ public GeneratorMenu(Player player, boolean autoLoad) { protected void setPreviewItems() { ArrayList houseLore = ListUtil.createList( "", - "§eDescription:", + DESCRIPTION_LABEL, "Generate basic building shells", "with multiple floors, windows and roofs", "", - "§eFeatures:", + FEATURES_LABEL, "- " + RoofType.values().length + " Roof Types", "- Custom Wall, Base and Roof Color", "- Custom Floor and Window Sizes", "", - "§8Left-click to generate", - "§8Right-click for Tutorial" + LEFT_CLICK_TO_GENERATE, + RIGHT_CLICK_FOR_TUTORIAL ); - ItemStack houseItem = Item.create(Objects.requireNonNull(XMaterial.BIRCH_DOOR.get()), "§cGenerate House", houseLore); + ItemStack houseItem = Item.create( + Objects.requireNonNull(XMaterial.BIRCH_DOOR.get()), + color(NamedTextColor.RED, "Generate House"), + houseLore + ); getMenu().getSlot(HOUSE_ITEM_SLOT).setItem(houseItem); ArrayList roadLore = ListUtil.createList( "", - "§eDescription:", + DESCRIPTION_LABEL, "Generate roads and highways", "with multiple lanes and sidewalks", "", - "§eFeatures:", + FEATURES_LABEL, "- Custom Road Width and Color", "- Custom Sidewalk Width and Color", "- Custom Lane Count", "", - "§8Left-click to generate", - "§8Right-click for Tutorial" + LEFT_CLICK_TO_GENERATE, + RIGHT_CLICK_FOR_TUTORIAL ); - ItemStack roadItem = Item.create(Objects.requireNonNull(XMaterial.SMOOTH_STONE_SLAB.get()), "§bGenerate Road", roadLore); + ItemStack roadItem = Item.create( + Objects.requireNonNull(XMaterial.SMOOTH_STONE_SLAB.get()), + color(NamedTextColor.AQUA, "Generate Road"), + roadLore + ); getMenu().getSlot(ROAD_ITEM_SLOT).setItem(roadItem); ArrayList railwayLore = ListUtil.createList( "", - "§eDescription:", + DESCRIPTION_LABEL, "Generate a predefined railway", "from your active WorldEdit selection", "", - "§eSupported selections:", + color(NamedTextColor.YELLOW, "Supported selections:"), "- Cuboid", "- Polygonal", "- Convex", "", - "§eFeatures:", + FEATURES_LABEL, "- Rail Type selection", "- Straight sections", "- Direction changes", "- Automatic side block orientation", "", - "§8Left-click to generate", - "§8Right-click for Tutorial" + LEFT_CLICK_TO_GENERATE, + RIGHT_CLICK_FOR_TUTORIAL ); - ItemStack railwayItem = Item.create(Objects.requireNonNull(XMaterial.RAIL.get()), "§9Generate Railway", railwayLore); + ItemStack railwayItem = Item.create( + Objects.requireNonNull(XMaterial.RAIL.get()), + color(NamedTextColor.BLUE, "Generate Railway"), + railwayLore + ); getMenu().getSlot(RAIL_ITEM_SLOT).setItem(railwayItem); if (!CommonModule.getInstance().getDependencyComponent().isSchematicBrushEnabled()) { ArrayList treeLore = ListUtil.createList( "", - "§cPlugin §eSchematicBrush §cis not installed", - "§cTree Generator is disabled", + color(NamedTextColor.RED, "Plugin ") + + color(NamedTextColor.YELLOW, "SchematicBrush ") + + color(NamedTextColor.RED, "is not installed"), + color(NamedTextColor.RED, "Tree Generator is disabled"), "", - "§8Leftclick for Installation Instructions" + color(NamedTextColor.DARK_GRAY, "Leftclick for Installation Instructions") ); - ItemStack treeItem = Item.create(Objects.requireNonNull(XMaterial.OAK_SAPLING.get()), "§aGenerate Tree & Forest §c(DISABLED)", treeLore); + ItemStack treeItem = createTreeItem(color(NamedTextColor.RED, " (DISABLED)"), treeLore); getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } else if (!GeneratorCollections.hasUpdatedGeneratorCollections(getMenuPlayer())) { ArrayList treeLore = ListUtil.createList( "", - "§cThe §eTree Pack " + Tree.TREE_PACK_VERSION + " §cis not installed", - "§cTree Generator is disabled", + color(NamedTextColor.RED, "The ") + + color(NamedTextColor.YELLOW, "Tree Pack " + Tree.TREE_PACK_VERSION + " ") + + color(NamedTextColor.RED, "is not installed"), + color(NamedTextColor.RED, "Tree Generator is disabled"), "", - "§8Leftclick for Installation Instructions" + color(NamedTextColor.DARK_GRAY, "Leftclick for Installation Instructions") ); - ItemStack treeItem = Item.create(Objects.requireNonNull(XMaterial.OAK_SAPLING.get()), "§aGenerate Tree & Forest §c(DISABLED)", treeLore); + ItemStack treeItem = createTreeItem(color(NamedTextColor.RED, " (DISABLED)"), treeLore); getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } else { ArrayList treeLore = ListUtil.createList( "", - "§eDescription:", + DESCRIPTION_LABEL, "Generate trees from a set of", "hundreds of different types", "", - "§eFeatures:", + FEATURES_LABEL, "- Custom Tree Type", "", - "§8Left-click to generate", - "§8Right-click for Tutorial" + LEFT_CLICK_TO_GENERATE, + RIGHT_CLICK_FOR_TUTORIAL ); - ItemStack treeItem = Item.create(Objects.requireNonNull(XMaterial.OAK_SAPLING.get()), "§aGenerate Tree & Forest", treeLore); + ItemStack treeItem = createTreeItem("", treeLore); getMenu().getSlot(TREE_ITEM_SLOT).setItem(treeItem); } ArrayList fieldLore = ListUtil.createList( "", - "§cThis §eGenerator §cis currently broken", - "§cField Generator is disabled", + color(NamedTextColor.RED, "This ") + + color(NamedTextColor.YELLOW, "Generator ") + + color(NamedTextColor.RED, "is currently broken"), + color(NamedTextColor.RED, "Field Generator is disabled"), "", - "§8If you want to help fixing ask on Dev Hub!" + color(NamedTextColor.DARK_GRAY, "If you want to help fixing ask on Dev Hub!") ); - ItemStack fieldItem = Item.create(Objects.requireNonNull(XMaterial.WHEAT.get()), "§6Generate Field §c(DISABLED)", fieldLore); + ItemStack fieldItem = Item.create( + Objects.requireNonNull(XMaterial.WHEAT.get()), + color(NamedTextColor.GOLD, "Generate Field ") + color(NamedTextColor.RED, "(DISABLED)"), + fieldLore + ); getMenu().getSlot(FIELD_ITEM_SLOT).setItem(fieldItem); super.setPreviewItems(); @@ -171,77 +201,98 @@ protected void setMenuItemsAsync() { @Override protected void setItemClickEventsAsync() { - getMenu().getSlot(HOUSE_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { - if (clickInformation.getClickType().equals(ClickType.RIGHT)) { - sendMoreInformation(clickPlayer, GeneratorType.HOUSE); - return; - } - - House house = GeneratorModule.getInstance().getHouse(); - house.getPlayerSettings().put(clickPlayer.getUniqueId(), new HouseSettings(clickPlayer)); - - if (!house.checkForPlayer(clickPlayer)) - return; - - clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - new WallColorMenu(clickPlayer, true); - })); - - getMenu().getSlot(ROAD_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { - if (clickInformation.getClickType().equals(ClickType.RIGHT)) { - sendMoreInformation(clickPlayer, GeneratorType.ROAD); - return; - } - - Road road = GeneratorModule.getInstance().getRoad(); - road.getPlayerSettings().put(clickPlayer.getUniqueId(), new RoadSettings(clickPlayer)); - - if (!road.checkForPlayer(clickPlayer)) - return; - - clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - new RoadColorMenu(clickPlayer, true); - })); - - getMenu().getSlot(RAIL_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { - if (clickInformation.getClickType().equals(ClickType.RIGHT)) { - sendMoreInformation(clickPlayer, GeneratorType.RAIL); - return; - } - - Rail rail = GeneratorModule.getInstance().getRail(); - rail.getPlayerSettings().put(clickPlayer.getUniqueId(), new RailSettings(clickPlayer)); - - if (!rail.checkForPlayer(clickPlayer)) - return; - - clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - new RailTypeMenu(clickPlayer, true); - })); - - getMenu().getSlot(TREE_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { - if (clickInformation.getClickType().equals(ClickType.RIGHT)) { - sendMoreInformation(clickPlayer, GeneratorType.TREE); - return; - } - - Tree tree = GeneratorModule.getInstance().getTree(); - tree.getPlayerSettings().put(clickPlayer.getUniqueId(), new TreeSettings(clickPlayer)); - - if (!tree.checkForPlayer(clickPlayer)) - return; - - clickPlayer.closeInventory(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); - new TreeTypeMenu(clickPlayer, true); - })); - - getMenu().getSlot(FIELD_ITEM_SLOT).setClickHandler(((clickPlayer, clickInformation) -> { - sendMoreInformation(clickPlayer, GeneratorType.FIELD); - })); + getMenu().getSlot(HOUSE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + handleHouseClick(clickPlayer, clickInformation.getClickType())); + getMenu().getSlot(ROAD_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + handleRoadClick(clickPlayer, clickInformation.getClickType())); + getMenu().getSlot(RAIL_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + handleRailClick(clickPlayer, clickInformation.getClickType())); + getMenu().getSlot(TREE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + handleTreeClick(clickPlayer, clickInformation.getClickType())); + getMenu().getSlot(FIELD_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + sendMoreInformation(clickPlayer, GeneratorType.FIELD)); + } + + private void handleHouseClick(Player player, ClickType clickType) { + if (showTutorialForRightClick(player, clickType, GeneratorType.HOUSE)) + return; + + House house = GeneratorModule.getInstance().getHouse(); + house.getPlayerSettings().put(player.getUniqueId(), new HouseSettings(player)); + + if (!house.checkForPlayer(player)) + return; + + closeMenuWithClickSound(player); + new WallColorMenu(player, true); + } + + private void handleRoadClick(Player player, ClickType clickType) { + if (showTutorialForRightClick(player, clickType, GeneratorType.ROAD)) + return; + + Road road = GeneratorModule.getInstance().getRoad(); + road.getPlayerSettings().put(player.getUniqueId(), new RoadSettings(player)); + + if (!road.checkForPlayer(player)) + return; + + closeMenuWithClickSound(player); + new RoadColorMenu(player, true); + } + + private void handleRailClick(Player player, ClickType clickType) { + if (showTutorialForRightClick(player, clickType, GeneratorType.RAIL) + || !RailPermissionGuard.check(player, Permissions.RAIL_TYPE_MENU)) + return; + + Rail rail = GeneratorModule.getInstance().getRail(); + rail.getPlayerSettings().put(player.getUniqueId(), new RailSettings(player)); + + if (!rail.checkForPlayer(player)) + return; + + closeMenuWithClickSound(player); + new RailTypeMenu(player, true); + } + + private void handleTreeClick(Player player, ClickType clickType) { + if (showTutorialForRightClick(player, clickType, GeneratorType.TREE)) + return; + + Tree tree = GeneratorModule.getInstance().getTree(); + tree.getPlayerSettings().put(player.getUniqueId(), new TreeSettings(player)); + + if (!tree.checkForPlayer(player)) + return; + + closeMenuWithClickSound(player); + new TreeTypeMenu(player, true); + } + + private boolean showTutorialForRightClick(Player player, ClickType clickType, GeneratorType generatorType) { + if (clickType != ClickType.RIGHT) + return false; + + sendMoreInformation(player, generatorType); + return true; + } + + private void closeMenuWithClickSound(Player player) { + player.closeInventory(); + player.playSound(player, Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); + } + + private ItemStack createTreeItem(String suffix, ArrayList lore) { + return Item.create( + Objects.requireNonNull(XMaterial.OAK_SAPLING.get()), + color(NamedTextColor.GREEN, "Generate Tree & Forest") + suffix, + lore + ); + } + + private static String color(NamedTextColor color, String text) { + return ChatHelper.getColorizedString(color, text, false); } private void sendMoreInformation(@NonNull Player clickPlayer, @NonNull GeneratorType generator) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java b/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java index 78d2b3e7..e1e8f16f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/network/model/Permissions.java @@ -15,6 +15,12 @@ public class Permissions { public static final String GENERATOR_USE = "btt.generator.use"; + public static final String RAIL_GENERATOR_USE = "btt.generator.rail.use"; + public static final String RAIL_TYPE_MENU = "btt.generator.rail.menu"; + public static final String RAIL_TYPE_CREATE = "btt.generator.rail.create"; + public static final String RAIL_TYPE_EDIT = "btt.generator.rail.edit"; + public static final String RAIL_TYPE_DELETE = "btt.generator.rail.delete"; + public static final String RAIL_MULTIPLE_TRACKS = "btt.generator.rail.multiple"; public static final String BLOCK_PALETTE_EDIT = "btt.bp.edit"; diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index d6a781e0..ba6d4843 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -57,6 +57,9 @@ permissions: default: op children: btt.generator.use: true + btt.generator.rail.use: true + btt.generator.rail.menu: true + btt.generator.rail.multiple: true btt.permpack.admin: description: Contains all permissions for admins default: op @@ -76,6 +79,28 @@ permissions: btt.warp.group.delete: true btt.notify.update: true btt.bp.edit: true + btt.generator.rail.create: true + btt.generator.rail.edit: true + btt.generator.rail.delete: true + + btt.generator.rail.use: + description: Allows players to use the rail generator + default: op + btt.generator.rail.menu: + description: Allows players to open the rail type menu + default: op + btt.generator.rail.create: + description: Allows players to create custom rail types + default: op + btt.generator.rail.edit: + description: Allows players to edit custom rail types + default: op + btt.generator.rail.delete: + description: Allows players to delete custom rail types + default: op + btt.generator.rail.multiple: + description: Allows players to generate multiple parallel tracks + default: op btt.bp.use: description: Allows players to use /blockpalette From ad7dcf4721dc5d06ddef60728606683b950aeee6 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 11:46:22 +0200 Subject: [PATCH 2/8] fix(rail): validate and orient generated track layouts --- .../generator/components/rail/Rail.java | 2 + .../components/rail/RailSettings.java | 1 + .../rail/{ => configuration}/RailType.java | 3 +- .../{ => configuration}/RailTypeManager.java | 2 +- .../rail/{ => generation}/PositionKey.java | 2 +- .../{ => generation}/RailBlockBuilder.java | 164 ++++++++++++++++-- .../rail/{ => generation}/RailColumnKey.java | 2 +- .../rail/generation/RailConnections.java | 31 ++++ .../{ => generation}/RailLanePathBuilder.java | 11 +- .../rail/{ => generation}/RailLimits.java | 2 +- .../generation/RailPathOverlapValidator.java | 54 ++++++ .../RailPreparationProgress.java | 2 +- .../rail/{ => generation}/RailScripts.java | 21 ++- .../rail/{ => generation}/RailSideBlock.java | 2 +- .../{ => generation}/RailSidePlacement.java | 2 +- .../rail/{ => generation}/RailStep.java | 2 +- .../{ => generation}/RailTerrainResolver.java | 2 +- .../components/rail/menu/RailTypeDraft.java | 2 +- .../rail/menu/RailTypeEditorMenu.java | 4 +- .../components/rail/menu/RailTypeMenu.java | 2 +- .../modules/generator/model/FlagType.java | 2 +- .../modules/generator/model/Settings.java | 2 +- 22 files changed, 282 insertions(+), 35 deletions(-) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => configuration}/RailType.java (98%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => configuration}/RailTypeManager.java (99%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/PositionKey.java (95%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailBlockBuilder.java (68%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailColumnKey.java (89%) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailConnections.java rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailLanePathBuilder.java (96%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailLimits.java (99%) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailPreparationProgress.java (99%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailScripts.java (94%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailSideBlock.java (97%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailSidePlacement.java (88%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailStep.java (81%) rename src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/{ => generation}/RailTerrainResolver.java (99%) 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 badec2bd..1227d88e 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 @@ -9,6 +9,8 @@ import lombok.Getter; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailTypeManager; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.generation.RailScripts; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorType; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java index 09629afa..665681e6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java @@ -1,5 +1,6 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; import org.bukkit.entity.Player; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java similarity index 98% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java index 5e5e9b39..1d8af831 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -1,8 +1,9 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration; import com.cryptomorin.xseries.XMaterial; import lombok.Getter; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; import org.bukkit.Material; import org.jspecify.annotations.Nullable; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java similarity index 99% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index 402530c3..88e94de5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration; import com.alpsbte.alpslib.utils.ChatHelper; import com.cryptomorin.xseries.XMaterial; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/PositionKey.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/PositionKey.java similarity index 95% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/PositionKey.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/PositionKey.java index c2dbd955..196433c0 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/PositionKey.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/PositionKey.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import org.bukkit.util.Vector; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java similarity index 68% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java index 78e7a878..244a4eab 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java @@ -1,13 +1,16 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.alpsbte.alpslib.utils.GeneratorUtils; import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; +import com.fastasyncworldedit.core.registry.state.PropertyKey; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import org.bukkit.util.Vector; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.HashSet; @@ -20,6 +23,7 @@ final class RailBlockBuilder { private static final Direction DEFAULT_FACING = Direction.EAST; private static final String FACING_PROPERTY = "facing"; + private static final String SHAPE_PROPERTY = "shape"; // Sleeper ends stick out one block beyond the rails, which sit at offset 1 from the track center. private static final int SLEEPER_SIDE_OFFSET = 2; @@ -67,7 +71,7 @@ Map build(List> railCenterPaths) { int processedSideBlocks = 0; for (RailSideBlock sideBlock : sideBlocks.values()) { - railBlocks.put(sideBlock.key(), createRailBlockState(resolveSideBlockFacing(sideBlock, sideBlocks))); + railBlocks.put(sideBlock.key(), createRailBlockState(sideBlock, sideBlocks)); processedSideBlocks++; preparationProgress.update(preparationProgress.scale(processedSideBlocks, sideBlocks.size(), 86L, 89L)); } @@ -213,20 +217,28 @@ private void addSideBlock( } private Direction resolveSideBlockFacing(RailSideBlock sideBlock, Map sideBlocks) { - PositionKey key = sideBlock.key(); - boolean east = sideBlocks.containsKey(PositionKey.of(key.x() + 1, key.y(), key.z())); - boolean west = sideBlocks.containsKey(PositionKey.of(key.x() - 1, key.y(), key.z())); - boolean south = sideBlocks.containsKey(PositionKey.of(key.x(), key.y(), key.z() + 1)); - boolean north = sideBlocks.containsKey(PositionKey.of(key.x(), key.y(), key.z() - 1)); - int xConnections = (east ? 1 : 0) + (west ? 1 : 0); - int zConnections = (south ? 1 : 0) + (north ? 1 : 0); + RailConnections connections = getConnections(sideBlock.key(), sideBlocks); + int xConnections = (connections.east() ? 1 : 0) + (connections.west() ? 1 : 0); + int zConnections = (connections.south() ? 1 : 0) + (connections.north() ? 1 : 0); Direction preferredFacing = sideBlock.getPreferredFacing(); if (xConnections > zConnections) - return resolveAxisFacing(preferredFacing, Direction.EAST, Direction.WEST, east, west); + return resolveAxisFacing( + preferredFacing, + Direction.EAST, + Direction.WEST, + connections.east(), + connections.west() + ); if (zConnections > xConnections) - return resolveAxisFacing(preferredFacing, Direction.SOUTH, Direction.NORTH, south, north); + return resolveAxisFacing( + preferredFacing, + Direction.SOUTH, + Direction.NORTH, + connections.south(), + connections.north() + ); return preferredFacing; } @@ -300,13 +312,141 @@ private BlockState createCenterBlockState(Vector position, boolean sleeperPoint) return GeneratorUtils.getBlockState(blocksBelow.get(index)); } - private BlockState createRailBlockState(Direction direction) { + private BlockState createRailBlockState( + RailSideBlock sideBlock, + Map sideBlocks + ) { + Direction direction = resolveSideBlockFacing(sideBlock, sideBlocks); + + if (railBlockType.getPropertyMap().containsKey(SHAPE_PROPERTY)) + return createRailShapeBlockState(direction, getConnections(sideBlock.key(), sideBlocks)); + if (!railBlockType.getPropertyMap().containsKey(FACING_PROPERTY)) return railBlockType.getDefaultState(); return GeneratorUtils.getBlockStateWithFacing(railBlockType, direction); } + private BlockState createRailShapeBlockState(Direction preferredDirection, RailConnections connections) { + String shape = resolveRailShape(preferredDirection, connections); + + try { + return railBlockType.getDefaultState().with( + PropertyKey.SHAPE, + shape + ); + } catch (IllegalArgumentException exception) { + return railBlockType.getDefaultState().with( + PropertyKey.SHAPE, + getStraightRailShape(preferredDirection) + ); + } + } + + private String resolveRailShape(Direction preferredDirection, RailConnections connections) { + String ascendingShape = getAscendingRailShape(connections); + + if (ascendingShape != null) + return ascendingShape; + + String cornerShape = getCornerRailShape(connections); + + if (cornerShape != null) + return cornerShape; + + return getAxisRailShape(preferredDirection, connections); + } + + private @Nullable String getAscendingRailShape(RailConnections connections) { + if (isRaised(connections.eastHeightDifference())) + return "ascending_east"; + + if (isRaised(connections.westHeightDifference())) + return "ascending_west"; + + if (isRaised(connections.southHeightDifference())) + return "ascending_south"; + + if (isRaised(connections.northHeightDifference())) + return "ascending_north"; + + return null; + } + + private @Nullable String getCornerRailShape(RailConnections connections) { + if (connections.connectionCount() != 2) + return null; + + if (connections.north() && connections.east()) + return "north_east"; + + if (connections.north() && connections.west()) + return "north_west"; + + if (connections.south() && connections.east()) + return "south_east"; + + return connections.south() && connections.west() ? "south_west" : null; + } + + private String getAxisRailShape(Direction preferredDirection, RailConnections connections) { + int xConnections = (connections.east() ? 1 : 0) + (connections.west() ? 1 : 0); + int zConnections = (connections.south() ? 1 : 0) + (connections.north() ? 1 : 0); + + if (xConnections > zConnections) + return "east_west"; + + if (zConnections > xConnections) + return "north_south"; + + return getStraightRailShape(preferredDirection); + } + + private String getStraightRailShape(Direction direction) { + return direction == Direction.NORTH || direction == Direction.SOUTH ? "north_south" : "east_west"; + } + + private boolean isRaised(@Nullable Integer heightDifference) { + return heightDifference != null && heightDifference > 0; + } + + private RailConnections getConnections( + PositionKey key, + Map sideBlocks + ) { + return new RailConnections( + getNeighborHeightDifference(key, 1, 0, sideBlocks), + getNeighborHeightDifference(key, -1, 0, sideBlocks), + getNeighborHeightDifference(key, 0, 1, sideBlocks), + getNeighborHeightDifference(key, 0, -1, sideBlocks) + ); + } + + private @Nullable Integer getNeighborHeightDifference( + PositionKey key, + int xOffset, + int zOffset, + Map sideBlocks + ) { + for (int heightDifference = 0; heightDifference <= 1; heightDifference++) { + if (sideBlocks.containsKey(PositionKey.of( + key.x() + xOffset, + key.y() + heightDifference, + key.z() + zOffset + ))) + return heightDifference; + + if (heightDifference > 0 && sideBlocks.containsKey(PositionKey.of( + key.x() + xOffset, + key.y() - heightDifference, + key.z() + zOffset + ))) + return -heightDifference; + } + + return null; + } + private BlockType getRailBlockType(RailType railType) { BlockType blockType = Item.convertXMaterialToWEBlockType(railType.getRailBlock()); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailColumnKey.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailColumnKey.java similarity index 89% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailColumnKey.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailColumnKey.java index ec459ab1..c57a6f97 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailColumnKey.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailColumnKey.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; record RailColumnKey(int x, int z) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailConnections.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailConnections.java new file mode 100644 index 00000000..dcadff9d --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailConnections.java @@ -0,0 +1,31 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; + +import org.jspecify.annotations.Nullable; + +record RailConnections( + @Nullable Integer eastHeightDifference, + @Nullable Integer westHeightDifference, + @Nullable Integer southHeightDifference, + @Nullable Integer northHeightDifference +) { + + boolean east() { + return eastHeightDifference != null; + } + + boolean west() { + return westHeightDifference != null; + } + + boolean south() { + return southHeightDifference != null; + } + + boolean north() { + return northHeightDifference != null; + } + + int connectionCount() { + return (east() ? 1 : 0) + (west() ? 1 : 0) + (south() ? 1 : 0) + (north() ? 1 : 0); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java similarity index 96% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java index bd24386b..02046330 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java @@ -1,7 +1,8 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.alpsbte.alpslib.utils.GeneratorUtils; import org.bukkit.util.Vector; +import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -105,6 +106,10 @@ private List createShiftedControlPoints(List candidatePoints, in normal.multiply(-1); Vector shiftedPoint = getBestShiftedPoint(basePoint, normal, candidatePoints, distance); + + if (shiftedPoint == null) + return Collections.emptyList(); + addIfDifferentFromPrevious(shiftedControlPoints, shiftedPoint); } @@ -142,7 +147,7 @@ private Vector getHorizontalDirection(Vector from, Vector to) { ); } - private Vector getBestShiftedPoint(Vector basePoint, Vector normal, List candidatePoints, int distance) { + private @Nullable Vector getBestShiftedPoint(Vector basePoint, Vector normal, List candidatePoints, int distance) { Vector idealPoint = getIdealShiftedPoint(basePoint, normal, distance); Vector bestPoint = null; double bestDistanceSquared = Double.MAX_VALUE; @@ -164,7 +169,7 @@ private Vector getBestShiftedPoint(Vector basePoint, Vector normal, List double maxCandidateDistanceSquared = Math.max(16D, distance * distance * 2.25D); if (bestPoint == null || bestDistanceSquared > maxCandidateDistanceSquared) - return idealPoint; + return null; return new Vector(bestPoint.getBlockX(), basePoint.getBlockY(), bestPoint.getBlockZ()); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java similarity index 99% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java index e287fecb..88611db8 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLimits.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.utils.io.ConfigPaths; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java new file mode 100644 index 00000000..85d4f753 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java @@ -0,0 +1,54 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; + +import org.bukkit.util.Vector; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +final class RailPathOverlapValidator { + + private static final int MINIMUM_TRACK_CENTER_DISTANCE = 3; + private static final int MAXIMUM_VERTICAL_DIFFERENCE = 1; + + boolean hasOverlap(List> railCenterPaths) { + Set previousLanePositions = new HashSet<>(); + + for (List railCenterPath : railCenterPaths) { + Set currentLanePositions = new HashSet<>(); + + for (Vector center : railCenterPath) { + PositionKey centerPosition = PositionKey.from(center); + + if (!currentLanePositions.add(centerPosition) || isTooCloseToPreviousLane(centerPosition, previousLanePositions)) + return true; + } + + previousLanePositions.addAll(currentLanePositions); + } + + return false; + } + + private boolean isTooCloseToPreviousLane(PositionKey center, Set previousLanePositions) { + for (int xOffset = -2; xOffset <= 2; xOffset++) { + for (int zOffset = -2; zOffset <= 2; zOffset++) { + if (xOffset * xOffset + zOffset * zOffset >= MINIMUM_TRACK_CENTER_DISTANCE * MINIMUM_TRACK_CENTER_DISTANCE) + continue; + + for (int yOffset = -MAXIMUM_VERTICAL_DIFFERENCE; yOffset <= MAXIMUM_VERTICAL_DIFFERENCE; yOffset++) { + PositionKey nearbyPosition = PositionKey.of( + center.x() + xOffset, + center.y() + yOffset, + center.z() + zOffset + ); + + if (previousLanePositions.contains(nearbyPosition)) + return true; + } + } + } + + return false; + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPreparationProgress.java similarity index 99% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPreparationProgress.java index b744170d..ef33ad8a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailPreparationProgress.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPreparationProgress.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.alpsbte.alpslib.utils.ChatHelper; import net.buildtheearth.buildteamtools.BuildTeamTools; 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/generation/RailScripts.java similarity index 94% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailScripts.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java index 64d6172e..b98b7f51 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/generation/RailScripts.java @@ -1,9 +1,13 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.GeneratorUtils; import com.sk89q.worldedit.world.block.BlockState; import net.buildtheearth.buildteamtools.BuildTeamTools; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailPermissionGuard; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.modules.generator.model.GeneratorComponent; import net.buildtheearth.buildteamtools.modules.generator.model.Script; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; @@ -162,14 +166,22 @@ private boolean queueRailGeneration() { if (railCenterPaths.size() < trackCount) { sendRailError( - "Rail Generator could not fit %s parallel tracks with %s blocks of spacing in this selection. " - + "Reduce the track count or spacing, or make the selection longer and less curved.", + "Rail Generator could not create %s parallel tracks with %s blocks of spacing along this path. " + + "Reduce the track count or spacing, or use a less sharp curve.", trackCount, trackSpacing ); return false; } + if (new RailPathOverlapValidator().hasOverlap(railCenterPaths)) { + sendRailError( + "The parallel tracks overlap in this curve. Reduce the track count or spacing, " + + "or make the selected path less sharp." + ); + return false; + } + Map railBlocks = buildRailBlocks(railCenterPaths); preparationProgress.completeStage(RAIL_BLOCK_BUILD_PROGRESS); @@ -415,7 +427,8 @@ private boolean resolveTrackLayout() { return false; } - return trackCount <= 1 || RailPermissionGuard.check(getPlayer(), Permissions.RAIL_MULTIPLE_TRACKS); + return trackCount == RailType.MIN_TRACK_COUNT + || RailPermissionGuard.check(getPlayer(), Permissions.RAIL_MULTIPLE_TRACKS); } private Integer getIntegerSetting(RailFlag flag) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSideBlock.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSideBlock.java similarity index 97% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSideBlock.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSideBlock.java index 67b91f2c..b6c6cc90 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSideBlock.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSideBlock.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.sk89q.worldedit.util.Direction; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSidePlacement.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSidePlacement.java similarity index 88% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSidePlacement.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSidePlacement.java index 20c40538..31fb5486 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSidePlacement.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailSidePlacement.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import com.sk89q.worldedit.util.Direction; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailStep.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java similarity index 81% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailStep.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java index 0de43935..04686e39 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailStep.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; record RailStep(int dx, int dz) { } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTerrainResolver.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailTerrainResolver.java similarity index 99% rename from src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTerrainResolver.java rename to src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailTerrainResolver.java index 2ae5ff29..4b55ff9d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailTerrainResolver.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailTerrainResolver.java @@ -1,4 +1,4 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; import net.buildtheearth.buildteamtools.utils.MenuItems; import org.bukkit.Material; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java index 86b93a43..3bf0ed12 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -3,7 +3,7 @@ import com.cryptomorin.xseries.XMaterial; import lombok.Getter; import lombok.Setter; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import java.util.List; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java index 09747f30..2b197a8a 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -9,8 +9,8 @@ import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailPermissionGuard; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailTypeManager; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailTypeManager; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import net.buildtheearth.buildteamtools.utils.MenuItems; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index d35483f6..76f98f87 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -8,7 +8,7 @@ import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailPermissionGuard; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailSettings; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.modules.generator.menu.GeneratorMenu; import net.buildtheearth.buildteamtools.modules.generator.model.Settings; import net.buildtheearth.buildteamtools.modules.network.model.Permissions; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/FlagType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/FlagType.java index d42f21b7..3914f724 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/FlagType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/FlagType.java @@ -7,7 +7,7 @@ import net.buildtheearth.buildteamtools.modules.generator.components.field.CropStage; import net.buildtheearth.buildteamtools.modules.generator.components.field.CropType; import net.buildtheearth.buildteamtools.modules.generator.components.house.RoofType; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.modules.generator.components.tree.TreeType; import net.buildtheearth.buildteamtools.modules.generator.components.tree.TreeWidth; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/Settings.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/Settings.java index 1b894500..4736006e 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/Settings.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/model/Settings.java @@ -7,7 +7,7 @@ import net.buildtheearth.buildteamtools.modules.generator.components.field.CropStage; import net.buildtheearth.buildteamtools.modules.generator.components.field.CropType; import net.buildtheearth.buildteamtools.modules.generator.components.house.RoofType; -import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.modules.generator.components.tree.TreeWidth; import org.bukkit.block.Block; import org.bukkit.entity.Player; From 8e827c5b2713aead7c3c235fa0f9164a402172b4 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 16:03:04 +0200 Subject: [PATCH 3/8] feat(rail): add configurable overhead wire settings --- .../rail/configuration/RailType.java | 144 +++++++++- .../rail/configuration/RailTypeManager.java | 87 +++++- .../rail/menu/RailBlockPickerMenu.java | 15 +- .../components/rail/menu/RailBlockRole.java | 43 +++ .../rail/menu/RailOverheadWireMenu.java | 253 ++++++++++++++++++ .../components/rail/menu/RailTypeDraft.java | 18 ++ .../rail/menu/RailTypeEditorMenu.java | 24 +- .../components/rail/menu/RailTypeMenu.java | 3 + 8 files changed, 570 insertions(+), 17 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java index 1d8af831..f6a26c04 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -21,11 +21,20 @@ public final class RailType { // A sleeper spacing of 0 disables sleepers entirely. public static final int MIN_SLEEPER_SPACING = 0; public static final int MAX_SLEEPER_SPACING = 16; + public static final int MIN_OVERHEAD_POLE_SPACING = 1; + public static final int MAX_OVERHEAD_POLE_SPACING = 64; + public static final int MIN_OVERHEAD_POLE_OFFSET = 2; + public static final int MAX_OVERHEAD_POLE_OFFSET = 16; + public static final int MIN_OVERHEAD_POLE_HEIGHT = 3; + public static final int MAX_OVERHEAD_POLE_HEIGHT = 16; public static final int MAX_IDENTIFIER_LENGTH = 32; public static final int MAX_DISPLAY_NAME_LENGTH = 48; public static final int DEFAULT_TRACK_COUNT = 1; public static final int DEFAULT_TRACK_SPACING = 5; + public static final int DEFAULT_OVERHEAD_POLE_SPACING = 16; + public static final int DEFAULT_OVERHEAD_POLE_OFFSET = 3; + public static final int DEFAULT_OVERHEAD_POLE_HEIGHT = 6; private static final String IDENTIFIER_PATTERN = "[a-z0-9_-]+"; @@ -38,6 +47,13 @@ public final class RailType { private final int sleeperSpacing; private final int trackCount; private final int trackSpacing; + private final boolean overheadPolesEnabled; + private final @Nullable XMaterial overheadPoleBlock; + private final int overheadPoleSpacing; + private final int overheadPoleOffset; + private final int overheadPoleHeight; + private final boolean overheadWiresEnabled; + private final @Nullable XMaterial overheadWireBlock; private final boolean builtIn; RailType(Configuration configuration, boolean builtIn) { @@ -52,6 +68,13 @@ public final class RailType { this.sleeperSpacing = configuration.sleeperSpacing(); this.trackCount = configuration.trackCount(); this.trackSpacing = configuration.trackSpacing(); + this.overheadPolesEnabled = configuration.overheadPolesEnabled(); + this.overheadPoleBlock = configuration.overheadPoleBlock(); + this.overheadPoleSpacing = configuration.overheadPoleSpacing(); + this.overheadPoleOffset = configuration.overheadPoleOffset(); + this.overheadPoleHeight = configuration.overheadPoleHeight(); + this.overheadWiresEnabled = configuration.overheadWiresEnabled(); + this.overheadWireBlock = configuration.overheadWireBlock(); this.builtIn = builtIn; } @@ -59,6 +82,14 @@ public boolean hasSleepers() { return sleeperSpacing > 0 && sleeperBlock != null; } + public boolean hasOverheadPoles() { + return overheadPolesEnabled && overheadPoleBlock != null; + } + + public boolean hasOverheadWires() { + return overheadWiresEnabled && overheadWireBlock != null; + } + /** * Creates a custom rail type with the rail block as menu icon. * Call {@link RailTypeManager#saveRailType(RailType)} to validate and persist it. @@ -77,7 +108,14 @@ static RailType createStandard() { .sleeperBlock(XMaterial.SPRUCE_PLANKS) .sleeperSpacing(0) .trackCount(DEFAULT_TRACK_COUNT) - .trackSpacing(DEFAULT_TRACK_SPACING), true); + .trackSpacing(DEFAULT_TRACK_SPACING) + .overheadPolesEnabled(false) + .overheadPoleBlock(XMaterial.LIGHT_GRAY_CONCRETE) + .overheadPoleSpacing(DEFAULT_OVERHEAD_POLE_SPACING) + .overheadPoleOffset(DEFAULT_OVERHEAD_POLE_OFFSET) + .overheadPoleHeight(DEFAULT_OVERHEAD_POLE_HEIGHT) + .overheadWiresEnabled(false) + .overheadWireBlock(XMaterial.IRON_BARS), true); } /** @@ -106,7 +144,12 @@ static RailType createStandard() { if (error != null) return error; - return validateTracks(configuration); + error = validateTracks(configuration); + + if (error != null) + return error; + + return validateOverheadWires(configuration); } private static @Nullable String validateIdentifier(@Nullable String identifier) { @@ -168,6 +211,33 @@ static RailType createStandard() { return null; } + private static @Nullable String validateOverheadWires(Configuration configuration) { + if (configuration.overheadPolesEnabled()) { + if (!isPlaceableBlock(configuration.overheadPoleBlock())) + return "Overhead pole block must be a valid placeable Minecraft block."; + + if (configuration.overheadPoleSpacing() < MIN_OVERHEAD_POLE_SPACING + || configuration.overheadPoleSpacing() > MAX_OVERHEAD_POLE_SPACING) + return "Overhead pole spacing must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_SPACING, MAX_OVERHEAD_POLE_SPACING); + + if (configuration.overheadPoleOffset() < MIN_OVERHEAD_POLE_OFFSET + || configuration.overheadPoleOffset() > MAX_OVERHEAD_POLE_OFFSET) + return "Overhead pole offset must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_OFFSET, MAX_OVERHEAD_POLE_OFFSET); + + if (configuration.overheadPoleHeight() < MIN_OVERHEAD_POLE_HEIGHT + || configuration.overheadPoleHeight() > MAX_OVERHEAD_POLE_HEIGHT) + return "Overhead pole height must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_HEIGHT, MAX_OVERHEAD_POLE_HEIGHT); + } + + if (configuration.overheadWiresEnabled() && !isPlaceableBlock(configuration.overheadWireBlock())) + return "Overhead wire block must be a valid placeable Minecraft block."; + + return null; + } + private static boolean hasInvalidIdentifier(@Nullable String identifier) { return identifier == null || identifier.isBlank() @@ -206,6 +276,13 @@ public static final class Configuration { private int sleeperSpacing; private int trackCount; private int trackSpacing; + private boolean overheadPolesEnabled; + private @Nullable XMaterial overheadPoleBlock; + private int overheadPoleSpacing = DEFAULT_OVERHEAD_POLE_SPACING; + private int overheadPoleOffset = DEFAULT_OVERHEAD_POLE_OFFSET; + private int overheadPoleHeight = DEFAULT_OVERHEAD_POLE_HEIGHT; + private boolean overheadWiresEnabled; + private @Nullable XMaterial overheadWireBlock; public @Nullable String identifier() { return identifier; @@ -287,5 +364,68 @@ public Configuration trackSpacing(int trackSpacing) { this.trackSpacing = trackSpacing; return this; } + + public boolean overheadPolesEnabled() { + return overheadPolesEnabled; + } + + public Configuration overheadPolesEnabled(boolean overheadPolesEnabled) { + this.overheadPolesEnabled = overheadPolesEnabled; + return this; + } + + public @Nullable XMaterial overheadPoleBlock() { + return overheadPoleBlock; + } + + public Configuration overheadPoleBlock(@Nullable XMaterial overheadPoleBlock) { + this.overheadPoleBlock = overheadPoleBlock; + return this; + } + + public int overheadPoleSpacing() { + return overheadPoleSpacing; + } + + public Configuration overheadPoleSpacing(int overheadPoleSpacing) { + this.overheadPoleSpacing = overheadPoleSpacing; + return this; + } + + public int overheadPoleOffset() { + return overheadPoleOffset; + } + + public Configuration overheadPoleOffset(int overheadPoleOffset) { + this.overheadPoleOffset = overheadPoleOffset; + return this; + } + + public int overheadPoleHeight() { + return overheadPoleHeight; + } + + public Configuration overheadPoleHeight(int overheadPoleHeight) { + this.overheadPoleHeight = overheadPoleHeight; + return this; + } + + public boolean overheadWiresEnabled() { + return overheadWiresEnabled; + } + + public Configuration overheadWiresEnabled(boolean overheadWiresEnabled) { + this.overheadWiresEnabled = overheadWiresEnabled; + return this; + } + + public @Nullable XMaterial overheadWireBlock() { + return overheadWireBlock; + } + + public Configuration overheadWireBlock(@Nullable XMaterial overheadWireBlock) { + this.overheadWireBlock = overheadWireBlock; + return this; + } } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index 88e94de5..61deeba2 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -33,6 +33,13 @@ public class RailTypeManager { private static final String KEY_SLEEPER_SPACING = "sleeper-spacing"; private static final String KEY_TRACK_COUNT = "track-count"; private static final String KEY_TRACK_SPACING = "track-spacing"; + private static final String KEY_OVERHEAD_POLES_ENABLED = "overhead.poles.enabled"; + private static final String KEY_OVERHEAD_POLE_BLOCK = "overhead.poles.block"; + private static final String KEY_OVERHEAD_POLE_SPACING = "overhead.poles.spacing"; + private static final String KEY_OVERHEAD_POLE_OFFSET = "overhead.poles.offset"; + private static final String KEY_OVERHEAD_POLE_HEIGHT = "overhead.poles.height"; + private static final String KEY_OVERHEAD_WIRES_ENABLED = "overhead.wires.enabled"; + private static final String KEY_OVERHEAD_WIRE_BLOCK = "overhead.wires.block"; private final Map railTypes = new LinkedHashMap<>(); private final File file; @@ -102,16 +109,7 @@ public String getNextCustomIdentifier() { * @return A human-readable error message, or null if the rail type was saved successfully. */ public @Nullable String saveRailType(RailType railType) { - String error = RailType.validate(new RailType.Configuration() - .identifier(railType.getIdentifier()) - .displayName(railType.getDisplayName()) - .icon(railType.getIcon()) - .railBlock(railType.getRailBlock()) - .blocksBelow(railType.getBlocksBelow()) - .sleeperBlock(railType.getSleeperBlock()) - .sleeperSpacing(railType.getSleeperSpacing()) - .trackCount(railType.getTrackCount()) - .trackSpacing(railType.getTrackSpacing())); + String error = RailType.validate(toConfiguration(railType)); if (error != null) return error; @@ -155,6 +153,22 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif int sleeperSpacing = section.getInt(KEY_SLEEPER_SPACING, RailType.MIN_SLEEPER_SPACING); int trackCount = section.getInt(KEY_TRACK_COUNT, RailType.DEFAULT_TRACK_COUNT); int trackSpacing = section.getInt(KEY_TRACK_SPACING, RailType.DEFAULT_TRACK_SPACING); + boolean overheadPolesEnabled = section.getBoolean(KEY_OVERHEAD_POLES_ENABLED, false); + XMaterial overheadPoleBlock = parseMaterial(section.getString(KEY_OVERHEAD_POLE_BLOCK)); + int overheadPoleSpacing = section.getInt( + KEY_OVERHEAD_POLE_SPACING, + RailType.DEFAULT_OVERHEAD_POLE_SPACING + ); + int overheadPoleOffset = section.getInt( + KEY_OVERHEAD_POLE_OFFSET, + RailType.DEFAULT_OVERHEAD_POLE_OFFSET + ); + int overheadPoleHeight = section.getInt( + KEY_OVERHEAD_POLE_HEIGHT, + RailType.DEFAULT_OVERHEAD_POLE_HEIGHT + ); + boolean overheadWiresEnabled = section.getBoolean(KEY_OVERHEAD_WIRES_ENABLED, false); + XMaterial overheadWireBlock = parseMaterial(section.getString(KEY_OVERHEAD_WIRE_BLOCK)); String error = RailType.validate(new RailType.Configuration() .identifier(normalizedIdentifier) @@ -165,7 +179,14 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .sleeperBlock(sleeperBlock) .sleeperSpacing(sleeperSpacing) .trackCount(trackCount) - .trackSpacing(trackSpacing)); + .trackSpacing(trackSpacing) + .overheadPolesEnabled(overheadPolesEnabled) + .overheadPoleBlock(overheadPoleBlock) + .overheadPoleSpacing(overheadPoleSpacing) + .overheadPoleOffset(overheadPoleOffset) + .overheadPoleHeight(overheadPoleHeight) + .overheadWiresEnabled(overheadWiresEnabled) + .overheadWireBlock(overheadWireBlock)); if (error != null) { ChatHelper.logError("Skipping invalid rail type '%s' in %s: %s", identifier, FILE_PATH, error); @@ -188,7 +209,14 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .sleeperBlock(sleeperBlock) .sleeperSpacing(sleeperSpacing) .trackCount(trackCount) - .trackSpacing(trackSpacing), false)); + .trackSpacing(trackSpacing) + .overheadPolesEnabled(overheadPolesEnabled) + .overheadPoleBlock(overheadPoleBlock) + .overheadPoleSpacing(overheadPoleSpacing) + .overheadPoleOffset(overheadPoleOffset) + .overheadPoleHeight(overheadPoleHeight) + .overheadWiresEnabled(overheadWiresEnabled) + .overheadWireBlock(overheadWireBlock), false)); } private @Nullable String save() { @@ -213,6 +241,21 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif config.set(path + "." + KEY_SLEEPER_SPACING, railType.getSleeperSpacing()); config.set(path + "." + KEY_TRACK_COUNT, railType.getTrackCount()); config.set(path + "." + KEY_TRACK_SPACING, railType.getTrackSpacing()); + config.set(path + "." + KEY_OVERHEAD_POLES_ENABLED, railType.isOverheadPolesEnabled()); + config.set(path + "." + KEY_OVERHEAD_POLE_SPACING, railType.getOverheadPoleSpacing()); + config.set(path + "." + KEY_OVERHEAD_POLE_OFFSET, railType.getOverheadPoleOffset()); + config.set(path + "." + KEY_OVERHEAD_POLE_HEIGHT, railType.getOverheadPoleHeight()); + config.set(path + "." + KEY_OVERHEAD_WIRES_ENABLED, railType.isOverheadWiresEnabled()); + + XMaterial overheadPoleBlock = railType.getOverheadPoleBlock(); + + if (overheadPoleBlock != null) + config.set(path + "." + KEY_OVERHEAD_POLE_BLOCK, overheadPoleBlock.name()); + + XMaterial overheadWireBlock = railType.getOverheadWireBlock(); + + if (overheadWireBlock != null) + config.set(path + "." + KEY_OVERHEAD_WIRE_BLOCK, overheadWireBlock.name()); } try { @@ -246,4 +289,24 @@ private List parseMaterials(List values) { return materials; } + + private RailType.Configuration toConfiguration(RailType railType) { + return new RailType.Configuration() + .identifier(railType.getIdentifier()) + .displayName(railType.getDisplayName()) + .icon(railType.getIcon()) + .railBlock(railType.getRailBlock()) + .blocksBelow(railType.getBlocksBelow()) + .sleeperBlock(railType.getSleeperBlock()) + .sleeperSpacing(railType.getSleeperSpacing()) + .trackCount(railType.getTrackCount()) + .trackSpacing(railType.getTrackSpacing()) + .overheadPolesEnabled(railType.isOverheadPolesEnabled()) + .overheadPoleBlock(railType.getOverheadPoleBlock()) + .overheadPoleSpacing(railType.getOverheadPoleSpacing()) + .overheadPoleOffset(railType.getOverheadPoleOffset()) + .overheadPoleHeight(railType.getOverheadPoleHeight()) + .overheadWiresEnabled(railType.isOverheadWiresEnabled()) + .overheadWireBlock(railType.getOverheadWireBlock()); + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java index f2d90ece..8d9fbcbe 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java @@ -2,6 +2,7 @@ import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; import net.buildtheearth.buildteamtools.utils.menus.BlockListMenu; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -19,7 +20,7 @@ public class RailBlockPickerMenu extends BlockListMenu { private final RailBlockRole role; RailBlockPickerMenu(Player player, RailTypeDraft draft, RailBlockRole role, boolean autoLoad) { - super(player, role.getMenuTitle(), role.createChoices(), new RailTypeEditorMenu(player, draft, false), autoLoad); + super(player, role.getMenuTitle(), role.createChoices(), createParentMenu(player, draft, role), autoLoad); this.draft = draft; this.role = role; @@ -65,10 +66,20 @@ protected void setItemClickEventsAsync() { clickPlayer.closeInventory(); playSound(clickPlayer, Sound.UI_BUTTON_CLICK); - new RailTypeEditorMenu(clickPlayer, draft, true); + if (role.isOverheadSetting()) + new RailOverheadWireMenu(clickPlayer, draft, true); + else + new RailTypeEditorMenu(clickPlayer, draft, true); }); } + private static AbstractMenu createParentMenu(Player player, RailTypeDraft draft, RailBlockRole role) { + if (role.isOverheadSetting()) + return new RailOverheadWireMenu(player, draft, false); + + return new RailTypeEditorMenu(player, draft, false); + } + private void playSound(Player player, Sound sound) { player.playSound(player, sound, 1.0F, 1.0F); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java index 349cd6c2..35a86031 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java @@ -61,6 +61,45 @@ List createChoices() { void apply(RailTypeDraft draft, XMaterial material) { draft.setSleeperBlock(material); } + }, + + OVERHEAD_POLE_BLOCK("Choose an Overhead Pole Block") { + @Override + List createChoices() { + return MenuItems.getSolidBlocks(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setOverheadPoleBlock(material); + } + + @Override + boolean isOverheadSetting() { + return true; + } + }, + + OVERHEAD_WIRE_BLOCK("Choose an Overhead Wire Block") { + @Override + List createChoices() { + return createItems(List.of( + XMaterial.IRON_BARS, + XMaterial.BLACK_WOOL, + XMaterial.GRAY_WOOL, + XMaterial.LIGHT_GRAY_WOOL + )); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setOverheadWireBlock(material); + } + + @Override + boolean isOverheadSetting() { + return true; + } }; @Getter @@ -74,6 +113,10 @@ void apply(RailTypeDraft draft, XMaterial material) { abstract void apply(RailTypeDraft draft, XMaterial material); + boolean isOverheadSetting() { + return false; + } + private static List createItems(List materials) { List items = new ArrayList<>(); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java new file mode 100644 index 00000000..720fb545 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java @@ -0,0 +1,253 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; +import net.buildtheearth.buildteamtools.utils.MenuItems; +import net.buildtheearth.buildteamtools.utils.heads.HeadColor; +import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; +import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; +import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.ipvp.canvas.mask.BinaryMask; +import org.ipvp.canvas.mask.Mask; + +import java.util.List; +import java.util.Objects; +import java.util.function.IntConsumer; +import java.util.function.IntSupplier; + +final class RailOverheadWireMenu extends AbstractMenu { + + private static final String MENU_TITLE = "Overhead Wire Settings"; + private static final String FILLED_MASK_ROW = "111111111"; + private static final String BLOCKS_UNIT = "Blocks"; + + private static final int POLES_TOGGLE_SLOT = 10; + private static final int POLE_BLOCK_SLOT = 16; + private static final int WIRES_TOGGLE_SLOT = 19; + private static final int WIRE_BLOCK_SLOT = 25; + private static final int POLE_SPACING_SLOT = 28; + private static final int POLE_OFFSET_SLOT = 31; + private static final int POLE_HEIGHT_SLOT = 34; + private static final int BACK_ITEM_SLOT = 36; + private static final int DONE_ITEM_SLOT = 44; + + private final RailTypeDraft draft; + + RailOverheadWireMenu(Player player, RailTypeDraft draft, boolean autoLoad) { + super(5, MENU_TITLE, player, false); + this.draft = draft; + + if (autoLoad) + reloadMenuAsync(); + } + + @Override + protected void setPreviewItems() { + if (getMask() != null) + getMask().apply(getMenu()); + + getMenu().getSlot(POLES_TOGGLE_SLOT).setItem(createToggleItem( + "Overhead Poles", + draft.isOverheadPolesEnabled(), + "Generate poles alongside the tracks." + )); + getMenu().getSlot(WIRES_TOGGLE_SLOT).setItem(createToggleItem( + "Overhead Wires", + draft.isOverheadWiresEnabled(), + "Generate wires above the tracks." + )); + getMenu().getSlot(POLE_BLOCK_SLOT).setItem(createBlockItem( + "Pole Block", + draft.getOverheadPoleBlock() + )); + getMenu().getSlot(WIRE_BLOCK_SLOT).setItem(createBlockItem( + "Wire Block", + draft.getOverheadWireBlock() + )); + + createCounter( + HeadColor.WHITE, + POLE_SPACING_SLOT, + "Pole Spacing", + draft.getOverheadPoleSpacing(), + RailType.MIN_OVERHEAD_POLE_SPACING, + RailType.MAX_OVERHEAD_POLE_SPACING, + BLOCKS_UNIT + ); + createCounter( + HeadColor.LIGHT_GRAY, + POLE_OFFSET_SLOT, + "Pole Offset", + draft.getOverheadPoleOffset(), + RailType.MIN_OVERHEAD_POLE_OFFSET, + RailType.MAX_OVERHEAD_POLE_OFFSET, + BLOCKS_UNIT + ); + createCounter( + HeadColor.WHITE, + POLE_HEIGHT_SLOT, + "Pole Height", + draft.getOverheadPoleHeight(), + RailType.MIN_OVERHEAD_POLE_HEIGHT, + RailType.MAX_OVERHEAD_POLE_HEIGHT, + BLOCKS_UNIT + ); + + setBackItem(BACK_ITEM_SLOT, new RailTypeEditorMenu(getMenuPlayer(), draft, false)); + getMenu().getSlot(DONE_ITEM_SLOT).setItem(HeadFactory.head( + HeadTexture.CHECKMARK, + green("Done") + )); + getMenu().open(getMenuPlayer()); + } + + @Override + protected void setMenuItemsAsync() { + // All items use local draft state. + } + + @Override + protected void setItemClickEventsAsync() { + getMenu().getSlot(POLES_TOGGLE_SLOT).setClickHandler((player, click) -> { + draft.setOverheadPolesEnabled(!draft.isOverheadPolesEnabled()); + playSound(player, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + }); + getMenu().getSlot(WIRES_TOGGLE_SLOT).setClickHandler((player, click) -> { + draft.setOverheadWiresEnabled(!draft.isOverheadWiresEnabled()); + playSound(player, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + }); + + setBlockPickerClickEvent(POLE_BLOCK_SLOT, RailBlockRole.OVERHEAD_POLE_BLOCK); + setBlockPickerClickEvent(WIRE_BLOCK_SLOT, RailBlockRole.OVERHEAD_WIRE_BLOCK); + setCounterClickEvents( + POLE_SPACING_SLOT, + RailType.MIN_OVERHEAD_POLE_SPACING, + RailType.MAX_OVERHEAD_POLE_SPACING, + draft::getOverheadPoleSpacing, + draft::setOverheadPoleSpacing + ); + setCounterClickEvents( + POLE_OFFSET_SLOT, + RailType.MIN_OVERHEAD_POLE_OFFSET, + RailType.MAX_OVERHEAD_POLE_OFFSET, + draft::getOverheadPoleOffset, + draft::setOverheadPoleOffset + ); + setCounterClickEvents( + POLE_HEIGHT_SLOT, + RailType.MIN_OVERHEAD_POLE_HEIGHT, + RailType.MAX_OVERHEAD_POLE_HEIGHT, + draft::getOverheadPoleHeight, + draft::setOverheadPoleHeight + ); + + getMenu().getSlot(DONE_ITEM_SLOT).setClickHandler((player, click) -> { + player.closeInventory(); + playSound(player, Sound.UI_BUTTON_CLICK); + new RailTypeEditorMenu(player, draft, true); + }); + } + + @Override + protected Mask getMask() { + return BinaryMask.builder(getMenu()) + .item(MenuItems.ITEM_BACKGROUND) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .build(); + } + + private void setBlockPickerClickEvent(int slot, RailBlockRole role) { + getMenu().getSlot(slot).setClickHandler((player, click) -> { + player.closeInventory(); + playSound(player, Sound.UI_BUTTON_CLICK); + new RailBlockPickerMenu(player, draft, role, true); + }); + } + + private void setCounterClickEvents( + int slot, + int minimum, + int maximum, + IntSupplier getter, + IntConsumer setter + ) { + getMenu().getSlot(slot - 1).setClickHandler((player, click) -> + changeCounter(player, minimum, getter.getAsInt() - 1, getter, setter)); + getMenu().getSlot(slot + 1).setClickHandler((player, click) -> + changeCounter(player, maximum, getter.getAsInt() + 1, getter, setter)); + } + + private void changeCounter( + Player player, + int boundary, + int newValue, + IntSupplier getter, + IntConsumer setter + ) { + if (getter.getAsInt() == boundary) { + playSound(player, Sound.ENTITY_ITEM_BREAK); + return; + } + + setter.accept(newValue); + playSound(player, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + } + + private ItemStack createToggleItem(String name, boolean enabled, String description) { + return Item.create( + Objects.requireNonNull(XMaterial.LEVER.get()), + yellow(name + ": ") + (enabled ? green("Enabled") : red("Disabled")), + List.of(gray(description), gray("Click to toggle.")) + ); + } + + private ItemStack createBlockItem(String name, XMaterial material) { + Material bukkitMaterial = material.get(); + + if (bukkitMaterial == null) + bukkitMaterial = Objects.requireNonNull(XMaterial.BARRIER.get()); + + return Item.create( + bukkitMaterial, + yellow(name + ": ") + white(RailTypeMenu.formatMaterial(material)), + List.of(gray("Click to choose a different block.")) + ); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); + } + + private static String gray(String text) { + return RailMenuText.color(NamedTextColor.GRAY, text); + } + + private static String white(String text) { + return RailMenuText.color(NamedTextColor.WHITE, text); + } + + private static String yellow(String text) { + return RailMenuText.color(NamedTextColor.YELLOW, text); + } + + private static String green(String text) { + return RailMenuText.color(NamedTextColor.GREEN, text); + } + + private static String red(String text) { + return RailMenuText.color(NamedTextColor.RED, text); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java index 3bf0ed12..e01e6d64 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -22,6 +22,13 @@ class RailTypeDraft { private int sleeperSpacing = RailType.MIN_SLEEPER_SPACING; private int trackCount = RailType.DEFAULT_TRACK_COUNT; private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; + private boolean overheadPolesEnabled; + private XMaterial overheadPoleBlock = XMaterial.LIGHT_GRAY_CONCRETE; + private int overheadPoleSpacing = RailType.DEFAULT_OVERHEAD_POLE_SPACING; + private int overheadPoleOffset = RailType.DEFAULT_OVERHEAD_POLE_OFFSET; + private int overheadPoleHeight = RailType.DEFAULT_OVERHEAD_POLE_HEIGHT; + private boolean overheadWiresEnabled; + private XMaterial overheadWireBlock = XMaterial.IRON_BARS; static RailTypeDraft from(RailType railType, boolean keepIdentity) { RailTypeDraft draft = new RailTypeDraft(); @@ -38,6 +45,17 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.sleeperSpacing = railType.getSleeperSpacing(); draft.trackCount = railType.getTrackCount(); draft.trackSpacing = railType.getTrackSpacing(); + draft.overheadPolesEnabled = railType.isOverheadPolesEnabled(); + draft.overheadPoleBlock = railType.getOverheadPoleBlock() == null + ? XMaterial.LIGHT_GRAY_CONCRETE + : railType.getOverheadPoleBlock(); + draft.overheadPoleSpacing = railType.getOverheadPoleSpacing(); + draft.overheadPoleOffset = railType.getOverheadPoleOffset(); + draft.overheadPoleHeight = railType.getOverheadPoleHeight(); + draft.overheadWiresEnabled = railType.isOverheadWiresEnabled(); + draft.overheadWireBlock = railType.getOverheadWireBlock() == null + ? XMaterial.IRON_BARS + : railType.getOverheadWireBlock(); return draft; } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java index 2b197a8a..f625cba7 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -50,6 +50,7 @@ public class RailTypeEditorMenu extends AbstractMenu { private static final int RAIL_BLOCK_SLOT = 7; private static final int BLOCK_BELOW_SLOT = 16; private static final int SLEEPER_BLOCK_SLOT = 25; + private static final int OVERHEAD_SETTINGS_SLOT = 32; private static final int BACK_ITEM_SLOT = 36; private static final int SAVE_ITEM_SLOT = 44; @@ -99,6 +100,15 @@ protected void setPreviewItems() { draft.getSleeperBlock(), "Sleeper Spacing 0 disables sleepers." )); + getMenu().getSlot(OVERHEAD_SETTINGS_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.IRON_BARS.get()), + yellow("Overhead Settings"), + List.of( + gray("Poles: ") + white(draft.isOverheadPolesEnabled() ? "Enabled" : "Disabled"), + gray("Wires: ") + white(draft.isOverheadWiresEnabled() ? "Enabled" : "Disabled"), + gray("Click to configure overhead wires.") + ) + )); setBackItem(BACK_ITEM_SLOT, new RailTypeMenu(getMenuPlayer(), false)); getMenu().getSlot(SAVE_ITEM_SLOT).setItem(HeadFactory.head( @@ -128,6 +138,11 @@ protected void setItemClickEventsAsync() { setBlockPickerClickEvents(RAIL_BLOCK_SLOT, RailBlockRole.RAIL_BLOCK); setBlockPickerClickEvents(BLOCK_BELOW_SLOT, RailBlockRole.BLOCK_BELOW); setBlockPickerClickEvents(SLEEPER_BLOCK_SLOT, RailBlockRole.SLEEPER_BLOCK); + getMenu().getSlot(OVERHEAD_SETTINGS_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + new RailOverheadWireMenu(clickPlayer, draft, true); + }); getMenu().getSlot(SAVE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> saveRailType(clickPlayer)); } @@ -206,7 +221,14 @@ private void saveRailType(Player clickPlayer) { .sleeperBlock(draft.getSleeperBlock()) .sleeperSpacing(draft.getSleeperSpacing()) .trackCount(draft.getTrackCount()) - .trackSpacing(draft.getTrackSpacing())); + .trackSpacing(draft.getTrackSpacing()) + .overheadPolesEnabled(draft.isOverheadPolesEnabled()) + .overheadPoleBlock(draft.getOverheadPoleBlock()) + .overheadPoleSpacing(draft.getOverheadPoleSpacing()) + .overheadPoleOffset(draft.getOverheadPoleOffset()) + .overheadPoleHeight(draft.getOverheadPoleHeight()) + .overheadWiresEnabled(draft.isOverheadWiresEnabled()) + .overheadWireBlock(draft.getOverheadWireBlock())); String error = railTypeManager.saveRailType(railType); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index 76f98f87..a0683f25 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -101,6 +101,9 @@ private static ItemStack createRailTypeItem(RailType railType) { else lore.add(gray("Tracks: ") + white("1")); + lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() ? "Enabled" : "Disabled")); + lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() ? "Enabled" : "Disabled")); + if (railType.isBuiltIn()) lore.add(darkGray("Right-Click to create an editable copy")); else From 1a98a335678020710e47ccc7f082d518f3f6f48d Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 16:14:26 +0200 Subject: [PATCH 4/8] feat(rail): generate overhead wires and prepare track switches --- .../rail/configuration/RailType.java | 15 +- .../rail/configuration/RailTypeManager.java | 12 +- .../rail/generation/RailOverheadBuilder.java | 137 ++++++++++++++++++ .../rail/generation/RailScripts.java | 6 +- .../components/rail/menu/RailTypeDraft.java | 2 + .../rail/menu/RailTypeEditorMenu.java | 32 +++- .../components/rail/menu/RailTypeMenu.java | 11 +- 7 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java index f6a26c04..fa7e8005 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -54,6 +54,7 @@ public final class RailType { private final int overheadPoleHeight; private final boolean overheadWiresEnabled; private final @Nullable XMaterial overheadWireBlock; + private final boolean trackSwitchesEnabled; private final boolean builtIn; RailType(Configuration configuration, boolean builtIn) { @@ -75,6 +76,7 @@ public final class RailType { this.overheadPoleHeight = configuration.overheadPoleHeight(); this.overheadWiresEnabled = configuration.overheadWiresEnabled(); this.overheadWireBlock = configuration.overheadWireBlock(); + this.trackSwitchesEnabled = configuration.trackSwitchesEnabled(); this.builtIn = builtIn; } @@ -115,7 +117,8 @@ static RailType createStandard() { .overheadPoleOffset(DEFAULT_OVERHEAD_POLE_OFFSET) .overheadPoleHeight(DEFAULT_OVERHEAD_POLE_HEIGHT) .overheadWiresEnabled(false) - .overheadWireBlock(XMaterial.IRON_BARS), true); + .overheadWireBlock(XMaterial.IRON_BARS) + .trackSwitchesEnabled(false), true); } /** @@ -283,6 +286,7 @@ public static final class Configuration { private int overheadPoleHeight = DEFAULT_OVERHEAD_POLE_HEIGHT; private boolean overheadWiresEnabled; private @Nullable XMaterial overheadWireBlock; + private boolean trackSwitchesEnabled; public @Nullable String identifier() { return identifier; @@ -427,5 +431,14 @@ public Configuration overheadWireBlock(@Nullable XMaterial overheadWireBlock) { this.overheadWireBlock = overheadWireBlock; return this; } + + public boolean trackSwitchesEnabled() { + return trackSwitchesEnabled; + } + + public Configuration trackSwitchesEnabled(boolean trackSwitchesEnabled) { + this.trackSwitchesEnabled = trackSwitchesEnabled; + return this; + } } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index 61deeba2..a0f53a67 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -40,6 +40,7 @@ public class RailTypeManager { private static final String KEY_OVERHEAD_POLE_HEIGHT = "overhead.poles.height"; private static final String KEY_OVERHEAD_WIRES_ENABLED = "overhead.wires.enabled"; private static final String KEY_OVERHEAD_WIRE_BLOCK = "overhead.wires.block"; + private static final String KEY_TRACK_SWITCHES_ENABLED = "track-switches.enabled"; private final Map railTypes = new LinkedHashMap<>(); private final File file; @@ -169,6 +170,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif ); boolean overheadWiresEnabled = section.getBoolean(KEY_OVERHEAD_WIRES_ENABLED, false); XMaterial overheadWireBlock = parseMaterial(section.getString(KEY_OVERHEAD_WIRE_BLOCK)); + boolean trackSwitchesEnabled = section.getBoolean(KEY_TRACK_SWITCHES_ENABLED, false); String error = RailType.validate(new RailType.Configuration() .identifier(normalizedIdentifier) @@ -186,7 +188,8 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .overheadPoleOffset(overheadPoleOffset) .overheadPoleHeight(overheadPoleHeight) .overheadWiresEnabled(overheadWiresEnabled) - .overheadWireBlock(overheadWireBlock)); + .overheadWireBlock(overheadWireBlock) + .trackSwitchesEnabled(trackSwitchesEnabled)); if (error != null) { ChatHelper.logError("Skipping invalid rail type '%s' in %s: %s", identifier, FILE_PATH, error); @@ -216,7 +219,8 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .overheadPoleOffset(overheadPoleOffset) .overheadPoleHeight(overheadPoleHeight) .overheadWiresEnabled(overheadWiresEnabled) - .overheadWireBlock(overheadWireBlock), false)); + .overheadWireBlock(overheadWireBlock) + .trackSwitchesEnabled(trackSwitchesEnabled), false)); } private @Nullable String save() { @@ -246,6 +250,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif config.set(path + "." + KEY_OVERHEAD_POLE_OFFSET, railType.getOverheadPoleOffset()); config.set(path + "." + KEY_OVERHEAD_POLE_HEIGHT, railType.getOverheadPoleHeight()); config.set(path + "." + KEY_OVERHEAD_WIRES_ENABLED, railType.isOverheadWiresEnabled()); + config.set(path + "." + KEY_TRACK_SWITCHES_ENABLED, railType.isTrackSwitchesEnabled()); XMaterial overheadPoleBlock = railType.getOverheadPoleBlock(); @@ -307,6 +312,7 @@ private RailType.Configuration toConfiguration(RailType railType) { .overheadPoleOffset(railType.getOverheadPoleOffset()) .overheadPoleHeight(railType.getOverheadPoleHeight()) .overheadWiresEnabled(railType.isOverheadWiresEnabled()) - .overheadWireBlock(railType.getOverheadWireBlock()); + .overheadWireBlock(railType.getOverheadWireBlock()) + .trackSwitchesEnabled(railType.isTrackSwitchesEnabled()); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java new file mode 100644 index 00000000..6414b4bb --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java @@ -0,0 +1,137 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; + +import com.alpsbte.alpslib.utils.GeneratorUtils; +import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; +import com.sk89q.worldedit.util.Direction; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; +import org.bukkit.util.Vector; + +import java.util.List; +import java.util.Map; + +final class RailOverheadBuilder { + + private static final Direction DEFAULT_FACING = Direction.EAST; + private static final String FACING_PROPERTY = "facing"; + + private final RailTerrainResolver terrainResolver; + private final RailType railType; + + RailOverheadBuilder(RailTerrainResolver terrainResolver, RailType railType) { + this.terrainResolver = terrainResolver; + this.railType = railType; + } + + void addTo(Map blocks, List> railCenterPaths) { + if (railType.hasOverheadWires()) + addWires(blocks, railCenterPaths); + + if (!railType.hasOverheadPoles() || railCenterPaths.isEmpty()) + return; + + addPoles(blocks, railCenterPaths.getFirst(), -1); + + List lastPath = railCenterPaths.getLast(); + addPoles(blocks, lastPath, 1); + } + + private void addWires(Map blocks, List> railCenterPaths) { + XMaterial wireBlock = railType.getOverheadWireBlock(); + + if (wireBlock == null) + return; + + BlockType wireBlockType = Item.convertXMaterialToWEBlockType(wireBlock); + + if (wireBlockType == null) + return; + + for (List path : railCenterPaths) { + for (int index = 0; index < path.size(); index++) { + Vector center = path.get(index); + Direction direction = getFacing(path, index); + BlockState wireState = createDirectionalState(wireBlockType, direction); + PositionKey wirePosition = PositionKey.of( + center.getBlockX(), + center.getBlockY() + railType.getOverheadPoleHeight(), + center.getBlockZ() + ); + blocks.putIfAbsent(wirePosition, wireState); + } + } + } + + private void addPoles(Map blocks, List path, int sideSign) { + XMaterial poleBlock = railType.getOverheadPoleBlock(); + + if (poleBlock == null) + return; + + BlockState poleState = GeneratorUtils.getBlockState(poleBlock); + + for (int index = 0; index < path.size(); index += railType.getOverheadPoleSpacing()) { + Vector center = path.get(index); + RailStep perpendicular = getPerpendicularStep(path, index, sideSign); + int poleX = center.getBlockX() + perpendicular.dx() * railType.getOverheadPoleOffset(); + int poleZ = center.getBlockZ() + perpendicular.dz() * railType.getOverheadPoleOffset(); + int poleY = terrainResolver.getNearestRailSurfaceY(poleX, poleZ, center.getBlockY()); + int topY = center.getBlockY() + railType.getOverheadPoleHeight(); + + for (int y = Math.min(poleY, topY); y <= topY; y++) + blocks.putIfAbsent(PositionKey.of(poleX, y, poleZ), poleState); + + if (railType.hasOverheadWires()) + addSupportArm(blocks, poleX, topY, poleZ, perpendicular, poleState); + } + } + + private void addSupportArm( + Map blocks, + int poleX, + int topY, + int poleZ, + RailStep perpendicular, + BlockState poleState + ) { + for (int offset = 1; offset <= railType.getOverheadPoleOffset(); offset++) { + PositionKey armPosition = PositionKey.of( + poleX - perpendicular.dx() * offset, + topY, + poleZ - perpendicular.dz() * offset + ); + blocks.putIfAbsent(armPosition, poleState); + } + } + + private RailStep getPerpendicularStep(List path, int index, int sideSign) { + RailStep direction = getStep(path, index); + return new RailStep(-direction.dz() * sideSign, direction.dx() * sideSign); + } + + private Direction getFacing(List path, int index) { + RailStep direction = getStep(path, index); + return GeneratorUtils.getFacing(direction.dx(), direction.dz(), DEFAULT_FACING); + } + + private RailStep getStep(List path, int index) { + Vector from = index > 0 ? path.get(index - 1) : path.get(index); + Vector to = index < path.size() - 1 ? path.get(index + 1) : path.get(index); + int xDirection = Integer.compare(to.getBlockX() - from.getBlockX(), 0); + int zDirection = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); + + if (xDirection == 0 && zDirection == 0) + return new RailStep(1, 0); + + return new RailStep(xDirection, zDirection); + } + + private BlockState createDirectionalState(BlockType blockType, Direction direction) { + if (!blockType.getPropertyMap().containsKey(FACING_PROPERTY)) + return blockType.getDefaultState(); + + return GeneratorUtils.getBlockStateWithFacing(blockType, direction); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java index b98b7f51..17ae0219 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java @@ -25,7 +25,7 @@ public class RailScripts extends Script { private static final int SELECTION_PADDING = 4; - private static final int SELECTION_VERTICAL_PADDING = 12; + private static final int SELECTION_VERTICAL_PADDING = RailType.MAX_OVERHEAD_POLE_HEIGHT + 2; private static final int PREPARE_SELECTION_EXPANSION = 8; private static final long BLOCK_PLACEMENT_START_PERCENTAGE = 95L; @@ -183,6 +183,7 @@ private boolean queueRailGeneration() { } Map railBlocks = buildRailBlocks(railCenterPaths); + new RailOverheadBuilder(terrainResolver, railType).addTo(railBlocks, railCenterPaths); preparationProgress.completeStage(RAIL_BLOCK_BUILD_PROGRESS); if (railBlocks.size() > limits.maxBlockPlacements()) { @@ -387,7 +388,8 @@ private List createRailSelectionPoints(List points) { private int getSelectionPadding() { int maxLaneOffset = (int) Math.round(((trackCount - 1) / 2.0) * trackSpacing); - return SELECTION_PADDING + maxLaneOffset + 2; + int overheadPoleOffset = railType.hasOverheadPoles() ? railType.getOverheadPoleOffset() : 0; + return SELECTION_PADDING + maxLaneOffset + overheadPoleOffset + 2; } private List createCenterPath(List points) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java index e01e6d64..fd76fe77 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -29,6 +29,7 @@ class RailTypeDraft { private int overheadPoleHeight = RailType.DEFAULT_OVERHEAD_POLE_HEIGHT; private boolean overheadWiresEnabled; private XMaterial overheadWireBlock = XMaterial.IRON_BARS; + private boolean trackSwitchesEnabled; static RailTypeDraft from(RailType railType, boolean keepIdentity) { RailTypeDraft draft = new RailTypeDraft(); @@ -56,6 +57,7 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.overheadWireBlock = railType.getOverheadWireBlock() == null ? XMaterial.IRON_BARS : railType.getOverheadWireBlock(); + draft.trackSwitchesEnabled = railType.isTrackSwitchesEnabled(); return draft; } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java index f625cba7..7b855eff 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -51,6 +51,7 @@ public class RailTypeEditorMenu extends AbstractMenu { private static final int BLOCK_BELOW_SLOT = 16; private static final int SLEEPER_BLOCK_SLOT = 25; private static final int OVERHEAD_SETTINGS_SLOT = 32; + private static final int TRACK_SWITCHES_SLOT = 30; private static final int BACK_ITEM_SLOT = 36; private static final int SAVE_ITEM_SLOT = 44; @@ -104,11 +105,26 @@ protected void setPreviewItems() { Objects.requireNonNull(XMaterial.IRON_BARS.get()), yellow("Overhead Settings"), List.of( - gray("Poles: ") + white(draft.isOverheadPolesEnabled() ? "Enabled" : "Disabled"), - gray("Wires: ") + white(draft.isOverheadWiresEnabled() ? "Enabled" : "Disabled"), + gray("Poles: ") + white(draft.isOverheadPolesEnabled() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED), + gray("Wires: ") + white(draft.isOverheadWiresEnabled() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED), gray("Click to configure overhead wires.") ) )); + getMenu().getSlot(TRACK_SWITCHES_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.LEVER.get()), + yellow("Track Switches: ") + + (draft.isTrackSwitchesEnabled() + ? green(RailMenuText.ENABLED) + : red(RailMenuText.DISABLED)), + List.of( + gray("Prepares this type for future switch generation."), + gray("Click to toggle.") + ) + )); setBackItem(BACK_ITEM_SLOT, new RailTypeMenu(getMenuPlayer(), false)); getMenu().getSlot(SAVE_ITEM_SLOT).setItem(HeadFactory.head( @@ -138,6 +154,11 @@ protected void setItemClickEventsAsync() { setBlockPickerClickEvents(RAIL_BLOCK_SLOT, RailBlockRole.RAIL_BLOCK); setBlockPickerClickEvents(BLOCK_BELOW_SLOT, RailBlockRole.BLOCK_BELOW); setBlockPickerClickEvents(SLEEPER_BLOCK_SLOT, RailBlockRole.SLEEPER_BLOCK); + getMenu().getSlot(TRACK_SWITCHES_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + draft.setTrackSwitchesEnabled(!draft.isTrackSwitchesEnabled()); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + }); getMenu().getSlot(OVERHEAD_SETTINGS_SLOT).setClickHandler((clickPlayer, clickInformation) -> { clickPlayer.closeInventory(); playSound(clickPlayer, Sound.UI_BUTTON_CLICK); @@ -228,7 +249,8 @@ private void saveRailType(Player clickPlayer) { .overheadPoleOffset(draft.getOverheadPoleOffset()) .overheadPoleHeight(draft.getOverheadPoleHeight()) .overheadWiresEnabled(draft.isOverheadWiresEnabled()) - .overheadWireBlock(draft.getOverheadWireBlock())); + .overheadWireBlock(draft.getOverheadWireBlock()) + .trackSwitchesEnabled(draft.isTrackSwitchesEnabled())); String error = railTypeManager.saveRailType(railType); @@ -330,4 +352,8 @@ private static String yellow(String text) { private static String green(String text) { return RailMenuText.color(NamedTextColor.GREEN, text); } + + private static String red(String text) { + return RailMenuText.color(NamedTextColor.RED, text); + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index a0683f25..2efd4998 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -101,8 +101,15 @@ private static ItemStack createRailTypeItem(RailType railType) { else lore.add(gray("Tracks: ") + white("1")); - lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() ? "Enabled" : "Disabled")); - lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() ? "Enabled" : "Disabled")); + lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); + lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); + lore.add(gray("Track Switches: ") + white(railType.isTrackSwitchesEnabled() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); if (railType.isBuiltIn()) lore.add(darkGray("Right-Click to create an editable copy")); From d08002903affb1f278b89b1341cc118be1a94c26 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 16:14:46 +0200 Subject: [PATCH 5/8] feat(rail): generate overhead wires and prepare track switches --- .../modules/generator/components/rail/menu/RailMenuText.java | 3 +++ .../generator/components/rail/menu/RailOverheadWireMenu.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java index 6f364ae8..1853c088 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java @@ -7,6 +7,9 @@ @UtilityClass final class RailMenuText { + static final String ENABLED = "Enabled"; + static final String DISABLED = "Disabled"; + static String color(NamedTextColor color, String text) { return ChatHelper.getColorizedString(color, text, false); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java index 720fb545..11515cbf 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java @@ -209,7 +209,9 @@ private void changeCounter( private ItemStack createToggleItem(String name, boolean enabled, String description) { return Item.create( Objects.requireNonNull(XMaterial.LEVER.get()), - yellow(name + ": ") + (enabled ? green("Enabled") : red("Disabled")), + yellow(name + ": ") + (enabled + ? green(RailMenuText.ENABLED) + : red(RailMenuText.DISABLED)), List.of(gray(description), gray("Click to toggle.")) ); } From 3847ac68b85443ec24b8d6edbcd8cebf1f980960 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 16:39:13 +0200 Subject: [PATCH 6/8] feat(rail): add versioned rail type storage --- .../rail/configuration/RailTypeManager.java | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index a0f53a67..5729c196 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -23,6 +23,8 @@ public class RailTypeManager { private static final String FILE_PATH = "modules/generator/rail-types.yml"; + private static final String SCHEMA_VERSION_KEY = "schema-version"; + static final int CURRENT_SCHEMA_VERSION = 2; private static final String TYPES_SECTION = "rail-types"; private static final String KEY_DISPLAY_NAME = "display-name"; @@ -59,7 +61,9 @@ public void reload() { if (!file.exists()) return; - ConfigurationSection typesSection = YamlConfiguration.loadConfiguration(file).getConfigurationSection(TYPES_SECTION); + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + migrateStorage(config); + ConfigurationSection typesSection = config.getConfigurationSection(TYPES_SECTION); if (typesSection == null) return; @@ -225,6 +229,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif private @Nullable String save() { YamlConfiguration config = new YamlConfiguration(); + config.set(SCHEMA_VERSION_KEY, CURRENT_SCHEMA_VERSION); for (RailType railType : railTypes.values()) { if (railType.isBuiltIn()) @@ -295,6 +300,59 @@ private List parseMaterials(List values) { return materials; } + private void migrateStorage(YamlConfiguration config) { + int schemaVersion = config.getInt(SCHEMA_VERSION_KEY, 1); + + if (schemaVersion > CURRENT_SCHEMA_VERSION) { + ChatHelper.logError( + "Rail type storage %s uses unsupported schema version %s. Loading known fields without rewriting it.", + FILE_PATH, + schemaVersion + ); + return; + } + + if (schemaVersion >= CURRENT_SCHEMA_VERSION) + return; + + migrateVersionOneToTwo(config); + config.set(SCHEMA_VERSION_KEY, CURRENT_SCHEMA_VERSION); + + try { + config.save(file); + } catch (IOException exception) { + ChatHelper.logError("Could not migrate rail types in %s.", exception, FILE_PATH); + } + } + + private void migrateVersionOneToTwo(YamlConfiguration config) { + ConfigurationSection typesSection = config.getConfigurationSection(TYPES_SECTION); + + if (typesSection == null) + return; + + for (String identifier : typesSection.getKeys(false)) { + ConfigurationSection section = typesSection.getConfigurationSection(identifier); + + if (section == null) + continue; + + setDefault(section, KEY_OVERHEAD_POLES_ENABLED, false); + setDefault(section, KEY_OVERHEAD_POLE_BLOCK, XMaterial.LIGHT_GRAY_CONCRETE.name()); + setDefault(section, KEY_OVERHEAD_POLE_SPACING, RailType.DEFAULT_OVERHEAD_POLE_SPACING); + setDefault(section, KEY_OVERHEAD_POLE_OFFSET, RailType.DEFAULT_OVERHEAD_POLE_OFFSET); + setDefault(section, KEY_OVERHEAD_POLE_HEIGHT, RailType.DEFAULT_OVERHEAD_POLE_HEIGHT); + setDefault(section, KEY_OVERHEAD_WIRES_ENABLED, false); + setDefault(section, KEY_OVERHEAD_WIRE_BLOCK, XMaterial.IRON_BARS.name()); + setDefault(section, KEY_TRACK_SWITCHES_ENABLED, false); + } + } + + private void setDefault(ConfigurationSection section, String path, Object value) { + if (!section.contains(path)) + section.set(path, value); + } + private RailType.Configuration toConfiguration(RailType railType) { return new RailType.Configuration() .identifier(railType.getIdentifier()) From d133d95433a59f5999f593ec820320be7af58707 Mon Sep 17 00:00:00 2001 From: Jasupa Date: Thu, 9 Jul 2026 21:38:14 +0200 Subject: [PATCH 7/8] fix(rail): improve overhead wire connectivity --- .../rail/configuration/RailType.java | 68 +++-- .../rail/configuration/RailTypeManager.java | 32 ++- .../rail/generation/RailOverheadBuilder.java | 242 ++++++++++++++---- .../generation/RailPathOverlapValidator.java | 26 +- .../rail/generation/RailScripts.java | 2 + .../components/rail/generation/RailStep.java | 4 + .../components/rail/menu/RailBlockRole.java | 31 ++- .../rail/menu/RailOverheadWireMenu.java | 95 +++++-- .../components/rail/menu/RailTypeDraft.java | 6 + .../rail/menu/RailTypeEditorMenu.java | 10 +- .../components/rail/menu/RailTypeMenu.java | 95 +++++-- 11 files changed, 493 insertions(+), 118 deletions(-) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java index fa7e8005..cd0e8cba 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -22,7 +22,7 @@ public final class RailType { public static final int MIN_SLEEPER_SPACING = 0; public static final int MAX_SLEEPER_SPACING = 16; public static final int MIN_OVERHEAD_POLE_SPACING = 1; - public static final int MAX_OVERHEAD_POLE_SPACING = 64; + public static final int MAX_OVERHEAD_POLE_SPACING = 512; public static final int MIN_OVERHEAD_POLE_OFFSET = 2; public static final int MAX_OVERHEAD_POLE_OFFSET = 16; public static final int MIN_OVERHEAD_POLE_HEIGHT = 3; @@ -49,6 +49,7 @@ public final class RailType { private final int trackSpacing; private final boolean overheadPolesEnabled; private final @Nullable XMaterial overheadPoleBlock; + private final @Nullable XMaterial overheadSupportBlock; private final int overheadPoleSpacing; private final int overheadPoleOffset; private final int overheadPoleHeight; @@ -71,6 +72,7 @@ public final class RailType { this.trackSpacing = configuration.trackSpacing(); this.overheadPolesEnabled = configuration.overheadPolesEnabled(); this.overheadPoleBlock = configuration.overheadPoleBlock(); + this.overheadSupportBlock = configuration.overheadSupportBlock(); this.overheadPoleSpacing = configuration.overheadPoleSpacing(); this.overheadPoleOffset = configuration.overheadPoleOffset(); this.overheadPoleHeight = configuration.overheadPoleHeight(); @@ -113,6 +115,7 @@ static RailType createStandard() { .trackSpacing(DEFAULT_TRACK_SPACING) .overheadPolesEnabled(false) .overheadPoleBlock(XMaterial.LIGHT_GRAY_CONCRETE) + .overheadSupportBlock(XMaterial.LIGHT_GRAY_CONCRETE) .overheadPoleSpacing(DEFAULT_OVERHEAD_POLE_SPACING) .overheadPoleOffset(DEFAULT_OVERHEAD_POLE_OFFSET) .overheadPoleHeight(DEFAULT_OVERHEAD_POLE_HEIGHT) @@ -215,25 +218,10 @@ static RailType createStandard() { } private static @Nullable String validateOverheadWires(Configuration configuration) { - if (configuration.overheadPolesEnabled()) { - if (!isPlaceableBlock(configuration.overheadPoleBlock())) - return "Overhead pole block must be a valid placeable Minecraft block."; + String poleError = validateOverheadPoles(configuration); - if (configuration.overheadPoleSpacing() < MIN_OVERHEAD_POLE_SPACING - || configuration.overheadPoleSpacing() > MAX_OVERHEAD_POLE_SPACING) - return "Overhead pole spacing must be between %s and %s." - .formatted(MIN_OVERHEAD_POLE_SPACING, MAX_OVERHEAD_POLE_SPACING); - - if (configuration.overheadPoleOffset() < MIN_OVERHEAD_POLE_OFFSET - || configuration.overheadPoleOffset() > MAX_OVERHEAD_POLE_OFFSET) - return "Overhead pole offset must be between %s and %s." - .formatted(MIN_OVERHEAD_POLE_OFFSET, MAX_OVERHEAD_POLE_OFFSET); - - if (configuration.overheadPoleHeight() < MIN_OVERHEAD_POLE_HEIGHT - || configuration.overheadPoleHeight() > MAX_OVERHEAD_POLE_HEIGHT) - return "Overhead pole height must be between %s and %s." - .formatted(MIN_OVERHEAD_POLE_HEIGHT, MAX_OVERHEAD_POLE_HEIGHT); - } + if (poleError != null) + return poleError; if (configuration.overheadWiresEnabled() && !isPlaceableBlock(configuration.overheadWireBlock())) return "Overhead wire block must be a valid placeable Minecraft block."; @@ -241,6 +229,38 @@ static RailType createStandard() { return null; } + private static @Nullable String validateOverheadPoles(Configuration configuration) { + if (!configuration.overheadPolesEnabled()) + return null; + + if (!isPlaceableBlock(configuration.overheadPoleBlock())) + return "Overhead pole block must be a valid placeable Minecraft block."; + + if (!isPlaceableBlock(configuration.overheadSupportBlock())) + return "Overhead support block must be a valid placeable Minecraft block."; + + return validateOverheadPoleMeasurements(configuration); + } + + private static @Nullable String validateOverheadPoleMeasurements(Configuration configuration) { + if (configuration.overheadPoleSpacing() < MIN_OVERHEAD_POLE_SPACING + || configuration.overheadPoleSpacing() > MAX_OVERHEAD_POLE_SPACING) + return "Overhead pole spacing must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_SPACING, MAX_OVERHEAD_POLE_SPACING); + + if (configuration.overheadPoleOffset() < MIN_OVERHEAD_POLE_OFFSET + || configuration.overheadPoleOffset() > MAX_OVERHEAD_POLE_OFFSET) + return "Overhead pole offset must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_OFFSET, MAX_OVERHEAD_POLE_OFFSET); + + if (configuration.overheadPoleHeight() < MIN_OVERHEAD_POLE_HEIGHT + || configuration.overheadPoleHeight() > MAX_OVERHEAD_POLE_HEIGHT) + return "Overhead pole height must be between %s and %s." + .formatted(MIN_OVERHEAD_POLE_HEIGHT, MAX_OVERHEAD_POLE_HEIGHT); + + return null; + } + private static boolean hasInvalidIdentifier(@Nullable String identifier) { return identifier == null || identifier.isBlank() @@ -281,6 +301,7 @@ public static final class Configuration { private int trackSpacing; private boolean overheadPolesEnabled; private @Nullable XMaterial overheadPoleBlock; + private @Nullable XMaterial overheadSupportBlock; private int overheadPoleSpacing = DEFAULT_OVERHEAD_POLE_SPACING; private int overheadPoleOffset = DEFAULT_OVERHEAD_POLE_OFFSET; private int overheadPoleHeight = DEFAULT_OVERHEAD_POLE_HEIGHT; @@ -387,6 +408,15 @@ public Configuration overheadPoleBlock(@Nullable XMaterial overheadPoleBlock) { return this; } + public @Nullable XMaterial overheadSupportBlock() { + return overheadSupportBlock; + } + + public Configuration overheadSupportBlock(@Nullable XMaterial overheadSupportBlock) { + this.overheadSupportBlock = overheadSupportBlock; + return this; + } + public int overheadPoleSpacing() { return overheadPoleSpacing; } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index 5729c196..f524e2ba 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -24,7 +24,7 @@ public class RailTypeManager { private static final String FILE_PATH = "modules/generator/rail-types.yml"; private static final String SCHEMA_VERSION_KEY = "schema-version"; - static final int CURRENT_SCHEMA_VERSION = 2; + static final int CURRENT_SCHEMA_VERSION = 3; private static final String TYPES_SECTION = "rail-types"; private static final String KEY_DISPLAY_NAME = "display-name"; @@ -37,6 +37,7 @@ public class RailTypeManager { private static final String KEY_TRACK_SPACING = "track-spacing"; private static final String KEY_OVERHEAD_POLES_ENABLED = "overhead.poles.enabled"; private static final String KEY_OVERHEAD_POLE_BLOCK = "overhead.poles.block"; + private static final String KEY_OVERHEAD_SUPPORT_BLOCK = "overhead.support.block"; private static final String KEY_OVERHEAD_POLE_SPACING = "overhead.poles.spacing"; private static final String KEY_OVERHEAD_POLE_OFFSET = "overhead.poles.offset"; private static final String KEY_OVERHEAD_POLE_HEIGHT = "overhead.poles.height"; @@ -160,6 +161,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif int trackSpacing = section.getInt(KEY_TRACK_SPACING, RailType.DEFAULT_TRACK_SPACING); boolean overheadPolesEnabled = section.getBoolean(KEY_OVERHEAD_POLES_ENABLED, false); XMaterial overheadPoleBlock = parseMaterial(section.getString(KEY_OVERHEAD_POLE_BLOCK)); + XMaterial overheadSupportBlock = parseMaterial(section.getString(KEY_OVERHEAD_SUPPORT_BLOCK)); int overheadPoleSpacing = section.getInt( KEY_OVERHEAD_POLE_SPACING, RailType.DEFAULT_OVERHEAD_POLE_SPACING @@ -188,6 +190,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .trackSpacing(trackSpacing) .overheadPolesEnabled(overheadPolesEnabled) .overheadPoleBlock(overheadPoleBlock) + .overheadSupportBlock(overheadSupportBlock) .overheadPoleSpacing(overheadPoleSpacing) .overheadPoleOffset(overheadPoleOffset) .overheadPoleHeight(overheadPoleHeight) @@ -219,6 +222,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .trackSpacing(trackSpacing) .overheadPolesEnabled(overheadPolesEnabled) .overheadPoleBlock(overheadPoleBlock) + .overheadSupportBlock(overheadSupportBlock) .overheadPoleSpacing(overheadPoleSpacing) .overheadPoleOffset(overheadPoleOffset) .overheadPoleHeight(overheadPoleHeight) @@ -262,6 +266,11 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif if (overheadPoleBlock != null) config.set(path + "." + KEY_OVERHEAD_POLE_BLOCK, overheadPoleBlock.name()); + XMaterial overheadSupportBlock = railType.getOverheadSupportBlock(); + + if (overheadSupportBlock != null) + config.set(path + "." + KEY_OVERHEAD_SUPPORT_BLOCK, overheadSupportBlock.name()); + XMaterial overheadWireBlock = railType.getOverheadWireBlock(); if (overheadWireBlock != null) @@ -316,6 +325,7 @@ private void migrateStorage(YamlConfiguration config) { return; migrateVersionOneToTwo(config); + migrateVersionTwoToThree(config); config.set(SCHEMA_VERSION_KEY, CURRENT_SCHEMA_VERSION); try { @@ -348,6 +358,25 @@ private void migrateVersionOneToTwo(YamlConfiguration config) { } } + private void migrateVersionTwoToThree(YamlConfiguration config) { + ConfigurationSection typesSection = config.getConfigurationSection(TYPES_SECTION); + + if (typesSection == null) + return; + + for (String identifier : typesSection.getKeys(false)) { + ConfigurationSection section = typesSection.getConfigurationSection(identifier); + + if (section == null) + continue; + + setDefault(section, KEY_OVERHEAD_SUPPORT_BLOCK, section.getString( + KEY_OVERHEAD_POLE_BLOCK, + XMaterial.LIGHT_GRAY_CONCRETE.name() + )); + } + } + private void setDefault(ConfigurationSection section, String path, Object value) { if (!section.contains(path)) section.set(path, value); @@ -366,6 +395,7 @@ private RailType.Configuration toConfiguration(RailType railType) { .trackSpacing(railType.getTrackSpacing()) .overheadPolesEnabled(railType.isOverheadPolesEnabled()) .overheadPoleBlock(railType.getOverheadPoleBlock()) + .overheadSupportBlock(railType.getOverheadSupportBlock()) .overheadPoleSpacing(railType.getOverheadPoleSpacing()) .overheadPoleOffset(railType.getOverheadPoleOffset()) .overheadPoleHeight(railType.getOverheadPoleHeight()) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java index 6414b4bb..d68fc980 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java @@ -3,14 +3,18 @@ import com.alpsbte.alpslib.utils.GeneratorUtils; import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; +import com.fastasyncworldedit.core.registry.state.PropertyKey; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import org.bukkit.util.Vector; +import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; final class RailOverheadBuilder { @@ -26,19 +30,28 @@ final class RailOverheadBuilder { } void addTo(Map blocks, List> railCenterPaths) { + int overheadY = getOverheadY(railCenterPaths); + if (railType.hasOverheadWires()) - addWires(blocks, railCenterPaths); + addWires(blocks, railCenterPaths, overheadY); if (!railType.hasOverheadPoles() || railCenterPaths.isEmpty()) return; - addPoles(blocks, railCenterPaths.getFirst(), -1); + addPortals(blocks, railCenterPaths.getFirst(), railCenterPaths.getLast(), overheadY); + } + + private int getOverheadY(List> railCenterPaths) { + int maxRailY = Integer.MIN_VALUE; + + for (List path : railCenterPaths) + for (Vector point : path) + maxRailY = Math.max(maxRailY, point.getBlockY()); - List lastPath = railCenterPaths.getLast(); - addPoles(blocks, lastPath, 1); + return (maxRailY == Integer.MIN_VALUE ? 0 : maxRailY) + railType.getOverheadPoleHeight(); } - private void addWires(Map blocks, List> railCenterPaths) { + private void addWires(Map blocks, List> railCenterPaths, int overheadY) { XMaterial wireBlock = railType.getOverheadWireBlock(); if (wireBlock == null) @@ -50,60 +63,61 @@ private void addWires(Map blocks, List> ra return; for (List path : railCenterPaths) { - for (int index = 0; index < path.size(); index++) { - Vector center = path.get(index); - Direction direction = getFacing(path, index); - BlockState wireState = createDirectionalState(wireBlockType, direction); - PositionKey wirePosition = PositionKey.of( - center.getBlockX(), - center.getBlockY() + railType.getOverheadPoleHeight(), - center.getBlockZ() - ); - blocks.putIfAbsent(wirePosition, wireState); - } + List wirePath = createOverheadPath(path, overheadY); + addPathBlocks(blocks, wirePath, wireBlockType, false); } } - private void addPoles(Map blocks, List path, int sideSign) { + private void addPortals(Map blocks, List leftPath, List rightPath, int overheadY) { XMaterial poleBlock = railType.getOverheadPoleBlock(); + XMaterial supportBlock = railType.getOverheadSupportBlock(); - if (poleBlock == null) + if (poleBlock == null || supportBlock == null) return; BlockState poleState = GeneratorUtils.getBlockState(poleBlock); + BlockType supportBlockType = Item.convertXMaterialToWEBlockType(supportBlock); - for (int index = 0; index < path.size(); index += railType.getOverheadPoleSpacing()) { - Vector center = path.get(index); - RailStep perpendicular = getPerpendicularStep(path, index, sideSign); - int poleX = center.getBlockX() + perpendicular.dx() * railType.getOverheadPoleOffset(); - int poleZ = center.getBlockZ() + perpendicular.dz() * railType.getOverheadPoleOffset(); - int poleY = terrainResolver.getNearestRailSurfaceY(poleX, poleZ, center.getBlockY()); - int topY = center.getBlockY() + railType.getOverheadPoleHeight(); + if (supportBlockType == null) + return; - for (int y = Math.min(poleY, topY); y <= topY; y++) - blocks.putIfAbsent(PositionKey.of(poleX, y, poleZ), poleState); + for (int index = 0; index < leftPath.size(); index += railType.getOverheadPoleSpacing()) { + Vector leftCenter = leftPath.get(index); + Vector rightCenter = rightPath.get(Math.min(index, rightPath.size() - 1)); + PortalPole leftPole = createPole(leftPath, index, leftCenter, -1); + PortalPole rightPole = createPole(rightPath, Math.min(index, rightPath.size() - 1), rightCenter, 1); - if (railType.hasOverheadWires()) - addSupportArm(blocks, poleX, topY, poleZ, perpendicular, poleState); + addPole(blocks, leftPole.x(), leftPole.z(), leftPole.surfaceY(), overheadY, poleState); + addPole(blocks, rightPole.x(), rightPole.z(), rightPole.surfaceY(), overheadY, poleState); + addPortalSupport(blocks, leftPole.x(), overheadY + 1, leftPole.z(), rightPole.x(), rightPole.z(), supportBlockType); } } - private void addSupportArm( + private PortalPole createPole(List path, int index, Vector center, int sideSign) { + RailStep perpendicular = getPerpendicularStep(path, index, sideSign); + int poleX = center.getBlockX() + perpendicular.dx() * railType.getOverheadPoleOffset(); + int poleZ = center.getBlockZ() + perpendicular.dz() * railType.getOverheadPoleOffset(); + int surfaceY = terrainResolver.getNearestRailSurfaceY(poleX, poleZ, center.getBlockY()); + return new PortalPole(poleX, poleZ, surfaceY); + } + + private void addPole(Map blocks, int poleX, int poleZ, int surfaceY, int topY, BlockState poleState) { + for (int y = Math.min(surfaceY, topY); y <= topY; y++) + blocks.putIfAbsent(PositionKey.of(poleX, y, poleZ), poleState); + } + + private void addPortalSupport( Map blocks, - int poleX, + int startX, int topY, - int poleZ, - RailStep perpendicular, - BlockState poleState + int startZ, + int endX, + int endZ, + BlockType supportBlockType ) { - for (int offset = 1; offset <= railType.getOverheadPoleOffset(); offset++) { - PositionKey armPosition = PositionKey.of( - poleX - perpendicular.dx() * offset, - topY, - poleZ - perpendicular.dz() * offset - ); - blocks.putIfAbsent(armPosition, poleState); - } + Vector start = new Vector(startX, topY, startZ); + Vector end = new Vector(endX, topY, endZ); + addPathBlocks(blocks, createOrthogonalPath(start, end), supportBlockType, true); } private RailStep getPerpendicularStep(List path, int index, int sideSign) { @@ -111,9 +125,144 @@ private RailStep getPerpendicularStep(List path, int index, int sideSign return new RailStep(-direction.dz() * sideSign, direction.dx() * sideSign); } - private Direction getFacing(List path, int index) { - RailStep direction = getStep(path, index); - return GeneratorUtils.getFacing(direction.dx(), direction.dz(), DEFAULT_FACING); + private List createOverheadPath(List path, int overheadY) { + List overheadPoints = path.stream() + .map(point -> new Vector(point.getBlockX(), overheadY, point.getBlockZ())) + .toList(); + + return GeneratorUtils.createShortestBlockPath(overheadPoints); + } + + private List createOrthogonalPath(Vector start, Vector end) { + return GeneratorUtils.createShortestBlockPath(List.of(start, end)); + } + + private void addPathBlocks(Map blocks, List path, BlockType blockType, boolean overwrite) { + Set positions = createConnectedPathPositions(path); + + for (PositionKey position : positions) { + BlockState blockState = createPathState(blockType, position, positions); + + if (overwrite) + blocks.put(position, blockState); + else + blocks.computeIfAbsent(position, ignored -> blockState); + } + } + + private Set createConnectedPathPositions(List path) { + Set positions = new LinkedHashSet<>(); + + for (Vector point : path) + positions.add(PositionKey.from(point)); + + addCornerConnectorPositions(positions, path); + return positions; + } + + private void addCornerConnectorPositions(Set positions, List path) { + for (int index = 1; index < path.size(); index++) { + Vector previous = path.get(index - 1); + Vector current = path.get(index); + RailStep step = getStepBetween(previous, current); + + if (step.dx() == 0 || step.dz() == 0) + continue; + + positions.add(PositionKey.from(new Vector( + previous.getBlockX() + step.dx(), + current.getBlockY(), + previous.getBlockZ() + ))); + positions.add(PositionKey.from(new Vector( + previous.getBlockX(), + current.getBlockY(), + previous.getBlockZ() + step.dz() + ))); + } + } + + private BlockState createPathState(BlockType blockType, PositionKey position, Set positions) { + Set connections = EnumSet.noneOf(Direction.class); + addConnectionIfPresent(connections, positions, position, Direction.NORTH, 0, -1); + addConnectionIfPresent(connections, positions, position, Direction.EAST, 1, 0); + addConnectionIfPresent(connections, positions, position, Direction.SOUTH, 0, 1); + addConnectionIfPresent(connections, positions, position, Direction.WEST, -1, 0); + + if (connections.isEmpty()) + connections.add(DEFAULT_FACING); + + return createConnectedState(blockType, connections); + } + + private void addConnectionIfPresent( + Set connections, + Set positions, + PositionKey position, + Direction direction, + int xOffset, + int zOffset + ) { + if (positions.contains(PositionKey.of(position.x() + xOffset, position.y(), position.z() + zOffset))) + connections.add(direction); + } + + private BlockState createConnectedState(BlockType blockType, Set connections) { + BlockState blockState = blockType.getDefaultState(); + + if (!hasDirectionalConnectionProperties(blockType)) + return createDirectionalSupportState(blockType, connections.iterator().next()); + + blockState = applyConnection(blockState, blockType, PropertyKey.NORTH, connections.contains(Direction.NORTH)); + blockState = applyConnection(blockState, blockType, PropertyKey.EAST, connections.contains(Direction.EAST)); + blockState = applyConnection(blockState, blockType, PropertyKey.SOUTH, connections.contains(Direction.SOUTH)); + blockState = applyConnection(blockState, blockType, PropertyKey.WEST, connections.contains(Direction.WEST)); + + if (hasProperty(blockType, PropertyKey.UP)) + blockState = blockState.with(PropertyKey.UP, true); + + return blockState; + } + + private BlockState createDirectionalSupportState(BlockType blockType, Direction direction) { + BlockState blockState = createDirectionalState(blockType, direction); + + if (!hasProperty(blockType, PropertyKey.TYPE)) + return blockState; + + try { + return blockState.with(PropertyKey.TYPE, "bottom"); + } catch (IllegalArgumentException exception) { + return blockState; + } + } + + private BlockState applyConnection(BlockState blockState, BlockType blockType, PropertyKey key, boolean connected) { + if (!hasProperty(blockType, key)) + return blockState; + + try { + return blockState.with(key, connected); + } catch (IllegalArgumentException exception) { + return blockState.with(key, connected ? "low" : "none"); + } + } + + private boolean hasDirectionalConnectionProperties(BlockType blockType) { + return hasProperty(blockType, PropertyKey.NORTH) + || hasProperty(blockType, PropertyKey.EAST) + || hasProperty(blockType, PropertyKey.SOUTH) + || hasProperty(blockType, PropertyKey.WEST); + } + + private boolean hasProperty(BlockType blockType, PropertyKey key) { + return blockType.getPropertyMap().containsKey(key.getName()); + } + + private RailStep getStepBetween(Vector from, Vector to) { + int xDirection = Integer.compare(to.getBlockX() - from.getBlockX(), 0); + int zDirection = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); + return new RailStep(xDirection, zDirection); } private RailStep getStep(List path, int index) { @@ -134,4 +283,7 @@ private BlockState createDirectionalState(BlockType blockType, Direction directi return GeneratorUtils.getBlockStateWithFacing(blockType, direction); } + + private record PortalPole(int x, int z, int surfaceY) { + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java index 85d4f753..95f23846 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java @@ -8,7 +8,6 @@ final class RailPathOverlapValidator { - private static final int MINIMUM_TRACK_CENTER_DISTANCE = 3; private static final int MAXIMUM_VERTICAL_DIFFERENCE = 1; boolean hasOverlap(List> railCenterPaths) { @@ -20,7 +19,7 @@ boolean hasOverlap(List> railCenterPaths) { for (Vector center : railCenterPath) { PositionKey centerPosition = PositionKey.from(center); - if (!currentLanePositions.add(centerPosition) || isTooCloseToPreviousLane(centerPosition, previousLanePositions)) + if (!currentLanePositions.add(centerPosition) || crossesPreviousLane(centerPosition, previousLanePositions)) return true; } @@ -30,23 +29,12 @@ boolean hasOverlap(List> railCenterPaths) { return false; } - private boolean isTooCloseToPreviousLane(PositionKey center, Set previousLanePositions) { - for (int xOffset = -2; xOffset <= 2; xOffset++) { - for (int zOffset = -2; zOffset <= 2; zOffset++) { - if (xOffset * xOffset + zOffset * zOffset >= MINIMUM_TRACK_CENTER_DISTANCE * MINIMUM_TRACK_CENTER_DISTANCE) - continue; - - for (int yOffset = -MAXIMUM_VERTICAL_DIFFERENCE; yOffset <= MAXIMUM_VERTICAL_DIFFERENCE; yOffset++) { - PositionKey nearbyPosition = PositionKey.of( - center.x() + xOffset, - center.y() + yOffset, - center.z() + zOffset - ); - - if (previousLanePositions.contains(nearbyPosition)) - return true; - } - } + private boolean crossesPreviousLane(PositionKey center, Set previousLanePositions) { + for (int yOffset = -MAXIMUM_VERTICAL_DIFFERENCE; yOffset <= MAXIMUM_VERTICAL_DIFFERENCE; yOffset++) { + PositionKey sameColumnPosition = PositionKey.of(center.x(), center.y() + yOffset, center.z()); + + if (previousLanePositions.contains(sameColumnPosition)) + return true; } return false; diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java index 17ae0219..7e9ab3d6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java @@ -216,6 +216,8 @@ private void queueRailBlockPlacements(Map railBlocks) { List positions = new ArrayList<>(limits.blockPlacementBatchSize()); List blockStates = new ArrayList<>(limits.blockPlacementBatchSize()); + createCommand("//perf neighbors on"); + for (Map.Entry entry : railBlocks.entrySet()) { positions.add(entry.getKey().toVector()); blockStates.add(entry.getValue()); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java index 04686e39..e6cc6472 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailStep.java @@ -1,4 +1,8 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; record RailStep(int dx, int dz) { + + RailStep opposite() { + return new RailStep(-dx, -dz); + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java index 35a86031..46a591a5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java @@ -63,10 +63,22 @@ void apply(RailTypeDraft draft, XMaterial material) { } }, + ICON("Choose a Rail Type Icon") { + @Override + List createChoices() { + return MenuItems.getBlocksByColor(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setIcon(material); + } + }, + OVERHEAD_POLE_BLOCK("Choose an Overhead Pole Block") { @Override List createChoices() { - return MenuItems.getSolidBlocks(); + return MenuItems.getBlocksByColor(); } @Override @@ -80,6 +92,23 @@ boolean isOverheadSetting() { } }, + OVERHEAD_SUPPORT_BLOCK("Choose a Top Support Block") { + @Override + List createChoices() { + return MenuItems.getBlocksByColor(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setOverheadSupportBlock(material); + } + + @Override + boolean isOverheadSetting() { + return true; + } + }, + OVERHEAD_WIRE_BLOCK("Choose an Overhead Wire Block") { @Override List createChoices() { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java index 11515cbf..9786f650 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java @@ -1,13 +1,16 @@ package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; +import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import net.buildtheearth.buildteamtools.utils.MenuItems; import net.buildtheearth.buildteamtools.utils.heads.HeadColor; import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; +import net.wesjd.anvilgui.AnvilGUI; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Material; import org.bukkit.Sound; @@ -29,6 +32,7 @@ final class RailOverheadWireMenu extends AbstractMenu { private static final int POLES_TOGGLE_SLOT = 10; private static final int POLE_BLOCK_SLOT = 16; + private static final int SUPPORT_BLOCK_SLOT = 22; private static final int WIRES_TOGGLE_SLOT = 19; private static final int WIRE_BLOCK_SLOT = 25; private static final int POLE_SPACING_SLOT = 28; @@ -66,20 +70,16 @@ protected void setPreviewItems() { "Pole Block", draft.getOverheadPoleBlock() )); + getMenu().getSlot(SUPPORT_BLOCK_SLOT).setItem(createBlockItem( + "Top Support Block", + draft.getOverheadSupportBlock() + )); getMenu().getSlot(WIRE_BLOCK_SLOT).setItem(createBlockItem( "Wire Block", draft.getOverheadWireBlock() )); - createCounter( - HeadColor.WHITE, - POLE_SPACING_SLOT, - "Pole Spacing", - draft.getOverheadPoleSpacing(), - RailType.MIN_OVERHEAD_POLE_SPACING, - RailType.MAX_OVERHEAD_POLE_SPACING, - BLOCKS_UNIT - ); + getMenu().getSlot(POLE_SPACING_SLOT).setItem(createSpacingItem()); createCounter( HeadColor.LIGHT_GRAY, POLE_OFFSET_SLOT, @@ -126,14 +126,9 @@ protected void setItemClickEventsAsync() { }); setBlockPickerClickEvent(POLE_BLOCK_SLOT, RailBlockRole.OVERHEAD_POLE_BLOCK); + setBlockPickerClickEvent(SUPPORT_BLOCK_SLOT, RailBlockRole.OVERHEAD_SUPPORT_BLOCK); setBlockPickerClickEvent(WIRE_BLOCK_SLOT, RailBlockRole.OVERHEAD_WIRE_BLOCK); - setCounterClickEvents( - POLE_SPACING_SLOT, - RailType.MIN_OVERHEAD_POLE_SPACING, - RailType.MAX_OVERHEAD_POLE_SPACING, - draft::getOverheadPoleSpacing, - draft::setOverheadPoleSpacing - ); + getMenu().getSlot(POLE_SPACING_SLOT).setClickHandler((player, click) -> openSpacingEditor(player)); setCounterClickEvents( POLE_OFFSET_SLOT, RailType.MIN_OVERHEAD_POLE_OFFSET, @@ -229,6 +224,70 @@ private ItemStack createBlockItem(String name, XMaterial material) { ); } + private ItemStack createSpacingItem() { + return Item.create( + Objects.requireNonNull(XMaterial.NAME_TAG.get()), + yellow("Pole Spacing: ") + white(draft.getOverheadPoleSpacing() + " " + BLOCKS_UNIT), + List.of( + gray("Click to enter a custom spacing."), + gray("Valid range: ") + white(RailType.MIN_OVERHEAD_POLE_SPACING + + "-" + RailType.MAX_OVERHEAD_POLE_SPACING + " " + BLOCKS_UNIT) + ) + ); + } + + private void openSpacingEditor(Player clickPlayer) { + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new AnvilGUI.Builder() + .onClick((slot, stateSnapshot) -> { + if (slot != AnvilGUI.Slot.OUTPUT) + return List.of(); + + String input = stateSnapshot.getText().trim(); + Integer spacing = parseSpacing(input); + + if (spacing == null) { + stateSnapshot.getPlayer().sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Pole spacing must be a number between %s and %s.", + RailType.MIN_OVERHEAD_POLE_SPACING, + RailType.MAX_OVERHEAD_POLE_SPACING + ))); + return List.of(AnvilGUI.ResponseAction.replaceInputText(String.valueOf(draft.getOverheadPoleSpacing()))); + } + + draft.setOverheadPoleSpacing(spacing); + playSound(stateSnapshot.getPlayer(), Sound.UI_BUTTON_CLICK); + stateSnapshot.getPlayer().sendMessage(ChatHelper.getStandardComponent( + true, + "Pole spacing set to %s blocks.", + spacing + )); + return List.of( + AnvilGUI.ResponseAction.close(), + AnvilGUI.ResponseAction.run(() -> new RailOverheadWireMenu(clickPlayer, draft, true)) + ); + }) + .text(String.valueOf(draft.getOverheadPoleSpacing())) + .itemLeft(Item.create(Objects.requireNonNull(XMaterial.NAME_TAG.get()), yellow("Change Pole Spacing"))) + .title(darkGray("Change pole spacing")) + .plugin(BuildTeamTools.getInstance()) + .open(clickPlayer); + } + + private Integer parseSpacing(String input) { + try { + int spacing = Integer.parseInt(input); + + if (spacing < RailType.MIN_OVERHEAD_POLE_SPACING || spacing > RailType.MAX_OVERHEAD_POLE_SPACING) + return null; + + return spacing; + } catch (NumberFormatException exception) { + return null; + } + } + private void playSound(Player player, Sound sound) { player.playSound(player, sound, 1.0F, 1.0F); } @@ -237,6 +296,10 @@ private static String gray(String text) { return RailMenuText.color(NamedTextColor.GRAY, text); } + private static String darkGray(String text) { + return RailMenuText.color(NamedTextColor.DARK_GRAY, text); + } + private static String white(String text) { return RailMenuText.color(NamedTextColor.WHITE, text); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java index fd76fe77..5d639347 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -16,6 +16,7 @@ class RailTypeDraft { private String identifier; private String displayName; + private XMaterial icon = XMaterial.RAIL; private XMaterial railBlock = XMaterial.ANVIL; private XMaterial blockBelow = XMaterial.GRAVEL; private XMaterial sleeperBlock = XMaterial.SPRUCE_PLANKS; @@ -24,6 +25,7 @@ class RailTypeDraft { private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; private boolean overheadPolesEnabled; private XMaterial overheadPoleBlock = XMaterial.LIGHT_GRAY_CONCRETE; + private XMaterial overheadSupportBlock = XMaterial.LIGHT_GRAY_CONCRETE; private int overheadPoleSpacing = RailType.DEFAULT_OVERHEAD_POLE_SPACING; private int overheadPoleOffset = RailType.DEFAULT_OVERHEAD_POLE_OFFSET; private int overheadPoleHeight = RailType.DEFAULT_OVERHEAD_POLE_HEIGHT; @@ -40,6 +42,7 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.displayName = railType.getDisplayName(); } + draft.icon = railType.getIcon(); draft.railBlock = railType.getRailBlock(); draft.blockBelow = blocksBelow.isEmpty() ? XMaterial.GRAVEL : blocksBelow.getFirst(); draft.sleeperBlock = railType.getSleeperBlock() == null ? XMaterial.SPRUCE_PLANKS : railType.getSleeperBlock(); @@ -50,6 +53,9 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.overheadPoleBlock = railType.getOverheadPoleBlock() == null ? XMaterial.LIGHT_GRAY_CONCRETE : railType.getOverheadPoleBlock(); + draft.overheadSupportBlock = railType.getOverheadSupportBlock() == null + ? XMaterial.LIGHT_GRAY_CONCRETE + : railType.getOverheadSupportBlock(); draft.overheadPoleSpacing = railType.getOverheadPoleSpacing(); draft.overheadPoleOffset = railType.getOverheadPoleOffset(); draft.overheadPoleHeight = railType.getOverheadPoleHeight(); diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java index 7b855eff..61505cf6 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -50,6 +50,7 @@ public class RailTypeEditorMenu extends AbstractMenu { private static final int RAIL_BLOCK_SLOT = 7; private static final int BLOCK_BELOW_SLOT = 16; private static final int SLEEPER_BLOCK_SLOT = 25; + private static final int ICON_SLOT = 23; private static final int OVERHEAD_SETTINGS_SLOT = 32; private static final int TRACK_SWITCHES_SLOT = 30; @@ -101,6 +102,11 @@ protected void setPreviewItems() { draft.getSleeperBlock(), "Sleeper Spacing 0 disables sleepers." )); + getMenu().getSlot(ICON_SLOT).setItem(createBlockItem( + "Menu Icon", + draft.getIcon(), + "The icon shown in the rail type menu." + )); getMenu().getSlot(OVERHEAD_SETTINGS_SLOT).setItem(Item.create( Objects.requireNonNull(XMaterial.IRON_BARS.get()), yellow("Overhead Settings"), @@ -154,6 +160,7 @@ protected void setItemClickEventsAsync() { setBlockPickerClickEvents(RAIL_BLOCK_SLOT, RailBlockRole.RAIL_BLOCK); setBlockPickerClickEvents(BLOCK_BELOW_SLOT, RailBlockRole.BLOCK_BELOW); setBlockPickerClickEvents(SLEEPER_BLOCK_SLOT, RailBlockRole.SLEEPER_BLOCK); + setBlockPickerClickEvents(ICON_SLOT, RailBlockRole.ICON); getMenu().getSlot(TRACK_SWITCHES_SLOT).setClickHandler((clickPlayer, clickInformation) -> { draft.setTrackSwitchesEnabled(!draft.isTrackSwitchesEnabled()); playSound(clickPlayer, Sound.UI_BUTTON_CLICK); @@ -236,7 +243,7 @@ private void saveRailType(Player clickPlayer) { RailType railType = RailType.createCustom(new RailType.Configuration() .identifier(identifier) .displayName(displayName) - .icon(draft.getRailBlock()) + .icon(draft.getIcon()) .railBlock(draft.getRailBlock()) .blocksBelow(List.of(draft.getBlockBelow())) .sleeperBlock(draft.getSleeperBlock()) @@ -245,6 +252,7 @@ private void saveRailType(Player clickPlayer) { .trackSpacing(draft.getTrackSpacing()) .overheadPolesEnabled(draft.isOverheadPolesEnabled()) .overheadPoleBlock(draft.getOverheadPoleBlock()) + .overheadSupportBlock(draft.getOverheadSupportBlock()) .overheadPoleSpacing(draft.getOverheadPoleSpacing()) .overheadPoleOffset(draft.getOverheadPoleOffset()) .overheadPoleHeight(draft.getOverheadPoleHeight()) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index 2efd4998..1980056b 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -22,7 +22,9 @@ import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -59,6 +61,8 @@ private void preselectCurrentRailType(Player player) { if (value instanceof RailType railType && RailType.byString(railType.getIdentifier()) != null) selectedNames.add(railType.getIdentifier()); + else if (value instanceof String identifier && RailType.byString(identifier) != null) + selectedNames.add(identifier); } private static @NonNull List> getRailTypes() { @@ -104,6 +108,11 @@ private static ItemStack createRailTypeItem(RailType railType) { lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() ? RailMenuText.ENABLED : RailMenuText.DISABLED)); + if (railType.hasOverheadPoles()) { + lore.add(gray("Pole Block: ") + white(formatMaterial(railType.getOverheadPoleBlock()))); + lore.add(gray("Top Support: ") + white(formatMaterial(railType.getOverheadSupportBlock()))); + lore.add(gray("Pole Spacing: ") + white(String.valueOf(railType.getOverheadPoleSpacing()))); + } lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() ? RailMenuText.ENABLED : RailMenuText.DISABLED)); @@ -173,11 +182,7 @@ protected void setPaginatedPreviewItems(@NonNull List source) { ItemStack displayedItem = item.getLeft(); if (selectedNames.contains(item.getRight())) - displayedItem = new Item(displayedItem.clone()) - .setAmount(1) - .addEnchantment(Enchantment.LUCK_OF_THE_SEA, 1) - .hideEnchantments(true) - .build(); + displayedItem = addSelectionGlow(displayedItem); getMenu().getSlot(slot).setItem(displayedItem); slot++; @@ -207,12 +212,20 @@ protected void setPaginatedItemClickEventsAsync(@NonNull List source) { if (selectedNames.contains(type)) { selectedNames.remove(type); - clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Deselected rail type '%s'.", item.getRight())); + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Deselected rail type '%s'.", + getRailTypeDisplayName(type) + )); playSound(clickPlayer, Sound.UI_BUTTON_CLICK); } else { selectedNames.clear(); selectedNames.add(type); - clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Selected rail type '%s'.", item.getRight())); + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Selected rail type '%s'.", + getRailTypeDisplayName(type) + )); playSound(clickPlayer, Sound.UI_BUTTON_CLICK); } @@ -235,6 +248,32 @@ private List> getPageItems(List source) { return items; } + private ItemStack addSelectionGlow(ItemStack item) { + ItemStack displayedItem = item.clone(); + displayedItem.setAmount(1); + displayedItem.addUnsafeEnchantment(Enchantment.LUCK_OF_THE_SEA, 1); + + ItemMeta meta = displayedItem.getItemMeta(); + + if (meta == null) + return displayedItem; + + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + displayedItem.setItemMeta(meta); + + return displayedItem; + } + + private String getRailTypeDisplayName(String identifier) { + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return identifier; + + RailType railType = rail.getRailTypeManager().byString(identifier); + return railType == null ? identifier : railType.getDisplayName(); + } + private void openEditorForRailType(Player clickPlayer, String identifier) { Rail rail = GeneratorModule.getInstance().getRail(); @@ -345,19 +384,43 @@ protected void setItemClickEventsAsync() { }); if (canProceed()) - getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { - Settings settings = GeneratorModule.getInstance().getRail().getPlayerSettings().get(clickPlayer.getUniqueId()); + getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> generateWithSelectedType(clickPlayer)); + } - if (!(settings instanceof RailSettings railSettings)) - return; + private void generateWithSelectedType(Player clickPlayer) { + if (selectedNames.isEmpty()) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Select a rail type before generating." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + reloadMenuAsync(); + return; + } - railSettings.setValue(RailFlag.RAIL_TYPE, selectedNames.getFirst()); + Rail rail = GeneratorModule.getInstance().getRail(); - clickPlayer.closeInventory(); - playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + if (rail == null) + return; - GeneratorModule.getInstance().getRail().generate(clickPlayer); - }); + Settings settings = rail.getPlayerSettings().get(clickPlayer.getUniqueId()); + + if (!(settings instanceof RailSettings railSettings)) + return; + + RailType railType = RailType.byString(selectedNames.getFirst()); + + if (railType == null) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "This rail type no longer exists." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + railSettings.setValue(RailFlag.RAIL_TYPE, railType); + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + rail.generate(clickPlayer); } private void setRailTypePageItems() { From 202eefd24af6a84547543288504fde16cd168c7c Mon Sep 17 00:00:00 2001 From: Jasupa Date: Fri, 10 Jul 2026 11:12:19 +0200 Subject: [PATCH 8/8] fix(rail/menu): rail type menu optimisations (enchantments, batch delete, search, way more blocks to select from, new menu to adjust spacing between certain track-"id's"), adjustments for better overhead wire connectivity and rails and overhead wire poles can now be placed directly besides the tracks --- build.gradle.kts | 20 +- .../rail/configuration/RailType.java | 47 +++- .../rail/configuration/RailTypeManager.java | 62 ++++- .../rail/generation/RailBlockBuilder.java | 64 +++-- .../rail/generation/RailLanePathBuilder.java | 178 +++++------- .../rail/generation/RailLimits.java | 58 +++- .../rail/generation/RailOverheadBuilder.java | 155 +++++++++-- .../generation/RailPathOverlapValidator.java | 59 +++- .../rail/generation/RailScripts.java | 40 ++- .../components/rail/menu/RailBlockRole.java | 35 +-- .../rail/menu/RailTrackSpacingMenu.java | 105 ++++++++ .../components/rail/menu/RailTypeDraft.java | 35 ++- .../rail/menu/RailTypeEditorMenu.java | 15 ++ .../components/rail/menu/RailTypeMenu.java | 255 +++++++++++------- .../rail/menu/RailTypeMenuItems.java | 221 +++++++++++++++ .../resources/modules/generator/config.yml | 12 +- 16 files changed, 1042 insertions(+), 319 deletions(-) create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTrackSpacingMenu.java create mode 100644 src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenuItems.java diff --git a/build.gradle.kts b/build.gradle.kts index 9dae114c..2eaba239 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(libs.clipper2) implementation(libs.json) implementation(libs.googlecode.gson) + implementation("org.apache.commons:commons-lang3:3.20.0") implementation(libs.okhttp.jvm) implementation(libs.javaapiforkml) { exclude(group = "com.sun.xml.bind", module = "jaxb-xjc") // Else Remapping will yell of duplicated classes @@ -99,12 +100,21 @@ tasks.withType { tasks.shadowJar { archiveClassifier = "" - relocationPrefix = "net.buildtheearth.buildteamtools.shaded" - enableAutoRelocation = true + // Relocate only known Java libraries. Broad/automatic relocation also opens + // Kotlin classes with ASM and can overflow their large metadata annotations. + enableAutoRelocation = false + enableKotlinModuleRemapping = false - // Prevents the plugin itself from being relocated by autorelocate - // This is needed because net.buildtheearth.projection shares the same package namespace - relocate("net.buildtheearth.buildteamtools", "net.buildtheearth.buildteamtools") + val shadePrefix = "net.buildtheearth.buildteamtools.shaded" + relocate("clipper2", "$shadePrefix.clipper2") + relocate("com.alpsbte.alpslib", "$shadePrefix.alpslib") + relocate("com.cryptomorin.xseries", "$shadePrefix.xseries") + relocate("com.google.gson", "$shadePrefix.gson") + relocate("de.micromata.opengis.kml", "$shadePrefix.kml") + relocate("org.apache.commons.lang3", "$shadePrefix.commonslang3") + relocate("org.bstats", "$shadePrefix.bstats") + relocate("org.ipvp.canvas", "$shadePrefix.canvas") + relocate("org.json", "$shadePrefix.json") } tasks.assemble { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java index cd0e8cba..92ee3d93 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -23,14 +23,14 @@ public final class RailType { public static final int MAX_SLEEPER_SPACING = 16; public static final int MIN_OVERHEAD_POLE_SPACING = 1; public static final int MAX_OVERHEAD_POLE_SPACING = 512; - public static final int MIN_OVERHEAD_POLE_OFFSET = 2; + public static final int MIN_OVERHEAD_POLE_OFFSET = 1; public static final int MAX_OVERHEAD_POLE_OFFSET = 16; public static final int MIN_OVERHEAD_POLE_HEIGHT = 3; public static final int MAX_OVERHEAD_POLE_HEIGHT = 16; public static final int MAX_IDENTIFIER_LENGTH = 32; public static final int MAX_DISPLAY_NAME_LENGTH = 48; - public static final int DEFAULT_TRACK_COUNT = 1; + public static final int DEFAULT_TRACK_COUNT = 2; public static final int DEFAULT_TRACK_SPACING = 5; public static final int DEFAULT_OVERHEAD_POLE_SPACING = 16; public static final int DEFAULT_OVERHEAD_POLE_OFFSET = 3; @@ -47,6 +47,7 @@ public final class RailType { private final int sleeperSpacing; private final int trackCount; private final int trackSpacing; + private final List trackSpacings; private final boolean overheadPolesEnabled; private final @Nullable XMaterial overheadPoleBlock; private final @Nullable XMaterial overheadSupportBlock; @@ -70,6 +71,11 @@ public final class RailType { this.sleeperSpacing = configuration.sleeperSpacing(); this.trackCount = configuration.trackCount(); this.trackSpacing = configuration.trackSpacing(); + this.trackSpacings = normalizeTrackSpacings( + configuration.trackSpacings(), + configuration.trackCount(), + configuration.trackSpacing() + ); this.overheadPolesEnabled = configuration.overheadPolesEnabled(); this.overheadPoleBlock = configuration.overheadPoleBlock(); this.overheadSupportBlock = configuration.overheadSupportBlock(); @@ -113,13 +119,14 @@ static RailType createStandard() { .sleeperSpacing(0) .trackCount(DEFAULT_TRACK_COUNT) .trackSpacing(DEFAULT_TRACK_SPACING) - .overheadPolesEnabled(false) + .trackSpacings(List.of(DEFAULT_TRACK_SPACING)) + .overheadPolesEnabled(true) .overheadPoleBlock(XMaterial.LIGHT_GRAY_CONCRETE) .overheadSupportBlock(XMaterial.LIGHT_GRAY_CONCRETE) .overheadPoleSpacing(DEFAULT_OVERHEAD_POLE_SPACING) .overheadPoleOffset(DEFAULT_OVERHEAD_POLE_OFFSET) .overheadPoleHeight(DEFAULT_OVERHEAD_POLE_HEIGHT) - .overheadWiresEnabled(false) + .overheadWiresEnabled(true) .overheadWireBlock(XMaterial.IRON_BARS) .trackSwitchesEnabled(false), true); } @@ -214,9 +221,31 @@ static RailType createStandard() { if (trackSpacing < MIN_TRACK_SPACING || trackSpacing > MAX_TRACK_SPACING) return "Track spacing must be between %s and %s.".formatted(MIN_TRACK_SPACING, MAX_TRACK_SPACING); + List trackSpacings = configuration.trackSpacings(); + + if (trackSpacings == null) + return null; + + if (trackSpacings.size() != Math.max(0, trackCount - 1)) + return "Rail type needs exactly one spacing value between every pair of tracks."; + + for (Integer spacing : trackSpacings) + if (spacing == null || spacing < MIN_TRACK_SPACING || spacing > MAX_TRACK_SPACING) + return "Every track spacing must be between %s and %s." + .formatted(MIN_TRACK_SPACING, MAX_TRACK_SPACING); + return null; } + private static List normalizeTrackSpacings(@Nullable List configured, int trackCount, int fallback) { + int spacingCount = Math.max(0, trackCount - 1); + + if (configured != null && configured.size() == spacingCount) + return List.copyOf(configured); + + return java.util.Collections.nCopies(spacingCount, fallback); + } + private static @Nullable String validateOverheadWires(Configuration configuration) { String poleError = validateOverheadPoles(configuration); @@ -299,6 +328,7 @@ public static final class Configuration { private int sleeperSpacing; private int trackCount; private int trackSpacing; + private @Nullable List trackSpacings; private boolean overheadPolesEnabled; private @Nullable XMaterial overheadPoleBlock; private @Nullable XMaterial overheadSupportBlock; @@ -390,6 +420,15 @@ public Configuration trackSpacing(int trackSpacing) { return this; } + public @Nullable List trackSpacings() { + return trackSpacings; + } + + public Configuration trackSpacings(@Nullable List trackSpacings) { + this.trackSpacings = trackSpacings; + return this; + } + public boolean overheadPolesEnabled() { return overheadPolesEnabled; } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java index f524e2ba..9e4500e4 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -24,7 +24,7 @@ public class RailTypeManager { private static final String FILE_PATH = "modules/generator/rail-types.yml"; private static final String SCHEMA_VERSION_KEY = "schema-version"; - static final int CURRENT_SCHEMA_VERSION = 3; + static final int CURRENT_SCHEMA_VERSION = 4; private static final String TYPES_SECTION = "rail-types"; private static final String KEY_DISPLAY_NAME = "display-name"; @@ -35,6 +35,7 @@ public class RailTypeManager { private static final String KEY_SLEEPER_SPACING = "sleeper-spacing"; private static final String KEY_TRACK_COUNT = "track-count"; private static final String KEY_TRACK_SPACING = "track-spacing"; + private static final String KEY_TRACK_SPACINGS = "track-spacings"; private static final String KEY_OVERHEAD_POLES_ENABLED = "overhead.poles.enabled"; private static final String KEY_OVERHEAD_POLE_BLOCK = "overhead.poles.block"; private static final String KEY_OVERHEAD_SUPPORT_BLOCK = "overhead.support.block"; @@ -147,6 +148,39 @@ public String getNextCustomIdentifier() { return save(); } + /** + * Deletes multiple custom types atomically from the in-memory collection and + * writes the storage file once. Built-in or missing identifiers abort the operation. + */ + public @Nullable String deleteRailTypes(Collection identifiers) { + List typesToDelete = new ArrayList<>(); + + for (String identifier : identifiers) { + RailType railType = railTypes.get(identifier.toLowerCase(Locale.ROOT)); + + if (railType == null) + return "The rail type '%s' does not exist.".formatted(identifier); + + if (railType.isBuiltIn()) + return "The rail type '%s' is built-in and cannot be deleted.".formatted(identifier); + + typesToDelete.add(railType); + } + + for (RailType railType : typesToDelete) + railTypes.remove(railType.getIdentifier()); + + String error = save(); + + if (error == null) + return null; + + for (RailType railType : typesToDelete) + railTypes.put(railType.getIdentifier(), railType); + + return error; + } + private void loadRailType(@Nullable ConfigurationSection section, String identifier) { if (section == null) return; @@ -159,6 +193,9 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif int sleeperSpacing = section.getInt(KEY_SLEEPER_SPACING, RailType.MIN_SLEEPER_SPACING); int trackCount = section.getInt(KEY_TRACK_COUNT, RailType.DEFAULT_TRACK_COUNT); int trackSpacing = section.getInt(KEY_TRACK_SPACING, RailType.DEFAULT_TRACK_SPACING); + List trackSpacings = section.contains(KEY_TRACK_SPACINGS) + ? section.getIntegerList(KEY_TRACK_SPACINGS) + : Collections.nCopies(Math.max(0, trackCount - 1), trackSpacing); boolean overheadPolesEnabled = section.getBoolean(KEY_OVERHEAD_POLES_ENABLED, false); XMaterial overheadPoleBlock = parseMaterial(section.getString(KEY_OVERHEAD_POLE_BLOCK)); XMaterial overheadSupportBlock = parseMaterial(section.getString(KEY_OVERHEAD_SUPPORT_BLOCK)); @@ -188,6 +225,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .sleeperSpacing(sleeperSpacing) .trackCount(trackCount) .trackSpacing(trackSpacing) + .trackSpacings(trackSpacings) .overheadPolesEnabled(overheadPolesEnabled) .overheadPoleBlock(overheadPoleBlock) .overheadSupportBlock(overheadSupportBlock) @@ -220,6 +258,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif .sleeperSpacing(sleeperSpacing) .trackCount(trackCount) .trackSpacing(trackSpacing) + .trackSpacings(trackSpacings) .overheadPolesEnabled(overheadPolesEnabled) .overheadPoleBlock(overheadPoleBlock) .overheadSupportBlock(overheadSupportBlock) @@ -254,6 +293,7 @@ private void loadRailType(@Nullable ConfigurationSection section, String identif config.set(path + "." + KEY_SLEEPER_SPACING, railType.getSleeperSpacing()); config.set(path + "." + KEY_TRACK_COUNT, railType.getTrackCount()); config.set(path + "." + KEY_TRACK_SPACING, railType.getTrackSpacing()); + config.set(path + "." + KEY_TRACK_SPACINGS, railType.getTrackSpacings()); config.set(path + "." + KEY_OVERHEAD_POLES_ENABLED, railType.isOverheadPolesEnabled()); config.set(path + "." + KEY_OVERHEAD_POLE_SPACING, railType.getOverheadPoleSpacing()); config.set(path + "." + KEY_OVERHEAD_POLE_OFFSET, railType.getOverheadPoleOffset()); @@ -326,6 +366,7 @@ private void migrateStorage(YamlConfiguration config) { migrateVersionOneToTwo(config); migrateVersionTwoToThree(config); + migrateVersionThreeToFour(config); config.set(SCHEMA_VERSION_KEY, CURRENT_SCHEMA_VERSION); try { @@ -377,6 +418,24 @@ private void migrateVersionTwoToThree(YamlConfiguration config) { } } + private void migrateVersionThreeToFour(YamlConfiguration config) { + ConfigurationSection typesSection = config.getConfigurationSection(TYPES_SECTION); + + if (typesSection == null) + return; + + for (String identifier : typesSection.getKeys(false)) { + ConfigurationSection section = typesSection.getConfigurationSection(identifier); + + if (section == null || section.contains(KEY_TRACK_SPACINGS)) + continue; + + int trackCount = section.getInt(KEY_TRACK_COUNT, RailType.DEFAULT_TRACK_COUNT); + int trackSpacing = section.getInt(KEY_TRACK_SPACING, RailType.DEFAULT_TRACK_SPACING); + section.set(KEY_TRACK_SPACINGS, Collections.nCopies(Math.max(0, trackCount - 1), trackSpacing)); + } + } + private void setDefault(ConfigurationSection section, String path, Object value) { if (!section.contains(path)) section.set(path, value); @@ -393,6 +452,7 @@ private RailType.Configuration toConfiguration(RailType railType) { .sleeperSpacing(railType.getSleeperSpacing()) .trackCount(railType.getTrackCount()) .trackSpacing(railType.getTrackSpacing()) + .trackSpacings(railType.getTrackSpacings()) .overheadPolesEnabled(railType.isOverheadPolesEnabled()) .overheadPoleBlock(railType.getOverheadPoleBlock()) .overheadSupportBlock(railType.getOverheadSupportBlock()) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java index 244a4eab..c48ee32f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java @@ -24,6 +24,7 @@ final class RailBlockBuilder { private static final Direction DEFAULT_FACING = Direction.EAST; private static final String FACING_PROPERTY = "facing"; private static final String SHAPE_PROPERTY = "shape"; + private static final int FACING_SMOOTHING_RADIUS = 3; // Sleeper ends stick out one block beyond the rails, which sit at offset 1 from the track center. private static final int SLEEPER_SIDE_OFFSET = 2; @@ -59,8 +60,9 @@ Map build(List> railCenterPaths) { for (List railCenterPath : railCenterPaths) { for (int index = 0; index < railCenterPath.size(); index++) { Vector center = railCenterPath.get(index); + Direction facing = getSmoothedFacing(railCenterPath, index); - for (RailSidePlacement sidePlacement : getSidePlacements(railCenterPath, index)) + for (RailSidePlacement sidePlacement : getSidePlacements(railCenterPath, index, facing)) addSideBlock(sideBlocks, center, sidePlacement, centerPositions); processedPathPoints++; @@ -147,43 +149,70 @@ private int getTotalPathPointCount(List> railCenterPaths) { return Math.max(1, totalPathPoints); } - private List getSidePlacements(List path, int index) { + private static List getSidePlacements(List path, int index, Direction facing) { List placements = new ArrayList<>(); Vector center = path.get(index); RailStep previousStep = index > 0 ? getStep(path.get(index - 1), center) : null; RailStep nextStep = index < path.size() - 1 ? getStep(center, path.get(index + 1)) : null; - addSidePlacements(placements, getRailStep(path, index, new RailStep(1, 0))); + addSidePlacements(placements, getRailStep(path, index, new RailStep(1, 0)), facing); if (previousStep != null) - addSidePlacements(placements, previousStep); + addSidePlacements(placements, previousStep, facing); if (nextStep != null) - addSidePlacements(placements, nextStep); + addSidePlacements(placements, nextStep, facing); return placements; } - private void addSidePlacements(List placements, RailStep step) { + private static void addSidePlacements(List placements, RailStep step, Direction facing) { if (step.dx() != 0 && step.dz() != 0) { - addSidePlacement(placements, new RailStep(step.dx(), 0), GeneratorUtils.getFacing(0, step.dz(), DEFAULT_FACING)); - addSidePlacement(placements, new RailStep(0, step.dz()), GeneratorUtils.getFacing(step.dx(), 0, DEFAULT_FACING)); + addSidePlacement(placements, new RailStep(step.dx(), 0), facing); + addSidePlacement(placements, new RailStep(0, step.dz()), facing); return; } if (step.dx() != 0) { - Direction facing = GeneratorUtils.getFacing(step.dx(), 0, DEFAULT_FACING); addSidePlacement(placements, new RailStep(0, 1), facing); addSidePlacement(placements, new RailStep(0, -1), facing); return; } - Direction facing = GeneratorUtils.getFacing(0, step.dz(), DEFAULT_FACING); addSidePlacement(placements, new RailStep(1, 0), facing); addSidePlacement(placements, new RailStep(-1, 0), facing); } - private void addSidePlacement(List placements, RailStep offset, Direction facing) { + private static Direction getSmoothedFacing(List path, int index) { + int fromIndex = Math.max(0, index - FACING_SMOOTHING_RADIUS); + int toIndex = Math.min(path.size() - 1, index + FACING_SMOOTHING_RADIUS); + Vector from = path.get(fromIndex); + Vector to = path.get(toIndex); + int dx = to.getBlockX() - from.getBlockX(); + int dz = to.getBlockZ() - from.getBlockZ(); + + if (Math.abs(dx) == Math.abs(dz)) { + Vector pathStart = path.getFirst(); + Vector pathEnd = path.getLast(); + int totalDx = pathEnd.getBlockX() - pathStart.getBlockX(); + int totalDz = pathEnd.getBlockZ() - pathStart.getBlockZ(); + + if (Math.abs(totalDx) != Math.abs(totalDz)) { + dx = totalDx; + dz = totalDz; + } + } + + if (Math.abs(dx) >= Math.abs(dz) && dx != 0) + return GeneratorUtils.getFacing(Integer.compare(dx, 0), 0, DEFAULT_FACING); + + if (dz != 0) + return GeneratorUtils.getFacing(0, Integer.compare(dz, 0), DEFAULT_FACING); + + return DEFAULT_FACING; + } + + private static void addSidePlacement(List placements, RailStep offset, Direction facing) { for (RailSidePlacement placement : placements) { if (placement.offset().equals(offset)) return; } @@ -272,7 +301,7 @@ private Set getCenterPositions(List> railCenterPaths) return centerPositions; } - private RailStep getRailStep(List path, int index, RailStep fallbackStep) { + private static RailStep getRailStep(List path, int index, RailStep fallbackStep) { RailStep previousStep = index > 0 ? getStep(path.get(index - 1), path.get(index)) : null; RailStep nextStep = index < path.size() - 1 ? getStep(path.get(index), path.get(index + 1)) : null; @@ -290,7 +319,7 @@ private RailStep getRailStep(List path, int index, RailStep fallbackStep return fallbackStep; } - private RailStep getStep(Vector from, Vector to) { + private static RailStep getStep(Vector from, Vector to) { int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); @@ -316,15 +345,18 @@ private BlockState createRailBlockState( RailSideBlock sideBlock, Map sideBlocks ) { - Direction direction = resolveSideBlockFacing(sideBlock, sideBlocks); + Direction preferredDirection = sideBlock.getPreferredFacing(); if (railBlockType.getPropertyMap().containsKey(SHAPE_PROPERTY)) - return createRailShapeBlockState(direction, getConnections(sideBlock.key(), sideBlocks)); + return createRailShapeBlockState( + resolveSideBlockFacing(sideBlock, sideBlocks), + getConnections(sideBlock.key(), sideBlocks) + ); if (!railBlockType.getPropertyMap().containsKey(FACING_PROPERTY)) return railBlockType.getDefaultState(); - return GeneratorUtils.getBlockStateWithFacing(railBlockType, direction); + return GeneratorUtils.getBlockStateWithFacing(railBlockType, preferredDirection); } private BlockState createRailShapeBlockState(Direction preferredDirection, RailConnections connections) { diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java index 02046330..72c05da5 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java @@ -2,7 +2,6 @@ import com.alpsbte.alpslib.utils.GeneratorUtils; import org.bukkit.util.Vector; -import org.jspecify.annotations.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -13,13 +12,18 @@ final class RailLanePathBuilder { private final List controlPoints; private final RailTerrainResolver terrainResolver; private final int railLaneCount; - private final int railLaneSpacing; - - RailLanePathBuilder(List controlPoints, RailTerrainResolver terrainResolver, int railLaneCount, int railLaneSpacing) { + private final List railLaneSpacings; + + RailLanePathBuilder( + List controlPoints, + RailTerrainResolver terrainResolver, + int railLaneCount, + List railLaneSpacings + ) { this.controlPoints = controlPoints; this.terrainResolver = terrainResolver; this.railLaneCount = railLaneCount; - this.railLaneSpacing = railLaneSpacing; + this.railLaneSpacings = railLaneSpacings; } List> createRailCenterPaths(List path) { @@ -27,12 +31,12 @@ List> createRailCenterPaths(List path) { return List.of(path); List> railCenterPaths = new ArrayList<>(); - double centerLaneIndex = (railLaneCount - 1) / 2.0; + List offsets = createLaneOffsets(); // Lanes are spread symmetrically around the selected path. Even track counts // have no lane on the path itself, only shifted lanes on both sides. for (int laneIndex = 0; laneIndex < railLaneCount; laneIndex++) { - int offset = (int) Math.round((laneIndex - centerLaneIndex) * railLaneSpacing); + int offset = offsets.get(laneIndex); if (offset == 0) { railCenterPaths.add(path); @@ -48,14 +52,22 @@ List> createRailCenterPaths(List path) { return railCenterPaths; } - private List createShiftedRailLane(int distance, int sideSign) { - List> shiftedLines = GeneratorUtils.shiftPointsAll(controlPoints, distance); - List candidatePoints = flattenShiftedLines(shiftedLines); + private List createLaneOffsets() { + List positions = new ArrayList<>(railLaneCount); + int position = 0; + positions.add(position); - if (candidatePoints.isEmpty()) - return Collections.emptyList(); + for (int spacing : railLaneSpacings) { + position += spacing; + positions.add(position); + } - List shiftedControlPoints = createShiftedControlPoints(candidatePoints, distance, sideSign); + double center = position / 2.0D; + return positions.stream().map(value -> (int) Math.round(value - center)).toList(); + } + + private List createShiftedRailLane(int distance, int sideSign) { + List shiftedControlPoints = createShiftedControlPoints(distance, sideSign); if (shiftedControlPoints.size() < 2) return Collections.emptyList(); @@ -69,124 +81,69 @@ private List createShiftedRailLane(int distance, int sideSign) { return shiftedPath; } - private List flattenShiftedLines(List> shiftedLines) { - if (shiftedLines == null || shiftedLines.isEmpty()) - return Collections.emptyList(); - - List candidatePoints = new ArrayList<>(); - - for (List shiftedLine : shiftedLines) { - if (shiftedLine == null || shiftedLine.isEmpty()) - continue; - - for (Vector point : shiftedLine) { - if (point != null) - candidatePoints.add(point); - } - } - - return candidatePoints; - } - - private List createShiftedControlPoints(List candidatePoints, int distance, int sideSign) { + private List createShiftedControlPoints(int distance, int sideSign) { List shiftedControlPoints = new ArrayList<>(); for (int index = 0; index < controlPoints.size(); index++) { Vector basePoint = controlPoints.get(index); - Vector direction = getControlPointDirection(index); - - if (direction.lengthSquared() == 0) - continue; - - direction.normalize(); - - Vector normal = new Vector(-direction.getZ(), 0, direction.getX()); - - if (sideSign < 0) - normal.multiply(-1); - - Vector shiftedPoint = getBestShiftedPoint(basePoint, normal, candidatePoints, distance); - - if (shiftedPoint == null) - return Collections.emptyList(); - - addIfDifferentFromPrevious(shiftedControlPoints, shiftedPoint); + Vector offset = getMiterOffset(index, distance, sideSign); + + if (offset.lengthSquared() != 0) + addIfDifferentFromPrevious(shiftedControlPoints, new Vector( + basePoint.getBlockX() + (int) Math.round(offset.getX()), + basePoint.getBlockY(), + basePoint.getBlockZ() + (int) Math.round(offset.getZ()) + )); } return shiftedControlPoints; } - private Vector getControlPointDirection(int index) { - if (controlPoints.size() < 2) - return new Vector(0, 0, 0); + private Vector getMiterOffset(int index, int distance, int sideSign) { + Vector previousDirection = index > 0 + ? normalizedHorizontalDirection(controlPoints.get(index - 1), controlPoints.get(index)) + : new Vector(); + Vector nextDirection = index < controlPoints.size() - 1 + ? normalizedHorizontalDirection(controlPoints.get(index), controlPoints.get(index + 1)) + : new Vector(); - if (index == 0) - return getHorizontalDirection(controlPoints.get(0), controlPoints.get(1)); + if (previousDirection.lengthSquared() == 0) + return perpendicular(nextDirection, sideSign).multiply(distance); - if (index == controlPoints.size() - 1) - return getHorizontalDirection(controlPoints.get(index - 1), controlPoints.get(index)); + if (nextDirection.lengthSquared() == 0) + return perpendicular(previousDirection, sideSign).multiply(distance); - Vector previousDirection = getHorizontalDirection(controlPoints.get(index - 1), controlPoints.get(index)); - Vector nextDirection = getHorizontalDirection(controlPoints.get(index), controlPoints.get(index + 1)); - Vector combinedDirection = previousDirection.add(nextDirection); + Vector previousNormal = perpendicular(previousDirection, sideSign); + Vector nextNormal = perpendicular(nextDirection, sideSign); + Vector miter = previousNormal.clone().add(nextNormal); - if (combinedDirection.lengthSquared() != 0) - return combinedDirection; + if (miter.lengthSquared() < 0.0001D) + return nextNormal.multiply(distance); - if (nextDirection.lengthSquared() != 0) - return nextDirection; + miter.normalize(); + double denominator = miter.dot(nextNormal); - return previousDirection; + if (Math.abs(denominator) < 0.25D) + return nextNormal.multiply(distance); + + // The miter keeps both adjacent segments at the requested perpendicular + // distance. Limit extreme spikes at very sharp control-point corners. + double miterLength = Math.min(distance / denominator, distance * 4.0D); + return miter.multiply(miterLength); } - private Vector getHorizontalDirection(Vector from, Vector to) { - return new Vector( + private Vector normalizedHorizontalDirection(Vector from, Vector to) { + Vector direction = new Vector( to.getBlockX() - from.getBlockX(), 0, to.getBlockZ() - from.getBlockZ() ); - } - - private @Nullable Vector getBestShiftedPoint(Vector basePoint, Vector normal, List candidatePoints, int distance) { - Vector idealPoint = getIdealShiftedPoint(basePoint, normal, distance); - Vector bestPoint = null; - double bestDistanceSquared = Double.MAX_VALUE; - - for (Vector candidatePoint : candidatePoints) { - double signedOffset = getSignedOffset(basePoint, candidatePoint, normal); - - if (signedOffset < 0.5D) - continue; - - double distanceSquared = getHorizontalDistanceSquared(candidatePoint, idealPoint); - - if (distanceSquared < bestDistanceSquared) { - bestDistanceSquared = distanceSquared; - bestPoint = candidatePoint; - } - } - - double maxCandidateDistanceSquared = Math.max(16D, distance * distance * 2.25D); - if (bestPoint == null || bestDistanceSquared > maxCandidateDistanceSquared) - return null; - - return new Vector(bestPoint.getBlockX(), basePoint.getBlockY(), bestPoint.getBlockZ()); + return direction.lengthSquared() == 0 ? direction : direction.normalize(); } - private Vector getIdealShiftedPoint(Vector basePoint, Vector normal, int distance) { - return new Vector( - basePoint.getBlockX() + (int) Math.round(normal.getX() * distance), - basePoint.getBlockY(), - basePoint.getBlockZ() + (int) Math.round(normal.getZ() * distance) - ); - } - - private double getSignedOffset(Vector basePoint, Vector candidatePoint, Vector normal) { - double offsetX = candidatePoint.getX() - basePoint.getX(); - double offsetZ = candidatePoint.getZ() - basePoint.getZ(); - - return offsetX * normal.getX() + offsetZ * normal.getZ(); + private Vector perpendicular(Vector direction, int sideSign) { + return new Vector(-direction.getZ() * sideSign, 0, direction.getX() * sideSign); } private void addIfDifferentFromPrevious(List points, Vector point) { @@ -203,13 +160,6 @@ private void addIfDifferentFromPrevious(List points, Vector point) { points.add(point); } - private double getHorizontalDistanceSquared(Vector first, Vector second) { - double dx = first.getX() - second.getX(); - double dz = first.getZ() - second.getZ(); - - return dx * dx + dz * dz; - } - private List createCenterPath(List points) { return GeneratorUtils.removeOrthogonalCorners(GeneratorUtils.createShortestBlockPath(points)); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java index 88611db8..e051f902 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLimits.java @@ -15,15 +15,15 @@ record RailLimits( ) { // Default amount of WorldEdit control points accepted before path interpolation starts. - private static final int DEFAULT_MAX_CONTROL_POINTS = 1_000; + private static final int DEFAULT_MAX_CONTROL_POINTS = 2_000; // Default amount of interpolated center-path blocks a rail may contain. - private static final int DEFAULT_MAX_PATH_POINTS = 24_000; + private static final int DEFAULT_MAX_PATH_POINTS = 75_000; // Default amount of final center, side and support blocks queued for placement. - private static final int DEFAULT_MAX_BLOCK_PLACEMENTS = 150_000; + private static final int DEFAULT_MAX_BLOCK_PLACEMENTS = 300_000; // Default volume of the prepared terrain lookup region around the selected rail path. - private static final long DEFAULT_MAX_PREPARED_REGION_VOLUME = 1_500_000L; + private static final long DEFAULT_MAX_PREPARED_REGION_VOLUME = 6_000_000L; // Default maximum width, height or depth of the prepared terrain lookup region. - private static final int DEFAULT_MAX_PREPARED_REGION_AXIS_LENGTH = 1_024; + private static final int DEFAULT_MAX_PREPARED_REGION_AXIS_LENGTH = 2_048; // Default amount of block changes applied per scheduler tick during execution. private static final int DEFAULT_BLOCK_PLACEMENT_BATCH_SIZE = 750; @@ -44,12 +44,48 @@ static RailLimits fromConfig() { FileConfiguration config = BuildTeamTools.getInstance().getConfig(ConfigUtil.GENERATOR); return new RailLimits( - getBoundedInt(config, ConfigPaths.Generator.Rail.MAX_CONTROL_POINTS, DEFAULT_MAX_CONTROL_POINTS, 2, MAX_CONTROL_POINTS), - getBoundedInt(config, ConfigPaths.Generator.Rail.MAX_PATH_POINTS, DEFAULT_MAX_PATH_POINTS, 2, MAX_PATH_POINTS), - getBoundedInt(config, ConfigPaths.Generator.Rail.MAX_BLOCK_PLACEMENTS, DEFAULT_MAX_BLOCK_PLACEMENTS, 1, MAX_BLOCK_PLACEMENTS), - getBoundedLong(config, ConfigPaths.Generator.Rail.MAX_PREPARED_REGION_VOLUME, DEFAULT_MAX_PREPARED_REGION_VOLUME, 1L, MAX_PREPARED_REGION_VOLUME), - getBoundedInt(config, ConfigPaths.Generator.Rail.MAX_PREPARED_REGION_AXIS_LENGTH, DEFAULT_MAX_PREPARED_REGION_AXIS_LENGTH, 1, MAX_PREPARED_REGION_AXIS_LENGTH), - getBoundedInt(config, ConfigPaths.Generator.Rail.BLOCK_PLACEMENT_BATCH_SIZE, DEFAULT_BLOCK_PLACEMENT_BATCH_SIZE, 1, MAX_BLOCK_PLACEMENT_BATCH_SIZE) + getBoundedInt( + config, + ConfigPaths.Generator.Rail.MAX_CONTROL_POINTS, + DEFAULT_MAX_CONTROL_POINTS, + 2, + MAX_CONTROL_POINTS + ), + getBoundedInt( + config, + ConfigPaths.Generator.Rail.MAX_PATH_POINTS, + DEFAULT_MAX_PATH_POINTS, + 2, + MAX_PATH_POINTS + ), + getBoundedInt( + config, + ConfigPaths.Generator.Rail.MAX_BLOCK_PLACEMENTS, + DEFAULT_MAX_BLOCK_PLACEMENTS, + 1, + MAX_BLOCK_PLACEMENTS + ), + getBoundedLong( + config, + ConfigPaths.Generator.Rail.MAX_PREPARED_REGION_VOLUME, + DEFAULT_MAX_PREPARED_REGION_VOLUME, + 1L, + MAX_PREPARED_REGION_VOLUME + ), + getBoundedInt( + config, + ConfigPaths.Generator.Rail.MAX_PREPARED_REGION_AXIS_LENGTH, + DEFAULT_MAX_PREPARED_REGION_AXIS_LENGTH, + 1, + MAX_PREPARED_REGION_AXIS_LENGTH + ), + getBoundedInt( + config, + ConfigPaths.Generator.Rail.BLOCK_PLACEMENT_BATCH_SIZE, + DEFAULT_BLOCK_PLACEMENT_BATCH_SIZE, + 1, + MAX_BLOCK_PLACEMENT_BATCH_SIZE + ) ); } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java index d68fc980..f2e3c882 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java @@ -9,8 +9,10 @@ import com.sk89q.worldedit.world.block.BlockType; import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; import org.bukkit.util.Vector; +import org.jspecify.annotations.Nullable; import java.util.EnumSet; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -20,6 +22,7 @@ final class RailOverheadBuilder { private static final Direction DEFAULT_FACING = Direction.EAST; private static final String FACING_PROPERTY = "facing"; + private static final int MAX_POLE_COLLISION_SHIFTS = RailType.MAX_TRACK_COUNT * RailType.MAX_TRACK_SPACING; private final RailTerrainResolver terrainResolver; private final RailType railType; @@ -31,6 +34,7 @@ final class RailOverheadBuilder { void addTo(Map blocks, List> railCenterPaths) { int overheadY = getOverheadY(railCenterPaths); + Set trackFootprint = getHorizontalFootprint(blocks.keySet()); if (railType.hasOverheadWires()) addWires(blocks, railCenterPaths, overheadY); @@ -38,7 +42,22 @@ void addTo(Map blocks, List> railCenterPat if (!railType.hasOverheadPoles() || railCenterPaths.isEmpty()) return; - addPortals(blocks, railCenterPaths.getFirst(), railCenterPaths.getLast(), overheadY); + addPortals( + blocks, + railCenterPaths.getFirst(), + railCenterPaths.getLast(), + overheadY, + trackFootprint + ); + } + + private Set getHorizontalFootprint(Set positions) { + Set footprint = new HashSet<>(); + + for (PositionKey position : positions) + footprint.add(new HorizontalPosition(position.x(), position.z())); + + return footprint; } private int getOverheadY(List> railCenterPaths) { @@ -68,7 +87,13 @@ private void addWires(Map blocks, List> ra } } - private void addPortals(Map blocks, List leftPath, List rightPath, int overheadY) { + private void addPortals( + Map blocks, + List leftPath, + List rightPath, + int overheadY, + Set trackFootprint + ) { XMaterial poleBlock = railType.getOverheadPoleBlock(); XMaterial supportBlock = railType.getOverheadSupportBlock(); @@ -83,9 +108,13 @@ private void addPortals(Map blocks, List leftPa for (int index = 0; index < leftPath.size(); index += railType.getOverheadPoleSpacing()) { Vector leftCenter = leftPath.get(index); - Vector rightCenter = rightPath.get(Math.min(index, rightPath.size() - 1)); - PortalPole leftPole = createPole(leftPath, index, leftCenter, -1); - PortalPole rightPole = createPole(rightPath, Math.min(index, rightPath.size() - 1), rightCenter, 1); + int rightIndex = getProportionalIndex(index, leftPath.size(), rightPath.size()); + Vector rightCenter = rightPath.get(rightIndex); + PortalPole leftPole = createPole(leftPath, index, leftCenter, -1, trackFootprint); + PortalPole rightPole = createPole(rightPath, rightIndex, rightCenter, 1, trackFootprint); + + if (leftPole == null || rightPole == null) + continue; addPole(blocks, leftPole.x(), leftPole.z(), leftPole.surfaceY(), overheadY, poleState); addPole(blocks, rightPole.x(), rightPole.z(), rightPole.surfaceY(), overheadY, poleState); @@ -93,15 +122,47 @@ private void addPortals(Map blocks, List leftPa } } - private PortalPole createPole(List path, int index, Vector center, int sideSign) { + private int getProportionalIndex(int sourceIndex, int sourceSize, int targetSize) { + if (sourceSize <= 1 || targetSize <= 1) + return 0; + + double progress = sourceIndex / (double) (sourceSize - 1); + return Math.clamp((int) Math.round(progress * (targetSize - 1)), 0, targetSize - 1); + } + + private @Nullable PortalPole createPole( + List path, + int index, + Vector center, + int sideSign, + Set trackFootprint + ) { RailStep perpendicular = getPerpendicularStep(path, index, sideSign); - int poleX = center.getBlockX() + perpendicular.dx() * railType.getOverheadPoleOffset(); - int poleZ = center.getBlockZ() + perpendicular.dz() * railType.getOverheadPoleOffset(); - int surfaceY = terrainResolver.getNearestRailSurfaceY(poleX, poleZ, center.getBlockY()); - return new PortalPole(poleX, poleZ, surfaceY); + int configuredDistance = railType.getOverheadPoleOffset(); + + for (int shift = 0; shift <= MAX_POLE_COLLISION_SHIFTS; shift++) { + int distance = configuredDistance + shift; + int poleX = center.getBlockX() + perpendicular.dx() * distance; + int poleZ = center.getBlockZ() + perpendicular.dz() * distance; + + if (trackFootprint.contains(new HorizontalPosition(poleX, poleZ))) + continue; + + int surfaceY = terrainResolver.getNearestRailSurfaceY(poleX, poleZ, center.getBlockY()); + return new PortalPole(poleX, poleZ, surfaceY); + } + + return null; } - private void addPole(Map blocks, int poleX, int poleZ, int surfaceY, int topY, BlockState poleState) { + private void addPole( + Map blocks, + int poleX, + int poleZ, + int surfaceY, + int topY, + BlockState poleState + ) { for (int y = Math.min(surfaceY, topY); y <= topY; y++) blocks.putIfAbsent(PositionKey.of(poleX, y, poleZ), poleState); } @@ -137,7 +198,12 @@ private List createOrthogonalPath(Vector start, Vector end) { return GeneratorUtils.createShortestBlockPath(List.of(start, end)); } - private void addPathBlocks(Map blocks, List path, BlockType blockType, boolean overwrite) { + private void addPathBlocks( + Map blocks, + List path, + BlockType blockType, + boolean overwrite + ) { Set positions = createConnectedPathPositions(path); for (PositionKey position : positions) { @@ -156,11 +222,17 @@ private Set createConnectedPathPositions(List path) { for (Vector point : path) positions.add(PositionKey.from(point)); - addCornerConnectorPositions(positions, path); + addDiagonalConnectorPositions(positions, path); return positions; } - private void addCornerConnectorPositions(Set positions, List path) { + /** + * Minecraft blocks only connect over cardinal edges. The rasterizer also + * emits diagonal steps, so bridge each one with a single orthogonal block. + * Continuing the preceding/following cardinal direction avoids bulky 2x2 + * corners while keeping panes, bars, fences, walls and full blocks connected. + */ + private void addDiagonalConnectorPositions(Set positions, List path) { for (int index = 1; index < path.size(); index++) { Vector previous = path.get(index - 1); Vector current = path.get(index); @@ -169,19 +241,51 @@ private void addCornerConnectorPositions(Set positions, List path, + int index, + Vector previous, + Vector current, + RailStep diagonalStep + ) { + Vector xFirst = new Vector( + previous.getBlockX() + diagonalStep.dx(), + current.getBlockY(), + previous.getBlockZ() + ); + Vector zFirst = new Vector( + previous.getBlockX(), + current.getBlockY(), + previous.getBlockZ() + diagonalStep.dz() + ); + + if (index > 1) { + RailStep previousStep = getStepBetween(path.get(index - 2), previous); + + if (previousStep.dx() != 0 && previousStep.dz() == 0) + return xFirst; + + if (previousStep.dz() != 0 && previousStep.dx() == 0) + return zFirst; + } + + if (index < path.size() - 1) { + RailStep nextStep = getStepBetween(current, path.get(index + 1)); + + if (nextStep.dx() != 0 && nextStep.dz() == 0) + return zFirst; + + if (nextStep.dz() != 0 && nextStep.dx() == 0) + return xFirst; + } + + return index % 2 == 0 ? xFirst : zFirst; + } + private BlockState createPathState(BlockType blockType, PositionKey position, Set positions) { Set connections = EnumSet.noneOf(Direction.class); addConnectionIfPresent(connections, positions, position, Direction.NORTH, 0, -1); @@ -286,4 +390,7 @@ private BlockState createDirectionalState(BlockType blockType, Direction directi private record PortalPole(int x, int z, int surfaceY) { } + + private record HorizontalPosition(int x, int z) { + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java index 95f23846..88d60843 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java @@ -12,18 +12,66 @@ final class RailPathOverlapValidator { boolean hasOverlap(List> railCenterPaths) { Set previousLanePositions = new HashSet<>(); + Set previousLaneDiagonals = new HashSet<>(); for (List railCenterPath : railCenterPaths) { - Set currentLanePositions = new HashSet<>(); + Set currentLaneCenters = new HashSet<>(); + Set currentLaneDiagonals = new HashSet<>(); - for (Vector center : railCenterPath) { + for (int index = 0; index < railCenterPath.size(); index++) { + Vector center = railCenterPath.get(index); PositionKey centerPosition = PositionKey.from(center); - if (!currentLanePositions.add(centerPosition) || crossesPreviousLane(centerPosition, previousLanePositions)) + if (!currentLaneCenters.add(centerPosition) + || crossesPreviousLane(centerPosition, previousLanePositions)) return true; + + if (index == 0) + continue; + + DiagonalStep diagonal = createDiagonalStep(railCenterPath.get(index - 1), center); + + if (diagonal != null && (crossesDiagonal(diagonal, currentLaneDiagonals) + || crossesDiagonal(diagonal, previousLaneDiagonals))) + return true; + + if (diagonal != null) + currentLaneDiagonals.add(diagonal); } - previousLanePositions.addAll(currentLanePositions); + previousLanePositions.addAll(currentLaneCenters); + previousLaneDiagonals.addAll(currentLaneDiagonals); + } + + return false; + } + + private DiagonalStep createDiagonalStep(Vector from, Vector to) { + int dx = to.getBlockX() - from.getBlockX(); + int dz = to.getBlockZ() - from.getBlockZ(); + + if (Math.abs(dx) != 1 || Math.abs(dz) != 1) + return null; + + return new DiagonalStep( + Math.min(from.getBlockX(), to.getBlockX()), + Math.min(from.getBlockZ(), to.getBlockZ()), + Math.min(from.getBlockY(), to.getBlockY()), + dx == dz + ); + } + + private boolean crossesDiagonal(DiagonalStep diagonal, Set otherDiagonals) { + for (int yOffset = -MAXIMUM_VERTICAL_DIFFERENCE; yOffset <= MAXIMUM_VERTICAL_DIFFERENCE; yOffset++) { + DiagonalStep crossing = new DiagonalStep( + diagonal.x(), + diagonal.z(), + diagonal.y() + yOffset, + !diagonal.positiveSlope() + ); + + if (otherDiagonals.contains(crossing)) + return true; } return false; @@ -39,4 +87,7 @@ private boolean crossesPreviousLane(PositionKey center, Set previou return false; } + + private record DiagonalStep(int x, int z, int y, boolean positiveSlope) { + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java index 7e9ab3d6..b0319fdc 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailScripts.java @@ -54,6 +54,7 @@ public class RailScripts extends Script { private RailType railType = RailType.getDefault(); private int trackCount = RailType.DEFAULT_TRACK_COUNT; private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; + private List trackSpacings = List.of(RailType.DEFAULT_TRACK_SPACING); private final RailLimits limits; private final RailPreparationProgress preparationProgress; private final Runnable preparationFinishedCallback; @@ -62,7 +63,11 @@ public class RailScripts extends Script { public RailScripts(Player player, GeneratorComponent generatorComponent, Runnable preparationFinishedCallback) { super(player, generatorComponent); this.limits = RailLimits.fromConfig(); - this.preparationProgress = new RailPreparationProgress(player, BLOCK_PLACEMENT_START_PERCENTAGE, PROGRESS_UPDATE_INTERVAL_TICKS); + this.preparationProgress = new RailPreparationProgress( + player, + BLOCK_PLACEMENT_START_PERCENTAGE, + PROGRESS_UPDATE_INTERVAL_TICKS + ); this.preparationFinishedCallback = preparationFinishedCallback; preparationProgress.start(); @@ -161,15 +166,15 @@ private boolean queueRailGeneration() { return false; preparationProgress.startStage(TERRAIN_ADJUST_PROGRESS, RAIL_BLOCK_BUILD_PROGRESS, RAIL_BLOCK_BUILD_ESTIMATED_MILLIS); - List> railCenterPaths = new RailLanePathBuilder(controlPoints, terrainResolver, trackCount, trackSpacing) + List> railCenterPaths = new RailLanePathBuilder(controlPoints, terrainResolver, trackCount, trackSpacings) .createRailCenterPaths(centerPath); if (railCenterPaths.size() < trackCount) { sendRailError( - "Rail Generator could not create %s parallel tracks with %s blocks of spacing along this path. " - + "Reduce the track count or spacing, or use a less sharp curve.", + "Rail Generator could not create %s parallel tracks with spacing %s along this path. " + + "Reduce the track count or spacings, or use a less sharp curve.", trackCount, - trackSpacing + trackSpacings ); return false; } @@ -389,7 +394,8 @@ private List createRailSelectionPoints(List points) { } private int getSelectionPadding() { - int maxLaneOffset = (int) Math.round(((trackCount - 1) / 2.0) * trackSpacing); + int totalTrackSpan = trackSpacings.stream().mapToInt(Integer::intValue).sum(); + int maxLaneOffset = (int) Math.ceil(totalTrackSpan / 2.0D); int overheadPoleOffset = railType.hasOverheadPoles() ? railType.getOverheadPoleOffset() : 0; return SELECTION_PADDING + maxLaneOffset + overheadPoleOffset + 2; } @@ -411,6 +417,7 @@ private Map buildRailBlocks(List> railCent private boolean resolveTrackLayout() { trackCount = railType.getTrackCount(); trackSpacing = railType.getTrackSpacing(); + trackSpacings = railType.getTrackSpacings(); Integer trackCountFlag = getIntegerSetting(RailFlag.TRACK_COUNT); Integer trackSpacingFlag = getIntegerSetting(RailFlag.TRACK_SPACING); @@ -418,8 +425,12 @@ private boolean resolveTrackLayout() { if (trackCountFlag != null) trackCount = trackCountFlag; - if (trackSpacingFlag != null) + if (trackSpacingFlag != null) { trackSpacing = trackSpacingFlag; + trackSpacings = Collections.nCopies(Math.max(0, trackCount - 1), trackSpacing); + } else if (trackCount != railType.getTrackCount()) { + trackSpacings = Collections.nCopies(Math.max(0, trackCount - 1), trackSpacing); + } if (trackCount < RailType.MIN_TRACK_COUNT || trackCount > RailType.MAX_TRACK_COUNT) { sendRailError("Track count must be between %s and %s.", RailType.MIN_TRACK_COUNT, RailType.MAX_TRACK_COUNT); @@ -431,6 +442,14 @@ private boolean resolveTrackLayout() { return false; } + if (trackSpacings.size() != Math.max(0, trackCount - 1) + || trackSpacings.stream().anyMatch(spacing -> spacing < RailType.MIN_TRACK_SPACING + || spacing > RailType.MAX_TRACK_SPACING)) { + sendRailError("Every track spacing must be between %s and %s.", + RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING); + return false; + } + return trackCount == RailType.MIN_TRACK_COUNT || RailPermissionGuard.check(getPlayer(), Permissions.RAIL_MULTIPLE_TRACKS); } @@ -458,7 +477,12 @@ private void adjustCenterPathToTerrain() { for (int index = 0; index < centerPath.size(); index++) { Vector point = centerPath.get(index); point.setY(terrainResolver.getNearestRailSurfaceY(point.getBlockX(), point.getBlockZ(), point.getBlockY())); - preparationProgress.update(preparationProgress.scale(index + 1, centerPath.size(), TERRAIN_PREPARE_PROGRESS, TERRAIN_ADJUST_PROGRESS)); + preparationProgress.update(preparationProgress.scale( + index + 1, + centerPath.size(), + TERRAIN_PREPARE_PROGRESS, + TERRAIN_ADJUST_PROGRESS + )); } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java index 46a591a5..3903de5f 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java @@ -5,7 +5,6 @@ import net.buildtheearth.buildteamtools.utils.MenuItems; import org.bukkit.inventory.ItemStack; -import java.util.ArrayList; import java.util.List; /** @@ -17,20 +16,7 @@ enum RailBlockRole { RAIL_BLOCK("Choose a Rail Block") { @Override List createChoices() { - return createItems(List.of( - XMaterial.ANVIL, - XMaterial.CHIPPED_ANVIL, - XMaterial.DAMAGED_ANVIL, - XMaterial.RAIL, - XMaterial.POWERED_RAIL, - XMaterial.DETECTOR_RAIL, - XMaterial.ACTIVATOR_RAIL, - XMaterial.IRON_BARS, - XMaterial.SMOOTH_STONE_SLAB, - XMaterial.STONE_SLAB, - XMaterial.ANDESITE_WALL, - XMaterial.POLISHED_BLACKSTONE_WALL - )); + return MenuItems.getBlocksByColor(); } @Override @@ -112,12 +98,7 @@ boolean isOverheadSetting() { OVERHEAD_WIRE_BLOCK("Choose an Overhead Wire Block") { @Override List createChoices() { - return createItems(List.of( - XMaterial.IRON_BARS, - XMaterial.BLACK_WOOL, - XMaterial.GRAY_WOOL, - XMaterial.LIGHT_GRAY_WOOL - )); + return MenuItems.getBlocksByColor(); } @Override @@ -146,16 +127,4 @@ boolean isOverheadSetting() { return false; } - private static List createItems(List materials) { - List items = new ArrayList<>(); - - for (XMaterial material : materials) { - ItemStack item = material.parseItem(); - - if (item != null) - items.add(item); - } - - return items; - } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTrackSpacingMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTrackSpacingMenu.java new file mode 100644 index 00000000..d8228439 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTrackSpacingMenu.java @@ -0,0 +1,105 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; +import net.buildtheearth.buildteamtools.utils.MenuItems; +import net.buildtheearth.buildteamtools.utils.heads.HeadColor; +import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; +import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; +import net.buildtheearth.buildteamtools.utils.menus.AbstractMenu; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.ipvp.canvas.mask.BinaryMask; +import org.ipvp.canvas.mask.Mask; + +/** Configures the distance for each gap between up to eight parallel tracks. */ +final class RailTrackSpacingMenu extends AbstractMenu { + + private static final int[] GAP_SLOTS = {2, 6, 11, 15, 20, 24, 29}; + private static final String FILLED_MASK_ROW = "111111111"; + private static final int BACK_ITEM_SLOT = 36; + private static final int DONE_ITEM_SLOT = 44; + + private final RailTypeDraft draft; + + RailTrackSpacingMenu(Player player, RailTypeDraft draft, boolean autoLoad) { + super(5, "Track Spacing Settings", player, false); + this.draft = draft; + + if (autoLoad) + reloadMenuAsync(); + } + + @Override + protected void setPreviewItems() { + if (getMask() != null) + getMask().apply(getMenu()); + + for (int gapIndex = 0; gapIndex < draft.getTrackSpacings().size(); gapIndex++) { + createCounter( + gapIndex % 2 == 0 ? HeadColor.WHITE : HeadColor.LIGHT_GRAY, + GAP_SLOTS[gapIndex], + "Tracks " + (gapIndex + 1) + " & " + (gapIndex + 2), + draft.getTrackSpacings().get(gapIndex), + RailType.MIN_TRACK_SPACING, + RailType.MAX_TRACK_SPACING, + "Blocks" + ); + } + + setBackItem(BACK_ITEM_SLOT, new RailTypeEditorMenu(getMenuPlayer(), draft, false)); + getMenu().getSlot(DONE_ITEM_SLOT).setItem(HeadFactory.head(HeadTexture.CHECKMARK, "§aDone")); + getMenu().open(getMenuPlayer()); + } + + @Override + protected void setMenuItemsAsync() { + // All values live in the local draft. + } + + @Override + protected void setItemClickEventsAsync() { + for (int gapIndex = 0; gapIndex < draft.getTrackSpacings().size(); gapIndex++) { + int selectedGap = gapIndex; + int slot = GAP_SLOTS[gapIndex]; + + getMenu().getSlot(slot - 1).setClickHandler((player, click) -> changeSpacing(player, selectedGap, -1)); + getMenu().getSlot(slot + 1).setClickHandler((player, click) -> changeSpacing(player, selectedGap, 1)); + } + + getMenu().getSlot(DONE_ITEM_SLOT).setClickHandler((player, click) -> { + player.closeInventory(); + playSound(player, Sound.UI_BUTTON_CLICK); + new RailTypeEditorMenu(player, draft, true); + }); + } + + private void changeSpacing(Player player, int gapIndex, int change) { + int current = draft.getTrackSpacings().get(gapIndex); + int updated = current + change; + + if (updated < RailType.MIN_TRACK_SPACING || updated > RailType.MAX_TRACK_SPACING) { + playSound(player, Sound.ENTITY_ITEM_BREAK); + return; + } + + draft.setTrackSpacing(gapIndex, updated); + playSound(player, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(); + } + + @Override + protected Mask getMask() { + return BinaryMask.builder(getMenu()) + .item(MenuItems.ITEM_BACKGROUND) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .pattern(FILLED_MASK_ROW) + .build(); + } + + private void playSound(Player player, Sound sound) { + player.playSound(player, sound, 1.0F, 1.0F); + } +} diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java index 5d639347..b79f610d 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -23,13 +23,14 @@ class RailTypeDraft { private int sleeperSpacing = RailType.MIN_SLEEPER_SPACING; private int trackCount = RailType.DEFAULT_TRACK_COUNT; private int trackSpacing = RailType.DEFAULT_TRACK_SPACING; - private boolean overheadPolesEnabled; + private List trackSpacings = new java.util.ArrayList<>(List.of(RailType.DEFAULT_TRACK_SPACING)); + private boolean overheadPolesEnabled = true; private XMaterial overheadPoleBlock = XMaterial.LIGHT_GRAY_CONCRETE; private XMaterial overheadSupportBlock = XMaterial.LIGHT_GRAY_CONCRETE; private int overheadPoleSpacing = RailType.DEFAULT_OVERHEAD_POLE_SPACING; private int overheadPoleOffset = RailType.DEFAULT_OVERHEAD_POLE_OFFSET; private int overheadPoleHeight = RailType.DEFAULT_OVERHEAD_POLE_HEIGHT; - private boolean overheadWiresEnabled; + private boolean overheadWiresEnabled = true; private XMaterial overheadWireBlock = XMaterial.IRON_BARS; private boolean trackSwitchesEnabled; @@ -49,6 +50,7 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.sleeperSpacing = railType.getSleeperSpacing(); draft.trackCount = railType.getTrackCount(); draft.trackSpacing = railType.getTrackSpacing(); + draft.trackSpacings = new java.util.ArrayList<>(railType.getTrackSpacings()); draft.overheadPolesEnabled = railType.isOverheadPolesEnabled(); draft.overheadPoleBlock = railType.getOverheadPoleBlock() == null ? XMaterial.LIGHT_GRAY_CONCRETE @@ -66,4 +68,33 @@ static RailTypeDraft from(RailType railType, boolean keepIdentity) { draft.trackSwitchesEnabled = railType.isTrackSwitchesEnabled(); return draft; } + + void setTrackCount(int trackCount) { + this.trackCount = trackCount; + resizeTrackSpacings(); + } + + void setTrackSpacing(int trackSpacing) { + this.trackSpacing = trackSpacing; + this.trackSpacings = new java.util.ArrayList<>(java.util.Collections.nCopies( + Math.max(0, trackCount - 1), + trackSpacing + )); + } + + void setTrackSpacing(int gapIndex, int spacing) { + resizeTrackSpacings(); + trackSpacings.set(gapIndex, spacing); + trackSpacing = trackSpacings.getFirst(); + } + + private void resizeTrackSpacings() { + int requiredSize = Math.max(0, trackCount - 1); + + while (trackSpacings.size() < requiredSize) + trackSpacings.add(trackSpacing); + + while (trackSpacings.size() > requiredSize) + trackSpacings.removeLast(); + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java index 61505cf6..896b16a9 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -84,6 +84,15 @@ protected void setPreviewItems() { RailType.MIN_TRACK_COUNT, RailType.MAX_TRACK_COUNT, "Tracks"); createCounter(HeadColor.LIGHT_GRAY, TRACK_SPACING_SLOT, "Track Spacing", draft.getTrackSpacing(), RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING, "Blocks"); + getMenu().getSlot(TRACK_SPACING_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.NAME_TAG.get()), + yellow("Track Spacings"), + List.of( + gray("Between tracks: ") + white(draft.getTrackSpacings().toString()), + gray("Use +/- to set all spacings."), + gray("Click to configure each gap separately.") + ) + )); createCounter(HeadColor.WHITE, SLEEPER_SPACING_SLOT, "Sleeper Spacing", draft.getSleeperSpacing(), RailType.MIN_SLEEPER_SPACING, RailType.MAX_SLEEPER_SPACING, "Blocks"); @@ -154,6 +163,11 @@ protected void setItemClickEventsAsync() { draft::getTrackCount, draft::setTrackCount); setCounterClickEvents(TRACK_SPACING_SLOT, RailType.MIN_TRACK_SPACING, RailType.MAX_TRACK_SPACING, draft::getTrackSpacing, draft::setTrackSpacing); + getMenu().getSlot(TRACK_SPACING_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + clickPlayer.closeInventory(); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + new RailTrackSpacingMenu(clickPlayer, draft, true); + }); setCounterClickEvents(SLEEPER_SPACING_SLOT, RailType.MIN_SLEEPER_SPACING, RailType.MAX_SLEEPER_SPACING, draft::getSleeperSpacing, draft::setSleeperSpacing); @@ -250,6 +264,7 @@ private void saveRailType(Player clickPlayer) { .sleeperSpacing(draft.getSleeperSpacing()) .trackCount(draft.getTrackCount()) .trackSpacing(draft.getTrackSpacing()) + .trackSpacings(draft.getTrackSpacings()) .overheadPolesEnabled(draft.isOverheadPolesEnabled()) .overheadPoleBlock(draft.getOverheadPoleBlock()) .overheadSupportBlock(draft.getOverheadSupportBlock()) diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java index 1980056b..945c88e7 100644 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenu.java @@ -3,6 +3,7 @@ import com.alpsbte.alpslib.utils.ChatHelper; import com.alpsbte.alpslib.utils.item.Item; import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.BuildTeamTools; import net.buildtheearth.buildteamtools.modules.generator.GeneratorModule; import net.buildtheearth.buildteamtools.modules.generator.components.rail.Rail; import net.buildtheearth.buildteamtools.modules.generator.components.rail.RailFlag; @@ -15,34 +16,45 @@ import net.buildtheearth.buildteamtools.utils.heads.HeadFactory; import net.buildtheearth.buildteamtools.utils.heads.HeadTexture; import net.buildtheearth.buildteamtools.utils.menus.NameListMenu; +import net.wesjd.anvilgui.AnvilGUI; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.commons.lang3.tuple.MutablePair; -import org.bukkit.Material; import org.bukkit.Sound; -import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Set; public class RailTypeMenu extends NameListMenu { public static final String RAIL_TYPE_INV_NAME = "Choose a Rail Type"; // Slots 30-32 belong to the page switcher, so the extra buttons sit next to it. + private static final int SEARCH_ITEM_SLOT = 28; private static final int CREATE_ITEM_SLOT = 29; private static final int RELOAD_ITEM_SLOT = 33; + private static final int BULK_DELETE_ITEM_SLOT = 34; + + private final String searchQuery; + private final Set selectedForDeletion; public RailTypeMenu(Player player, boolean autoLoad) { - super(player, RAIL_TYPE_INV_NAME, getRailTypes(), new GeneratorMenu(player, false), autoLoad); + this(player, "", Set.of(), autoLoad); + } + + private RailTypeMenu(Player player, String searchQuery, Collection selectedForDeletion, boolean autoLoad) { + super(player, RAIL_TYPE_INV_NAME, getRailTypes(searchQuery), new GeneratorMenu(player, false), autoLoad); + this.searchQuery = searchQuery; + this.selectedForDeletion = new LinkedHashSet<>(selectedForDeletion); preselectCurrentRailType(player); } @@ -65,89 +77,33 @@ else if (value instanceof String identifier && RailType.byString(identifier) != selectedNames.add(identifier); } - private static @NonNull List> getRailTypes() { + private static @NonNull List> getRailTypes(String searchQuery) { List> railTypes = new ArrayList<>(); Rail rail = GeneratorModule.getInstance().getRail(); if (rail == null) return railTypes; - for (RailType railType : rail.getRailTypeManager().getRailTypes()) - railTypes.add(new MutablePair<>(createRailTypeItem(railType), railType.getIdentifier())); - - return railTypes; - } - - private static ItemStack createRailTypeItem(RailType railType) { - Material icon = railType.getIcon().get(); - - if (icon == null) - icon = Objects.requireNonNull(XMaterial.RAIL.get()); - - List lore = new ArrayList<>(); - lore.add(gray("Rail Block: ") + white(formatMaterial(railType.getRailBlock()))); - lore.add(gray("Blocks Below: ") + formatMaterials(railType.getBlocksBelow())); + String normalizedQuery = searchQuery.trim().toLowerCase(Locale.ROOT); - if (railType.hasSleepers()) - lore.add(gray("Sleepers: ") - + white(formatMaterial(railType.getSleeperBlock())) - + gray(" every ") - + white(String.valueOf(railType.getSleeperSpacing())) - + gray(" blocks")); - else - lore.add(gray("Sleepers: ") + white("None")); + for (RailType railType : rail.getRailTypeManager().getRailTypes()) { + if (!normalizedQuery.isEmpty() + && !railType.getIdentifier().toLowerCase(Locale.ROOT).contains(normalizedQuery) + && !railType.getDisplayName().toLowerCase(Locale.ROOT).contains(normalizedQuery)) + continue; - if (railType.getTrackCount() > 1) - lore.add(gray("Tracks: ") - + white(String.valueOf(railType.getTrackCount())) - + gray(" with spacing ") - + white(String.valueOf(railType.getTrackSpacing()))); - else - lore.add(gray("Tracks: ") + white("1")); - - lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() - ? RailMenuText.ENABLED - : RailMenuText.DISABLED)); - if (railType.hasOverheadPoles()) { - lore.add(gray("Pole Block: ") + white(formatMaterial(railType.getOverheadPoleBlock()))); - lore.add(gray("Top Support: ") + white(formatMaterial(railType.getOverheadSupportBlock()))); - lore.add(gray("Pole Spacing: ") + white(String.valueOf(railType.getOverheadPoleSpacing()))); + railTypes.add(new MutablePair<>(RailTypeMenuItems.createRailTypeItem(railType), railType.getIdentifier())); } - lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() - ? RailMenuText.ENABLED - : RailMenuText.DISABLED)); - lore.add(gray("Track Switches: ") + white(railType.isTrackSwitchesEnabled() - ? RailMenuText.ENABLED - : RailMenuText.DISABLED)); - - if (railType.isBuiltIn()) - lore.add(darkGray("Right-Click to create an editable copy")); - else - lore.add(darkGray("Right-Click to edit ") + gray("- ") + darkGray("Shift+Right-Click to delete")); - return Item.create(icon, yellow(railType.getDisplayName()), lore); + return railTypes; } static String formatMaterials(List materials) { - List names = new ArrayList<>(); - - for (XMaterial material : materials) - names.add(white(formatMaterial(material))); - - return String.join(gray(", "), names); + return RailTypeMenuItems.formatMaterials(materials); } static String formatMaterial(@Nullable XMaterial material) { - if (material == null) - return "None"; - - String[] words = material.name().toLowerCase(Locale.ROOT).split("_"); - List capitalizedWords = new ArrayList<>(); - - for (String word : words) - capitalizedWords.add(word.isEmpty() ? word : Character.toUpperCase(word.charAt(0)) + word.substring(1)); - - return String.join(" ", capitalizedWords); + return RailTypeMenuItems.formatMaterial(material); } @Override @@ -156,6 +112,16 @@ protected void setPreviewItems() { setRailTypePageItems(); + getMenu().getSlot(SEARCH_ITEM_SLOT).setItem(Item.create( + Objects.requireNonNull(XMaterial.COMPASS.get()), + yellow("Search Rail Types"), + List.of( + gray("Current filter: ") + white(searchQuery.isBlank() ? "All" : searchQuery), + gray("Left-click to search."), + gray("Right-click to clear the filter.") + ) + )); + getMenu().getSlot(CREATE_ITEM_SLOT).setItem(Item.create( Objects.requireNonNull(XMaterial.NETHER_STAR.get()), green("Create a Rail Type"), @@ -170,6 +136,18 @@ protected void setPreviewItems() { gray("so you can test changes without a restart.") ) )); + + getMenu().getSlot(BULK_DELETE_ITEM_SLOT).setItem(Item.create( + Objects.requireNonNull((selectedForDeletion.isEmpty() ? XMaterial.GRAY_DYE : XMaterial.RED_DYE).get()), + selectedForDeletion.isEmpty() ? gray("Bulk Delete") : red("Delete Selected Rail Types"), + List.of( + gray("Selected: ") + white(String.valueOf(selectedForDeletion.size())), + gray("Shift+Left-Click custom types to select them."), + selectedForDeletion.isEmpty() + ? darkGray("No custom rail types selected.") + : red("Click to permanently delete all selected types.") + ) + )); } @Override @@ -182,7 +160,10 @@ protected void setPaginatedPreviewItems(@NonNull List source) { ItemStack displayedItem = item.getLeft(); if (selectedNames.contains(item.getRight())) - displayedItem = addSelectionGlow(displayedItem); + displayedItem = RailTypeMenuItems.addSelectionGlow(displayedItem); + + if (selectedForDeletion.contains(item.getRight())) + displayedItem = RailTypeMenuItems.addDeletionMarker(displayedItem); getMenu().getSlot(slot).setItem(displayedItem); slot++; @@ -200,6 +181,11 @@ protected void setPaginatedItemClickEventsAsync(@NonNull List source) { getMenu().getSlot(_slot).setClickHandler((clickPlayer, clickInformation) -> { String type = item.getRight().toLowerCase(); + if (clickInformation.getClickType() == ClickType.SHIFT_LEFT) { + toggleBulkDeleteSelection(clickPlayer, type); + return; + } + if (clickInformation.getClickType() == ClickType.SHIFT_RIGHT) { deleteCustomRailType(clickPlayer, type); return; @@ -248,22 +234,6 @@ private List> getPageItems(List source) { return items; } - private ItemStack addSelectionGlow(ItemStack item) { - ItemStack displayedItem = item.clone(); - displayedItem.setAmount(1); - displayedItem.addUnsafeEnchantment(Enchantment.LUCK_OF_THE_SEA, 1); - - ItemMeta meta = displayedItem.getItemMeta(); - - if (meta == null) - return displayedItem; - - meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); - displayedItem.setItemMeta(meta); - - return displayedItem; - } - private String getRailTypeDisplayName(String identifier) { Rail rail = GeneratorModule.getInstance().getRail(); @@ -348,12 +318,81 @@ private void deleteCustomRailType(Player clickPlayer, String identifier) { new RailTypeMenu(clickPlayer, true); } + private void toggleBulkDeleteSelection(Player clickPlayer, String identifier) { + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + RailType railType = rail.getRailTypeManager().byString(identifier); + + if (railType == null || railType.isBuiltIn()) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent( + "Only custom rail types can be selected for deletion." + ))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + if (selectedForDeletion.remove(identifier)) + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Removed rail type '%s' from bulk deletion.", + identifier + )); + else + selectedForDeletion.add(identifier); + + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + reloadMenuAsync(false); + } + + private void deleteSelectedRailTypes(Player clickPlayer) { + if (selectedForDeletion.isEmpty()) { + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + if (!RailPermissionGuard.check(clickPlayer, Permissions.RAIL_TYPE_DELETE)) + return; + + Rail rail = GeneratorModule.getInstance().getRail(); + + if (rail == null) + return; + + int amount = selectedForDeletion.size(); + String error = rail.getRailTypeManager().deleteRailTypes(selectedForDeletion); + + if (error != null) { + clickPlayer.sendMessage(ChatHelper.PREFIX_COMPONENT.append(ChatHelper.getErrorComponent(error))); + playSound(clickPlayer, Sound.ENTITY_ITEM_BREAK); + return; + } + + selectedNames.removeAll(selectedForDeletion); + selectedForDeletion.clear(); + clickPlayer.sendMessage(ChatHelper.getStandardComponent(true, "Deleted %s custom rail types.", amount)); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + new RailTypeMenu(clickPlayer, searchQuery, selectedForDeletion, true); + } + @Override protected void setItemClickEventsAsync() { super.setItemClickEventsAsync(); setRailTypePageClickEvents(); + getMenu().getSlot(SEARCH_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { + if (clickInformation.getClickType() == ClickType.RIGHT) { + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + new RailTypeMenu(clickPlayer, "", selectedForDeletion, true); + return; + } + + openSearchEditor(clickPlayer); + }); + getMenu().getSlot(CREATE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> { if (!RailPermissionGuard.check(clickPlayer, Permissions.RAIL_TYPE_CREATE)) return; @@ -380,11 +419,15 @@ protected void setItemClickEventsAsync() { rail.getRailTypeManager().getRailTypes().size() )); - new RailTypeMenu(clickPlayer, true); + new RailTypeMenu(clickPlayer, searchQuery, selectedForDeletion, true); }); + getMenu().getSlot(BULK_DELETE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + deleteSelectedRailTypes(clickPlayer)); + if (canProceed()) - getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> generateWithSelectedType(clickPlayer)); + getMenu().getSlot(NEXT_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + generateWithSelectedType(clickPlayer)); } private void generateWithSelectedType(Player clickPlayer) { @@ -423,6 +466,32 @@ private void generateWithSelectedType(Player clickPlayer) { rail.generate(clickPlayer); } + private void openSearchEditor(Player clickPlayer) { + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + + new AnvilGUI.Builder() + .onClick((slot, stateSnapshot) -> { + if (slot != AnvilGUI.Slot.OUTPUT) + return List.of(); + + String query = stateSnapshot.getText().trim(); + return List.of( + AnvilGUI.ResponseAction.close(), + AnvilGUI.ResponseAction.run(() -> new RailTypeMenu( + clickPlayer, + query, + selectedForDeletion, + true + )) + ); + }) + .text(searchQuery.isBlank() ? "Search" : searchQuery) + .itemLeft(Item.create(Objects.requireNonNull(XMaterial.NAME_TAG.get()), yellow("Search Rail Types"))) + .title(darkGray("Search rail types")) + .plugin(BuildTeamTools.getInstance()) + .open(clickPlayer); + } + private void setRailTypePageItems() { getMenu().getSlot(SWITCH_PAGE_ITEM_SLOT - 1).setItem(createPreviousPageItem()); @@ -518,4 +587,8 @@ private static String yellow(String text) { private static String green(String text) { return RailMenuText.color(NamedTextColor.GREEN, text); } + + private static String red(String text) { + return RailMenuText.color(NamedTextColor.RED, text); + } } diff --git a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenuItems.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenuItems.java new file mode 100644 index 00000000..3edeeda6 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeMenuItems.java @@ -0,0 +1,221 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.menu; + +import com.alpsbte.alpslib.utils.item.Item; +import com.cryptomorin.xseries.XMaterial; +import net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration.RailType; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** Builds rail type icons and their visual selection states. */ +final class RailTypeMenuItems { + + private RailTypeMenuItems() { + } + + static ItemStack createRailTypeItem(RailType railType) { + Material icon = railType.getIcon().get(); + + if (icon == null) + icon = Objects.requireNonNull(XMaterial.RAIL.get()); + + List lore = createConfigurationLore(railType); + + if (railType.isBuiltIn()) + lore.add(darkGray("Right-Click to create an editable copy")); + else { + lore.add(darkGray("Right-Click to edit ") + gray("- ") + darkGray("Shift+Right-Click to delete")); + lore.add(darkGray("Shift+Left-Click to select for bulk deletion")); + } + + return Item.create(icon, yellow(railType.getDisplayName()), lore); + } + + private static List createConfigurationLore(RailType railType) { + List lore = new ArrayList<>(); + lore.add(gray("Rail Block: ") + white(formatMaterial(railType.getRailBlock()))); + lore.add(gray("Blocks Below: ") + formatMaterials(railType.getBlocksBelow())); + addSleeperLore(lore, railType); + addTrackLore(lore, railType); + addOverheadLore(lore, railType); + lore.add(gray("Track Switches: ") + white(railType.isTrackSwitchesEnabled() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); + return lore; + } + + private static void addSleeperLore(List lore, RailType railType) { + if (!railType.hasSleepers()) { + lore.add(gray("Sleepers: ") + white("None")); + return; + } + + lore.add(gray("Sleepers: ") + + white(formatMaterial(railType.getSleeperBlock())) + + gray(" every ") + + white(String.valueOf(railType.getSleeperSpacing())) + + gray(" blocks")); + } + + private static void addTrackLore(List lore, RailType railType) { + if (railType.getTrackCount() == 1) { + lore.add(gray("Tracks: ") + white("1")); + return; + } + + lore.add(gray("Tracks: ") + + white(String.valueOf(railType.getTrackCount())) + + gray(" with spacing ") + + white(railType.getTrackSpacings().toString())); + } + + private static void addOverheadLore(List lore, RailType railType) { + lore.add(gray("Overhead Poles: ") + white(railType.hasOverheadPoles() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); + + if (railType.hasOverheadPoles()) { + lore.add(gray("Pole Block: ") + white(formatMaterial(railType.getOverheadPoleBlock()))); + lore.add(gray("Top Support: ") + white(formatMaterial(railType.getOverheadSupportBlock()))); + lore.add(gray("Pole Spacing: ") + white(String.valueOf(railType.getOverheadPoleSpacing()))); + } + + lore.add(gray("Overhead Wires: ") + white(railType.hasOverheadWires() + ? RailMenuText.ENABLED + : RailMenuText.DISABLED)); + } + + static ItemStack addSelectionGlow(ItemStack item) { + ItemStack displayedItem = createGlintCompatibleDisplayItem(item); + displayedItem.setAmount(1); + displayedItem.addUnsafeEnchantment(Enchantment.LUCK_OF_THE_SEA, 1); + + ItemMeta meta = displayedItem.getItemMeta(); + + if (meta != null) { + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + forceEnchantmentGlint(meta); + displayedItem.setItemMeta(meta); + } + + return displayedItem; + } + + /** + * Vanilla does not render enchantment glint on block-entity item models, + * even when enchantment_glint_override is true (MC-69683). Use an enchanted + * book as the selected-state icon while retaining the rail type name, lore + * and the original icon name. + */ + private static ItemStack createGlintCompatibleDisplayItem(ItemStack item) { + if (!usesBlockEntityItemRenderer(item.getType())) + return item.clone(); + + ItemMeta originalMeta = item.getItemMeta(); + String displayName = originalMeta != null && originalMeta.hasDisplayName() + ? originalMeta.getDisplayName() + : yellow("Selected Rail Type"); + List lore = originalMeta != null && originalMeta.hasLore() && originalMeta.getLore() != null + ? new ArrayList<>(originalMeta.getLore()) + : new ArrayList<>(); + lore.add(gray("Original icon: ") + white(formatMaterialName(item.getType()))); + lore.add(darkGray("Minecraft cannot render glint on this block-entity icon.")); + return Item.create(Material.ENCHANTED_BOOK, displayName, lore); + } + + private static boolean usesBlockEntityItemRenderer(Material material) { + String name = material.name(); + return name.endsWith("_CHEST") + || name.endsWith("_SHULKER_BOX") + || name.endsWith("_HEAD") + || name.endsWith("_SKULL") + || name.endsWith("_BANNER") + || name.endsWith("_BED") + || material == Material.CONDUIT + || material == Material.DECORATED_POT; + } + + private static String formatMaterialName(Material material) { + return formatWords(material.name()); + } + + /** Paper 1.20.5+ exposes an explicit glint override; reflection preserves compatibility with older servers. */ + private static void forceEnchantmentGlint(ItemMeta meta) { + try { + ItemMeta.class + .getMethod("setEnchantmentGlintOverride", Boolean.class) + .invoke(meta, Boolean.TRUE); + } catch (ReflectiveOperationException ignored) { + // The hidden unsafe enchantment remains the fallback on older servers. + } + } + + static ItemStack addDeletionMarker(ItemStack item) { + ItemStack displayedItem = addSelectionGlow(item); + ItemMeta meta = displayedItem.getItemMeta(); + + if (meta == null) + return displayedItem; + + List lore = meta.hasLore() && meta.getLore() != null + ? new ArrayList<>(meta.getLore()) + : new ArrayList<>(); + lore.add(red("Selected for bulk deletion")); + meta.setLore(lore); + displayedItem.setItemMeta(meta); + return displayedItem; + } + + static String formatMaterials(List materials) { + return String.join(gray(", "), materials.stream() + .map(RailTypeMenuItems::formatMaterial) + .map(RailTypeMenuItems::white) + .toList()); + } + + static String formatMaterial(@Nullable XMaterial material) { + if (material == null) + return "None"; + + return formatWords(material.name()); + } + + private static String formatWords(String value) { + String[] words = value.toLowerCase(Locale.ROOT).split("_"); + List capitalizedWords = new ArrayList<>(); + + for (String word : words) + capitalizedWords.add(word.isEmpty() ? word : Character.toUpperCase(word.charAt(0)) + word.substring(1)); + + return String.join(" ", capitalizedWords); + } + + private static String darkGray(String text) { + return RailMenuText.color(NamedTextColor.DARK_GRAY, text); + } + + private static String gray(String text) { + return RailMenuText.color(NamedTextColor.GRAY, text); + } + + private static String white(String text) { + return RailMenuText.color(NamedTextColor.WHITE, text); + } + + private static String yellow(String text) { + return RailMenuText.color(NamedTextColor.YELLOW, text); + } + + private static String red(String text) { + return RailMenuText.color(NamedTextColor.RED, text); + } +} diff --git a/src/main/resources/modules/generator/config.yml b/src/main/resources/modules/generator/config.yml index 86e13d9f..0aaa5301 100644 --- a/src/main/resources/modules/generator/config.yml +++ b/src/main/resources/modules/generator/config.yml @@ -7,12 +7,12 @@ # ---------------------------------------------------------------------------------------------- rail: - # Conservative defaults for a 4 GB server. Lower these if rail generation is too heavy for your server. - max-control-points: 1000 - max-path-points: 24000 - max-block-placements: 150000 - max-prepared-region-volume: 1500000 - max-prepared-region-axis-length: 1024 + # Upper safe defaults. Lower these if rail generation is too heavy for your server. + max-control-points: 2000 + max-path-points: 75000 + max-block-placements: 300000 + max-prepared-region-volume: 6000000 + max-prepared-region-axis-length: 2048 block-placement-batch-size: 750 # NOTE: Do not change