From f6f2f97f86418f4dc5466fc0f811a59055c174e1 Mon Sep 17 00:00:00 2001 From: Michael Burgess Date: Mon, 17 Aug 2026 12:21:59 -0400 Subject: [PATCH] feat: allow temporary forced entry during fires --- README.md | 11 ++- .../EmergencyConstructionManager.java | 94 ++++++++++++++++++- .../firefighter/GriefPreventionBridge.java | 6 +- src/main/resources/config.yml | 5 + 4 files changed, 107 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 354f974..2ed004e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Creative and spectator players do not receive rewards by default. Creative earni - Java 25 for compilation/server runtime - JobsReborn 5.2.6.x - CMILib, as required by JobsReborn -- GriefPrevention 16.18.6+ (optional; enables protected-claim emergency building) +- GriefPrevention 16.18.6+ (optional; enables protected-claim emergency building and forced entry) - A JobsReborn-compatible economy if monetary payouts are desired ## Build @@ -121,7 +121,8 @@ Emergency fire generation and response behavior: - Cancels nearby block burning. - Tracks and removes remaining emergency fire on completion, manual stop, expiry, plugin disable, or server shutdown. - Grants on-duty Firefighters the temporary `rosefirefighter.emergency.build` runtime permission while an incident is active. -- When GriefPrevention is installed, uses its claim-permission API to allow block placement and water-bucket response only inside the active site. Emergency fire and tracked temporary response blocks can also be broken there. +- When GriefPrevention is installed, uses its claim-permission API to allow emergency building, water use, and temporary forced entry only inside the active site. +- With `emergencies.forced-entry: true`, protected doors, windows, walls, and other blocks can be temporarily breached by an on-duty Firefighter. Forced-entry blocks produce no item drops or XP and are restored to their original block states when the incident ends. - Does **not** grant `/IgnoreClaims` or global `griefprevention.adminclaims`, so an incident does not become a server-wide claim bypass. - Tracks blocks Firefighters place at the incident and restores the replaced block state when the incident ends. - Returns one consumed placement item per still-tracked placement to the Firefighter who placed it. Blocks the player already broke are removed from the return ledger to avoid duplication. @@ -145,9 +146,9 @@ With GriefPrevention installed, RoseFirefighter listens to `ClaimPermissionCheck - The player is currently working the JobsReborn Firefighter job. - The player currently has RoseFirefighter's temporary emergency-build attachment. - The affected block is inside the active site's configured radius. -- The action is placing a block, emptying a bucket, breaking an emergency fire, or breaking a tracked temporary Firefighter block. +- The action is emergency response construction, water use, extinguishing emergency fire, breaking a tracked temporary Firefighter block, or a forced-entry break when `emergencies.forced-entry` is enabled. -Normal protected structures cannot be broken merely because an emergency is active. +Forced-entry damage is temporary. The plugin snapshots the original block states, suppresses drops and XP, removes the breached blocks without physics, and restores those original states when the emergency is cleared, stopped, expires, or the plugin shuts down. Multi-block doors and beds are handled together so the structure can be restored correctly. `emergencies.cleanup-firefighter-blocks` controls whether placements made by Firefighters inside the active site are added to the temporary response-construction ledger. @@ -167,7 +168,7 @@ Normal protected structures cannot be broken merely because an emergency is acti - Emergency reward multiplier. - Announcement behavior. - Permitted emergency worlds. -- Protected-site emergency building and temporary Firefighter block cleanup. +- Protected-site emergency building, temporary forced entry, and temporary Firefighter block cleanup. - Saved emergency sites. ## Development notes diff --git a/src/main/java/net/therosegarden/firefighter/EmergencyConstructionManager.java b/src/main/java/net/therosegarden/firefighter/EmergencyConstructionManager.java index 08377dc..a491185 100644 --- a/src/main/java/net/therosegarden/firefighter/EmergencyConstructionManager.java +++ b/src/main/java/net/therosegarden/firefighter/EmergencyConstructionManager.java @@ -11,6 +11,8 @@ import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.type.Bed; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -28,6 +30,7 @@ final class EmergencyConstructionManager implements Listener { private final EmergencyPermissionManager permissions; private final Map placements = new HashMap<>(); private final Map byBlock = new HashMap<>(); + private final Map forcedEntries = new HashMap<>(); private final Map> pendingReturns = new HashMap<>(); private final File pendingFile; private EmergencySite activeSite; @@ -44,14 +47,19 @@ final class EmergencyConstructionManager implements Listener { activeSite = site; placements.clear(); byBlock.clear(); + forcedEntries.clear(); } boolean isTemporaryBlock(Block block) { return block != null && byBlock.containsKey(BlockKey.of(block)); } + boolean isForcedEntryEnabled() { + return plugin.getConfig().getBoolean("emergencies.forced-entry", true); + } + void cleanupAndReturn() { - if (placements.isEmpty()) { + if (placements.isEmpty() && forcedEntries.isEmpty()) { activeSite = null; return; } @@ -73,6 +81,15 @@ final class EmergencyConstructionManager implements Listener { placements.clear(); byBlock.clear(); + + // Restore forced-entry damage after temporary response construction has + // been removed. BlockState snapshots preserve block data and tile state. + for (ForcedEntry entry : new ArrayList<>(forcedEntries.values())) { + for (BlockState state : entry.originalStates()) { + state.update(true, false); + } + } + forcedEntries.clear(); activeSite = null; for (Map.Entry> entry : returns.entrySet()) { @@ -134,6 +151,79 @@ final class EmergencyConstructionManager implements Listener { } } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onForcedEntry(BlockBreakEvent event) { + if (activeSite == null || !isForcedEntryEnabled()) { + return; + } + + Player player = event.getPlayer(); + Block block = event.getBlock(); + if (!jobs.isFirefighter(player) + || !permissions.hasBuildAccess(player) + || !activeSite.contains(block.getLocation()) + || isTemporaryBlock(block) + || block.getType() == Material.FIRE + || block.getType() == Material.SOUL_FIRE) { + return; + } + + List states = snapshotForcedEntryStates(block); + if (states.isEmpty()) { + return; + } + + BlockKey primary = BlockKey.of(block); + forcedEntries.putIfAbsent(primary, new ForcedEntry(player.getUniqueId(), List.copyOf(states))); + + // Forced entry is temporary incident damage, not resource harvesting. + // Remove involved blocks without physics, item drops, or experience. + event.setDropItems(false); + event.setExpToDrop(0); + for (BlockState state : states) { + Block affected = state.getBlock(); + if (affected.getType() != Material.AIR) { + affected.setType(Material.AIR, false); + } + } + event.setCancelled(true); + } + + private List snapshotForcedEntryStates(Block block) { + List states = new ArrayList<>(); + addUniqueState(states, block); + + if (block.getBlockData() instanceof Bisected bisected) { + Block other = bisected.getHalf() == Bisected.Half.TOP + ? block.getRelative(0, -1, 0) + : block.getRelative(0, 1, 0); + if (activeSite.contains(other.getLocation())) { + addUniqueState(states, other); + } + } + + if (block.getBlockData() instanceof Bed bed) { + Block other = bed.getPart() == Bed.Part.FOOT + ? block.getRelative(bed.getFacing()) + : block.getRelative(bed.getFacing().getOppositeFace()); + if (activeSite.contains(other.getLocation())) { + addUniqueState(states, other); + } + } + + return states; + } + + private void addUniqueState(List states, Block block) { + BlockKey key = BlockKey.of(block); + for (BlockState state : states) { + if (BlockKey.of(state.getBlock()).equals(key)) { + return; + } + } + states.add(block.getState()); + } + @EventHandler public void onJoin(PlayerJoinEvent event) { List items = pendingReturns.remove(event.getPlayer().getUniqueId()); @@ -215,5 +305,5 @@ final class EmergencyConstructionManager implements Listener { private record Placement(UUID id, UUID owner, ItemStack returnItem, List blocks) {} private record PlacedBlock(BlockKey key, Material placedType, BlockState replacedState) {} + private record ForcedEntry(UUID firefighter, List originalStates) {} } - diff --git a/src/main/java/net/therosegarden/firefighter/GriefPreventionBridge.java b/src/main/java/net/therosegarden/firefighter/GriefPreventionBridge.java index f8f79a4..61d590e 100644 --- a/src/main/java/net/therosegarden/firefighter/GriefPreventionBridge.java +++ b/src/main/java/net/therosegarden/firefighter/GriefPreventionBridge.java @@ -49,8 +49,10 @@ final class GriefPreventionBridge implements Listener { if (trigger instanceof BlockBreakEvent breakEvent) { Block block = breakEvent.getBlock(); - if ((emergencies.isEmergencyFire(block) || construction.isTemporaryBlock(block)) - && permissions.isInsideActiveSite(block.getLocation())) { + if (permissions.isInsideActiveSite(block.getLocation()) + && (emergencies.isEmergencyFire(block) + || construction.isTemporaryBlock(block) + || construction.isForcedEntryEnabled())) { event.setDenialReason(null); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 5111ddd..18bcd47 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -51,6 +51,11 @@ emergencies: protected-site-building: true cleanup-firefighter-blocks: true + # Allow on-duty Firefighters to make temporary forced entry through protected + # doors, windows, walls, etc. inside the active response site. Broken blocks + # produce no drops/XP and are restored when the incident ends. + forced-entry: true + # Applies to both automatic and manual emergency starts. Set false if admins # should be able to start/test an emergency with no Firefighters online. require-firefighters-online: true