Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ repositories {
name = "opencollab"
url = "https://repo.opencollab.dev/main/"
}
maven {
name = "minebench"
url = "https://repo.minebench.de/"
}
}

dependencies {
Expand All @@ -44,6 +48,7 @@ dependencies {
compileOnly("com.sk89q.worldguard:worldguard-bukkit:7.0.14")
compileOnly("net.essentialsx:EssentialsX:2.21.2")
compileOnly("org.geysermc.floodgate:api:2.2.4-SNAPSHOT")
compileOnly("com.acrobot.chestshop:chestshop:3.12.2")
}

tasks {
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
10 changes: 10 additions & 0 deletions src/main/java/net/democracycraft/vault/VaultStoragePlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class VaultStoragePlugin extends JavaPlugin {
private BoltService boltService;
private MailService mailService;
private Essentials essentials;
private ChestShopService chestShopService;
private DemocracyLibApi democracyLibApi;
private BedrockUniqueIdentifierRetriever bedrockUniqueIdentifierRetriever;

Expand Down Expand Up @@ -130,6 +131,10 @@ public void onEnable() {
this.essentials = ess;
}

// ChestShop soft dependency: real impl when present, no-op otherwise.
var chestShop = getServer().getPluginManager().getPlugin("ChestShop");
this.chestShopService = chestShop != null ? new ChestShopServiceImp() : new NoOpChestShopService();

// Domain services
this.captureService = new VaultCaptureService();
this.placementService = new VaultPlacementService();
Expand Down Expand Up @@ -209,4 +214,9 @@ public Essentials getEssentials() {
return essentials;
}

/** ChestShop integration; returns a no-op implementation when ChestShop is not installed. */
public ChestShopService getChestShopService() {
return chestShopService;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.democracycraft.vault.api.service;

import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

/**
* Service for integrating with the optional ChestShop plugin.
* Resolves to a no-op implementation when ChestShop is not installed.
*/
public interface ChestShopService extends Service {

/** @return true if ChestShop is installed and this integration is active. */
boolean isAvailable();

/** @return true if the block is a valid ChestShop sign. */
boolean isShopSign(@Nullable Block block);

/** @return the shop sign blocks attached to the container, or an empty list. */
@NotNull List<Block> findShopSigns(@Nullable Block containerBlock);

/**
* Notifies ChestShop and removes the shop sign block. Must run on the main thread.
* @return true if the block was a shop sign and was removed
*/
boolean removeShopSign(@Nullable Player actor, @Nullable Block signBlock);
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,66 @@ private static boolean computeHangableRestricted(Player actor, Block block, UUID
private static Decision evaluateBlockPolicy(Player actor, Block block, UUID originalOwner, boolean hangableRestricted) {
boolean hasOverride = VaultPermission.ACTION_PLACE_OVERRIDE.has(actor);
boolean actorIsContainerOwner = originalOwner != null && originalOwner.equals(actor.getUniqueId());

RegionParticipation participation = computeRegionParticipation(actor, block, originalOwner);
boolean actorParticipantAny = participation.actorParticipantAny();
boolean containerOwnerParticipantAny = participation.containerOwnerParticipantAny();
boolean inAnyRegion = participation.inAnyRegion();
List<RegionStatus> perRegionDebug = participation.perRegionDebug();

boolean disallowedOwnerSelf = actorParticipantAny && actorIsContainerOwner;
boolean baseAllowed;

if (actorParticipantAny) {
// Actor is a participant of at least one relevant (non-parent) region
// Can vault if: block has owner, actor is not the owner, and owner is not participant of any relevant region
baseAllowed = (originalOwner != null) && !actorIsContainerOwner && !containerOwnerParticipantAny;
} else {
// Actor is not a participant of any relevant region
// Can only vault their own blocks
baseAllowed = actorIsContainerOwner;
}


boolean allowed = (originalOwner != null)
&& !disallowedOwnerSelf
&& !hangableRestricted
&& ((inAnyRegion && baseAllowed) || hasOverride);

Decision.Reason reason = switch (Boolean.TRUE) {
case Boolean b when allowed -> Decision.Reason.ALLOWED;
case Boolean b when hangableRestricted -> Decision.Reason.ENTITIES_REQUIRE_ADMIN;
case Boolean b when !inAnyRegion -> Decision.Reason.NOT_IN_REGION;
case Boolean b when disallowedOwnerSelf -> Decision.Reason.OWNER_SELF_IN_REGION;
case Boolean b when actorParticipantAny && containerOwnerParticipantAny -> Decision.Reason.CONTAINER_OWNER_IN_OVERLAP;
case Boolean b when originalOwner == null -> Decision.Reason.UNPROTECTED_NO_OVERRIDE;
default -> Decision.Reason.NOT_INVOLVED_NOT_OWNER;
};

return new Decision(
allowed,
hasOverride,
actorParticipantAny,
containerOwnerParticipantAny,
actorIsContainerOwner,
disallowedOwnerSelf,
baseAllowed,
originalOwner,
reason,
perRegionDebug
);
}

private record RegionParticipation(boolean inAnyRegion, boolean actorParticipantAny,
boolean containerOwnerParticipantAny, List<RegionStatus> perRegionDebug) {}

/** Scans WorldGuard regions at the block and computes actor/owner participation flags. */
private static RegionParticipation computeRegionParticipation(Player actor, Block block, UUID originalOwner) {
WorldGuardService wgs = VaultStoragePlugin.getInstance().getWorldGuardService();

boolean actorParticipantAny = false;
boolean containerOwnerParticipantAny = false;
boolean inAnyRegion = false;

List<RegionStatus> perRegionDebug = List.of();

if (wgs != null) {
Expand Down Expand Up @@ -196,10 +250,8 @@ private static Decision evaluateBlockPolicy(Player actor, Block block, UUID orig

final BoundingBox currentBox = box;
boolean isParent = false;
boolean isChild = false;
try {
isParent = regions.stream().anyMatch(r -> r != region && safeContains(currentBox, r));
isChild = regions.stream().anyMatch(r -> r != region && safeContains(r.boundingBox(), region));
} catch (Throwable t) {
VaultStoragePlugin.getInstance().getLogger().warning("[VaultCapturePolicy] Error evaluating parent/child for region "
+ region.id() + ": " + t.getClass().getSimpleName() + " - " + t.getMessage());
Expand All @@ -220,53 +272,47 @@ private static Decision evaluateBlockPolicy(Player actor, Block block, UUID orig
perRegionDebug = Collections.unmodifiableList(debugList);
}

boolean disallowedOwnerSelf = actorParticipantAny && actorIsContainerOwner;
boolean baseAllowed;
return new RegionParticipation(inAnyRegion, actorParticipantAny, containerOwnerParticipantAny, perRegionDebug);
}

if (actorParticipantAny) {
// Actor is a participant of at least one relevant (non-parent) region
// Can vault if: block has owner, actor is not the owner, and owner is not participant of any relevant region
baseAllowed = (originalOwner != null) && !actorIsContainerOwner && !containerOwnerParticipantAny;
} else {
// Actor is not a participant of any relevant region
// Can only vault their own blocks
baseAllowed = actorIsContainerOwner;
/**
* Evaluate whether the actor may remove a ChestShop sign on the given container.
* Same region participation rule as {@link #evaluate(Player, Block)}, but does not require a Bolt owner.
*/
public static Decision evaluateChestShopSignRemoval(Player actor, Block containerBlock) {
BoltService bolt = VaultStoragePlugin.getInstance().getBoltService();
UUID originalOwner = null;
if (bolt != null) {
try {
originalOwner = bolt.getOwner(containerBlock);
} catch (Throwable t) {
VaultStoragePlugin.getInstance().getLogger().warning("[VaultCapturePolicy] Error obtaining Bolt owner for block in "
+ formatBlock(containerBlock) + ": " + t.getClass().getSimpleName() + " - " + t.getMessage());
}
}

boolean hasOverride = VaultPermission.ACTION_PLACE_OVERRIDE.has(actor);
boolean actorIsContainerOwner = originalOwner != null && originalOwner.equals(actor.getUniqueId());
RegionParticipation participation = computeRegionParticipation(actor, containerBlock, originalOwner);

boolean allowed = (originalOwner != null)
&& !disallowedOwnerSelf
&& !hangableRestricted
&& ((inAnyRegion && baseAllowed) || hasOverride);
// Same region gate as capture, but no Bolt-owner requirement.
boolean allowed = (participation.inAnyRegion() && participation.actorParticipantAny()) || hasOverride;

Decision.Reason reason;
if (allowed) {
reason = Decision.Reason.ALLOWED;
} else if (hangableRestricted) {
reason = Decision.Reason.ENTITIES_REQUIRE_ADMIN;
} else if (!inAnyRegion) {
reason = Decision.Reason.NOT_IN_REGION;
} else if (disallowedOwnerSelf) {
reason = Decision.Reason.OWNER_SELF_IN_REGION;
} else if (actorParticipantAny && containerOwnerParticipantAny) {
reason = Decision.Reason.CONTAINER_OWNER_IN_OVERLAP;
} else if (originalOwner == null) {
reason = Decision.Reason.UNPROTECTED_NO_OVERRIDE;
} else {
reason = Decision.Reason.NOT_INVOLVED_NOT_OWNER;
}
Decision.Reason reason = allowed
? Decision.Reason.ALLOWED
: (!participation.inAnyRegion() ? Decision.Reason.NOT_IN_REGION : Decision.Reason.NOT_INVOLVED_NOT_OWNER);

return new Decision(
allowed,
hasOverride,
actorParticipantAny,
containerOwnerParticipantAny,
participation.actorParticipantAny(),
participation.containerOwnerParticipantAny(),
actorIsContainerOwner,
disallowedOwnerSelf,
baseAllowed,
false,
allowed,
originalOwner,
reason,
perRegionDebug
participation.perRegionDebug()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package net.democracycraft.vault.internal.service;

import com.Acrobot.ChestShop.Events.ShopDestroyedEvent;
import com.Acrobot.ChestShop.Signs.ChestShopSign;
import com.Acrobot.ChestShop.Utils.uBlock;
import net.democracycraft.vault.api.service.ChestShopService;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Container;
import org.bukkit.block.Sign;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;

/**
* {@link ChestShopService} backed by the ChestShop API. Only instantiated when ChestShop is installed,
* so that the {@code com.Acrobot} imports are never linked otherwise.
*/
public final class ChestShopServiceImp implements ChestShopService {

@Override
public boolean isAvailable() {
return true;
}

@Override
public boolean isShopSign(@Nullable Block block) {
return block != null && ChestShopSign.isValid(block);
}

@Override
public @NotNull List<Block> findShopSigns(@Nullable Block containerBlock) {
if (containerBlock == null) {
return List.of();
}
List<Sign> signs = uBlock.findConnectedShopSigns(containerBlock);
if (signs == null || signs.isEmpty()) {
return List.of();
}
List<Block> blocks = new ArrayList<>(signs.size());
for (Sign sign : signs) {
if (sign != null) {
blocks.add(sign.getBlock());
}
}
return blocks;
}

@Override
public boolean removeShopSign(@Nullable Player actor, @Nullable Block signBlock) {
if (signBlock == null || !(signBlock.getState() instanceof Sign sign)) {
return false;
}
if (!ChestShopSign.isValid(sign)) {
return false;
}
Container container = uBlock.findConnectedContainer(signBlock);
Bukkit.getPluginManager().callEvent(new ShopDestroyedEvent(actor, sign, container));
signBlock.setType(Material.AIR);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.democracycraft.vault.internal.service;

import net.democracycraft.vault.api.service.ChestShopService;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

/** No-op {@link ChestShopService} used when ChestShop is not installed. */
public final class NoOpChestShopService implements ChestShopService {

@Override
public boolean isAvailable() {
return false;
}

@Override
public boolean isShopSign(@Nullable Block block) {
return false;
}

@Override
public @NotNull List<Block> findShopSigns(@Nullable Block containerBlock) {
return List.of();
}

@Override
public boolean removeShopSign(@Nullable Player actor, @Nullable Block signBlock) {
return false;
}
}
Loading