Add claim access permissions

This commit is contained in:
CoalGov Deploy
2026-05-04 18:00:51 +00:00
parent e65097ed2c
commit 8b5ff1f92f
13 changed files with 527 additions and 5 deletions

View File

@@ -93,9 +93,31 @@ CoalGov supports rectangular and polygon claims.
- Claim corners receive protected marker blocks - Claim corners receive protected marker blocks
- `/claim appraise` estimates resale value from land condition and configured resources - `/claim appraise` estimates resale value from land condition and configured resources
- `/claim sell` sells the claim back to the Ministry - `/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. 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 ## Land Classes And Mining
Admins can define land regions with classes such as freehold, government land, protected preserves, border zones, and mining concessions. 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 sell [id]
/claim transfer <id> <player> /claim transfer <id> <player>
/claim abandon <id> /claim abandon <id>
/claim trust <id> <player> <build|interact|container|animal|manage|all>
/claim untrust <id> <player> <build|interact|container|animal|manage|all>
/claim permissions <id>
/permit buy mining <regionName> /permit buy mining <regionName>
/permit list /permit list
/diviningrod /diviningrod
/showpropertylines
/land info /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` shows per-item and batch prices
- Market buy/sell updates stock and balance - Market buy/sell updates stock and balance
- `/claim buy` charges the displayed Coal Cents amount - `/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 - `/claim appraise` and `/claim sell` display formatted money
- Mining protected coal is denied - Mining protected coal is denied
- Mining concession coal requires a valid permit - Mining concession coal requires a valid permit

View File

@@ -9,13 +9,16 @@ import com.librewiki.coalgov.command.MarketCommand;
import com.librewiki.coalgov.command.NpcCommand; import com.librewiki.coalgov.command.NpcCommand;
import com.librewiki.coalgov.command.PermitCommand; import com.librewiki.coalgov.command.PermitCommand;
import com.librewiki.coalgov.command.PoliceCommand; import com.librewiki.coalgov.command.PoliceCommand;
import com.librewiki.coalgov.command.ShowPropertyLinesCommand;
import com.librewiki.coalgov.listener.BlockBreakListener; import com.librewiki.coalgov.listener.BlockBreakListener;
import com.librewiki.coalgov.listener.BlockPlaceListener; import com.librewiki.coalgov.listener.BlockPlaceListener;
import com.librewiki.coalgov.listener.AnimalProtectionListener;
import com.librewiki.coalgov.listener.ClaimWandListener; import com.librewiki.coalgov.listener.ClaimWandListener;
import com.librewiki.coalgov.listener.DiviningRodListener; import com.librewiki.coalgov.listener.DiviningRodListener;
import com.librewiki.coalgov.listener.MarketListener; import com.librewiki.coalgov.listener.MarketListener;
import com.librewiki.coalgov.listener.NpcListener; import com.librewiki.coalgov.listener.NpcListener;
import com.librewiki.coalgov.listener.PlayerJoinListener; import com.librewiki.coalgov.listener.PlayerJoinListener;
import com.librewiki.coalgov.listener.PropertyInteractListener;
import com.librewiki.coalgov.listener.SuperFurnaceListener; import com.librewiki.coalgov.listener.SuperFurnaceListener;
import com.librewiki.coalgov.service.BuildProtectionService; import com.librewiki.coalgov.service.BuildProtectionService;
import com.librewiki.coalgov.service.ClaimMarkerService; 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("land")).setExecutor(new LandCommand(this));
Objects.requireNonNull(getCommand("market")).setExecutor(new MarketCommand(this)); Objects.requireNonNull(getCommand("market")).setExecutor(new MarketCommand(this));
Objects.requireNonNull(getCommand("diviningrod")).setExecutor(new DiviningRodCommand(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("police")).setExecutor(new PoliceCommand(this));
Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this)); Objects.requireNonNull(getCommand("cgnpc")).setExecutor(new NpcCommand(this));
Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this)); Objects.requireNonNull(getCommand("coalgov")).setExecutor(new AdminCommand(this));
getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this); getServer().getPluginManager().registerEvents(new BlockBreakListener(this), this);
getServer().getPluginManager().registerEvents(new BlockPlaceListener(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 ClaimWandListener(this), this);
getServer().getPluginManager().registerEvents(new DiviningRodListener(this), this); getServer().getPluginManager().registerEvents(new DiviningRodListener(this), this);
getServer().getPluginManager().registerEvents(new MarketListener(this), this); getServer().getPluginManager().registerEvents(new MarketListener(this), this);

View File

@@ -3,6 +3,8 @@ package com.librewiki.coalgov.command;
import com.librewiki.coalgov.CoalGovPlugin; import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint; 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.model.LandRegion;
import com.librewiki.coalgov.service.ClaimService; import com.librewiki.coalgov.service.ClaimService;
import com.librewiki.coalgov.service.LandAppraisalService; import com.librewiki.coalgov.service.LandAppraisalService;
@@ -16,8 +18,11 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
public final class ClaimCommand implements CommandExecutor { public final class ClaimCommand implements CommandExecutor {
private final CoalGovPlugin plugin; private final CoalGovPlugin plugin;
@@ -33,7 +38,7 @@ public final class ClaimCommand implements CommandExecutor {
return true; return true;
} }
if (args.length == 0) { if (args.length == 0) {
player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>")); usage(player);
return true; return true;
} }
try { try {
@@ -48,7 +53,10 @@ public final class ClaimCommand implements CommandExecutor {
case "sell" -> sell(player, args); case "sell" -> sell(player, args);
case "transfer" -> transfer(player, args); case "transfer" -> transfer(player, args);
case "abandon" -> abandon(player, args); case "abandon" -> abandon(player, args);
default -> player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon>")); case "trust" -> trust(player, args);
case "untrust" -> untrust(player, args);
case "permissions" -> permissions(player, args);
default -> usage(player);
} }
} catch (SQLException exception) { } catch (SQLException exception) {
plugin.getLogger().warning("Claim command failed: " + exception.getMessage()); plugin.getLogger().warning("Claim command failed: " + exception.getMessage());
@@ -57,6 +65,10 @@ public final class ClaimCommand implements CommandExecutor {
return true; return true;
} }
private void usage(Player player) {
player.sendMessage(plugin.messages().text("&eUsage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon|trust|untrust|permissions>"));
}
private void buy(Player player, String[] args) throws SQLException { private void buy(Player player, String[] args) throws SQLException {
if (args.length != 2 && args.length != 3) { if (args.length != 2 && args.length != 3) {
player.sendMessage(plugin.messages().text("&eUsage: /claim buy <homestead|industrial> [radius]")); player.sendMessage(plugin.messages().text("&eUsage: /claim buy <homestead|industrial> [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 <id> <player> <build|interact|container|animal|manage|all>"));
return;
}
long id = parseLong(args[1]);
List<ClaimPermission> 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 <id> <player> <build|interact|container|animal|manage|all>"));
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<ClaimPermission> 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 <id>"));
return;
}
long id = parseLong(args[1]);
if (id <= 0L) {
player.sendMessage(plugin.messages().text("&cInvalid claim id."));
return;
}
List<ClaimPermissionGrant> 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<UUID, StringBuilder> 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<Claim> resolveOwnedClaim(Player player, String[] args, String action) throws SQLException { private Optional<Claim> resolveOwnedClaim(Player player, String[] args, String action) throws SQLException {
if (args.length > 2) { if (args.length > 2) {
player.sendMessage(plugin.messages().text("&eUsage: /claim " + action + " [id]")); player.sendMessage(plugin.messages().text("&eUsage: /claim " + action + " [id]"));
@@ -261,6 +363,10 @@ public final class ClaimCommand implements CommandExecutor {
return material.name().toLowerCase().replace('_', ' '); 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 { private void abandon(Player player, String[] args) throws SQLException {
if (args.length != 2) { if (args.length != 2) {
player.sendMessage(plugin.messages().text("&eUsage: /claim abandon <id>")); player.sendMessage(plugin.messages().text("&eUsage: /claim abandon <id>"));

View File

@@ -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> 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;
}
}

View File

@@ -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> 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;
}
}

View File

@@ -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> 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;
}
}

View File

@@ -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<ClaimPermission> parse(String raw) {
return Arrays.stream(values())
.filter(permission -> permission.name().equalsIgnoreCase(raw))
.findFirst();
}
public static List<ClaimPermission> 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();
}
}

View File

@@ -0,0 +1,11 @@
package com.librewiki.coalgov.model;
import java.util.UUID;
public record ClaimPermissionGrant(
long claimId,
UUID playerUuid,
ClaimPermission permission,
long grantedAt
) {
}

View File

@@ -2,6 +2,7 @@ package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin; import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPermission;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -25,7 +26,7 @@ public final class BuildProtectionService {
if (claim.isEmpty()) { if (claim.isEmpty()) {
return BuildDecision.deny("This land is unclaimed and protected by the Ministry."); 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.deny("This claim belongs to another citizen.");
} }
return BuildDecision.allow(); return BuildDecision.allow();

View File

@@ -2,6 +2,8 @@ package com.librewiki.coalgov.service;
import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint; 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.storage.ClaimRepository;
import com.librewiki.coalgov.util.CoalMoney; import com.librewiki.coalgov.util.CoalMoney;
import com.librewiki.coalgov.util.Cuboid2D; import com.librewiki.coalgov.util.Cuboid2D;
@@ -142,6 +144,66 @@ public final class ClaimService {
return TransferResult.success(claim.get()); 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> 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> 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> 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<ClaimPermissionGrant> permissions(UUID actor, long claimId) throws SQLException {
Optional<Claim> claim = findManageableClaim(actor, claimId);
if (claim.isEmpty()) {
return List.of();
}
return repository.permissions(claimId);
}
private Optional<Claim> findManageableClaim(UUID actor, long claimId) throws SQLException {
Optional<Claim> owned = findOwnedById(actor, claimId);
if (owned.isPresent()) {
return owned;
}
Optional<Claim> claim = repository.findById(claimId);
if (claim.isPresent() && repository.hasPermission(claimId, actor, ClaimPermission.MANAGE)) {
return claim;
}
return Optional.empty();
}
public long claimPurchaseCost(Claim claim) { public long claimPurchaseCost(Claim claim) {
if (claim.purchaseCost() >= 0L) { if (claim.purchaseCost() >= 0L) {
return claim.purchaseCost(); return claim.purchaseCost();
@@ -222,4 +284,14 @@ public final class ClaimService {
return new TransferResult(true, "", claim); 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, "");
}
}
} }

View File

@@ -2,6 +2,8 @@ package com.librewiki.coalgov.storage;
import com.librewiki.coalgov.model.Claim; import com.librewiki.coalgov.model.Claim;
import com.librewiki.coalgov.model.ClaimPoint; 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 com.librewiki.coalgov.util.Cuboid2D;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
@@ -69,6 +71,11 @@ public final class ClaimRepository {
deleteVertices.setLong(1, id); deleteVertices.setLong(1, id);
deleteVertices.executeUpdate(); 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( try (PreparedStatement delete = database.connection().prepareStatement(
"DELETE FROM claims WHERE id = ? AND owner_uuid = ?")) { "DELETE FROM claims WHERE id = ? AND owner_uuid = ?")) {
delete.setLong(1, id); delete.setLong(1, id);
@@ -134,13 +141,109 @@ public final class ClaimRepository {
} }
} }
public Optional<Claim> 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 { public boolean transferOwned(UUID owner, long id, UUID newOwner) throws SQLException {
try (PreparedStatement statement = database.connection().prepareStatement( try (PreparedStatement statement = database.connection().prepareStatement(
"UPDATE claims SET owner_uuid = ? WHERE id = ? AND owner_uuid = ?")) { "UPDATE claims SET owner_uuid = ? WHERE id = ? AND owner_uuid = ?")) {
statement.setString(1, newOwner.toString()); statement.setString(1, newOwner.toString());
statement.setLong(2, id); statement.setLong(2, id);
statement.setString(3, owner.toString()); 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<ClaimPermissionGrant> 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<ClaimPermissionGrant> 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();
} }
} }

View File

@@ -84,6 +84,15 @@ public final class Database {
PRIMARY KEY (claim_id, vertex_order) 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(""" statement.execute("""
CREATE TABLE IF NOT EXISTS land_regions ( CREATE TABLE IF NOT EXISTS land_regions (
id INTEGER PRIMARY KEY AUTOINCREMENT, 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_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_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_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_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)"); statement.execute("CREATE INDEX IF NOT EXISTS idx_fines_player ON fines(player_uuid, paid_at)");

View File

@@ -13,7 +13,7 @@ commands:
permission: coalgov.coal permission: coalgov.coal
claim: claim:
description: CoalGov claim commands. description: CoalGov claim commands.
usage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon> usage: /claim <wand|buy|info|list|show|appraise|sell|transfer|abandon|trust|untrust|permissions>
permission: coalgov.claim permission: coalgov.claim
permit: permit:
description: CoalGov permit commands. description: CoalGov permit commands.
@@ -30,6 +30,10 @@ commands:
description: Receive a CoalGov divining rod. description: Receive a CoalGov divining rod.
usage: /diviningrod usage: /diviningrod
permission: coalgov.diviningrod permission: coalgov.diviningrod
showpropertylines:
description: Show the property lines for the claim you are standing in.
usage: /showpropertylines
permission: coalgov.claim
police: police:
description: CoalGov police commands. description: CoalGov police commands.
usage: /police <fine> usage: /police <fine>