feat: add protected emergency build cleanup
Build / build (push) Failing after 11s

This commit is contained in:
Michael Burgess
2026-08-17 09:00:09 -04:00
parent d0f0133f57
commit d3ea6c5cc2
10 changed files with 488 additions and 3 deletions
+33 -3
View File
@@ -34,6 +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)
- A JobsReborn-compatible economy if monetary payouts are desired - A JobsReborn-compatible economy if monetary payouts are desired
## Build ## Build
@@ -110,7 +111,7 @@ Other management commands:
Automatic emergencies default to a random 30-60 minute interval and require at least one online Firefighter. Manual starts use the same online-Firefighter requirement unless `emergencies.require-firefighters-online` is disabled. Automatic emergencies default to a random 30-60 minute interval and require at least one online Firefighter. Manual starts use the same online-Firefighter requirement unless `emergencies.require-firefighters-online` is disabled.
Emergency fire generation: Emergency fire generation and response behavior:
- Uses admin-defined named sites and radii. - Uses admin-defined named sites and radii.
- Can be limited to configured world names with `emergencies.allowed-worlds`. - Can be limited to configured world names with `emergencies.allowed-worlds`.
@@ -119,7 +120,13 @@ Emergency fire generation:
- Cancels emergency-fire spread and spread ignition. - Cancels emergency-fire spread and spread ignition.
- 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.
- Periodically removes stale tracked entries if an emergency fire disappears through another legitimate game event. - 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.
- 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.
- Saves returns for offline Firefighters in `pending-returns.yml` and delivers them on their next join.
- Periodically removes stale tracked fire entries if an emergency fire disappears through another legitimate game event.
- Broadcasts configurable alerts, by default to all online players. - Broadcasts configurable alerts, by default to all online players.
Example alert: Example alert:
@@ -128,6 +135,22 @@ Example alert:
🔥 FIRE ALERT! A fire has been reported at RoseGarden near X:123 Y:64 Z:-245. Firefighters are needed immediately! 🔥 FIRE ALERT! A fire has been reported at RoseGarden near X:123 Y:64 Z:-245. Firefighters are needed immediately!
``` ```
## Protected-site integration
`emergencies.protected-site-building` controls temporary emergency build access. The permission is granted with Bukkit's runtime `PermissionAttachment`, so it is temporary and does not create permanent LuckPerms/permissions-plugin data.
With GriefPrevention installed, RoseFirefighter listens to `ClaimPermissionCheckEvent` and clears a denied `Build` check only when all of the following are true:
- An emergency is active.
- 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.
Normal protected structures cannot be broken merely because an emergency is active.
`emergencies.cleanup-firefighter-blocks` controls whether placements made by Firefighters inside the active site are added to the temporary response-construction ledger.
## Configuration ## Configuration
`src/main/resources/config.yml` includes controls for: `src/main/resources/config.yml` includes controls for:
@@ -144,6 +167,7 @@ Example alert:
- 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.
- Saved emergency sites. - Saved emergency sites.
## Development notes ## Development notes
@@ -160,4 +184,10 @@ JobsReborn is referenced through its documented JitPack coordinate:
compileOnly("com.github.Zrips:Jobs:v5.2.6.2") compileOnly("com.github.Zrips:Jobs:v5.2.6.2")
``` ```
The JobsReborn dependency is non-transitive because RoseFirefighter only needs the JobsReborn API classes and should not pull JobsReborn's optional integration dependencies into its compile classpath. GriefPrevention is an optional compile-time API dependency and a runtime soft dependency:
```kotlin
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.6")
```
The JobsReborn and GriefPrevention dependencies are non-transitive because RoseFirefighter only needs their public API classes and should not pull their optional integrations into its compile classpath.
+3
View File
@@ -18,6 +18,9 @@ dependencies {
compileOnly("com.github.Zrips:Jobs:v5.2.6.2") { compileOnly("com.github.Zrips:Jobs:v5.2.6.2") {
isTransitive = false isTransitive = false
} }
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.6") {
isTransitive = false
}
} }
java { java {
@@ -0,0 +1,219 @@
package net.therosegarden.firefighter;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockMultiPlaceEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.inventory.ItemStack;
final class EmergencyConstructionManager implements Listener {
private final RoseFirefighterPlugin plugin;
private final JobsFacade jobs;
private final EmergencyPermissionManager permissions;
private final Map<UUID, Placement> placements = new HashMap<>();
private final Map<BlockKey, UUID> byBlock = new HashMap<>();
private final Map<UUID, List<ItemStack>> pendingReturns = new HashMap<>();
private final File pendingFile;
private EmergencySite activeSite;
EmergencyConstructionManager(RoseFirefighterPlugin plugin, JobsFacade jobs, EmergencyPermissionManager permissions) {
this.plugin = plugin;
this.jobs = jobs;
this.permissions = permissions;
this.pendingFile = new File(plugin.getDataFolder(), "pending-returns.yml");
loadPendingReturns();
}
void begin(EmergencySite site) {
activeSite = site;
placements.clear();
byBlock.clear();
}
boolean isTemporaryBlock(Block block) {
return block != null && byBlock.containsKey(BlockKey.of(block));
}
void cleanupAndReturn() {
if (placements.isEmpty()) {
activeSite = null;
return;
}
Map<UUID, List<ItemStack>> returns = new HashMap<>();
for (Placement placement : new ArrayList<>(placements.values())) {
boolean restoredAny = false;
for (PlacedBlock placed : placement.blocks()) {
Block current = placed.key().block();
if (current != null && current.getType() == placed.placedType()) {
placed.replacedState().update(true, false);
restoredAny = true;
}
}
if (restoredAny) {
returns.computeIfAbsent(placement.owner(), ignored -> new ArrayList<>()).add(placement.returnItem().clone());
}
}
placements.clear();
byBlock.clear();
activeSite = null;
for (Map.Entry<UUID, List<ItemStack>> entry : returns.entrySet()) {
returnItems(entry.getKey(), entry.getValue());
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlace(BlockPlaceEvent event) {
if (activeSite == null || !plugin.getConfig().getBoolean("emergencies.cleanup-firefighter-blocks", true)) {
return;
}
Player player = event.getPlayer();
if (!jobs.isFirefighter(player) || !permissions.hasBuildAccess(player) || !activeSite.contains(event.getBlock().getLocation())) {
return;
}
List<BlockState> replacedStates = new ArrayList<>();
if (event instanceof BlockMultiPlaceEvent multi) {
replacedStates.addAll(multi.getReplacedBlockStates());
} else {
replacedStates.add(event.getBlockReplacedState());
}
List<PlacedBlock> blocks = new ArrayList<>();
for (BlockState replaced : replacedStates) {
Block current = replaced.getBlock();
if (!activeSite.contains(current.getLocation())) {
continue;
}
blocks.add(new PlacedBlock(BlockKey.of(current), current.getType(), replaced));
}
if (blocks.isEmpty()) {
return;
}
ItemStack returned = event.getItemInHand().clone();
returned.setAmount(1);
UUID placementId = UUID.randomUUID();
Placement placement = new Placement(placementId, player.getUniqueId(), returned, List.copyOf(blocks));
placements.put(placementId, placement);
for (PlacedBlock block : blocks) {
byBlock.put(block.key(), placementId);
}
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onBreak(BlockBreakEvent event) {
UUID placementId = byBlock.get(BlockKey.of(event.getBlock()));
if (placementId == null) {
return;
}
Placement placement = placements.remove(placementId);
if (placement == null) {
return;
}
for (PlacedBlock block : placement.blocks()) {
byBlock.remove(block.key());
}
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
List<ItemStack> items = pendingReturns.remove(event.getPlayer().getUniqueId());
if (items == null || items.isEmpty()) {
return;
}
giveItems(event.getPlayer(), items);
savePendingReturns();
}
private void returnItems(UUID owner, List<ItemStack> items) {
Player player = Bukkit.getPlayer(owner);
if (player != null && player.isOnline()) {
giveItems(player, items);
return;
}
pendingReturns.computeIfAbsent(owner, ignored -> new ArrayList<>()).addAll(items);
savePendingReturns();
}
private void giveItems(Player player, List<ItemStack> items) {
int count = 0;
for (ItemStack item : items) {
if (item == null || item.getType() == Material.AIR || item.getAmount() <= 0) {
continue;
}
count += item.getAmount();
Map<Integer, ItemStack> overflow = player.getInventory().addItem(item.clone());
for (ItemStack extra : overflow.values()) {
player.getWorld().dropItemNaturally(player.getLocation(), extra);
}
}
if (count > 0) {
String message = plugin.message("messages.returned-blocks").replace("%count%", Integer.toString(count));
if (!message.isEmpty()) {
player.sendMessage(plugin.prefix() + message);
}
}
}
private void loadPendingReturns() {
if (!pendingFile.isFile()) {
return;
}
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(pendingFile);
for (String key : yaml.getKeys(false)) {
try {
UUID uuid = UUID.fromString(key);
List<ItemStack> items = new ArrayList<>();
for (Object value : yaml.getList(key, List.of())) {
if (value instanceof ItemStack item) {
items.add(item);
}
}
if (!items.isEmpty()) {
pendingReturns.put(uuid, items);
}
} catch (IllegalArgumentException ignored) {
plugin.getLogger().warning("Ignoring invalid pending-return UUID: " + key);
}
}
}
private void savePendingReturns() {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, List<ItemStack>> entry : pendingReturns.entrySet()) {
yaml.set(entry.getKey().toString(), entry.getValue());
}
try {
if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) {
plugin.getLogger().warning("Could not create plugin data folder for pending returns.");
return;
}
yaml.save(pendingFile);
} catch (IOException ex) {
plugin.getLogger().severe("Could not save pending firefighter block returns: " + ex.getMessage());
}
}
private record Placement(UUID id, UUID owner, ItemStack returnItem, List<PlacedBlock> blocks) {}
private record PlacedBlock(BlockKey key, Material placedType, BlockState replacedState) {}
}
@@ -27,6 +27,8 @@ final class EmergencyManager {
private final Random random = new Random(); private final Random random = new Random();
private final Map<String, EmergencySite> sites = new LinkedHashMap<>(); private final Map<String, EmergencySite> sites = new LinkedHashMap<>();
private final Set<BlockKey> activeFires = new HashSet<>(); private final Set<BlockKey> activeFires = new HashSet<>();
private EmergencyPermissionManager permissions;
private EmergencyConstructionManager construction;
private EmergencySite activeSite; private EmergencySite activeSite;
private BukkitTask expiryTask; private BukkitTask expiryTask;
@@ -38,6 +40,11 @@ final class EmergencyManager {
this.jobs = jobs; this.jobs = jobs;
} }
void setEmergencyServices(EmergencyPermissionManager permissions, EmergencyConstructionManager construction) {
this.permissions = permissions;
this.construction = construction;
}
void reload() { void reload() {
loadSites(); loadSites();
scheduleNextAutomaticEmergency(); scheduleNextAutomaticEmergency();
@@ -48,6 +55,12 @@ final class EmergencyManager {
cancelExpiryTask(); cancelExpiryTask();
cancelIntegrityTask(); cancelIntegrityTask();
clearEmergencyFireBlocks(); clearEmergencyFireBlocks();
if (construction != null) {
construction.cleanupAndReturn();
}
if (permissions != null) {
permissions.deactivate();
}
activeFires.clear(); activeFires.clear();
activeSite = null; activeSite = null;
} }
@@ -216,6 +229,12 @@ final class EmergencyManager {
activeSite = site; activeSite = site;
activeFires.addAll(spawned); activeFires.addAll(spawned);
if (construction != null) {
construction.begin(site);
}
if (permissions != null) {
permissions.activate(site);
}
int durationMinutes = Math.max(1, plugin.getConfig().getInt("emergencies.duration-minutes", 10)); int durationMinutes = Math.max(1, plugin.getConfig().getInt("emergencies.duration-minutes", 10));
expiryTask = Bukkit.getScheduler().runTaskLater(plugin, () -> endEmergency(false), durationMinutes * 60L * 20L); expiryTask = Bukkit.getScheduler().runTaskLater(plugin, () -> endEmergency(false), durationMinutes * 60L * 20L);
@@ -317,6 +336,10 @@ final class EmergencyManager {
return; return;
} }
if (permissions != null) {
permissions.syncResponders();
}
activeFires.removeIf(key -> { activeFires.removeIf(key -> {
Block block = key.block(); Block block = key.block();
return block == null || (block.getType() != Material.FIRE && block.getType() != Material.SOUL_FIRE); return block == null || (block.getType() != Material.FIRE && block.getType() != Material.SOUL_FIRE);
@@ -338,6 +361,12 @@ final class EmergencyManager {
cancelIntegrityTask(); cancelIntegrityTask();
clearEmergencyFireBlocks(); clearEmergencyFireBlocks();
if (construction != null) {
construction.cleanupAndReturn();
}
if (permissions != null) {
permissions.deactivate();
}
activeFires.clear(); activeFires.clear();
activeSite = null; activeSite = null;
@@ -0,0 +1,104 @@
package net.therosegarden.firefighter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.permissions.PermissionAttachment;
final class EmergencyPermissionManager implements Listener {
static final String BUILD_PERMISSION = "rosefirefighter.emergency.build";
private final RoseFirefighterPlugin plugin;
private final JobsFacade jobs;
private final Map<UUID, PermissionAttachment> attachments = new HashMap<>();
private EmergencySite activeSite;
EmergencyPermissionManager(RoseFirefighterPlugin plugin, JobsFacade jobs) {
this.plugin = plugin;
this.jobs = jobs;
}
void activate(EmergencySite site) {
activeSite = site;
syncResponders();
}
void syncResponders() {
if (activeSite == null || !plugin.getConfig().getBoolean("emergencies.protected-site-building", true)) {
revokeAll(true);
return;
}
for (Player player : Bukkit.getOnlinePlayers()) {
if (jobs.isFirefighter(player)) {
grant(player);
} else {
revoke(player, true);
}
}
}
void deactivate() {
activeSite = null;
revokeAll(true);
}
boolean hasBuildAccess(Player player) {
return activeSite != null && player != null && player.hasPermission(BUILD_PERMISSION) && jobs.isFirefighter(player);
}
boolean isInsideActiveSite(org.bukkit.Location location) {
return activeSite != null && activeSite.contains(location);
}
private void grant(Player player) {
if (attachments.containsKey(player.getUniqueId())) {
return;
}
PermissionAttachment attachment = player.addAttachment(plugin);
attachment.setPermission(BUILD_PERMISSION, true);
attachments.put(player.getUniqueId(), attachment);
String message = plugin.message("messages.temporary-build-enabled");
if (!message.isEmpty()) {
player.sendMessage(plugin.prefix() + message);
}
}
private void revoke(Player player, boolean notify) {
PermissionAttachment attachment = attachments.remove(player.getUniqueId());
if (attachment == null) {
return;
}
player.removeAttachment(attachment);
if (notify) {
String message = plugin.message("messages.temporary-build-disabled");
if (!message.isEmpty()) {
player.sendMessage(plugin.prefix() + message);
}
}
}
private void revokeAll(boolean notify) {
for (Player player : Bukkit.getOnlinePlayers()) {
revoke(player, notify);
}
attachments.clear();
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
if (activeSite != null) {
Bukkit.getScheduler().runTask(plugin, this::syncResponders);
}
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
revoke(event.getPlayer(), false);
}
}
@@ -9,4 +9,13 @@ record EmergencySite(String name, String worldName, int x, int y, int z, int rad
World world = Bukkit.getWorld(worldName); World world = Bukkit.getWorld(worldName);
return world == null ? null : new Location(world, x + 0.5, y, z + 0.5); return world == null ? null : new Location(world, x + 0.5, y, z + 0.5);
} }
boolean contains(Location location) {
if (location == null || location.getWorld() == null || !location.getWorld().getName().equals(worldName)) {
return false;
}
long dx = (long) location.getBlockX() - x;
long dz = (long) location.getBlockZ() - z;
return (dx * dx) + (dz * dz) <= (long) radius * radius;
}
} }
@@ -0,0 +1,64 @@
package net.therosegarden.firefighter;
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.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerBucketEmptyEvent;
final class GriefPreventionBridge implements Listener {
private final EmergencyManager emergencies;
private final EmergencyPermissionManager permissions;
private final EmergencyConstructionManager construction;
GriefPreventionBridge(EmergencyManager emergencies, EmergencyPermissionManager permissions,
EmergencyConstructionManager construction) {
this.emergencies = emergencies;
this.permissions = permissions;
this.construction = construction;
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onClaimPermissionCheck(ClaimPermissionCheckEvent event) {
if (event.getRequiredPermission() != ClaimPermission.Build || event.getDenialReason() == null) {
return;
}
Player player = event.getCheckedPlayer();
if (player == null || !permissions.hasBuildAccess(player)) {
return;
}
Event trigger = event.getTriggeringEvent();
if (trigger instanceof BlockPlaceEvent placeEvent) {
allowIfInside(event, placeEvent.getBlock().getLocation());
return;
}
if (trigger instanceof PlayerBucketEmptyEvent bucketEvent) {
allowIfInside(event, bucketEvent.getBlock().getLocation());
return;
}
if (trigger instanceof BlockBreakEvent breakEvent) {
Block block = breakEvent.getBlock();
if ((emergencies.isEmergencyFire(block) || construction.isTemporaryBlock(block))
&& permissions.isInsideActiveSite(block.getLocation())) {
event.setDenialReason(null);
}
}
}
private void allowIfInside(ClaimPermissionCheckEvent event, Location location) {
if (permissions.isInsideActiveSite(location)) {
event.setDenialReason(null);
}
}
}
@@ -6,6 +6,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
public final class RoseFirefighterPlugin extends JavaPlugin { public final class RoseFirefighterPlugin extends JavaPlugin {
@@ -16,6 +17,8 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
private FirefighterRewardService rewards; private FirefighterRewardService rewards;
private EmergencyManager emergencies; private EmergencyManager emergencies;
private FireSafetyAndOriginListener safetyListener; private FireSafetyAndOriginListener safetyListener;
private EmergencyPermissionManager emergencyPermissions;
private EmergencyConstructionManager emergencyConstruction;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -26,9 +29,23 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
emergencies = new EmergencyManager(this, jobs); emergencies = new EmergencyManager(this, jobs);
rewards.setEmergencyManager(emergencies); rewards.setEmergencyManager(emergencies);
safetyListener = new FireSafetyAndOriginListener(this, rewards, emergencies); safetyListener = new FireSafetyAndOriginListener(this, rewards, emergencies);
emergencyPermissions = new EmergencyPermissionManager(this, jobs);
emergencyConstruction = new EmergencyConstructionManager(this, jobs, emergencyPermissions);
emergencies.setEmergencyServices(emergencyPermissions, emergencyConstruction);
Bukkit.getPluginManager().registerEvents(new FireInteractionListener(this, rewards, emergencies), this); Bukkit.getPluginManager().registerEvents(new FireInteractionListener(this, rewards, emergencies), this);
Bukkit.getPluginManager().registerEvents(safetyListener, this); Bukkit.getPluginManager().registerEvents(safetyListener, this);
Bukkit.getPluginManager().registerEvents(emergencyPermissions, this);
Bukkit.getPluginManager().registerEvents(emergencyConstruction, this);
Plugin griefPrevention = Bukkit.getPluginManager().getPlugin("GriefPrevention");
if (griefPrevention != null && griefPrevention.isEnabled()) {
Bukkit.getPluginManager().registerEvents(
new GriefPreventionBridge(emergencies, emergencyPermissions, emergencyConstruction), this);
getLogger().info("GriefPrevention detected. Emergency-site build overrides are enabled.");
} else {
getLogger().info("GriefPrevention not detected. Emergency build permission will remain internal to RoseFirefighter.");
}
FirefighterCommand commandHandler = new FirefighterCommand(this, jobs, emergencies); FirefighterCommand commandHandler = new FirefighterCommand(this, jobs, emergencies);
PluginCommand command = Objects.requireNonNull(getCommand("firefighter"), "firefighter command missing from plugin.yml"); PluginCommand command = Objects.requireNonNull(getCommand("firefighter"), "firefighter command missing from plugin.yml");
+6
View File
@@ -48,6 +48,9 @@ emergencies:
enabled: true enabled: true
automatic: true automatic: true
protected-site-building: true
cleanup-firefighter-blocks: 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
@@ -80,6 +83,9 @@ messages:
cleared: '&aFire at &f%site% &ahas been brought under control.' cleared: '&aFire at &f%site% &ahas been brought under control.'
expired: '&eThe fire response at &f%site% &ehas ended.' expired: '&eThe fire response at &f%site% &ehas ended.'
joined-needed: '&7You must be working as a &cFirefighter &7to receive firefighting pay.' joined-needed: '&7You must be working as a &cFirefighter &7to receive firefighting pay.'
temporary-build-enabled: '&eEmergency build access is active inside the response site.'
temporary-build-disabled: '&7Emergency build access has ended.'
returned-blocks: '&aReturned &f%count% &atemporary response block item(s) from the completed incident.'
# Managed with /firefighter site add <name> [radius] # Managed with /firefighter site add <name> [radius]
sites: {} sites: {}
+4
View File
@@ -3,6 +3,7 @@ version: '${version}'
main: net.therosegarden.firefighter.RoseFirefighterPlugin main: net.therosegarden.firefighter.RoseFirefighterPlugin
api-version: '26.2' api-version: '26.2'
depend: [Jobs] depend: [Jobs]
softdepend: [GriefPrevention]
description: Firefighter profession companion and controlled fire emergencies for JobsReborn. description: Firefighter profession companion and controlled fire emergencies for JobsReborn.
commands: commands:
firefighter: firefighter:
@@ -13,3 +14,6 @@ permissions:
rosefirefighter.admin: rosefirefighter.admin:
description: Manage Firefighter sites and emergencies. description: Manage Firefighter sites and emergencies.
default: op default: op
rosefirefighter.emergency.build:
description: Temporary runtime permission granted to on-duty Firefighters at an active emergency site.
default: false