Fix GriefPrevention firefighter incident bypass
Build / build (push) Failing after 7s

This commit is contained in:
Michael Burgess
2026-08-17 20:10:03 -04:00
parent 0df9b059e2
commit a99e15dc2f
7 changed files with 285 additions and 2 deletions
+15
View File
@@ -21,6 +21,17 @@ dependencies {
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.6") {
isTransitive = false
}
testImplementation("io.papermc.paper:paper-api:26.2.build.+")
testImplementation("com.github.Zrips:Jobs:v5.2.6.2") {
isTransitive = false
}
testImplementation("com.github.GriefPrevention:GriefPrevention:16.18.6") {
isTransitive = false
}
testImplementation("org.junit.jupiter:junit-jupiter:5.11.3")
testImplementation("org.mockito:mockito-core:5.23.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.3")
}
java {
@@ -32,6 +43,10 @@ tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked"))
}
tasks.test {
useJUnitPlatform()
}
tasks.processResources {
inputs.property("pluginVersion", pluginVersion)
filesMatching("plugin.yml") {
@@ -8,5 +8,7 @@ import org.bukkit.World;
interface EmergencyLocationProvider {
EmergencySite randomClaimSite(Random random, Predicate<World> worldAllowed, int maxRadius, int minClaimArea);
EmergencySite associateClaim(EmergencySite site);
String nearbyClaims(Location location, int radius, int maxResults);
}
@@ -280,6 +280,10 @@ final class EmergencyManager {
return false;
}
if (locationProvider != null) {
site = locationProvider.associateClaim(site);
}
activeSite = site;
activeFires.addAll(spawned);
if (construction != null) {
@@ -4,7 +4,15 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
record EmergencySite(String name, String worldName, int x, int y, int z, int radius) {
record EmergencySite(String name, String worldName, int x, int y, int z, int radius, Long griefPreventionClaimId) {
EmergencySite(String name, String worldName, int x, int y, int z, int radius) {
this(name, worldName, x, y, z, radius, null);
}
EmergencySite withGriefPreventionClaimId(Long claimId) {
return new EmergencySite(name, worldName, x, y, z, radius, claimId);
}
Location center() {
World world = Bukkit.getWorld(worldName);
return world == null ? null : new Location(world, x + 0.5, y, z + 0.5);
@@ -12,6 +12,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerBucketEmptyEvent;
import org.bukkit.event.player.PlayerInteractEvent;
final class GriefPreventionBridge implements Listener {
private final EmergencyManager emergencies;
@@ -25,7 +26,7 @@ final class GriefPreventionBridge implements Listener {
this.construction = construction;
}
@EventHandler(priority = EventPriority.HIGHEST)
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onClaimPermissionCheck(ClaimPermissionCheckEvent event) {
if (event.getRequiredPermission() != ClaimPermission.Build || event.getDenialReason() == null) {
return;
@@ -36,6 +37,11 @@ final class GriefPreventionBridge implements Listener {
return;
}
EmergencySite activeSite = emergencies.activeSite();
if (!isIncidentClaim(activeSite, event)) {
return;
}
Event trigger = event.getTriggeringEvent();
if (trigger instanceof BlockPlaceEvent placeEvent) {
allowIfInside(event, placeEvent.getBlock().getLocation());
@@ -47,6 +53,14 @@ final class GriefPreventionBridge implements Listener {
return;
}
if (trigger instanceof PlayerInteractEvent interactEvent) {
Block block = interactEvent.getClickedBlock();
if (block != null) {
allowIfInside(event, block.getLocation());
}
return;
}
if (trigger instanceof BlockBreakEvent breakEvent) {
Block block = breakEvent.getBlock();
if (permissions.isInsideActiveSite(block.getLocation())
@@ -58,6 +72,14 @@ final class GriefPreventionBridge implements Listener {
}
}
private boolean isIncidentClaim(EmergencySite activeSite, ClaimPermissionCheckEvent event) {
if (activeSite == null || activeSite.griefPreventionClaimId() == null || event.getClaim() == null) {
return false;
}
Long checkedClaimId = event.getClaim().getID();
return checkedClaimId != null && checkedClaimId.equals(activeSite.griefPreventionClaimId());
}
private void allowIfInside(ClaimPermissionCheckEvent event, Location location) {
if (permissions.isInsideActiveSite(location)) {
event.setDenialReason(null);
@@ -60,6 +60,21 @@ final class GriefPreventionLocationProvider implements EmergencyLocationProvider
return new EmergencySite(owner + "'s home", world.getName(), centerX, y, centerZ, radius);
}
@Override
public EmergencySite associateClaim(EmergencySite site) {
if (site == null) {
return null;
}
Location center = site.center();
if (center == null || center.getWorld() == null) {
return site.withGriefPreventionClaimId(null);
}
Claim claim = GriefPrevention.instance.dataStore.getClaimAt(center, false, null);
return site.withGriefPreventionClaimId(claim == null ? null : claim.getID());
}
@Override
public String nearbyClaims(Location location, int radius, int maxResults) {
if (location == null || location.getWorld() == null) {
@@ -0,0 +1,217 @@
package net.therosegarden.firefighter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.function.Supplier;
import me.ryanhamshire.GriefPrevention.Claim;
import me.ryanhamshire.GriefPrevention.ClaimPermission;
import me.ryanhamshire.GriefPrevention.events.ClaimPermissionCheckEvent;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class GriefPreventionBridgeTest {
private static final Long INCIDENT_CLAIM_ID = 42L;
private static final Supplier<String> DENIED = () -> "denied";
private EmergencyManager emergencies;
private EmergencyPermissionManager permissions;
private EmergencyConstructionManager construction;
private GriefPreventionBridge bridge;
private Player player;
private EmergencySite incidentSite;
@BeforeEach
void setUp() {
emergencies = mock(EmergencyManager.class);
permissions = mock(EmergencyPermissionManager.class);
construction = mock(EmergencyConstructionManager.class);
bridge = new GriefPreventionBridge(emergencies, permissions, construction);
player = mock(Player.class);
when(player.getUniqueId()).thenReturn(UUID.randomUUID());
incidentSite = new EmergencySite("Incident", "world", 100, 64, 100, 18, INCIDENT_CLAIM_ID);
}
@Test
void alreadyDeniedPlayerInteractBuildIsProcessedAndClearedForEligibleIncident() {
Block block = mock(Block.class);
Location location = mock(Location.class);
when(block.getLocation()).thenReturn(location);
PlayerInteractEvent trigger = mock(PlayerInteractEvent.class);
when(trigger.getClickedBlock()).thenReturn(block);
allowEligibleIncident(location);
ClaimPermissionCheckEvent event = deniedEvent(ClaimPermission.Build, trigger, INCIDENT_CLAIM_ID);
assertTrue(event.isCancelled(), "GriefPrevention denial should make the permission event cancelled before RoseFirefighter handles it");
bridge.onClaimPermissionCheck(event);
assertFalse(event.isCancelled());
assertNull(event.getDenialReason());
}
@Test
void correctIncidentClaimAllowsPlaceAndBreakWithoutUncancellingTriggerEvents() {
Block placeBlock = mock(Block.class);
Location placeLocation = mock(Location.class);
when(placeBlock.getLocation()).thenReturn(placeLocation);
BlockPlaceEvent placeTrigger = mock(BlockPlaceEvent.class);
when(placeTrigger.getBlock()).thenReturn(placeBlock);
Block breakBlock = mock(Block.class);
Location breakLocation = mock(Location.class);
when(breakBlock.getLocation()).thenReturn(breakLocation);
BlockBreakEvent breakTrigger = mock(BlockBreakEvent.class);
when(breakTrigger.getBlock()).thenReturn(breakBlock);
when(permissions.hasBuildAccess(player)).thenReturn(true);
when(emergencies.activeSite()).thenReturn(incidentSite);
when(permissions.isInsideActiveSite(placeLocation)).thenReturn(true);
when(permissions.isInsideActiveSite(breakLocation)).thenReturn(true);
when(construction.isForcedEntryEnabled()).thenReturn(true);
ClaimPermissionCheckEvent placeEvent = deniedEvent(ClaimPermission.Build, placeTrigger, INCIDENT_CLAIM_ID);
ClaimPermissionCheckEvent breakEvent = deniedEvent(ClaimPermission.Build, breakTrigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(placeEvent);
bridge.onClaimPermissionCheck(breakEvent);
assertNull(placeEvent.getDenialReason());
assertNull(breakEvent.getDenialReason());
verify(placeTrigger, never()).setCancelled(anyBoolean());
verify(breakTrigger, never()).setCancelled(anyBoolean());
}
@Test
void noActiveEmergencyKeepsGriefPreventionDenial() {
Block block = blockAt(mock(Location.class));
BlockPlaceEvent trigger = mock(BlockPlaceEvent.class);
when(trigger.getBlock()).thenReturn(block);
when(permissions.hasBuildAccess(player)).thenReturn(true);
when(emergencies.activeSite()).thenReturn(null);
ClaimPermissionCheckEvent event = deniedEvent(ClaimPermission.Build, trigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(event);
assertNotNull(event.getDenialReason());
assertTrue(event.isCancelled());
}
@Test
void nonFirefighterOrOtherwiseIneligiblePlayerKeepsDenial() {
Location location = mock(Location.class);
Block block = blockAt(location);
BlockPlaceEvent trigger = mock(BlockPlaceEvent.class);
when(trigger.getBlock()).thenReturn(block);
when(permissions.hasBuildAccess(player)).thenReturn(false);
when(emergencies.activeSite()).thenReturn(incidentSite);
ClaimPermissionCheckEvent event = deniedEvent(ClaimPermission.Build, trigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(event);
assertNotNull(event.getDenialReason());
}
@Test
void unrelatedOrChildClaimKeepsDenialEvenInsideResponseRadius() {
Location location = mock(Location.class);
Block block = blockAt(location);
BlockPlaceEvent trigger = mock(BlockPlaceEvent.class);
when(trigger.getBlock()).thenReturn(block);
allowEligibleIncident(location);
ClaimPermissionCheckEvent unrelatedClaim = deniedEvent(ClaimPermission.Build, trigger, 99L);
ClaimPermissionCheckEvent childClaim = deniedEvent(ClaimPermission.Build, trigger, 43L);
bridge.onClaimPermissionCheck(unrelatedClaim);
bridge.onClaimPermissionCheck(childClaim);
assertNotNull(unrelatedClaim.getDenialReason());
assertNotNull(childClaim.getDenialReason());
}
@Test
void incidentEndImmediatelyRestoresDenial() {
Location location = mock(Location.class);
Block block = blockAt(location);
BlockPlaceEvent trigger = mock(BlockPlaceEvent.class);
when(trigger.getBlock()).thenReturn(block);
allowEligibleIncident(location);
ClaimPermissionCheckEvent duringIncident = deniedEvent(ClaimPermission.Build, trigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(duringIncident);
assertNull(duringIncident.getDenialReason());
when(permissions.hasBuildAccess(player)).thenReturn(false);
when(emergencies.activeSite()).thenReturn(null);
ClaimPermissionCheckEvent afterIncident = deniedEvent(ClaimPermission.Build, trigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(afterIncident);
assertNotNull(afterIncident.getDenialReason());
assertTrue(afterIncident.isCancelled());
}
@Test
void onlyBuildPermissionIsBypassed() {
Location location = mock(Location.class);
Block block = blockAt(location);
BlockPlaceEvent trigger = mock(BlockPlaceEvent.class);
when(trigger.getBlock()).thenReturn(block);
allowEligibleIncident(location);
ClaimPermissionCheckEvent editEvent = deniedEvent(ClaimPermission.Edit, trigger, INCIDENT_CLAIM_ID);
bridge.onClaimPermissionCheck(editEvent);
assertNotNull(editEvent.getDenialReason());
assertTrue(editEvent.isCancelled());
}
@Test
void listenerExplicitlyReceivesCancelledPermissionChecksAtHighestPriority() throws Exception {
Method method = GriefPreventionBridge.class.getMethod("onClaimPermissionCheck", ClaimPermissionCheckEvent.class);
EventHandler handler = method.getAnnotation(EventHandler.class);
assertNotNull(handler);
assertEquals(EventPriority.HIGHEST, handler.priority());
assertFalse(handler.ignoreCancelled());
}
private void allowEligibleIncident(Location location) {
when(permissions.hasBuildAccess(player)).thenReturn(true);
when(emergencies.activeSite()).thenReturn(incidentSite);
when(permissions.isInsideActiveSite(location)).thenReturn(true);
}
private Block blockAt(Location location) {
Block block = mock(Block.class);
when(block.getLocation()).thenReturn(location);
return block;
}
private ClaimPermissionCheckEvent deniedEvent(ClaimPermission permission, org.bukkit.event.Event trigger, Long claimId) {
Claim claim = mock(Claim.class);
when(claim.getID()).thenReturn(claimId);
ClaimPermissionCheckEvent event = new ClaimPermissionCheckEvent(player, claim, permission, trigger);
event.setDenialReason(DENIED);
return event;
}
}