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/Rail.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/Rail.java index 83809e8c..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 @@ -6,9 +6,14 @@ 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.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; import org.bukkit.Sound; import org.bukkit.entity.Player; @@ -20,8 +25,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 +47,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 +59,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 +79,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 deleted file mode 100644 index b2e130e8..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailBlockBuilder.java +++ /dev/null @@ -1,268 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; - -import com.alpsbte.alpslib.utils.GeneratorUtils; -import com.cryptomorin.xseries.XMaterial; -import com.sk89q.worldedit.util.Direction; -import com.sk89q.worldedit.world.block.BlockState; -import com.sk89q.worldedit.world.block.BlockTypes; -import org.bukkit.util.Vector; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -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 final List controlPoints; - private final RailTerrainResolver terrainResolver; - private final RailType railType; - 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.preparationProgress = preparationProgress; - this.railLaneCount = railLaneCount; - this.railLaneSpacing = railLaneSpacing; - this.terrainAdjustedPercentage = terrainAdjustedPercentage; - this.buildFinishedPercentage = buildFinishedPercentage; - } - - Map build(List path) { - 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); - int processedPathPoints = 0; - - for (List railCenterPath : railCenterPaths) { - for (int index = 0; index < railCenterPath.size(); index++) { - Vector center = railCenterPath.get(index); - - for (RailSidePlacement sidePlacement : getSidePlacements(railCenterPath, index)) - addSideBlock(sideBlocks, center, sidePlacement, centerPositions); - - processedPathPoints++; - preparationProgress.update(preparationProgress.scale(processedPathPoints, totalPathPoints, terrainAdjustedPercentage, 86L)); - } - } - - int processedSideBlocks = 0; - - for (RailSideBlock sideBlock : sideBlocks.values()) { - railBlocks.put(sideBlock.key(), createAnvilBlockState(resolveSideBlockFacing(sideBlock, sideBlocks))); - processedSideBlocks++; - preparationProgress.update(preparationProgress.scale(processedSideBlocks, sideBlocks.size(), 86L, 89L)); - } - - int processedCenterPoints = 0; - - for (List railCenterPath : railCenterPaths) { - for (Vector center : railCenterPath) { - railBlocks.put(PositionKey.from(center), createCenterBlockState(center)); - processedCenterPoints++; - preparationProgress.update(preparationProgress.scale(processedCenterPoints, totalPathPoints, 89L, buildFinishedPercentage)); - } - } - - return railBlocks; - } - - private int getTotalPathPointCount(List> railCenterPaths) { - int totalPathPoints = 0; - - for (List railCenterPath : railCenterPaths) - totalPathPoints += railCenterPath.size(); - - return Math.max(1, totalPathPoints); - } - - private List getSidePlacements(List path, int index) { - 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))); - - if (previousStep != null) - addSidePlacements(placements, previousStep); - - if (nextStep != null) - addSidePlacements(placements, nextStep); - - return placements; - } - - private void addSidePlacements(List placements, RailStep step) { - 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)); - 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) { - for (RailSidePlacement placement : placements) { - if (placement.offset().equals(offset)) return; - } - - placements.add(new RailSidePlacement(offset, facing)); - } - - private void addSideBlock( - Map sideBlocks, - Vector center, - RailSidePlacement sidePlacement, - Set centerPositions - ) { - RailStep sideOffset = sidePlacement.offset(); - - if (sideOffset.dx() == 0 && sideOffset.dz() == 0) - return; - - int x = center.getBlockX() + sideOffset.dx(); - int z = center.getBlockZ() + sideOffset.dz(); - int y = terrainResolver.getNearestRailSurfaceY(x, z, center.getBlockY()); - - PositionKey key = PositionKey.of(x, y, z); - - if (centerPositions.contains(key)) - return; - - sideBlocks - .computeIfAbsent(key, ignored -> new RailSideBlock(key, DEFAULT_FACING)) - .addFacing(sidePlacement.facing()); - } - - 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); - Direction preferredFacing = sideBlock.getPreferredFacing(); - - if (xConnections > zConnections) - return resolveAxisFacing(preferredFacing, Direction.EAST, Direction.WEST, east, west); - - if (zConnections > xConnections) - return resolveAxisFacing(preferredFacing, Direction.SOUTH, Direction.NORTH, south, north); - - return preferredFacing; - } - - private Direction resolveAxisFacing( - Direction preferredFacing, - Direction positiveFacing, - Direction negativeFacing, - boolean hasPositiveNeighbor, - boolean hasNegativeNeighbor - ) { - if (preferredFacing == positiveFacing && hasPositiveNeighbor || preferredFacing == negativeFacing && hasNegativeNeighbor) - return preferredFacing; - - if (hasPositiveNeighbor && !hasNegativeNeighbor) - return positiveFacing; - - if (hasNegativeNeighbor && !hasPositiveNeighbor) - return negativeFacing; - - return preferredFacing == negativeFacing ? negativeFacing : positiveFacing; - } - - private Set getCenterPositions(List> railCenterPaths) { - Set centerPositions = new HashSet<>(); - - for (List railCenterPath : railCenterPaths) - for (Vector center : railCenterPath) - centerPositions.add(PositionKey.from(center)); - - return centerPositions; - } - - private 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; - - if (previousStep != null && nextStep != null) { - int dx = Integer.compare(previousStep.dx() + nextStep.dx(), 0); - int dz = Integer.compare(previousStep.dz() + nextStep.dz(), 0); - - if (dx != 0 || dz != 0) return new RailStep(dx, dz); - } - - if (nextStep != null) return nextStep; - - if (previousStep != null) return previousStep; - - return fallbackStep; - } - - private RailStep getStep(Vector from, Vector to) { - int dx = Integer.compare(to.getBlockX() - from.getBlockX(), 0); - int dz = Integer.compare(to.getBlockZ() - from.getBlockZ(), 0); - - if (dx == 0 && dz == 0) return null; - - return new RailStep(dx, dz); - } - - private BlockState createCenterBlockState(Vector position) { - return switch (railType) { - case STANDARD -> createStandardCenterBlockState(position); - }; - } - - private BlockState createStandardCenterBlockState(Vector position) { - int index = Math.floorMod( - position.getBlockX() * 31 + position.getBlockY() * 23 + position.getBlockZ() * 17, - CENTER_MATERIALS.length - ); - - return GeneratorUtils.getBlockState(CENTER_MATERIALS[index]); - } - - private BlockState createAnvilBlockState(Direction direction) { - return GeneratorUtils.getBlockStateWithFacing(BlockTypes.ANVIL, direction); - } -} 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 deleted file mode 100644 index 62f2ae9d..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailLanePathBuilder.java +++ /dev/null @@ -1,211 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; - -import com.alpsbte.alpslib.utils.GeneratorUtils; -import org.bukkit.util.Vector; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -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) { - this.controlPoints = controlPoints; - this.terrainResolver = terrainResolver; - this.railLaneCount = railLaneCount; - this.railLaneSpacing = railLaneSpacing; - } - - List> createRailCenterPaths(List path) { - if (railLaneCount <= 1) - return List.of(path); - - List> railCenterPaths = new ArrayList<>(); - int sideLaneCount = (railLaneCount - 1) / 2; - - for (int laneIndex = sideLaneCount; laneIndex >= 1; laneIndex--) { - List leftLane = createShiftedRailLane(laneIndex * railLaneSpacing, 1); - - if (leftLane.size() >= 2) - railCenterPaths.add(leftLane); - } - - railCenterPaths.add(path); - - for (int laneIndex = 1; laneIndex <= sideLaneCount; laneIndex++) { - List rightLane = createShiftedRailLane(laneIndex * railLaneSpacing, -1); - - if (rightLane.size() >= 2) - railCenterPaths.add(rightLane); - } - - return railCenterPaths; - } - - private List createShiftedRailLane(int distance, int sideSign) { - List> shiftedLines = GeneratorUtils.shiftPointsAll(controlPoints, distance); - List candidatePoints = flattenShiftedLines(shiftedLines); - - if (candidatePoints.isEmpty()) - return Collections.emptyList(); - - List shiftedControlPoints = createShiftedControlPoints(candidatePoints, distance, sideSign); - - if (shiftedControlPoints.size() < 2) - return Collections.emptyList(); - - List shiftedPath = createCenterPath(shiftedControlPoints); - - if (shiftedPath.size() < 2) - return Collections.emptyList(); - - terrainResolver.adjustPathToTerrain(shiftedPath); - 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) { - 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); - addIfDifferentFromPrevious(shiftedControlPoints, shiftedPoint); - } - - return shiftedControlPoints; - } - - private Vector getControlPointDirection(int index) { - if (controlPoints.size() < 2) - return new Vector(0, 0, 0); - - if (index == 0) - return getHorizontalDirection(controlPoints.get(0), controlPoints.get(1)); - - if (index == controlPoints.size() - 1) - return getHorizontalDirection(controlPoints.get(index - 1), controlPoints.get(index)); - - 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); - - if (combinedDirection.lengthSquared() != 0) - return combinedDirection; - - if (nextDirection.lengthSquared() != 0) - return nextDirection; - - return previousDirection; - } - - private Vector getHorizontalDirection(Vector from, Vector to) { - return new Vector( - to.getBlockX() - from.getBlockX(), - 0, - to.getBlockZ() - from.getBlockZ() - ); - } - - private 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 idealPoint; - - return new Vector(bestPoint.getBlockX(), basePoint.getBlockY(), bestPoint.getBlockZ()); - } - - 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 void addIfDifferentFromPrevious(List points, Vector point) { - if (points.isEmpty()) { - points.add(point); - return; - } - - Vector previousPoint = points.get(points.size() - 1); - - if (previousPoint.getBlockX() == point.getBlockX() && previousPoint.getBlockZ() == point.getBlockZ()) - return; - - 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/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/RailSettings.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailSettings.java index ae244c3d..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; @@ -11,6 +12,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 deleted file mode 100644 index 6c641532..00000000 --- a/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/RailType.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; - -import com.cryptomorin.xseries.XMaterial; -import lombok.Getter; -import org.jspecify.annotations.Nullable; - -public enum RailType { - - STANDARD("standard", "Standard", XMaterial.RAIL); - - @Getter - private final String identifier; - @Getter - private final String displayName; - @Getter - private final XMaterial icon; - - RailType(String identifier, String displayName, XMaterial icon) { - this.identifier = identifier; - this.displayName = displayName; - this.icon = icon; - } - - public static @Nullable RailType byString(String value) { - for (RailType railType : RailType.values()) { - if (railType.getIdentifier().equalsIgnoreCase(value) || railType.name().equalsIgnoreCase(value)) - return railType; - } - - return null; - } -} 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 new file mode 100644 index 00000000..92ee3d93 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailType.java @@ -0,0 +1,513 @@ +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; + +import java.util.List; + +@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 MIN_OVERHEAD_POLE_SPACING = 1; + public static final int MAX_OVERHEAD_POLE_SPACING = 512; + 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 = 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; + public static final int DEFAULT_OVERHEAD_POLE_HEIGHT = 6; + + private static final String IDENTIFIER_PATTERN = "[a-z0-9_-]+"; + + private final String identifier; + private final String displayName; + 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 List trackSpacings; + 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; + private final boolean overheadWiresEnabled; + private final @Nullable XMaterial overheadWireBlock; + private final boolean trackSwitchesEnabled; + 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.trackSpacings = normalizeTrackSpacings( + configuration.trackSpacings(), + configuration.trackCount(), + 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(); + this.overheadWiresEnabled = configuration.overheadWiresEnabled(); + this.overheadWireBlock = configuration.overheadWireBlock(); + this.trackSwitchesEnabled = configuration.trackSwitchesEnabled(); + this.builtIn = builtIn; + } + + 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. + */ + 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) + .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(true) + .overheadWireBlock(XMaterial.IRON_BARS) + .trackSwitchesEnabled(false), 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; + + error = validateTracks(configuration); + + if (error != null) + return error; + + return validateOverheadWires(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); + + 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); + + if (poleError != null) + return poleError; + + if (configuration.overheadWiresEnabled() && !isPlaceableBlock(configuration.overheadWireBlock())) + return "Overhead wire block must be a valid placeable Minecraft block."; + + 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() + || identifier.length() > MAX_IDENTIFIER_LENGTH + || !identifier.toLowerCase().matches(IDENTIFIER_PATTERN); + } + + private static boolean isPlaceableBlock(@Nullable XMaterial material) { + if (material == null) + return false; + + Material bukkitMaterial = material.get(); + return bukkitMaterial != null && bukkitMaterial.isBlock(); + } + + public static @Nullable RailType byString(String value) { + 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; + private @Nullable List trackSpacings; + 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; + private boolean overheadWiresEnabled; + private @Nullable XMaterial overheadWireBlock; + private boolean trackSwitchesEnabled; + + public @Nullable String identifier() { + return identifier; + } + + 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; + } + + public @Nullable List trackSpacings() { + return trackSpacings; + } + + public Configuration trackSpacings(@Nullable List trackSpacings) { + this.trackSpacings = trackSpacings; + 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 @Nullable XMaterial overheadSupportBlock() { + return overheadSupportBlock; + } + + public Configuration overheadSupportBlock(@Nullable XMaterial overheadSupportBlock) { + this.overheadSupportBlock = overheadSupportBlock; + 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; + } + + 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 new file mode 100644 index 00000000..9e4500e4 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/configuration/RailTypeManager.java @@ -0,0 +1,466 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.configuration; + +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 SCHEMA_VERSION_KEY = "schema-version"; + static final int CURRENT_SCHEMA_VERSION = 4; + 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 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"; + 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 static final String KEY_TRACK_SWITCHES_ENABLED = "track-switches.enabled"; + + 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; + + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + migrateStorage(config); + ConfigurationSection typesSection = config.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(toConfiguration(railType)); + + 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(); + } + + /** + * 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; + + 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); + 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)); + 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)); + boolean trackSwitchesEnabled = section.getBoolean(KEY_TRACK_SWITCHES_ENABLED, false); + + 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) + .trackSpacings(trackSpacings) + .overheadPolesEnabled(overheadPolesEnabled) + .overheadPoleBlock(overheadPoleBlock) + .overheadSupportBlock(overheadSupportBlock) + .overheadPoleSpacing(overheadPoleSpacing) + .overheadPoleOffset(overheadPoleOffset) + .overheadPoleHeight(overheadPoleHeight) + .overheadWiresEnabled(overheadWiresEnabled) + .overheadWireBlock(overheadWireBlock) + .trackSwitchesEnabled(trackSwitchesEnabled)); + + 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) + .trackSpacings(trackSpacings) + .overheadPolesEnabled(overheadPolesEnabled) + .overheadPoleBlock(overheadPoleBlock) + .overheadSupportBlock(overheadSupportBlock) + .overheadPoleSpacing(overheadPoleSpacing) + .overheadPoleOffset(overheadPoleOffset) + .overheadPoleHeight(overheadPoleHeight) + .overheadWiresEnabled(overheadWiresEnabled) + .overheadWireBlock(overheadWireBlock) + .trackSwitchesEnabled(trackSwitchesEnabled), false)); + } + + private @Nullable String save() { + YamlConfiguration config = new YamlConfiguration(); + config.set(SCHEMA_VERSION_KEY, CURRENT_SCHEMA_VERSION); + + 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()); + 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()); + 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(); + + 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) + config.set(path + "." + KEY_OVERHEAD_WIRE_BLOCK, overheadWireBlock.name()); + } + + 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; + } + + 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); + migrateVersionTwoToThree(config); + migrateVersionThreeToFour(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 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 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); + } + + 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()) + .trackSpacings(railType.getTrackSpacings()) + .overheadPolesEnabled(railType.isOverheadPolesEnabled()) + .overheadPoleBlock(railType.getOverheadPoleBlock()) + .overheadSupportBlock(railType.getOverheadSupportBlock()) + .overheadPoleSpacing(railType.getOverheadPoleSpacing()) + .overheadPoleOffset(railType.getOverheadPoleOffset()) + .overheadPoleHeight(railType.getOverheadPoleHeight()) + .overheadWiresEnabled(railType.isOverheadWiresEnabled()) + .overheadWireBlock(railType.getOverheadWireBlock()) + .trackSwitchesEnabled(railType.isTrackSwitchesEnabled()); + } +} 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/generation/RailBlockBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java new file mode 100644 index 00000000..c48ee32f --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailBlockBuilder.java @@ -0,0 +1,487 @@ +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; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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; + + private final RailTerrainResolver terrainResolver; + private final RailType railType; + private final BlockType railBlockType; + private final RailPreparationProgress preparationProgress; + private final long terrainAdjustedPercentage; + private final long buildFinishedPercentage; + + RailBlockBuilder( + RailTerrainResolver terrainResolver, + RailType railType, + RailPreparationProgress preparationProgress, + long terrainAdjustedPercentage, + long buildFinishedPercentage + ) { + this.terrainResolver = terrainResolver; + this.railType = railType; + this.railBlockType = getRailBlockType(railType); + this.preparationProgress = preparationProgress; + this.terrainAdjustedPercentage = terrainAdjustedPercentage; + this.buildFinishedPercentage = buildFinishedPercentage; + } + + Map build(List> railCenterPaths) { + Map railBlocks = new LinkedHashMap<>(); + Set centerPositions = getCenterPositions(railCenterPaths); + Map sideBlocks = new LinkedHashMap<>(); + int totalPathPoints = getTotalPathPointCount(railCenterPaths); + int processedPathPoints = 0; + + 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, facing)) + addSideBlock(sideBlocks, center, sidePlacement, centerPositions); + + processedPathPoints++; + preparationProgress.update(preparationProgress.scale(processedPathPoints, totalPathPoints, terrainAdjustedPercentage, 86L)); + } + } + + int processedSideBlocks = 0; + + for (RailSideBlock sideBlock : sideBlocks.values()) { + railBlocks.put(sideBlock.key(), createRailBlockState(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 (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)); + } + } + + 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; + + for (List railCenterPath : railCenterPaths) + totalPathPoints += railCenterPath.size(); + + return Math.max(1, totalPathPoints); + } + + 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)), facing); + + if (previousStep != null) + addSidePlacements(placements, previousStep, facing); + + if (nextStep != null) + addSidePlacements(placements, nextStep, facing); + + return placements; + } + + private static void addSidePlacements(List placements, RailStep step, Direction facing) { + if (step.dx() != 0 && step.dz() != 0) { + addSidePlacement(placements, new RailStep(step.dx(), 0), facing); + addSidePlacement(placements, new RailStep(0, step.dz()), facing); + return; + } + + if (step.dx() != 0) { + addSidePlacement(placements, new RailStep(0, 1), facing); + addSidePlacement(placements, new RailStep(0, -1), facing); + return; + } + + addSidePlacement(placements, new RailStep(1, 0), facing); + addSidePlacement(placements, new RailStep(-1, 0), 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; + } + + placements.add(new RailSidePlacement(offset, facing)); + } + + private void addSideBlock( + Map sideBlocks, + Vector center, + RailSidePlacement sidePlacement, + Set centerPositions + ) { + RailStep sideOffset = sidePlacement.offset(); + + if (sideOffset.dx() == 0 && sideOffset.dz() == 0) + return; + + int x = center.getBlockX() + sideOffset.dx(); + int z = center.getBlockZ() + sideOffset.dz(); + int y = terrainResolver.getNearestRailSurfaceY(x, z, center.getBlockY()); + + PositionKey key = PositionKey.of(x, y, z); + + if (centerPositions.contains(key)) + return; + + sideBlocks + .computeIfAbsent(key, ignored -> new RailSideBlock(key, DEFAULT_FACING)) + .addFacing(sidePlacement.facing()); + } + + private Direction resolveSideBlockFacing(RailSideBlock sideBlock, Map sideBlocks) { + 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, + connections.east(), + connections.west() + ); + + if (zConnections > xConnections) + return resolveAxisFacing( + preferredFacing, + Direction.SOUTH, + Direction.NORTH, + connections.south(), + connections.north() + ); + + return preferredFacing; + } + + private Direction resolveAxisFacing( + Direction preferredFacing, + Direction positiveFacing, + Direction negativeFacing, + boolean hasPositiveNeighbor, + boolean hasNegativeNeighbor + ) { + if (preferredFacing == positiveFacing && hasPositiveNeighbor || preferredFacing == negativeFacing && hasNegativeNeighbor) + return preferredFacing; + + if (hasPositiveNeighbor && !hasNegativeNeighbor) + return positiveFacing; + + if (hasNegativeNeighbor && !hasPositiveNeighbor) + return negativeFacing; + + return preferredFacing == negativeFacing ? negativeFacing : positiveFacing; + } + + private Set getCenterPositions(List> railCenterPaths) { + Set centerPositions = new HashSet<>(); + + for (List railCenterPath : railCenterPaths) + for (Vector center : railCenterPath) + centerPositions.add(PositionKey.from(center)); + + return centerPositions; + } + + 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; + + if (previousStep != null && nextStep != null) { + int dx = Integer.compare(previousStep.dx() + nextStep.dx(), 0); + int dz = Integer.compare(previousStep.dz() + nextStep.dz(), 0); + + if (dx != 0 || dz != 0) return new RailStep(dx, dz); + } + + if (nextStep != null) return nextStep; + + if (previousStep != null) return previousStep; + + return fallbackStep; + } + + 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); + + if (dx == 0 && dz == 0) return null; + + return new RailStep(dx, dz); + } + + private BlockState createCenterBlockState(Vector position, boolean sleeperPoint) { + if (sleeperPoint) + return GeneratorUtils.getBlockState(railType.getSleeperBlock()); + + List blocksBelow = railType.getBlocksBelow(); + int index = Math.floorMod( + position.getBlockX() * 31 + position.getBlockY() * 23 + position.getBlockZ() * 17, + blocksBelow.size() + ); + + return GeneratorUtils.getBlockState(blocksBelow.get(index)); + } + + private BlockState createRailBlockState( + RailSideBlock sideBlock, + Map sideBlocks + ) { + Direction preferredDirection = sideBlock.getPreferredFacing(); + + if (railBlockType.getPropertyMap().containsKey(SHAPE_PROPERTY)) + return createRailShapeBlockState( + resolveSideBlockFacing(sideBlock, sideBlocks), + getConnections(sideBlock.key(), sideBlocks) + ); + + if (!railBlockType.getPropertyMap().containsKey(FACING_PROPERTY)) + return railBlockType.getDefaultState(); + + return GeneratorUtils.getBlockStateWithFacing(railBlockType, preferredDirection); + } + + 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()); + + return blockType == null ? BlockTypes.ANVIL : blockType; + } +} 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/generation/RailLanePathBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java new file mode 100644 index 00000000..72c05da5 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailLanePathBuilder.java @@ -0,0 +1,166 @@ +package net.buildtheearth.buildteamtools.modules.generator.components.rail.generation; + +import com.alpsbte.alpslib.utils.GeneratorUtils; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +final class RailLanePathBuilder { + + private final List controlPoints; + private final RailTerrainResolver terrainResolver; + private final int railLaneCount; + private final List railLaneSpacings; + + RailLanePathBuilder( + List controlPoints, + RailTerrainResolver terrainResolver, + int railLaneCount, + List railLaneSpacings + ) { + this.controlPoints = controlPoints; + this.terrainResolver = terrainResolver; + this.railLaneCount = railLaneCount; + this.railLaneSpacings = railLaneSpacings; + } + + List> createRailCenterPaths(List path) { + if (railLaneCount <= 1) + return List.of(path); + + List> railCenterPaths = new ArrayList<>(); + 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 = offsets.get(laneIndex); + + if (offset == 0) { + railCenterPaths.add(path); + continue; + } + + List shiftedLane = createShiftedRailLane(Math.abs(offset), offset > 0 ? 1 : -1); + + if (shiftedLane.size() >= 2) + railCenterPaths.add(shiftedLane); + } + + return railCenterPaths; + } + + private List createLaneOffsets() { + List positions = new ArrayList<>(railLaneCount); + int position = 0; + positions.add(position); + + for (int spacing : railLaneSpacings) { + position += spacing; + positions.add(position); + } + + 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(); + + List shiftedPath = createCenterPath(shiftedControlPoints); + + if (shiftedPath.size() < 2) + return Collections.emptyList(); + + terrainResolver.adjustPathToTerrain(shiftedPath); + return shiftedPath; + } + + 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 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 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 (previousDirection.lengthSquared() == 0) + return perpendicular(nextDirection, sideSign).multiply(distance); + + if (nextDirection.lengthSquared() == 0) + return perpendicular(previousDirection, sideSign).multiply(distance); + + Vector previousNormal = perpendicular(previousDirection, sideSign); + Vector nextNormal = perpendicular(nextDirection, sideSign); + Vector miter = previousNormal.clone().add(nextNormal); + + if (miter.lengthSquared() < 0.0001D) + return nextNormal.multiply(distance); + + miter.normalize(); + double denominator = miter.dot(nextNormal); + + 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 normalizedHorizontalDirection(Vector from, Vector to) { + Vector direction = new Vector( + to.getBlockX() - from.getBlockX(), + 0, + to.getBlockZ() - from.getBlockZ() + ); + + return direction.lengthSquared() == 0 ? direction : direction.normalize(); + } + + private Vector perpendicular(Vector direction, int sideSign) { + return new Vector(-direction.getZ() * sideSign, 0, direction.getX() * sideSign); + } + + private void addIfDifferentFromPrevious(List points, Vector point) { + if (points.isEmpty()) { + points.add(point); + return; + } + + Vector previousPoint = points.get(points.size() - 1); + + if (previousPoint.getBlockX() == point.getBlockX() && previousPoint.getBlockZ() == point.getBlockZ()) + return; + + points.add(point); + } + + private List createCenterPath(List points) { + return GeneratorUtils.removeOrthogonalCorners(GeneratorUtils.createShortestBlockPath(points)); + } +} 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 54% 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 860fd0ae..e051f902 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; @@ -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,20 +44,56 @@ 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 + ) ); } 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/generation/RailOverheadBuilder.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java new file mode 100644 index 00000000..f2e3c882 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailOverheadBuilder.java @@ -0,0 +1,396 @@ +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 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; +import java.util.Set; + +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; + + RailOverheadBuilder(RailTerrainResolver terrainResolver, RailType railType) { + this.terrainResolver = terrainResolver; + this.railType = railType; + } + + void addTo(Map blocks, List> railCenterPaths) { + int overheadY = getOverheadY(railCenterPaths); + Set trackFootprint = getHorizontalFootprint(blocks.keySet()); + + if (railType.hasOverheadWires()) + addWires(blocks, railCenterPaths, overheadY); + + if (!railType.hasOverheadPoles() || railCenterPaths.isEmpty()) + return; + + 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) { + int maxRailY = Integer.MIN_VALUE; + + for (List path : railCenterPaths) + for (Vector point : path) + maxRailY = Math.max(maxRailY, point.getBlockY()); + + return (maxRailY == Integer.MIN_VALUE ? 0 : maxRailY) + railType.getOverheadPoleHeight(); + } + + private void addWires(Map blocks, List> railCenterPaths, int overheadY) { + XMaterial wireBlock = railType.getOverheadWireBlock(); + + if (wireBlock == null) + return; + + BlockType wireBlockType = Item.convertXMaterialToWEBlockType(wireBlock); + + if (wireBlockType == null) + return; + + for (List path : railCenterPaths) { + List wirePath = createOverheadPath(path, overheadY); + addPathBlocks(blocks, wirePath, wireBlockType, false); + } + } + + private void addPortals( + Map blocks, + List leftPath, + List rightPath, + int overheadY, + Set trackFootprint + ) { + XMaterial poleBlock = railType.getOverheadPoleBlock(); + XMaterial supportBlock = railType.getOverheadSupportBlock(); + + if (poleBlock == null || supportBlock == null) + return; + + BlockState poleState = GeneratorUtils.getBlockState(poleBlock); + BlockType supportBlockType = Item.convertXMaterialToWEBlockType(supportBlock); + + if (supportBlockType == null) + return; + + for (int index = 0; index < leftPath.size(); index += railType.getOverheadPoleSpacing()) { + Vector leftCenter = leftPath.get(index); + 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); + addPortalSupport(blocks, leftPole.x(), overheadY + 1, leftPole.z(), rightPole.x(), rightPole.z(), supportBlockType); + } + } + + 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 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 + ) { + for (int y = Math.min(surfaceY, topY); y <= topY; y++) + blocks.putIfAbsent(PositionKey.of(poleX, y, poleZ), poleState); + } + + private void addPortalSupport( + Map blocks, + int startX, + int topY, + int startZ, + int endX, + int endZ, + BlockType supportBlockType + ) { + 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) { + RailStep direction = getStep(path, index); + return new RailStep(-direction.dz() * sideSign, direction.dx() * sideSign); + } + + 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)); + + addDiagonalConnectorPositions(positions, path); + return positions; + } + + /** + * 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); + RailStep step = getStepBetween(previous, current); + + if (step.dx() == 0 || step.dz() == 0) + continue; + + positions.add(PositionKey.from(selectDiagonalBridge(path, index, previous, current, step))); + } + } + + private Vector selectDiagonalBridge( + 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); + 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) { + 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); + } + + 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 new file mode 100644 index 00000000..88d60843 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/generation/RailPathOverlapValidator.java @@ -0,0 +1,93 @@ +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 MAXIMUM_VERTICAL_DIFFERENCE = 1; + + boolean hasOverlap(List> railCenterPaths) { + Set previousLanePositions = new HashSet<>(); + Set previousLaneDiagonals = new HashSet<>(); + + for (List railCenterPath : railCenterPaths) { + Set currentLaneCenters = new HashSet<>(); + Set currentLaneDiagonals = new HashSet<>(); + + for (int index = 0; index < railCenterPath.size(); index++) { + Vector center = railCenterPath.get(index); + PositionKey centerPosition = PositionKey.from(center); + + 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(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; + } + + 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; + } + + private record DiagonalStep(int x, int z, int y, boolean positiveSlope) { + } +} 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 57% 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 f653b082..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; @@ -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/generation/RailScripts.java similarity index 75% 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 1d055b74..b0319fdc 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,12 +1,17 @@ -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; +import net.buildtheearth.buildteamtools.modules.network.model.Permissions; import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -20,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; @@ -42,14 +47,14 @@ 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 List trackSpacings = List.of(RailType.DEFAULT_TRACK_SPACING); private final RailLimits limits; private final RailPreparationProgress preparationProgress; private final Runnable preparationFinishedCallback; @@ -58,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(); @@ -90,6 +99,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 +154,6 @@ private boolean prepareSession() { snapMissingControlPointHeightsToTerrain(controlPoints); centerPath = createCenterPath(controlPoints); adjustCenterPathToTerrain(); - railType = getRailType(); preparationProgress.completeStage(TERRAIN_ADJUST_PROGRESS); return true; @@ -154,7 +166,29 @@ 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, trackSpacings) + .createRailCenterPaths(centerPath); + + if (railCenterPaths.size() < trackCount) { + sendRailError( + "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, + trackSpacings + ); + 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); + new RailOverheadBuilder(terrainResolver, railType).addTo(railBlocks, railCenterPaths); preparationProgress.completeStage(RAIL_BLOCK_BUILD_PROGRESS); if (railBlocks.size() > limits.maxBlockPlacements()) { @@ -187,6 +221,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()); @@ -256,7 +292,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 +394,74 @@ private List createRailSelectionPoints(List points) { } private int getSelectionPadding() { - int sideLaneCount = (getRailLaneCount() - 1) / 2; - return SELECTION_PADDING + (getRailLaneSpacing() * sideLaneCount) + 2; + 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; } 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(); + trackSpacings = railType.getTrackSpacings(); + + Integer trackCountFlag = getIntegerSetting(RailFlag.TRACK_COUNT); + Integer trackSpacingFlag = getIntegerSetting(RailFlag.TRACK_SPACING); + + if (trackCountFlag != null) + trackCount = trackCountFlag; + + 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); + 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; + } + + 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); } - 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) { @@ -400,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 + )); } } @@ -429,10 +511,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/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 51% 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..e6cc6472 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,8 @@ -package net.buildtheearth.buildteamtools.modules.generator.components.rail; +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/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/RailBlockPickerMenu.java b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java new file mode 100644 index 00000000..8d9fbcbe --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockPickerMenu.java @@ -0,0 +1,86 @@ +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.AbstractMenu; +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(), createParentMenu(player, draft, role), 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); + + 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 new file mode 100644 index 00000000..3903de5f --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailBlockRole.java @@ -0,0 +1,130 @@ +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.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 MenuItems.getBlocksByColor(); + } + + @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); + } + }, + + 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.getBlocksByColor(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setOverheadPoleBlock(material); + } + + @Override + boolean isOverheadSetting() { + return true; + } + }, + + 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() { + return MenuItems.getBlocksByColor(); + } + + @Override + void apply(RailTypeDraft draft, XMaterial material) { + draft.setOverheadWireBlock(material); + } + + @Override + boolean isOverheadSetting() { + return true; + } + }; + + @Getter + private final String menuTitle; + + RailBlockRole(String menuTitle) { + this.menuTitle = menuTitle; + } + + abstract List createChoices(); + + abstract void apply(RailTypeDraft draft, XMaterial material); + + boolean isOverheadSetting() { + return false; + } + +} 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..1853c088 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailMenuText.java @@ -0,0 +1,16 @@ +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 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 new file mode 100644 index 00000000..9786f650 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailOverheadWireMenu.java @@ -0,0 +1,318 @@ +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; +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 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; + 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(SUPPORT_BLOCK_SLOT).setItem(createBlockItem( + "Top Support Block", + draft.getOverheadSupportBlock() + )); + getMenu().getSlot(WIRE_BLOCK_SLOT).setItem(createBlockItem( + "Wire Block", + draft.getOverheadWireBlock() + )); + + getMenu().getSlot(POLE_SPACING_SLOT).setItem(createSpacingItem()); + 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(SUPPORT_BLOCK_SLOT, RailBlockRole.OVERHEAD_SUPPORT_BLOCK); + setBlockPickerClickEvent(WIRE_BLOCK_SLOT, RailBlockRole.OVERHEAD_WIRE_BLOCK); + getMenu().getSlot(POLE_SPACING_SLOT).setClickHandler((player, click) -> openSpacingEditor(player)); + 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(RailMenuText.ENABLED) + : red(RailMenuText.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 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); + } + + 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); + } + + 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/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 new file mode 100644 index 00000000..b79f610d --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeDraft.java @@ -0,0 +1,100 @@ +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.configuration.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 icon = XMaterial.RAIL; + 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; + 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 = true; + private XMaterial overheadWireBlock = XMaterial.IRON_BARS; + private boolean trackSwitchesEnabled; + + 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.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(); + 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 + : 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(); + draft.overheadWiresEnabled = railType.isOverheadWiresEnabled(); + draft.overheadWireBlock = railType.getOverheadWireBlock() == null + ? XMaterial.IRON_BARS + : railType.getOverheadWireBlock(); + 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 new file mode 100644 index 00000000..896b16a9 --- /dev/null +++ b/src/main/java/net/buildtheearth/buildteamtools/modules/generator/components/rail/menu/RailTypeEditorMenu.java @@ -0,0 +1,382 @@ +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.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; +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 ICON_SLOT = 23; + 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; + + 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"); + 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"); + + 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." + )); + 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"), + List.of( + 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( + 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); + 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); + + 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); + reloadMenuAsync(); + }); + 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)); + } + + @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.getIcon()) + .railBlock(draft.getRailBlock()) + .blocksBelow(List.of(draft.getBlockBelow())) + .sleeperBlock(draft.getSleeperBlock()) + .sleeperSpacing(draft.getSleeperSpacing()) + .trackCount(draft.getTrackCount()) + .trackSpacing(draft.getTrackSpacing()) + .trackSpacings(draft.getTrackSpacings()) + .overheadPolesEnabled(draft.isOverheadPolesEnabled()) + .overheadPoleBlock(draft.getOverheadPoleBlock()) + .overheadSupportBlock(draft.getOverheadSupportBlock()) + .overheadPoleSpacing(draft.getOverheadPoleSpacing()) + .overheadPoleOffset(draft.getOverheadPoleOffset()) + .overheadPoleHeight(draft.getOverheadPoleHeight()) + .overheadWiresEnabled(draft.isOverheadWiresEnabled()) + .overheadWireBlock(draft.getOverheadWireBlock()) + .trackSwitchesEnabled(draft.isTrackSwitchesEnabled())); + + 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); + } + + 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 8bb5f6ad..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 @@ -1,61 +1,594 @@ 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.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; +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.Sound; 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.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); + } + + 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()); + else if (value instanceof String identifier && RailType.byString(identifier) != null) + selectedNames.add(identifier); } - private static @NonNull List> getRailTypes() { + private static @NonNull List> getRailTypes(String searchQuery) { 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; + + String normalizedQuery = searchQuery.trim().toLowerCase(Locale.ROOT); + + for (RailType railType : rail.getRailTypeManager().getRailTypes()) { + if (!normalizedQuery.isEmpty() + && !railType.getIdentifier().toLowerCase(Locale.ROOT).contains(normalizedQuery) + && !railType.getDisplayName().toLowerCase(Locale.ROOT).contains(normalizedQuery)) + continue; + + railTypes.add(new MutablePair<>(RailTypeMenuItems.createRailTypeItem(railType), railType.getIdentifier())); } return railTypes; } + static String formatMaterials(List materials) { + return RailTypeMenuItems.formatMaterials(materials); + } + + static String formatMaterial(@Nullable XMaterial material) { + return RailTypeMenuItems.formatMaterial(material); + } + + @Override + protected void setPreviewItems() { + super.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"), + 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.") + ) + )); + + 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 + 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 = RailTypeMenuItems.addSelectionGlow(displayedItem); + + if (selectedForDeletion.contains(item.getRight())) + displayedItem = RailTypeMenuItems.addDeletionMarker(displayedItem); + + 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_LEFT) { + toggleBulkDeleteSelection(clickPlayer, type); + return; + } + + 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'.", + getRailTypeDisplayName(type) + )); + playSound(clickPlayer, Sound.UI_BUTTON_CLICK); + } else { + selectedNames.clear(); + selectedNames.add(type); + clickPlayer.sendMessage(ChatHelper.getStandardComponent( + true, + "Selected rail type '%s'.", + getRailTypeDisplayName(type) + )); + 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 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(); + + 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); + } + + 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; + + 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, searchQuery, selectedForDeletion, true); + }); + + getMenu().getSlot(BULK_DELETE_ITEM_SLOT).setClickHandler((clickPlayer, clickInformation) -> + deleteSelectedRailTypes(clickPlayer)); + 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(); - clickPlayer.playSound(clickPlayer.getLocation(), Sound.UI_BUTTON_CLICK, 1.0F, 1.0F); + 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 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()); + + 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); + } + + 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/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/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; 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/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 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