diff --git a/README.md b/README.md index c7e5769..26a6696 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ slightly faster round. Last player standing wins. - PaperMC 1.21.x (API version `1.21`) - Java 21 -- No other plugins required. Optional soft-dependencies: PlaceholderAPI, Vault. +- No other plugins required. Optional soft-dependencies: PlaceholderAPI, Vault, WorldEdit. ## Installation @@ -97,6 +97,14 @@ Commands that require a physical location (`setlobby`, `setspawn`, `setspectator ### Floor selection details - `pos1` / `pos2` define the two opposite corners of the rectangular floor region (any Y). + If [WorldEdit](https://enginehub.org/worldedit) is installed and you have an active WorldEdit + selection (made with the wand — left-click for position 1, right-click for position 2, or + `//pos1` / `//pos2`), running `/bp pos1 ` / `/bp pos2 ` adopts that selection's + corners directly instead of your standing location — no need to physically stand at each + corner. This is entirely optional: without a WorldEdit selection (or without WorldEdit + installed at all), the commands fall back to your current location exactly as before. Disable + it with `integrations.worldedit: false` in `config.yml` if you'd rather always use your + location even when WorldEdit is present. - `setfloor` only changes which materials are *allowed* to be used/regenerated — it does not touch blocks in the world. - `generate` is the step that actually writes a random layout (biased evenly across the diff --git a/build.gradle b/build.gradle index 2ccbbca..82d8c17 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ repositories { maven { url = 'https://repo.papermc.io/repository/maven-public/' } maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' } maven { url = 'https://jitpack.io' } + maven { url = 'https://maven.enginehub.org/repo/' } } dependencies { @@ -25,6 +26,15 @@ dependencies { compileOnly('com.github.MilkBowl:VaultAPI:1.7') { exclude group: 'org.bukkit', module: 'bukkit' } + // transitive = false: we only need WorldEdit's API types at compile time (it's never + // shaded/bundled - the real WorldEdit plugin provides everything at runtime), and its + // transitive deps pin strict versions of guava/gson/fastutil that conflict with paper-api's. + compileOnly('com.sk89q.worldedit:worldedit-bukkit:7.3.8') { + transitive = false + } + compileOnly('com.sk89q.worldedit:worldedit-core:7.3.8') { + transitive = false + } implementation 'org.xerial:sqlite-jdbc:3.47.1.0' implementation 'com.mysql:mysql-connector-j:9.1.0' diff --git a/src/main/java/us/tss3/blockparty/command/BlockPartyCommand.java b/src/main/java/us/tss3/blockparty/command/BlockPartyCommand.java index 2907ec5..5a4511b 100644 --- a/src/main/java/us/tss3/blockparty/command/BlockPartyCommand.java +++ b/src/main/java/us/tss3/blockparty/command/BlockPartyCommand.java @@ -13,6 +13,7 @@ import us.tss3.blockparty.config.ArenaConfig; import us.tss3.blockparty.logic.ArenaPhase; import us.tss3.blockparty.persistence.PlayerStats; import us.tss3.blockparty.session.PlayerSession; +import us.tss3.blockparty.worldedit.WorldEditHook; import java.util.ArrayList; import java.util.List; @@ -219,7 +220,16 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter { private void pos1(CommandSender sender, String[] args) { if (!requireAdmin(sender, "blockparty.admin.setup") || !requirePlayer(sender)) return; withArena(sender, args, arena -> { - Location loc = ((Player) sender).getLocation(); + Player player = (Player) sender; + WorldEditHook.Corners selection = worldEditSelection(player); + if (selection != null) { + arena.getConfig().setPos1(selection.min()); + arena.getConfig().setWorldName(selection.worldName()); + plugin.getArenaManager().save(arena); + sender.sendMessage(plugin.getMessages().get("admin.pos1-set-we", Map.of("arena", arena.getConfig().getName()))); + return; + } + Location loc = player.getLocation(); arena.getConfig().setPos1(new int[]{loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()}); arena.getConfig().setWorldName(loc.getWorld().getName()); plugin.getArenaManager().save(arena); @@ -230,13 +240,36 @@ public class BlockPartyCommand implements CommandExecutor, TabCompleter { private void pos2(CommandSender sender, String[] args) { if (!requireAdmin(sender, "blockparty.admin.setup") || !requirePlayer(sender)) return; withArena(sender, args, arena -> { - Location loc = ((Player) sender).getLocation(); + Player player = (Player) sender; + WorldEditHook.Corners selection = worldEditSelection(player); + if (selection != null) { + arena.getConfig().setPos2(selection.max()); + plugin.getArenaManager().save(arena); + sender.sendMessage(plugin.getMessages().get("admin.pos2-set-we", Map.of("arena", arena.getConfig().getName()))); + return; + } + Location loc = player.getLocation(); arena.getConfig().setPos2(new int[]{loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()}); plugin.getArenaManager().save(arena); sender.sendMessage(plugin.getMessages().get("admin.pos2-set", Map.of("arena", arena.getConfig().getName()))); }); } + /** Returns the player's active WorldEdit selection if the WorldEdit plugin is installed, + * the integration is enabled, and they have one - otherwise null so callers fall back to + * the player's standing location. WorldEdit classes are only referenced once its plugin + * is confirmed present, so a server without WorldEdit installed never loads them. */ + private WorldEditHook.Corners worldEditSelection(Player player) { + if (!plugin.getConfigManager().isWorldEditEnabled() || Bukkit.getPluginManager().getPlugin("WorldEdit") == null) { + return null; + } + try { + return WorldEditHook.getSelection(player); + } catch (Throwable t) { + return null; + } + } + private void setFloor(CommandSender sender, String[] args) { if (!requireAdmin(sender, "blockparty.admin.setup")) return; // /bp setfloor diff --git a/src/main/java/us/tss3/blockparty/config/ConfigManager.java b/src/main/java/us/tss3/blockparty/config/ConfigManager.java index 4d680f5..20020e1 100644 --- a/src/main/java/us/tss3/blockparty/config/ConfigManager.java +++ b/src/main/java/us/tss3/blockparty/config/ConfigManager.java @@ -65,6 +65,10 @@ public class ConfigManager { return config.getBoolean("integrations.vault", true); } + public boolean isWorldEditEnabled() { + return config.getBoolean("integrations.worldedit", true); + } + public List getDefaultFloorMaterials() { return config.getStringList("default-floor-materials"); } diff --git a/src/main/java/us/tss3/blockparty/worldedit/WorldEditHook.java b/src/main/java/us/tss3/blockparty/worldedit/WorldEditHook.java new file mode 100644 index 0000000..7c6a45a --- /dev/null +++ b/src/main/java/us/tss3/blockparty/worldedit/WorldEditHook.java @@ -0,0 +1,43 @@ +package us.tss3.blockparty.worldedit; + +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import org.bukkit.entity.Player; + +/** + * Reads a player's active WorldEdit selection (made with the WE wand / {@code //pos1} and + * {@code //pos2}) so BlockParty's floor-region commands can adopt it directly instead of + * requiring the player to stand at each corner separately. This class is only ever touched + * once the caller has confirmed the WorldEdit plugin is actually present (see + * BlockPartyCommand#worldEditSelection), so a server without WorldEdit installed never + * attempts to load these classes. + */ +public final class WorldEditHook { + + private WorldEditHook() { + } + + /** Two opposite corners of a cuboid selection, plus the world they're in. */ + public record Corners(int[] min, int[] max, String worldName) { + } + + /** Returns the player's current WorldEdit cuboid selection corners, or null if they have + * no active/complete selection (or anything about the lookup fails). */ + public static Corners getSelection(Player player) { + try { + LocalSession session = WorldEdit.getInstance().getSessionManager().get(BukkitAdapter.adapt(player)); + Region region = session.getSelection(BukkitAdapter.adapt(player.getWorld())); + BlockVector3 min = region.getMinimumPoint(); + BlockVector3 max = region.getMaximumPoint(); + return new Corners( + new int[]{min.x(), min.y(), min.z()}, + new int[]{max.x(), max.y(), max.z()}, + player.getWorld().getName()); + } catch (Exception ex) { + return null; + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 118fc1f..5530b06 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -80,3 +80,6 @@ integrations: placeholderapi: true # If false, money rewards are skipped even when Vault is installed (console commands are unaffected). vault: true + # If true (default) and WorldEdit is installed, /blockparty pos1 and pos2 use your active + # WorldEdit wand selection instead of your standing location, when you have one selected. + worldedit: true diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 71a0919..ccb99fe 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -24,6 +24,8 @@ admin: billboard-removed: "Billboard removed for '%arena%'." pos1-set: "Position 1 set for '%arena%'." pos2-set: "Position 2 set for '%arena%'." + pos1-set-we: "Position 1 set for '%arena%' from your WorldEdit selection." + pos2-set-we: "Position 2 set for '%arena%' from your WorldEdit selection." floor-set: "Floor palette for '%arena%' set to %count% material(s)." floor-generated: "Floor generated for '%arena%'." reloaded: "BlockParty configuration reloaded." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f419762..71e8b1e 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,7 +4,7 @@ main: us.tss3.blockparty.BlockPartyPlugin api-version: '1.21' author: tss3 description: Colored-floor elimination minigame. -softdepend: [PlaceholderAPI, Vault] +softdepend: [PlaceholderAPI, Vault, WorldEdit] commands: blockparty: