diff --git a/CoalGov/README.md b/CoalGov/README.md index d7790ef..f73fdde 100644 --- a/CoalGov/README.md +++ b/CoalGov/README.md @@ -93,9 +93,31 @@ CoalGov supports rectangular and polygon claims. - Claim corners receive protected marker blocks - `/claim appraise` estimates resale value from land condition and configured resources - `/claim sell` sells the claim back to the Ministry +- `/showpropertylines` shows the property lines for the claim you are standing in +- `/claim trust` and `/claim untrust` grant or revoke claim-scoped access for other players The first small homestead can be free, depending on config. +Claim owners can grant granular access: + +- `build`: place and break blocks +- `interact`: use non-container interactable blocks such as doors, buttons, levers, gates, and similar blocks +- `container`: open inventory blocks such as chests, barrels, furnaces, and hoppers +- `animal`: damage animals inside the claim +- `manage`: manage claim permissions +- `all`: grant or revoke every permission above + +Example: + +```text +/claim trust 12 Alex build +/claim trust 12 Alex container +/claim untrust 12 Alex build +/claim permissions 12 +``` + +Unclaimed land does not block animal damage for now. Claimed land blocks animal damage unless the player owns the claim, has `animal`, has `manage`, or is using admin bypass. + ## Land Classes And Mining Admins can define land regions with classes such as freehold, government land, protected preserves, border zones, and mining concessions. @@ -138,9 +160,13 @@ CoalGov adds a persisted Super Furnace recipe. Craft a furnace with stone in all /claim sell [id] /claim transfer /claim abandon +/claim trust +/claim untrust +/claim permissions /permit buy mining /permit list /diviningrod +/showpropertylines /land info ``` @@ -229,6 +255,10 @@ Always back up the current plugin jar and `plugins/CoalGov/coalgov.db` before de - `/market` shows per-item and batch prices - Market buy/sell updates stock and balance - `/claim buy` charges the displayed Coal Cents amount +- `/claim trust` allows another player to use only the granted access +- A player without `animal` access cannot kill animals inside another player's claim +- Animals on unclaimed land can still be killed +- `/showpropertylines` displays polygon edges between claim marker points - `/claim appraise` and `/claim sell` display formatted money - Mining protected coal is denied - Mining concession coal requires a valid permit diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java b/CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java index 17b8195..589efe9 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/CoalGovPlugin.java @@ -9,13 +9,16 @@ import com.librewiki.coalgov.command.MarketCommand; import com.librewiki.coalgov.command.NpcCommand; import com.librewiki.coalgov.command.PermitCommand; import com.librewiki.coalgov.command.PoliceCommand; +import com.librewiki.coalgov.command.ShowPropertyLinesCommand; import com.librewiki.coalgov.listener.BlockBreakListener; import com.librewiki.coalgov.listener.BlockPlaceListener; +import com.librewiki.coalgov.listener.AnimalProtectionListener; import com.librewiki.coalgov.listener.ClaimWandListener; import com.librewiki.coalgov.listener.DiviningRodListener; import com.librewiki.coalgov.listener.MarketListener; import com.librewiki.coalgov.listener.NpcListener; import com.librewiki.coalgov.listener.PlayerJoinListener; +import com.librewiki.coalgov.listener.PropertyInteractListener; import com.librewiki.coalgov.listener.SuperFurnaceListener; import com.librewiki.coalgov.service.BuildProtectionService; import com.librewiki.coalgov.service.ClaimMarkerService; @@ -125,12 +128,15 @@ public final class CoalGovPlugin extends JavaPlugin { Objects.requireNonNull(getCommand("land")).setExecutor(new LandCommand(this)); Objects.requireNonNull(getCommand("market")).setExecutor(new MarketCommand(this)); Objects.requireNonNull(getCommand("diviningrod")).setExecutor(new DiviningRodCommand(this)); + Objects.requireNonNull(getCommand("showpropertylines")).setExecutor(new ShowPropertyLinesCommand(this)); Objects.requireNonNull(getCommand("police")).setExecutor(new PoliceCommand(this)); Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this)); Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this)); getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); getServer().getPluginManager().registerEvents(new BlockPlaceListener(this), this); + getServer().getPluginManager().registerEvents(new PropertyInteractListener(this), this); + getServer().getPluginManager().registerEvents(new AnimalProtectionListener(this), this); getServer().getPluginManager().registerEvents(new ClaimWandListener(this), this); getServer().getPluginManager().registerEvents(new DiviningRodListener(this), this); getServer().getPluginManager().registerEvents(new MarketListener(this), this); diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/command/ClaimCommand.java b/CoalGov/src/main/java/com/librewiki/coalgov/command/ClaimCommand.java index e5be353..ee43e86 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/command/ClaimCommand.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/command/ClaimCommand.java @@ -3,6 +3,8 @@ package com.librewiki.coalgov.command; import com.librewiki.coalgov.CoalGovPlugin; import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.ClaimPoint; +import com.librewiki.coalgov.model.ClaimPermission; +import com.librewiki.coalgov.model.ClaimPermissionGrant; import com.librewiki.coalgov.model.LandRegion; import com.librewiki.coalgov.service.ClaimService; import com.librewiki.coalgov.service.LandAppraisalService; @@ -16,8 +18,11 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import java.sql.SQLException; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.UUID; public final class ClaimCommand implements CommandExecutor { private final CoalGovPlugin plugin; @@ -33,7 +38,7 @@ public final class ClaimCommand implements CommandExecutor { return true; } if (args.length == 0) { - player.sendMessage(plugin.messages().text("&eUsage: /claim ")); + usage(player); return true; } try { @@ -48,7 +53,10 @@ public final class ClaimCommand implements CommandExecutor { case "sell" -> sell(player, args); case "transfer" -> transfer(player, args); case "abandon" -> abandon(player, args); - default -> player.sendMessage(plugin.messages().text("&eUsage: /claim ")); + case "trust" -> trust(player, args); + case "untrust" -> untrust(player, args); + case "permissions" -> permissions(player, args); + default -> usage(player); } } catch (SQLException exception) { plugin.getLogger().warning("Claim command failed: " + exception.getMessage()); @@ -57,6 +65,10 @@ public final class ClaimCommand implements CommandExecutor { return true; } + private void usage(Player player) { + player.sendMessage(plugin.messages().text("&eUsage: /claim ")); + } + private void buy(Player player, String[] args) throws SQLException { if (args.length != 2 && args.length != 3) { player.sendMessage(plugin.messages().text("&eUsage: /claim buy [radius]")); @@ -200,6 +212,96 @@ public final class ClaimCommand implements CommandExecutor { } } + private void trust(Player player, String[] args) throws SQLException { + if (args.length != 5) { + player.sendMessage(plugin.messages().text("&eUsage: /claim trust ")); + return; + } + long id = parseLong(args[1]); + List permissions = ClaimPermission.grantSet(args[4]); + if (id <= 0L || permissions.isEmpty()) { + player.sendMessage(plugin.messages().text("&cInvalid claim id or permission.")); + return; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]); + plugin.economyService().ensurePlayer(target); + for (ClaimPermission permission : permissions) { + ClaimService.PermissionResult result = plugin.claimService() + .grantPermission(player.getUniqueId(), id, target.getUniqueId(), permission); + if (!result.success()) { + player.sendMessage(plugin.messages().text("&c" + result.message())); + return; + } + } + player.sendMessage(plugin.messages().text("&aGranted &f" + args[4].toLowerCase() + " &aaccess on claim #" + id + " to &f" + targetName(target) + "&a.")); + } + + private void untrust(Player player, String[] args) throws SQLException { + if (args.length != 5) { + player.sendMessage(plugin.messages().text("&eUsage: /claim untrust ")); + return; + } + long id = parseLong(args[1]); + if (id <= 0L) { + player.sendMessage(plugin.messages().text("&cInvalid claim id.")); + return; + } + OfflinePlayer target = Bukkit.getOfflinePlayer(args[2]); + if (args[4].equalsIgnoreCase("all")) { + ClaimService.PermissionResult result = plugin.claimService() + .revokeAllPermissions(player.getUniqueId(), id, target.getUniqueId()); + if (!result.success()) { + player.sendMessage(plugin.messages().text("&c" + result.message())); + return; + } + player.sendMessage(plugin.messages().text("&aRemoved all claim access for &f" + targetName(target) + "&a.")); + return; + } + Optional permission = ClaimPermission.parse(args[4]); + if (permission.isEmpty()) { + player.sendMessage(plugin.messages().text("&cInvalid permission.")); + return; + } + ClaimService.PermissionResult result = plugin.claimService() + .revokePermission(player.getUniqueId(), id, target.getUniqueId(), permission.get()); + if (!result.success()) { + player.sendMessage(plugin.messages().text("&c" + result.message())); + return; + } + player.sendMessage(plugin.messages().text("&aRemoved &f" + permission.get().label() + " &aaccess for &f" + targetName(target) + "&a.")); + } + + private void permissions(Player player, String[] args) throws SQLException { + if (args.length != 2) { + player.sendMessage(plugin.messages().text("&eUsage: /claim permissions ")); + return; + } + long id = parseLong(args[1]); + if (id <= 0L) { + player.sendMessage(plugin.messages().text("&cInvalid claim id.")); + return; + } + List grants = plugin.claimService().permissions(player.getUniqueId(), id); + if (grants.isEmpty()) { + player.sendMessage(plugin.messages().text("&eNo permissions listed for claim #" + id + ", or you cannot manage that claim.")); + return; + } + Map byPlayer = new LinkedHashMap<>(); + for (ClaimPermissionGrant grant : grants) { + byPlayer.computeIfAbsent(grant.playerUuid(), ignored -> new StringBuilder()); + StringBuilder builder = byPlayer.get(grant.playerUuid()); + if (builder.length() > 0) { + builder.append(", "); + } + builder.append(grant.permission().label()); + } + player.sendMessage(plugin.messages().text("&eClaim #" + id + " permissions:")); + for (var entry : byPlayer.entrySet()) { + OfflinePlayer target = Bukkit.getOfflinePlayer(entry.getKey()); + player.sendMessage(plugin.messages().raw("&7" + targetName(target) + ": &f" + entry.getValue())); + } + } + private Optional resolveOwnedClaim(Player player, String[] args, String action) throws SQLException { if (args.length > 2) { player.sendMessage(plugin.messages().text("&eUsage: /claim " + action + " [id]")); @@ -261,6 +363,10 @@ public final class ClaimCommand implements CommandExecutor { return material.name().toLowerCase().replace('_', ' '); } + private String targetName(OfflinePlayer target) { + return target.getName() == null ? target.getUniqueId().toString() : target.getName(); + } + private void abandon(Player player, String[] args) throws SQLException { if (args.length != 2) { player.sendMessage(plugin.messages().text("&eUsage: /claim abandon ")); diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/command/ShowPropertyLinesCommand.java b/CoalGov/src/main/java/com/librewiki/coalgov/command/ShowPropertyLinesCommand.java new file mode 100644 index 0000000..725916b --- /dev/null +++ b/CoalGov/src/main/java/com/librewiki/coalgov/command/ShowPropertyLinesCommand.java @@ -0,0 +1,40 @@ +package com.librewiki.coalgov.command; + +import com.librewiki.coalgov.CoalGovPlugin; +import com.librewiki.coalgov.model.Claim; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.sql.SQLException; +import java.util.Optional; + +public final class ShowPropertyLinesCommand implements CommandExecutor { + private final CoalGovPlugin plugin; + + public ShowPropertyLinesCommand(CoalGovPlugin plugin) { + this.plugin = plugin; + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Players only."); + return true; + } + try { + Optional claim = plugin.claimService().claimAt(player.getLocation()); + if (claim.isEmpty()) { + player.sendMessage(plugin.messages().text("&eYou are not standing inside a claim.")); + return true; + } + plugin.claimVisualService().show(player, claim.get()); + player.sendMessage(plugin.messages().text("&aShowing property lines for claim #" + claim.get().id() + ".")); + } catch (SQLException exception) { + plugin.getLogger().warning("Show property lines failed: " + exception.getMessage()); + player.sendMessage(plugin.messages().text("&cClaim registry unavailable.")); + } + return true; + } +} diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/listener/AnimalProtectionListener.java b/CoalGov/src/main/java/com/librewiki/coalgov/listener/AnimalProtectionListener.java new file mode 100644 index 0000000..aaa0c0f --- /dev/null +++ b/CoalGov/src/main/java/com/librewiki/coalgov/listener/AnimalProtectionListener.java @@ -0,0 +1,55 @@ +package com.librewiki.coalgov.listener; + +import com.librewiki.coalgov.CoalGovPlugin; +import com.librewiki.coalgov.model.Claim; +import com.librewiki.coalgov.model.ClaimPermission; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; + +import java.sql.SQLException; +import java.util.Optional; + +public final class AnimalProtectionListener implements Listener { + private final CoalGovPlugin plugin; + + public AnimalProtectionListener(CoalGovPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true) + public void onEntityDamage(EntityDamageByEntityEvent event) { + if (!(event.getEntity() instanceof Animals)) { + return; + } + Player player = attackingPlayer(event.getDamager()); + if (player == null || plugin.hasAdminBypass(player)) { + return; + } + try { + Optional claim = plugin.claimService().claimAt(event.getEntity().getLocation()); + if (claim.isEmpty() || plugin.claimService().canUse(player.getUniqueId(), claim.get(), ClaimPermission.ANIMAL)) { + return; + } + event.setCancelled(true); + player.sendMessage(plugin.messages().text("&cYou do not have animal access on this claim.")); + } catch (SQLException exception) { + event.setCancelled(true); + plugin.getLogger().warning("Animal protection check failed: " + exception.getMessage()); + player.sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later.")); + } + } + + private Player attackingPlayer(Entity damager) { + if (damager instanceof Player player) { + return player; + } + if (damager instanceof org.bukkit.entity.Projectile projectile && projectile.getShooter() instanceof Player player) { + return player; + } + return null; + } +} diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/listener/PropertyInteractListener.java b/CoalGov/src/main/java/com/librewiki/coalgov/listener/PropertyInteractListener.java new file mode 100644 index 0000000..8fb8125 --- /dev/null +++ b/CoalGov/src/main/java/com/librewiki/coalgov/listener/PropertyInteractListener.java @@ -0,0 +1,53 @@ +package com.librewiki.coalgov.listener; + +import com.librewiki.coalgov.CoalGovPlugin; +import com.librewiki.coalgov.model.Claim; +import com.librewiki.coalgov.model.ClaimPermission; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.InventoryHolder; + +import java.sql.SQLException; +import java.util.Optional; + +public final class PropertyInteractListener implements Listener { + private final CoalGovPlugin plugin; + + public PropertyInteractListener(CoalGovPlugin plugin) { + this.plugin = plugin; + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerInteract(PlayerInteractEvent event) { + if (plugin.hasAdminBypass(event.getPlayer()) || event.getAction() != Action.RIGHT_CLICK_BLOCK) { + return; + } + Block block = event.getClickedBlock(); + if (block == null || !block.getType().isInteractable()) { + return; + } + try { + Optional claim = plugin.claimService().claimAt(block.getLocation()); + if (claim.isEmpty()) { + return; + } + ClaimPermission permission = isContainer(block) ? ClaimPermission.CONTAINER : ClaimPermission.INTERACT; + if (plugin.claimService().canUse(event.getPlayer().getUniqueId(), claim.get(), permission)) { + return; + } + event.setCancelled(true); + event.getPlayer().sendMessage(plugin.messages().text("&cYou do not have " + permission.label() + " access on this claim.")); + } catch (SQLException exception) { + event.setCancelled(true); + plugin.getLogger().warning("Property interaction check failed: " + exception.getMessage()); + event.getPlayer().sendMessage(plugin.messages().text("&cLand registry unavailable. Try again later.")); + } + } + + private boolean isContainer(Block block) { + return block.getState() instanceof InventoryHolder; + } +} diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermission.java b/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermission.java new file mode 100644 index 0000000..a003056 --- /dev/null +++ b/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermission.java @@ -0,0 +1,30 @@ +package com.librewiki.coalgov.model; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public enum ClaimPermission { + BUILD, + INTERACT, + CONTAINER, + ANIMAL, + MANAGE; + + public static Optional parse(String raw) { + return Arrays.stream(values()) + .filter(permission -> permission.name().equalsIgnoreCase(raw)) + .findFirst(); + } + + public static List grantSet(String raw) { + if (raw.equalsIgnoreCase("all")) { + return List.of(values()); + } + return parse(raw).map(List::of).orElse(List.of()); + } + + public String label() { + return name().toLowerCase(); + } +} diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermissionGrant.java b/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermissionGrant.java new file mode 100644 index 0000000..545131b --- /dev/null +++ b/CoalGov/src/main/java/com/librewiki/coalgov/model/ClaimPermissionGrant.java @@ -0,0 +1,11 @@ +package com.librewiki.coalgov.model; + +import java.util.UUID; + +public record ClaimPermissionGrant( + long claimId, + UUID playerUuid, + ClaimPermission permission, + long grantedAt +) { +} diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/service/BuildProtectionService.java b/CoalGov/src/main/java/com/librewiki/coalgov/service/BuildProtectionService.java index d6f50ad..c2a711a 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/service/BuildProtectionService.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/service/BuildProtectionService.java @@ -2,6 +2,7 @@ package com.librewiki.coalgov.service; import com.librewiki.coalgov.CoalGovPlugin; import com.librewiki.coalgov.model.Claim; +import com.librewiki.coalgov.model.ClaimPermission; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -25,7 +26,7 @@ public final class BuildProtectionService { if (claim.isEmpty()) { return BuildDecision.deny("This land is unclaimed and protected by the Ministry."); } - if (!claim.get().ownerUuid().equals(player.getUniqueId())) { + if (!claimService.canUse(player.getUniqueId(), claim.get(), ClaimPermission.BUILD)) { return BuildDecision.deny("This claim belongs to another citizen."); } return BuildDecision.allow(); diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/service/ClaimService.java b/CoalGov/src/main/java/com/librewiki/coalgov/service/ClaimService.java index a3ee694..0c45be6 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/service/ClaimService.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/service/ClaimService.java @@ -2,6 +2,8 @@ package com.librewiki.coalgov.service; import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.ClaimPoint; +import com.librewiki.coalgov.model.ClaimPermission; +import com.librewiki.coalgov.model.ClaimPermissionGrant; import com.librewiki.coalgov.storage.ClaimRepository; import com.librewiki.coalgov.util.CoalMoney; import com.librewiki.coalgov.util.Cuboid2D; @@ -142,6 +144,66 @@ public final class ClaimService { return TransferResult.success(claim.get()); } + public boolean canUse(UUID playerUuid, Claim claim, ClaimPermission permission) throws SQLException { + if (claim.ownerUuid().equals(playerUuid)) { + return true; + } + return repository.hasPermission(claim.id(), playerUuid, permission) + || repository.hasPermission(claim.id(), playerUuid, ClaimPermission.MANAGE); + } + + public PermissionResult grantPermission(UUID actor, long claimId, UUID target, ClaimPermission permission) throws SQLException { + Optional claim = findManageableClaim(actor, claimId); + if (claim.isEmpty()) { + return PermissionResult.fail("No owned or manageable claim with that id."); + } + if (claim.get().ownerUuid().equals(target)) { + return PermissionResult.fail("The owner already has all permissions."); + } + repository.grantPermission(claimId, target, permission); + return PermissionResult.ok(); + } + + public PermissionResult revokePermission(UUID actor, long claimId, UUID target, ClaimPermission permission) throws SQLException { + Optional claim = findManageableClaim(actor, claimId); + if (claim.isEmpty()) { + return PermissionResult.fail("No owned or manageable claim with that id."); + } + return repository.revokePermission(claimId, target, permission) + ? PermissionResult.ok() + : PermissionResult.fail("That player did not have that permission."); + } + + public PermissionResult revokeAllPermissions(UUID actor, long claimId, UUID target) throws SQLException { + Optional claim = findManageableClaim(actor, claimId); + if (claim.isEmpty()) { + return PermissionResult.fail("No owned or manageable claim with that id."); + } + return repository.revokeAllPermissions(claimId, target) + ? PermissionResult.ok() + : PermissionResult.fail("That player had no permissions on that claim."); + } + + public List permissions(UUID actor, long claimId) throws SQLException { + Optional claim = findManageableClaim(actor, claimId); + if (claim.isEmpty()) { + return List.of(); + } + return repository.permissions(claimId); + } + + private Optional findManageableClaim(UUID actor, long claimId) throws SQLException { + Optional owned = findOwnedById(actor, claimId); + if (owned.isPresent()) { + return owned; + } + Optional claim = repository.findById(claimId); + if (claim.isPresent() && repository.hasPermission(claimId, actor, ClaimPermission.MANAGE)) { + return claim; + } + return Optional.empty(); + } + public long claimPurchaseCost(Claim claim) { if (claim.purchaseCost() >= 0L) { return claim.purchaseCost(); @@ -222,4 +284,14 @@ public final class ClaimService { return new TransferResult(true, "", claim); } } + + public record PermissionResult(boolean success, String message) { + public static PermissionResult fail(String message) { + return new PermissionResult(false, message); + } + + public static PermissionResult ok() { + return new PermissionResult(true, ""); + } + } } diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/storage/ClaimRepository.java b/CoalGov/src/main/java/com/librewiki/coalgov/storage/ClaimRepository.java index fe582ca..9a9a070 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/storage/ClaimRepository.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/storage/ClaimRepository.java @@ -2,6 +2,8 @@ package com.librewiki.coalgov.storage; import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.ClaimPoint; +import com.librewiki.coalgov.model.ClaimPermission; +import com.librewiki.coalgov.model.ClaimPermissionGrant; import com.librewiki.coalgov.util.Cuboid2D; import java.sql.PreparedStatement; @@ -69,6 +71,11 @@ public final class ClaimRepository { deleteVertices.setLong(1, id); deleteVertices.executeUpdate(); } + try (PreparedStatement deletePermissions = database.connection().prepareStatement( + "DELETE FROM claim_permissions WHERE claim_id = ?")) { + deletePermissions.setLong(1, id); + deletePermissions.executeUpdate(); + } try (PreparedStatement delete = database.connection().prepareStatement( "DELETE FROM claims WHERE id = ? AND owner_uuid = ?")) { delete.setLong(1, id); @@ -134,13 +141,109 @@ public final class ClaimRepository { } } + public Optional findById(long id) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement( + "SELECT * FROM claims WHERE id = ?")) { + statement.setLong(1, id); + try (ResultSet result = statement.executeQuery()) { + return result.next() ? Optional.of(map(result)) : Optional.empty(); + } + } + } + public boolean transferOwned(UUID owner, long id, UUID newOwner) throws SQLException { try (PreparedStatement statement = database.connection().prepareStatement( "UPDATE claims SET owner_uuid = ? WHERE id = ? AND owner_uuid = ?")) { statement.setString(1, newOwner.toString()); statement.setLong(2, id); statement.setString(3, owner.toString()); - return statement.executeUpdate() == 1; + boolean transferred = statement.executeUpdate() == 1; + if (transferred) { + clearPermissions(id); + } + return transferred; + } + } + + public void grantPermission(long claimId, UUID playerUuid, ClaimPermission permission) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement(""" + INSERT INTO claim_permissions(claim_id, player_uuid, permission, granted_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(claim_id, player_uuid, permission) DO UPDATE SET granted_at = excluded.granted_at + """)) { + statement.setLong(1, claimId); + statement.setString(2, playerUuid.toString()); + statement.setString(3, permission.name()); + statement.setLong(4, System.currentTimeMillis()); + statement.executeUpdate(); + } + } + + public boolean revokePermission(long claimId, UUID playerUuid, ClaimPermission permission) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement(""" + DELETE FROM claim_permissions + WHERE claim_id = ? AND player_uuid = ? AND permission = ? + """)) { + statement.setLong(1, claimId); + statement.setString(2, playerUuid.toString()); + statement.setString(3, permission.name()); + return statement.executeUpdate() > 0; + } + } + + public boolean revokeAllPermissions(long claimId, UUID playerUuid) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement(""" + DELETE FROM claim_permissions + WHERE claim_id = ? AND player_uuid = ? + """)) { + statement.setLong(1, claimId); + statement.setString(2, playerUuid.toString()); + return statement.executeUpdate() > 0; + } + } + + public boolean hasPermission(long claimId, UUID playerUuid, ClaimPermission permission) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement(""" + SELECT 1 FROM claim_permissions + WHERE claim_id = ? AND player_uuid = ? AND permission = ? + LIMIT 1 + """)) { + statement.setLong(1, claimId); + statement.setString(2, playerUuid.toString()); + statement.setString(3, permission.name()); + try (ResultSet result = statement.executeQuery()) { + return result.next(); + } + } + } + + public List permissions(long claimId) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement(""" + SELECT * FROM claim_permissions + WHERE claim_id = ? + ORDER BY player_uuid, permission + """)) { + statement.setLong(1, claimId); + List grants = new ArrayList<>(); + try (ResultSet result = statement.executeQuery()) { + while (result.next()) { + grants.add(new ClaimPermissionGrant( + result.getLong("claim_id"), + UUID.fromString(result.getString("player_uuid")), + ClaimPermission.valueOf(result.getString("permission")), + result.getLong("granted_at") + )); + } + } + return grants; + } + } + + private void clearPermissions(long claimId) throws SQLException { + try (PreparedStatement statement = database.connection().prepareStatement( + "DELETE FROM claim_permissions WHERE claim_id = ?")) { + statement.setLong(1, claimId); + statement.executeUpdate(); } } diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/storage/Database.java b/CoalGov/src/main/java/com/librewiki/coalgov/storage/Database.java index db6ef78..4d55f05 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/storage/Database.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/storage/Database.java @@ -84,6 +84,15 @@ public final class Database { PRIMARY KEY (claim_id, vertex_order) ) """); + statement.execute(""" + CREATE TABLE IF NOT EXISTS claim_permissions ( + claim_id INTEGER NOT NULL, + player_uuid TEXT NOT NULL, + permission TEXT NOT NULL, + granted_at INTEGER NOT NULL, + PRIMARY KEY (claim_id, player_uuid, permission) + ) + """); statement.execute(""" CREATE TABLE IF NOT EXISTS land_regions ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -204,6 +213,8 @@ public final class Database { """); statement.execute("CREATE INDEX IF NOT EXISTS idx_claims_location ON claims(world, x1, z1, x2, z2)"); statement.execute("CREATE INDEX IF NOT EXISTS idx_claim_vertices_claim ON claim_vertices(claim_id)"); + statement.execute("CREATE INDEX IF NOT EXISTS idx_claim_permissions_claim ON claim_permissions(claim_id)"); + statement.execute("CREATE INDEX IF NOT EXISTS idx_claim_permissions_player ON claim_permissions(player_uuid)"); statement.execute("CREATE INDEX IF NOT EXISTS idx_land_location ON land_regions(world, x1, z1, x2, z2)"); statement.execute("CREATE INDEX IF NOT EXISTS idx_permits_owner ON permits(owner_uuid, permit_type, region_name)"); statement.execute("CREATE INDEX IF NOT EXISTS idx_fines_player ON fines(player_uuid, paid_at)"); diff --git a/CoalGov/src/main/resources/plugin.yml b/CoalGov/src/main/resources/plugin.yml index 7a51807..0f60d7f 100644 --- a/CoalGov/src/main/resources/plugin.yml +++ b/CoalGov/src/main/resources/plugin.yml @@ -13,7 +13,7 @@ commands: permission: coalgov.coal claim: description: CoalGov claim commands. - usage: /claim + usage: /claim permission: coalgov.claim permit: description: CoalGov permit commands. @@ -30,6 +30,10 @@ commands: description: Receive a CoalGov divining rod. usage: /diviningrod permission: coalgov.diviningrod + showpropertylines: + description: Show the property lines for the claim you are standing in. + usage: /showpropertylines + permission: coalgov.claim police: description: CoalGov police commands. usage: /police