diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e35210
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.apt_generated
+.apt_generated_tests
+.vscode
+target
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..88ffc1d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,120 @@
+
+
+
+ 4.0.0
+
+ xyz.dcaron.bridges
+ RetractableBridges
+ 1.0-SNAPSHOT
+
+ RetractableBridges
+
+ http://www.example.com
+
+
+ UTF-8
+ 1.8
+ 1.8
+
+
+
+
+ papermc
+ https://papermc.io/repo/repository/maven-public/
+
+
+
+
+
+ junit
+ junit
+ 4.11
+ test
+
+
+ io.papermc.paper
+ paper-api
+ 1.19-R0.1-SNAPSHOT
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+ provided
+
+
+
+
+
+
+ src/main/resources
+ true
+
+ *.yml
+
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.1.0
+
+
+
+ maven-resources-plugin
+ 3.0.2
+
+
+ maven-compiler-plugin
+ 3.8.0
+
+ 17
+
+
+
+ maven-surefire-plugin
+ 2.22.1
+
+
+ maven-jar-plugin
+ 3.0.2
+
+
+ maven-install-plugin
+ 2.5.2
+
+
+ maven-deploy-plugin
+ 2.8.2
+
+
+
+ maven-site-plugin
+ 3.7.1
+
+
+ maven-project-info-reports-plugin
+ 3.0.0
+
+
+ org.projectlombok
+ lombok-maven-plugin
+ 1.18.20.0
+
+
+ generate-sources
+
+ delombok
+
+
+
+
+
+
+
+
diff --git a/src/main/java/xyz/dcaron/bridges/Bridge.java b/src/main/java/xyz/dcaron/bridges/Bridge.java
new file mode 100644
index 0000000..ed73380
--- /dev/null
+++ b/src/main/java/xyz/dcaron/bridges/Bridge.java
@@ -0,0 +1,53 @@
+package xyz.dcaron.bridges;
+
+import java.util.Set;
+
+import org.bukkit.Material;
+import org.bukkit.block.BlockFace;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+
+@EqualsAndHashCode
+public class Bridge {
+
+ @Getter
+ private final String worldName;
+ @Getter
+ @Setter
+ private int x, z;
+ @Getter
+ private final int y, width, height;
+ @Getter
+ private final Material type;
+ @Getter
+ @Setter
+ private Set blockedDirections;
+
+ public Bridge(final String worldName, final int x, final int z, final int y, final int width, final int height,
+ final Material type, final Set blockedDirections) {
+ this.worldName = worldName;
+ this.x = x;
+ this.z = z;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ this.type = type;
+ this.blockedDirections = blockedDirections;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("x: ").append(x);
+ sb.append(" z: ").append(z);
+ sb.append(" y: ").append(y);
+ sb.append(" width: ").append(width);
+ sb.append(" height: ").append(height);
+ sb.append(" material: ").append(type);
+ sb.append(" directions blocked: ").append(blockedDirections.toString());
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/xyz/dcaron/bridges/BridgeBlockListener.java b/src/main/java/xyz/dcaron/bridges/BridgeBlockListener.java
new file mode 100644
index 0000000..69a6e57
--- /dev/null
+++ b/src/main/java/xyz/dcaron/bridges/BridgeBlockListener.java
@@ -0,0 +1,243 @@
+package xyz.dcaron.bridges;
+
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockRedstoneEvent;
+
+public class BridgeBlockListener implements Listener {
+
+ private final BridgeOptions options;
+
+ private final BlockFace[] searchDirections = {
+ BlockFace.UP,
+ BlockFace.NORTH,
+ BlockFace.EAST,
+ BlockFace.WEST,
+ BlockFace.SOUTH
+ };
+
+ private final Set bridgeMovers = new HashSet();
+
+ public BridgeBlockListener(final BridgeOptions options) {
+ this.options = options;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onRedstoneBlockChange(final BlockRedstoneEvent event) {
+ final Block block = event.getBlock();
+ final boolean isPowerOn = event.getOldCurrent() == 0;
+
+ if (!this.isPowerOnOrOffEvent(event)) {
+ return;
+ }
+
+ for (final BlockFace direction : this.searchDirections) {
+ this.findBridge(block, direction).ifPresent((bridge) -> {
+ BridgesPlugin.log("Bridge found " + bridge.toString(), Level.FINE);
+
+ Set blockedDirections = bridge.getBlockedDirections();
+
+ if (isPowerOn) {
+ if (blockedDirections.size() == 3) {
+ if (!blockedDirections.contains(BlockFace.SOUTH)) {
+ move(bridge, BlockFace.SOUTH);
+ } else if (!blockedDirections.contains(BlockFace.EAST)) {
+ move(bridge, BlockFace.EAST);
+ }
+ } else {
+ if (blockedDirections.contains(BlockFace.NORTH) &&
+ blockedDirections.contains(BlockFace.SOUTH)) {
+ move(bridge, BlockFace.EAST);
+ } else {
+ move(bridge, BlockFace.SOUTH);
+ }
+ }
+ } else {
+ if (blockedDirections.size() == 3) {
+ if (!blockedDirections.contains(BlockFace.NORTH)) {
+ move(bridge, BlockFace.NORTH);
+ } else if (!blockedDirections.contains(BlockFace.WEST)) {
+ move(bridge, BlockFace.WEST);
+ }
+ } else {
+ if (blockedDirections.contains(BlockFace.NORTH) &&
+ blockedDirections.contains(BlockFace.SOUTH)) {
+ move(bridge, BlockFace.WEST);
+ } else {
+ move(bridge, BlockFace.NORTH);
+ }
+ }
+ }
+
+ });
+ }
+
+ }
+
+ /**
+ * Find a bridge above the adjacent block. A bridge is a rectangular area of
+ * slabs or double slabs, parallel to the ground. It can have no holes or bits
+ * sticking out, and it must be contrained on three sides, or on two opposite
+ * sides.
+ *
+ * @param block Origin block
+ * @param direction Direction of adjacent block
+ * @return Some Bridge or None
+ */
+ private Optional findBridge(final Block block, final BlockFace direction) {
+ final Block targetBlock = block.getRelative(direction);
+
+ if (!isBlockBridgePowerBlock(targetBlock.getType())) {
+ BridgesPlugin.log("Block is not bridge power block", Level.FINE);
+ return Optional.empty();
+ }
+
+ final Block aboveBlock = targetBlock.getRelative(BlockFace.UP);
+ if (isPotentialBridgeBlock(aboveBlock.getType())) {
+
+ int x = aboveBlock.getX();
+ int y = aboveBlock.getY();
+ int z = aboveBlock.getZ();
+ int width = 1, height = 1;
+ final Material material = aboveBlock.getType();
+
+ Block adjacentBlock = aboveBlock.getRelative(BlockFace.WEST);
+ while (adjacentBlock.getType().equals(material)) {
+ x--;
+ width++;
+ adjacentBlock = adjacentBlock.getRelative(BlockFace.WEST);
+ }
+
+ adjacentBlock = aboveBlock.getRelative(BlockFace.NORTH);
+ while (adjacentBlock.getType().equals(material)) {
+ z--;
+ height++;
+ adjacentBlock = adjacentBlock.getRelative(BlockFace.NORTH);
+ }
+
+ adjacentBlock = aboveBlock.getRelative(BlockFace.EAST);
+ while (adjacentBlock.getType().equals(material)) {
+ width++;
+ adjacentBlock = adjacentBlock.getRelative(BlockFace.EAST);
+ }
+
+ adjacentBlock = aboveBlock.getRelative(BlockFace.SOUTH);
+ while (adjacentBlock.getType().equals(material)) {
+ height++;
+ adjacentBlock = adjacentBlock.getRelative(BlockFace.SOUTH);
+ }
+
+ if (width >= 2 && height >= 2) {
+ // Bridges must be completely filled with no holes and
+ // have no material obtruding it's square form.
+ final World world = targetBlock.getWorld();
+ Set blockedDirections = EnumSet.noneOf(BlockFace.class);
+
+ for (int dz = 0; dz < height; dz++) {
+ Block edgeBlock = world.getBlockAt(x - 1, y, z + dz);
+ if (edgeBlock.getType().equals(material)) {
+ // A block is sticking out.
+ return Optional.empty();
+ } else if (!edgeBlock.getType().isAir()) {
+ blockedDirections.add(BlockFace.WEST);
+ }
+
+ edgeBlock = world.getBlockAt(x + width, y, z + dz);
+ if (edgeBlock.getType().equals(material)) {
+ // A block is sticking out.
+ return Optional.empty();
+ } else if (!edgeBlock.getType().isAir()) {
+ blockedDirections.add(BlockFace.EAST);
+ }
+ }
+
+ for (int dx = 0; dx < width; dx++) {
+ Block edgeBlock = world.getBlockAt(x + dx, y, z - 1);
+ if (edgeBlock.getType().equals(material)) {
+ // A block is sticking out.
+ return Optional.empty();
+ } else if (!edgeBlock.getType().isAir()) {
+ blockedDirections.add(BlockFace.NORTH);
+ }
+
+ for (int dz = 0; dz < height; dz++) {
+ final Block bridgeBlock = world.getBlockAt(x + dx, y, z + dz);
+ if (!bridgeBlock.getType().equals(material)) {
+ // There is a hole in the bridge.
+ return Optional.empty();
+ }
+ }
+
+ edgeBlock = world.getBlockAt(x + dx, y, z + height);
+ if (edgeBlock.getType().equals(material)) {
+ // A block is sticking out.
+ return Optional.empty();
+ } else if (!edgeBlock.getType().isAir()) {
+ blockedDirections.add(BlockFace.SOUTH);
+ }
+ }
+
+ // The bridge is a proper rectangle.
+ if (this.blockedInThreeDirections(blockedDirections) ||
+ this.opposingDirectionsAreBlocked(blockedDirections)) {
+ return Optional.of(new Bridge(world.getName(), x, z, y, width, height, material, blockedDirections));
+ }
+ }
+
+ }
+
+ return Optional.empty();
+ }
+
+ private void move(final Bridge bridge, final BlockFace direction) {
+ this.bridgeMovers.stream()
+ .filter(potentialMover -> potentialMover.getBridge().equals(bridge))
+ .findFirst()
+ .ifPresentOrElse(
+ mover -> {
+ // The bridge material may have changed since it was last moved.
+ mover.setBridge(bridge);
+ mover.move(direction);
+ },
+ () -> {
+ BridgeMover mover = new BridgeMover(bridge, this.options);
+ this.bridgeMovers.add(mover);
+ mover.move(direction);
+ }
+ );
+ }
+
+ private boolean blockedInThreeDirections(final Set blockedDirections) {
+ return blockedDirections.size() == 3;
+ }
+
+ private boolean opposingDirectionsAreBlocked(final Set blockedDirections) {
+ return blockedDirections.size() == 2 &&
+ ((blockedDirections.contains(BlockFace.NORTH) && blockedDirections.contains(BlockFace.SOUTH)) ||
+ (blockedDirections.contains(BlockFace.EAST) && blockedDirections.contains(BlockFace.WEST)));
+ }
+
+ private boolean isPotentialBridgeBlock(final Material material) {
+ return this.options.getBridgeMaterials().contains(material);
+ }
+
+ private boolean isBlockBridgePowerBlock(final Material material) {
+ return this.options.isAllPowerBlocksAllowed() || this.options.getBridgePowerBlocks().contains(material);
+ }
+
+ private boolean isPowerOnOrOffEvent(final BlockRedstoneEvent event) {
+ return event.getOldCurrent() == 0 || !(event.getNewCurrent() == 0);
+ }
+
+}
diff --git a/src/main/java/xyz/dcaron/bridges/BridgeMover.java b/src/main/java/xyz/dcaron/bridges/BridgeMover.java
new file mode 100644
index 0000000..7983341
--- /dev/null
+++ b/src/main/java/xyz/dcaron/bridges/BridgeMover.java
@@ -0,0 +1,287 @@
+package xyz.dcaron.bridges;
+
+import java.awt.Point;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.block.data.type.Slab;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.scheduler.BukkitScheduler;
+
+import lombok.Getter;
+import lombok.Setter;
+
+public class BridgeMover implements Runnable {
+
+ @Getter
+ @Setter
+ private Bridge bridge;
+ private final BridgeOptions options;
+
+ private BlockFace direction = null;
+ private int taskId = 0, boosts, movingDelay;
+
+ public BridgeMover(final Bridge bridge, final BridgeOptions options) {
+ this.bridge = bridge;
+ this.options = options;
+ }
+
+ public void move(final BlockFace direction) {
+ if (direction != this.direction) {
+ this.direction = direction;
+ this.movingDelay = this.options.getTicksPerBridgeMovement();
+ } else if (this.boosts < this.options.getMaximumMultiplePowerBoost()) {
+ this.movingDelay /= 2;
+ if (this.movingDelay < 1) {
+ this.movingDelay = 1;
+ }
+ this.boosts++;
+ }
+
+ BukkitScheduler scheduler = BridgesPlugin.getPlugin().getServer().getScheduler();
+ if (this.taskId != 0) {
+ scheduler.cancelTask(this.taskId);
+ }
+ this.taskId = scheduler.scheduleSyncRepeatingTask(BridgesPlugin.getPlugin(),
+ this, Math.max(this.movingDelay / 2, 1), this.movingDelay);
+ }
+
+ @Override
+ public void run() {
+ if (!tryMoveBridge()) {
+ BridgesPlugin.getPlugin().getServer().getScheduler().cancelTask(this.taskId);
+ this.taskId = 0;
+ this.direction = null;
+ }
+ }
+
+ private boolean tryMoveBridge() {
+ final World world = BridgesPlugin.getPlugin().getServer().getWorld(bridge.getWorldName());
+ if (world == null) {
+ BridgesPlugin.log("World of name " + bridge.getWorldName() + " is missing!", Level.FINE);
+ return false;
+ }
+
+ Set chunkCoordinates = getChunkCoords(bridge.getX(), bridge.getZ(), bridge.getWidth(), bridge.getHeight());
+ if (!this.areAllChunksLoaded(world, chunkCoordinates)) {
+ BridgesPlugin.log("Chunks not loaded, cancelling bridge move!", Level.FINE);
+ return false;
+ }
+
+ if (!this.isBridgeWhole(world)) {
+ BridgesPlugin.log("Bridge is no longer valid, cancelling bridge move!", Level.FINE);
+ return false;
+ }
+
+ if (!this.tryToMove(direction, world, chunkCoordinates)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean bridgeFloatingChecks(final int dx, final int dz, final World world) {
+ boolean floatingOnAir = true;
+ final int ddx = (int) Math.signum(dx);
+ final int ddz = (int) Math.signum(dz);
+ for (int x = bridge.getX() + ddx; x < bridge.getX() + ddx + bridge.getWidth(); x++) {
+ for (int z = bridge.getZ() + ddz; z < bridge.getZ() + ddz + bridge.getHeight(); z++) {
+ final Material materialBeneathBridge = world.getBlockAt(x, bridge.getY() - 1, z).getType();
+
+ if (!materialBeneathBridge.isAir()) {
+ floatingOnAir = false;
+ }
+ }
+ }
+
+ if (floatingOnAir && !this.options.isAllowFloatingBridges()) {
+ BridgesPlugin.log("No solid block beneath bridge and floating not allowed", Level.FINE);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean tryToMove(final BlockFace direction, final World world, final Set chunksLoaded) {
+ int length, dx = 0, dz = 0, xMult = 0, zMult = 0;
+ switch (direction) {
+ case WEST:
+ length = bridge.getHeight();
+ dx = -1;
+ zMult = 1;
+ break;
+ case NORTH:
+ length = bridge.getWidth();
+ dz = -1;
+ xMult = 1;
+ break;
+ case EAST:
+ length = bridge.getHeight();
+ dx = bridge.getWidth();
+ zMult = 1;
+ break;
+ case SOUTH:
+ length = bridge.getWidth();
+ dz = bridge.getHeight();
+ xMult = 1;
+ break;
+ default:
+ throw new AssertionError(direction);
+ }
+
+ for (int i = 0; i < length; i++) {
+ final Material blockType = world.getBlockAt(bridge.getX() + dx + i * xMult, bridge.getY(), bridge.getZ() + dz + i * zMult).getType();
+ if (!blockType.isAir()) {
+ BridgesPlugin.log("Not enough room besides bridge; blocked by " + blockType.toString(), Level.FINE);
+ return false;
+ }
+ }
+
+ if (!bridgeFloatingChecks(dx, dz, world)) {
+ return false;
+ }
+
+ // Its bridge moving time.
+ BridgesPlugin.log("Moving bridge " + direction + " one row", Level.FINE);
+ for (int i = 0; i < length; i++) {
+ world.getBlockAt(bridge.getX() + dx + i * xMult, bridge.getY(), bridge.getZ() + dz + i * zMult).setType(bridge.getType());
+ }
+
+ // Moves entities standing on the bridge.
+ tryMoveEntities(world, chunksLoaded);
+
+ if (dx == -1) {
+ dx = bridge.getWidth() - 1;
+ } else if (dx == bridge.getWidth()) {
+ dx = 0;
+ } else if (dz == -1) {
+ dz = bridge.getHeight() - 1;
+ } else {
+ dz = 0;
+ }
+
+ for (int i = 0; i < length; i++) {
+ // Set block behind to air.
+ world.getBlockAt(bridge.getX() + dx + i * xMult, bridge.getY(), bridge.getZ() + dz + i * zMult).setType(Material.AIR);
+ }
+
+ bridge.setX(bridge.getX() + direction.getModX());
+ bridge.setZ(bridge.getZ() + direction.getModZ());
+
+ return true;
+ }
+
+ private void tryMoveEntities(final World world, final Set chunksLoaded) {
+ if (this.options.isMoveEntitiesOnBridge()) {
+ chunksLoaded.stream()
+ .map(point -> world.getChunkAt(point.x, point.y))
+ .map(chunk -> Arrays.asList(chunk.getEntities()))
+ .flatMap(Collection::stream)
+ .forEach(entity -> {
+ if (this.isOnBridge(entity.getLocation()) && isSpaceToMove(world, entity, entity.getLocation())) {
+ final Location location = entity.getLocation();
+ location.setX(entity.getLocation().getX() + direction.getModX());
+ location.setZ(entity.getLocation().getZ() + direction.getModZ());
+ entity.teleport(location);
+ }
+ });
+ }
+ }
+
+ private boolean isSpaceToMove(final World world, final Entity entity, final Location location) {
+ final int newBlockX = location.getBlockX() + direction.getModX();
+ final int newBlockY = location.getBlockY() + direction.getModY() + ((this.materialIsBottomSlab(world.getBlockAt(location))) ? 1 : 0);
+ final int newBlockZ = location.getBlockZ() + direction.getModZ();
+
+ if (!world.getBlockAt(newBlockX, newBlockY, newBlockZ).getType().isAir()) {
+ return false;
+ }
+
+ if (entity instanceof LivingEntity && ((LivingEntity) entity).getEyeHeight(true) > 1.0) {
+ // Check the block above for double high entities.
+ if (!world.getBlockAt(newBlockX, newBlockY + 1, newBlockZ).getType().isAir()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private boolean materialIsBottomSlab(final Block block) {
+ final BlockData data = block.getBlockData();
+ return (data instanceof Slab && ((Slab) data).getType().equals(Slab.Type.BOTTOM));
+ }
+
+ private boolean isOnBridge(final Location location) {
+ final float dy = (materialIsBottomSlab(location.getBlock().getRelative(BlockFace.SOUTH))) ? 0.5f : 1.0f;
+ return location.getX() >= bridge.getX() && location.getX() < bridge.getX() + bridge.getWidth() &&
+ location.getZ() >= bridge.getZ() && location.getZ() < bridge.getZ() + bridge.getHeight() &&
+ location.getY() >= bridge.getY() + dy && location.getY() < bridge.getY() + dy + 0.25;
+ }
+
+ private boolean isBridgeWhole(final World world) {
+ int bridgeX1 = bridge.getX(), bridgeX2 = bridgeX1 + bridge.getWidth();
+ int bridgeZ1 = bridge.getZ(), bridgeZ2 = bridgeZ1 + bridge.getHeight();
+ final Material bridgeMaterial = bridge.getType();
+
+ for (int x = bridgeX1; x < bridgeX2; x++) {
+ for (int z = bridgeZ1; z < bridgeZ2; z++) {
+ final Block block = world.getBlockAt(x, bridge.getY(), z);
+ if (!block.getType().equals(bridgeMaterial)) {
+ BridgesPlugin.log("Bridge is no longer valid, cancelling bridge move!", Level.FINE);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private boolean areAllChunksLoaded(final World world, final Set chunkCoords) {
+ return !chunkCoords.stream()
+ .anyMatch(point -> !world.isChunkLoaded(point.x, point.y));
+ }
+
+ private Set getChunkCoords(int x, int z, int width, int height) {
+ switch (direction) {
+ case WEST:
+ x--;
+ width++;
+ break;
+ case NORTH:
+ z--;
+ height++;
+ break;
+ case EAST:
+ width++;
+ break;
+ case SOUTH:
+ height++;
+ break;
+ default:
+ break;
+ }
+
+ Set chunkCoords = new HashSet();
+ for (int u = x; u <= (x + width - 1); u += 16) {
+ for (int v = z; v <= (z + height - 1); v += 16) {
+ chunkCoords.add(new Point(u >> 4, v >> 4));
+ }
+ chunkCoords.add(new Point(u >> 4, (z + height - 1) >> 4));
+ }
+ chunkCoords.add(new Point((x + width - 1) >> 4, (z + height - 1) >> 4));
+
+ return chunkCoords;
+ }
+
+}
diff --git a/src/main/java/xyz/dcaron/bridges/BridgeOptions.java b/src/main/java/xyz/dcaron/bridges/BridgeOptions.java
new file mode 100644
index 0000000..51492b5
--- /dev/null
+++ b/src/main/java/xyz/dcaron/bridges/BridgeOptions.java
@@ -0,0 +1,64 @@
+package xyz.dcaron.bridges;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.bukkit.Material;
+import org.bukkit.configuration.file.FileConfiguration;
+
+import lombok.Getter;
+
+public class BridgeOptions {
+
+ @Getter
+ private final boolean moveEntitiesOnBridge;
+ @Getter
+ private final int ticksPerBridgeMovement;
+ @Getter
+ private final Set bridgeMaterials;
+ @Getter
+ private final int maximumMultiplePowerBoost;
+ @Getter
+ private final boolean allowFloatingBridges;
+ @Getter
+ private final Set bridgePowerBlocks;
+ @Getter
+ private final boolean allPowerBlocksAllowed;
+
+ public BridgeOptions(final FileConfiguration configuration) {
+
+ moveEntitiesOnBridge = configuration.getBoolean("moveEntitiesOnBridge");
+
+ ticksPerBridgeMovement = configuration.getInt("ticksPerBridgeMovement");
+
+ bridgeMaterials = configuration.getStringList("bridgeMaterials")
+ .stream()
+ .map(material -> Material.getMaterial(material))
+ .collect(Collectors.toUnmodifiableSet());
+
+ maximumMultiplePowerBoost = configuration.getInt("maximumMultiplePowerBoost");
+
+ allowFloatingBridges = configuration.getBoolean("allowFloatingBridges");
+
+ bridgePowerBlocks = configuration.getStringList("bridgePowerBlocks")
+ .stream()
+ .map(material -> Material.getMaterial(material))
+ .collect(Collectors.toUnmodifiableSet());
+
+ allPowerBlocksAllowed = bridgePowerBlocks.isEmpty();
+ }
+
+ public String getOptionsPrintable() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("\n");
+ sb.append("moveEntitiesOnBridge: ").append(moveEntitiesOnBridge).append("\n");
+ sb.append("ticksPerBridgeMovement: ").append(ticksPerBridgeMovement).append("\n");
+ sb.append("bridgeMaterials: ").append(bridgeMaterials.toString()).append("\n");
+ sb.append("maximumMultiplePowerBoost: ").append(maximumMultiplePowerBoost).append("\n");
+ sb.append("allowFloatingBridges: ").append(allowFloatingBridges).append("\n");
+ sb.append("bridgePowerBlocks: ").append(bridgePowerBlocks.toString()).append("\n");
+ sb.append("allPowerBlocksAllowed: ").append(allPowerBlocksAllowed);
+ return sb.toString();
+ }
+
+}
diff --git a/src/main/java/xyz/dcaron/bridges/BridgesPlugin.java b/src/main/java/xyz/dcaron/bridges/BridgesPlugin.java
new file mode 100644
index 0000000..f3f0cc4
--- /dev/null
+++ b/src/main/java/xyz/dcaron/bridges/BridgesPlugin.java
@@ -0,0 +1,79 @@
+package xyz.dcaron.bridges;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class BridgesPlugin extends JavaPlugin {
+
+ private static Plugin plugin;
+
+ public static Plugin getPlugin() {
+ return BridgesPlugin.plugin;
+ }
+
+ private static final Logger LOGGER = Logger.getLogger("Minecraft.xyz.dcaron.bridges");
+ private static final String CONFIG_YML = "config.yml";
+
+ public static void log(final String message, final Level level) {
+ if (BridgesPlugin.LOGGER.isLoggable(level)) {
+ BridgesPlugin.LOGGER.log(level, "[RetractableBridges] " + message);
+ }
+ }
+
+ @Override
+ public void onDisable() {
+ BridgesPlugin.log("Plugin disabled.", Level.INFO);
+ }
+
+ @Override
+ public void onEnable() {
+ BridgesPlugin.log("Plugin enabled.", Level.INFO);
+ BridgesPlugin.plugin = this;
+
+ final BridgeOptions bridgeOptions = readConfigurationFiles();
+ BridgesPlugin.log(bridgeOptions.getOptionsPrintable(), Level.INFO);
+
+ final PluginManager pluginManager = super.getServer().getPluginManager();
+ pluginManager.registerEvents(new BridgeBlockListener(bridgeOptions), this);
+ }
+
+ private BridgeOptions readConfigurationFiles() {
+ final File configurationFile = new File(super.getDataFolder(), CONFIG_YML);
+ firstRun(configurationFile);
+ final FileConfiguration configData = YamlConfiguration.loadConfiguration(configurationFile);
+ return new BridgeOptions(configData);
+ }
+
+ private void firstRun(final File configurationFile) {
+ if (!configurationFile.exists()) {
+ configurationFile.getParentFile().mkdirs();
+ copyFile(getResource(CONFIG_YML), configurationFile);
+ }
+ }
+
+ private void copyFile(InputStream in, File file) {
+ try {
+ final OutputStream out = new FileOutputStream(file);
+ byte[] buf = new byte[1024];
+ int len;
+ while ((len = in.read(buf)) > 0) {
+ out.write(buf, 0, len);
+ }
+ out.close();
+ in.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
new file mode 100644
index 0000000..710bc2f
--- /dev/null
+++ b/src/main/resources/config.yml
@@ -0,0 +1,99 @@
+# Whether or not to move entities on the bridge along:
+moveEntitiesOnBridge: true
+
+# The speed of the bridge. The number is the number of "ticks" between
+# movements of the bridge. A tick is 1/20 second. In other words, the
+# default value of 30 means 1.5 seconds between movements.
+#
+# If you set this too low your server load may increase exponentially,
+# especially if you have many bridges on your server!
+#
+# Also don't forget that this is a server wide setting for all bridges.
+# Make sure that your users have a say and know about it if you change
+# this setting:
+ticksPerBridgeMovement: 30
+
+# The allowed materials for the bridge. This is a list of block materials.
+# You can find a list of all possible block materials at
+# https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html
+#
+# Be very careful when adding block materials to this list. You don't want any
+# flat surface which just happens to be next to a bit of redstone to
+# start flying around!
+#
+# Also don't forget that this is a server wide setting for all bridges.
+# Make sure that your users have a say and know about it if you change
+# this setting:
+bridgeMaterials: [
+ COBBLESTONE_SLAB,
+ DARK_OAK_SLAB,
+ DIORITE_SLAB,
+ ANDESITE_SLAB,
+ GRANITE_SLAB,
+ JUNGLE_SLAB,
+ NETHER_BRICK_SLAB,
+ OAK_SLAB,
+ QUARTZ_SLAB,
+ SPRUCE_SLAB,
+ STONE_BRICK_SLAB,
+ STONE_SLAB,
+ BIRCH_SLAB,
+ ACACIA_SLAB,
+ MANGROVE_SLAB,
+ SMOOTH_STONE_SLAB
+]
+
+# The maximum number of speed boosts allowed by having multiple power
+# blocks (the support blocks beneath the bridge through which the
+# redstone power is delivered to the bridge).
+#
+# If you set this too high your server load may increase exponentially,
+# especially if you have many bridges on your server!
+#
+# Also don't forget that this is a server wide setting for all bridges.
+# Make sure that your users have a say and know about it if you change
+# this setting:
+maximumMultiplePowerBoost: 2
+
+# Whether to allow "floating" bridges; which in this case specifically
+# means bridges that are not touching any solid blocks directly
+# underneath. You can use this to prevent people from having bridges fly
+# away uncontrolled, either by accident or on purpose.
+#
+# Note that water or lava counts as a solid block for this purpose, so
+# you can still create ferries if you disable this.
+#
+# If you set this to "false", bridges will stop moving if they would
+# lose contact with all solid blocks directly underneath them. In other
+# words, it will always remain in contact with at least one solid block
+# underneath.
+#
+# Note that it can still lose contact with its power block(s). This means
+# that it is still possible to bridge large distances or to have two
+# power blocks at the extreme ends of the bridge's travel.
+#
+# Also don't forget that this is a server wide setting for all bridges.
+# Make sure that your users have a say and know about it if you change
+# this setting:
+allowFloatingBridges: true
+
+# The allowed materials for the power blocks (the support blocks beneath
+# the bridge through which the redstone power is delivered to the
+# bridge). This is a list of block materials. You can find a list of all
+# possible block materials at
+# https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/Material.html.
+# If it is empty (the default), any solid block is allowed.
+#
+# By default, the power blocks can be of any solid type. This setting allows
+# you to restrict that, in order to make it harder to build bridges. It
+# may also slightly improve the performance of the plugin.
+#
+# The value is a list of block materials between brackets, separated by
+# commas. For instance, if you want to limit power blocks to diamond or
+# dirt blocks, use the value [DIAMOND_BLOCK, DIRT]. If you want to allow any
+# solid block material, the list should be empty.
+#
+# Don't forget that this is a server wide setting for all bridges. Make
+# sure that your users have a say and know about it if you change this
+# setting:
+bridgePowerBlocks: []
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..51faa65
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,4 @@
+name: RetractableBridges
+version: 1.0.0
+main: xyz.dcaron.bridges.BridgesPlugin
+api-version: 1.19
\ No newline at end of file
diff --git a/src/test/java/xyz/dcaron/bridges/AppTest.java b/src/test/java/xyz/dcaron/bridges/AppTest.java
new file mode 100644
index 0000000..49620f9
--- /dev/null
+++ b/src/test/java/xyz/dcaron/bridges/AppTest.java
@@ -0,0 +1,14 @@
+package xyz.dcaron.bridges;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class AppTest
+{
+ @Test
+ public void shouldAnswerWithTrue()
+ {
+ assertTrue( true );
+ }
+}