feat: allow temporary forced entry during fires
Build / build (push) Failing after 7s

This commit is contained in:
Michael Burgess
2026-08-17 12:21:59 -04:00
parent d3ea6c5cc2
commit f6f2f97f86
4 changed files with 107 additions and 9 deletions
+6 -5
View File
@@ -34,7 +34,7 @@ Creative and spectator players do not receive rewards by default. Creative earni
- Java 25 for compilation/server runtime - Java 25 for compilation/server runtime
- JobsReborn 5.2.6.x - JobsReborn 5.2.6.x
- CMILib, as required by JobsReborn - 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 - A JobsReborn-compatible economy if monetary payouts are desired
## Build ## Build
@@ -121,7 +121,8 @@ Emergency fire generation and response behavior:
- Cancels nearby block burning. - Cancels nearby block burning.
- Tracks and removes remaining emergency fire on completion, manual stop, expiry, plugin disable, or server shutdown. - 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. - 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. - 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. - 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. - 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 is currently working the JobsReborn Firefighter job.
- The player currently has RoseFirefighter's temporary emergency-build attachment. - The player currently has RoseFirefighter's temporary emergency-build attachment.
- The affected block is inside the active site's configured radius. - 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. `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. - Emergency reward multiplier.
- Announcement behavior. - Announcement behavior.
- Permitted emergency worlds. - 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. - Saved emergency sites.
## Development notes ## Development notes
@@ -11,6 +11,8 @@ import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import org.bukkit.block.BlockState; 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.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@@ -28,6 +30,7 @@ final class EmergencyConstructionManager implements Listener {
private final EmergencyPermissionManager permissions; private final EmergencyPermissionManager permissions;
private final Map<UUID, Placement> placements = new HashMap<>(); private final Map<UUID, Placement> placements = new HashMap<>();
private final Map<BlockKey, UUID> byBlock = new HashMap<>(); private final Map<BlockKey, UUID> byBlock = new HashMap<>();
private final Map<BlockKey, ForcedEntry> forcedEntries = new HashMap<>();
private final Map<UUID, List<ItemStack>> pendingReturns = new HashMap<>(); private final Map<UUID, List<ItemStack>> pendingReturns = new HashMap<>();
private final File pendingFile; private final File pendingFile;
private EmergencySite activeSite; private EmergencySite activeSite;
@@ -44,14 +47,19 @@ final class EmergencyConstructionManager implements Listener {
activeSite = site; activeSite = site;
placements.clear(); placements.clear();
byBlock.clear(); byBlock.clear();
forcedEntries.clear();
} }
boolean isTemporaryBlock(Block block) { boolean isTemporaryBlock(Block block) {
return block != null && byBlock.containsKey(BlockKey.of(block)); return block != null && byBlock.containsKey(BlockKey.of(block));
} }
boolean isForcedEntryEnabled() {
return plugin.getConfig().getBoolean("emergencies.forced-entry", true);
}
void cleanupAndReturn() { void cleanupAndReturn() {
if (placements.isEmpty()) { if (placements.isEmpty() && forcedEntries.isEmpty()) {
activeSite = null; activeSite = null;
return; return;
} }
@@ -73,6 +81,15 @@ final class EmergencyConstructionManager implements Listener {
placements.clear(); placements.clear();
byBlock.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; activeSite = null;
for (Map.Entry<UUID, List<ItemStack>> entry : returns.entrySet()) { for (Map.Entry<UUID, List<ItemStack>> 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<BlockState> 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<BlockState> snapshotForcedEntryStates(Block block) {
List<BlockState> 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<BlockState> 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 @EventHandler
public void onJoin(PlayerJoinEvent event) { public void onJoin(PlayerJoinEvent event) {
List<ItemStack> items = pendingReturns.remove(event.getPlayer().getUniqueId()); List<ItemStack> 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<PlacedBlock> blocks) {} private record Placement(UUID id, UUID owner, ItemStack returnItem, List<PlacedBlock> blocks) {}
private record PlacedBlock(BlockKey key, Material placedType, BlockState replacedState) {} private record PlacedBlock(BlockKey key, Material placedType, BlockState replacedState) {}
private record ForcedEntry(UUID firefighter, List<BlockState> originalStates) {}
} }
@@ -49,8 +49,10 @@ final class GriefPreventionBridge implements Listener {
if (trigger instanceof BlockBreakEvent breakEvent) { if (trigger instanceof BlockBreakEvent breakEvent) {
Block block = breakEvent.getBlock(); Block block = breakEvent.getBlock();
if ((emergencies.isEmergencyFire(block) || construction.isTemporaryBlock(block)) if (permissions.isInsideActiveSite(block.getLocation())
&& permissions.isInsideActiveSite(block.getLocation())) { && (emergencies.isEmergencyFire(block)
|| construction.isTemporaryBlock(block)
|| construction.isForcedEntryEnabled())) {
event.setDenialReason(null); event.setDenialReason(null);
} }
} }
+5
View File
@@ -51,6 +51,11 @@ emergencies:
protected-site-building: true protected-site-building: true
cleanup-firefighter-blocks: 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 # Applies to both automatic and manual emergency starts. Set false if admins
# should be able to start/test an emergency with no Firefighters online. # should be able to start/test an emergency with no Firefighters online.
require-firefighters-online: true require-firefighters-online: true