diff --git a/README.md b/README.md index e2563b0..a6a6db1 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,14 @@ A Minecraft plugin that renders visible particle forcefields around WorldGuard r ## Features - Automatically detects WorldGuard regions with `entry deny` flag -- Renders particle forcefields only visible to players who cannot enter +- Renders **visible glass pane barriers** and particle effects for blocked regions +- Only shows forcefields to players who **actually cannot enter** (respects bypass permissions and ops) +- Glass panes only placed where there's currently air (doesn't cover existing blocks) - Configurable particle color, size, spacing, and render distance +- Configurable block material (glass panes, barriers, etc.) - Supports cuboid and polygonal region types - Performance-optimized with distance-based rendering +- Automatic cleanup when players move away or disconnect - Clean, readable, and well-documented code ## Requirements @@ -85,14 +89,27 @@ particle-color: # Particle size (0.5-2.0 recommended) particle-size: 1.0 + +# Block rendering settings +# Whether to render actual blocks (glass panes) in addition to particles +render-blocks: true + +# Distance between blocks (in blocks) +block-spacing: 1.0 + +# Block material to use (e.g., PURPLE_STAINED_GLASS_PANE, BARRIER, GLASS) +block-material: PURPLE_STAINED_GLASS_PANE ``` ## Performance Tips - Reduce `max-render-distance` for servers with many regions - Increase `particle-spacing` to reduce particle count +- Increase `block-spacing` to reduce block count +- Set `render-blocks: false` to disable glass panes and only use particles - Set `render-walls: false` to only show edges - Increase `update-interval-ticks` if you don't need real-time updates +- Use `BARRIER` blocks instead of glass panes (less visible but lighter) ## Troubleshooting @@ -102,8 +119,10 @@ particle-size: 1.0 2. Check the server console for debug messages 3. Use `/forcefield info` to see if regions are being detected 4. Verify the region has `entry deny` set: `/rg info ` -5. Check you're not a member/owner of the region +5. **Check you can't actually enter** - ops and members won't see forcefields 6. Ensure you're within render distance of the region (default: 100 blocks) +7. Try setting `render-blocks: true` if you only see particles +8. Check that locations aren't already occupied by blocks ### Plugin won't load diff --git a/src/main/java/loganintech/regionforcefield/RegionForcefieldPlugin.java b/src/main/java/loganintech/regionforcefield/RegionForcefieldPlugin.java index 128098c..875dec5 100644 --- a/src/main/java/loganintech/regionforcefield/RegionForcefieldPlugin.java +++ b/src/main/java/loganintech/regionforcefield/RegionForcefieldPlugin.java @@ -2,6 +2,7 @@ package loganintech.regionforcefield; import loganintech.regionforcefield.command.ForcefieldCommand; import loganintech.regionforcefield.forcefield.ForcefieldRenderer; +import loganintech.regionforcefield.listener.PlayerListener; import loganintech.regionforcefield.region.RegionPermissionChecker; import loganintech.regionforcefield.task.ForcefieldUpdateTask; import org.bukkit.command.PluginCommand; @@ -35,6 +36,9 @@ public final class RegionForcefieldPlugin extends JavaPlugin { this.permissionChecker = new RegionPermissionChecker(this); this.forcefieldRenderer = new ForcefieldRenderer(this); + // Register listeners + getServer().getPluginManager().registerEvents(new PlayerListener(this), this); + // Register commands ForcefieldCommand commandExecutor = new ForcefieldCommand(this); PluginCommand command = getCommand("forcefield"); diff --git a/src/main/java/loganintech/regionforcefield/forcefield/ForcefieldRenderer.java b/src/main/java/loganintech/regionforcefield/forcefield/ForcefieldRenderer.java index d9b92ff..2f69ca4 100644 --- a/src/main/java/loganintech/regionforcefield/forcefield/ForcefieldRenderer.java +++ b/src/main/java/loganintech/regionforcefield/forcefield/ForcefieldRenderer.java @@ -5,15 +5,19 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import com.sk89q.worldguard.protection.regions.ProtectedPolygonalRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import loganintech.regionforcefield.RegionForcefieldPlugin; import org.bukkit.Color; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.Particle; -import loganintech.regionforcefield.RegionForcefieldPlugin; import org.bukkit.World; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Renders particle forcefields around protected regions. @@ -23,6 +27,8 @@ public class ForcefieldRenderer { private final RegionForcefieldPlugin plugin; private final double particleSpacing; private final Particle.DustOptions dustOptions; + private final PlayerBlockTracker blockTracker; + private final BlockData glassBlockData; /** * Creates a new forcefield renderer. @@ -32,6 +38,7 @@ public class ForcefieldRenderer { public ForcefieldRenderer(@NotNull RegionForcefieldPlugin plugin) { this.plugin = plugin; this.particleSpacing = plugin.getConfig().getDouble("particle-spacing", 0.5); + this.blockTracker = new PlayerBlockTracker(); // Get color from config or use default (purple) int red = plugin.getConfig().getInt("particle-color.red", 147); @@ -40,6 +47,27 @@ public class ForcefieldRenderer { float size = (float) plugin.getConfig().getDouble("particle-size", 1.0); this.dustOptions = new Particle.DustOptions(Color.fromRGB(red, green, blue), size); + + // Get block material from config or use purple stained glass pane + String materialName = plugin.getConfig().getString("block-material", "PURPLE_STAINED_GLASS_PANE"); + Material material; + try { + material = Material.valueOf(materialName.toUpperCase()); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Invalid block material '" + materialName + "', using PURPLE_STAINED_GLASS_PANE"); + material = Material.PURPLE_STAINED_GLASS_PANE; + } + this.glassBlockData = material.createBlockData(); + } + + /** + * Gets the block tracker for managing fake blocks. + * + * @return the block tracker + */ + @NotNull + public PlayerBlockTracker getBlockTracker() { + return blockTracker; } /** @@ -48,49 +76,59 @@ public class ForcefieldRenderer { * @param player the player to show the forcefield to * @param region the region to render * @param world the world the region is in + * @return set of block locations that were rendered */ - public void renderForcefield(@NotNull Player player, @NotNull ProtectedRegion region, @NotNull World world) { + @NotNull + public Set renderForcefield(@NotNull Player player, @NotNull ProtectedRegion region, @NotNull World world) { + Set newBlocks = new HashSet<>(); + try { plugin.debug("Rendering forcefield for region " + region.getId() + " to player " + player.getName()); if (region instanceof ProtectedCuboidRegion) { - renderCuboidForcefield(player, (ProtectedCuboidRegion) region, world); + renderCuboidForcefield(player, (ProtectedCuboidRegion) region, world, newBlocks); } else if (region instanceof ProtectedPolygonalRegion) { - renderPolygonalForcefield(player, (ProtectedPolygonalRegion) region, world); + renderPolygonalForcefield(player, (ProtectedPolygonalRegion) region, world, newBlocks); } else { // For other region types, fall back to rendering a bounding box plugin.debug("Using bounding box for region type: " + region.getClass().getSimpleName()); - renderBoundingBoxForcefield(player, region, world); + renderBoundingBoxForcefield(player, region, world, newBlocks); } + + plugin.debug("Rendered " + newBlocks.size() + " blocks for region " + region.getId()); } catch (Exception e) { plugin.getLogger().warning("Error rendering forcefield for region " + region.getId() + ": " + e.getMessage()); e.printStackTrace(); } + + return newBlocks; } /** * Renders a forcefield for a cuboid region. */ - private void renderCuboidForcefield(@NotNull Player player, @NotNull ProtectedCuboidRegion region, @NotNull World world) { + private void renderCuboidForcefield(@NotNull Player player, @NotNull ProtectedCuboidRegion region, + @NotNull World world, @NotNull Set blocks) { BlockVector3 min = region.getMinimumPoint(); BlockVector3 max = region.getMaximumPoint(); // Render vertical edges - renderVerticalEdges(player, world, min, max); + renderVerticalEdges(player, world, min, max, blocks); // Render horizontal edges at top and bottom - renderHorizontalEdges(player, world, min, max); + renderHorizontalEdges(player, world, min, max, blocks); // Optionally render faces (walls) if (plugin.getConfig().getBoolean("render-walls", true)) { - renderWalls(player, world, min, max); + renderWalls(player, world, min, max, blocks); } } /** * Renders a forcefield for a polygonal region. */ - private void renderPolygonalForcefield(@NotNull Player player, @NotNull ProtectedPolygonalRegion region, @NotNull World world) { + private void renderPolygonalForcefield(@NotNull Player player, @NotNull ProtectedPolygonalRegion region, + @NotNull World world, @NotNull Set blocks) { List points = region.getPoints(); int minY = region.getMinimumPoint().y(); int maxY = region.getMaximumPoint().y(); @@ -100,64 +138,71 @@ public class ForcefieldRenderer { BlockVector2 point1 = points.get(i); BlockVector2 point2 = points.get((i + 1) % points.size()); - renderVerticalWall(player, world, point1.x(), point1.z(), point2.x(), point2.z(), minY, maxY); + renderVerticalWall(player, world, point1.x(), point1.z(), point2.x(), point2.z(), minY, maxY, blocks); } } /** * Renders a bounding box forcefield for unsupported region types. */ - private void renderBoundingBoxForcefield(@NotNull Player player, @NotNull ProtectedRegion region, @NotNull World world) { + private void renderBoundingBoxForcefield(@NotNull Player player, @NotNull ProtectedRegion region, + @NotNull World world, @NotNull Set blocks) { BlockVector3 min = region.getMinimumPoint(); BlockVector3 max = region.getMaximumPoint(); - renderVerticalEdges(player, world, min, max); - renderHorizontalEdges(player, world, min, max); + renderVerticalEdges(player, world, min, max, blocks); + renderHorizontalEdges(player, world, min, max, blocks); } /** * Renders the vertical edges of a cuboid. */ - private void renderVerticalEdges(@NotNull Player player, @NotNull World world, @NotNull BlockVector3 min, @NotNull BlockVector3 max) { + private void renderVerticalEdges(@NotNull Player player, @NotNull World world, + @NotNull BlockVector3 min, @NotNull BlockVector3 max, + @NotNull Set blocks) { // Four vertical edges - renderLine(player, world, min.x(), min.y(), min.z(), min.x(), max.y(), min.z()); - renderLine(player, world, max.x(), min.y(), min.z(), max.x(), max.y(), min.z()); - renderLine(player, world, min.x(), min.y(), max.z(), min.x(), max.y(), max.z()); - renderLine(player, world, max.x(), min.y(), max.z(), max.x(), max.y(), max.z()); + renderLine(player, world, min.x(), min.y(), min.z(), min.x(), max.y(), min.z(), blocks); + renderLine(player, world, max.x(), min.y(), min.z(), max.x(), max.y(), min.z(), blocks); + renderLine(player, world, min.x(), min.y(), max.z(), min.x(), max.y(), max.z(), blocks); + renderLine(player, world, max.x(), min.y(), max.z(), max.x(), max.y(), max.z(), blocks); } /** * Renders the horizontal edges of a cuboid. */ - private void renderHorizontalEdges(@NotNull Player player, @NotNull World world, @NotNull BlockVector3 min, @NotNull BlockVector3 max) { + private void renderHorizontalEdges(@NotNull Player player, @NotNull World world, + @NotNull BlockVector3 min, @NotNull BlockVector3 max, + @NotNull Set blocks) { // Bottom edges - renderLine(player, world, min.x(), min.y(), min.z(), max.x(), min.y(), min.z()); - renderLine(player, world, min.x(), min.y(), max.z(), max.x(), min.y(), max.z()); - renderLine(player, world, min.x(), min.y(), min.z(), min.x(), min.y(), max.z()); - renderLine(player, world, max.x(), min.y(), min.z(), max.x(), min.y(), max.z()); + renderLine(player, world, min.x(), min.y(), min.z(), max.x(), min.y(), min.z(), blocks); + renderLine(player, world, min.x(), min.y(), max.z(), max.x(), min.y(), max.z(), blocks); + renderLine(player, world, min.x(), min.y(), min.z(), min.x(), min.y(), max.z(), blocks); + renderLine(player, world, max.x(), min.y(), min.z(), max.x(), min.y(), max.z(), blocks); // Top edges - renderLine(player, world, min.x(), max.y(), min.z(), max.x(), max.y(), min.z()); - renderLine(player, world, min.x(), max.y(), max.z(), max.x(), max.y(), max.z()); - renderLine(player, world, min.x(), max.y(), min.z(), min.x(), max.y(), max.z()); - renderLine(player, world, max.x(), max.y(), min.z(), max.x(), max.y(), max.z()); + renderLine(player, world, min.x(), max.y(), min.z(), max.x(), max.y(), min.z(), blocks); + renderLine(player, world, min.x(), max.y(), max.z(), max.x(), max.y(), max.z(), blocks); + renderLine(player, world, min.x(), max.y(), min.z(), min.x(), max.y(), max.z(), blocks); + renderLine(player, world, max.x(), max.y(), min.z(), max.x(), max.y(), max.z(), blocks); } /** * Renders the walls (faces) of a cuboid. */ - private void renderWalls(@NotNull Player player, @NotNull World world, @NotNull BlockVector3 min, @NotNull BlockVector3 max) { + private void renderWalls(@NotNull Player player, @NotNull World world, + @NotNull BlockVector3 min, @NotNull BlockVector3 max, + @NotNull Set blocks) { // North wall (min Z) - renderVerticalWall(player, world, min.x(), min.z(), max.x(), min.z(), min.y(), max.y()); + renderVerticalWall(player, world, min.x(), min.z(), max.x(), min.z(), min.y(), max.y(), blocks); // South wall (max Z) - renderVerticalWall(player, world, min.x(), max.z(), max.x(), max.z(), min.y(), max.y()); + renderVerticalWall(player, world, min.x(), max.z(), max.x(), max.z(), min.y(), max.y(), blocks); // West wall (min X) - renderVerticalWall(player, world, min.x(), min.z(), min.x(), max.z(), min.y(), max.y()); + renderVerticalWall(player, world, min.x(), min.z(), min.x(), max.z(), min.y(), max.y(), blocks); // East wall (max X) - renderVerticalWall(player, world, max.x(), min.z(), max.x(), max.z(), min.y(), max.y()); + renderVerticalWall(player, world, max.x(), min.z(), max.x(), max.z(), min.y(), max.y(), blocks); } /** @@ -165,10 +210,11 @@ public class ForcefieldRenderer { */ private void renderVerticalWall(@NotNull Player player, @NotNull World world, double x1, double z1, double x2, double z2, - double minY, double maxY) { + double minY, double maxY, @NotNull Set blocks) { double distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(z2 - z1, 2)); int horizontalSteps = (int) Math.ceil(distance / particleSpacing); int verticalSteps = (int) Math.ceil((maxY - minY) / particleSpacing); + double blockSpacing = plugin.getConfig().getDouble("block-spacing", 1.0); for (int i = 0; i <= horizontalSteps; i++) { double t = horizontalSteps > 0 ? (double) i / horizontalSteps : 0; @@ -178,6 +224,12 @@ public class ForcefieldRenderer { for (int j = 0; j <= verticalSteps; j++) { double y = minY + (maxY - minY) * ((double) j / verticalSteps); spawnParticle(player, world, x, y, z); + + // Place blocks at intervals + if (i % ((int) Math.max(1, blockSpacing / particleSpacing)) == 0 && + j % ((int) Math.max(1, blockSpacing / particleSpacing)) == 0) { + placeBlock(player, world, x, y, z, blocks); + } } } } @@ -187,7 +239,8 @@ public class ForcefieldRenderer { */ private void renderLine(@NotNull Player player, @NotNull World world, double x1, double y1, double z1, - double x2, double y2, double z2) { + double x2, double y2, double z2, + @NotNull Set blocks) { double distance = Math.sqrt( Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2) + @@ -195,6 +248,7 @@ public class ForcefieldRenderer { ); int steps = (int) Math.ceil(distance / particleSpacing); + double blockSpacing = plugin.getConfig().getDouble("block-spacing", 1.0); for (int i = 0; i <= steps; i++) { double t = steps > 0 ? (double) i / steps : 0; @@ -203,6 +257,11 @@ public class ForcefieldRenderer { double z = z1 + (z2 - z1) * t; spawnParticle(player, world, x, y, z); + + // Place blocks at intervals + if (i % ((int) Math.max(1, blockSpacing / particleSpacing)) == 0) { + placeBlock(player, world, x, y, z, blocks); + } } } @@ -213,4 +272,63 @@ public class ForcefieldRenderer { Location location = new Location(world, x, y, z); player.spawnParticle(Particle.DUST, location, 1, 0, 0, 0, 0, dustOptions); } + + /** + * Places a fake block at the specified location if it's air. + * + * @param player the player to send the block to + * @param world the world + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param blocks the set to add this block location to + */ + private void placeBlock(@NotNull Player player, @NotNull World world, + double x, double y, double z, @NotNull Set blocks) { + if (!plugin.getConfig().getBoolean("render-blocks", true)) { + return; + } + + Location location = new Location(world, Math.floor(x), Math.floor(y), Math.floor(z)); + + // Only place blocks where there's currently air + if (location.getBlock().getType() == Material.AIR) { + player.sendBlockChange(location, glassBlockData); + blocks.add(location); + } + } + + /** + * Clears all fake blocks for a player by restoring the real blocks. + * + * @param player the player + */ + public void clearBlocks(@NotNull Player player) { + Set blocks = blockTracker.getBlocks(player); + for (Location location : blocks) { + // Send the real block data back to the player + player.sendBlockChange(location, location.getBlock().getBlockData()); + } + blockTracker.clearPlayer(player); + } + + /** + * Updates blocks for a player based on new blocks that should be visible. + * + * @param player the player + * @param newBlocks the new set of blocks to show + */ + public void updateBlocks(@NotNull Player player, @NotNull Set newBlocks) { + Set oldBlocks = blockTracker.getBlocks(player); + + // Remove blocks that are no longer needed + for (Location location : oldBlocks) { + if (!newBlocks.contains(location)) { + player.sendBlockChange(location, location.getBlock().getBlockData()); + } + } + + // Update the tracker + blockTracker.setBlocks(player, newBlocks); + } } diff --git a/src/main/java/loganintech/regionforcefield/region/RegionPermissionChecker.java b/src/main/java/loganintech/regionforcefield/region/RegionPermissionChecker.java index 9035353..e449b98 100644 --- a/src/main/java/loganintech/regionforcefield/region/RegionPermissionChecker.java +++ b/src/main/java/loganintech/regionforcefield/region/RegionPermissionChecker.java @@ -83,21 +83,32 @@ public class RegionPermissionChecker { } /** - * Checks if a player can enter a specific region. + * Checks if a player can actually enter a region. + * Takes into account entry deny flag, bypass permissions, and member/owner status. * * @param player the player to check * @param region the region to check - * @return true if the player can enter, false otherwise + * @return true if the player CAN enter (no forcefield), false if blocked (show forcefield) */ private boolean canEnterRegion(@NotNull LocalPlayer player, @NotNull ProtectedRegion region) { // Check the ENTRY flag - // If ENTRY is set to DENY, the player cannot enter unless they have bypass permissions if (region.getFlag(Flags.ENTRY) == com.sk89q.worldguard.protection.flags.StateFlag.State.DENY) { + // Check if player has bypass permission (includes ops) + if (player.hasPermission("worldguard.region.bypass." + region.getId()) || + player.hasPermission("worldguard.region.bypass.*")) { + return true; // Can enter (has bypass), no forcefield + } + // Check if the player is a member or owner of the region - return region.isMember(player) || region.isOwner(player); + if (region.isMember(player) || region.isOwner(player)) { + return true; // Can enter (is member/owner), no forcefield + } + + // Player cannot enter, show forcefield + return false; } - // If ENTRY is not explicitly denied, the player can enter + // If ENTRY is not explicitly denied, player can enter return true; } } diff --git a/src/main/java/loganintech/regionforcefield/task/ForcefieldUpdateTask.java b/src/main/java/loganintech/regionforcefield/task/ForcefieldUpdateTask.java index 4e28cac..f4db2cc 100644 --- a/src/main/java/loganintech/regionforcefield/task/ForcefieldUpdateTask.java +++ b/src/main/java/loganintech/regionforcefield/task/ForcefieldUpdateTask.java @@ -4,10 +4,12 @@ import loganintech.regionforcefield.RegionForcefieldPlugin; import loganintech.regionforcefield.forcefield.ForcefieldRenderer; import loganintech.regionforcefield.region.RegionPermissionChecker; import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; +import java.util.HashSet; import java.util.Set; /** @@ -41,24 +43,30 @@ public class ForcefieldUpdateTask extends BukkitRunnable { try { // Iterate through all online players for (Player player : plugin.getServer().getOnlinePlayers()) { - // Get all regions the player cannot enter in their current world + // Get all regions the player should see forcefields for Set blockedRegions = permissionChecker.getBlockedRegions(player, player.getWorld()); if (!blockedRegions.isEmpty()) { plugin.debug("Processing " + blockedRegions.size() + " blocked regions for " + player.getName()); } - // Render forcefields for nearby blocked regions + // Collect all blocks that should be rendered for this player + Set allBlocks = new HashSet<>(); int rendered = 0; + for (ProtectedRegion region : blockedRegions) { if (isRegionNearPlayer(player, region)) { - forcefieldRenderer.renderForcefield(player, region, player.getWorld()); + Set regionBlocks = forcefieldRenderer.renderForcefield(player, region, player.getWorld()); + allBlocks.addAll(regionBlocks); rendered++; } } + // Update the player's blocks (remove old ones, keep new ones) + forcefieldRenderer.updateBlocks(player, allBlocks); + if (rendered > 0) { - plugin.debug("Rendered " + rendered + " forcefields for " + player.getName()); + plugin.debug("Rendered " + rendered + " forcefields (" + allBlocks.size() + " blocks) for " + player.getName()); } } } catch (Exception e) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 68bd388..7ba7af4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -26,3 +26,17 @@ particle-color: # Particle size (recommended range: 0.5 to 2.0) particle-size: 1.0 + +# Block rendering settings +# Whether to render actual blocks (glass panes) in addition to particles +render-blocks: true + +# Distance between blocks (in blocks) +# Higher values = fewer blocks = better performance +# Must be >= particle-spacing +block-spacing: 1.0 + +# Block material to use for forcefields +# Examples: PURPLE_STAINED_GLASS_PANE, BARRIER, GLASS, LIGHT_BLUE_STAINED_GLASS +# See https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Material.html for all options +block-material: PURPLE_STAINED_GLASS_PANE