This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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: {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user