This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# RoseFirefighter
|
||||
|
||||
RoseFirefighter is a Paper 26.2 companion plugin for **JobsReborn**. It does not implement its own economy or leveling system. Instead, it detects legitimate firefighting actions and submits matching JobsReborn actions so JobsReborn continues to handle job membership, levels, job XP, income progression, bonuses, taxes, payment limits, payment visualization, and level-ups.
|
||||
RoseFirefighter is a Paper 26.2 companion plugin for **JobsReborn**. It detects legitimate firefighting, runs temporary fire emergencies, integrates with GriefPrevention, and submits JobsReborn actions so JobsReborn remains responsible for job membership, levels, XP, income, bonuses, taxes, payment limits, and progression.
|
||||
|
||||
## Supported firefighting
|
||||
|
||||
@@ -9,9 +9,9 @@ RoseFirefighter is a Paper 26.2 companion plugin for **JobsReborn**. It does not
|
||||
- Extinguishing lit `CAMPFIRE` blocks.
|
||||
- Extinguishing lit `SOUL_CAMPFIRE` blocks.
|
||||
- Water-bucket extinguishing when the affected fire can be attributed to the player who emptied the bucket.
|
||||
- Controlled RoseFirefighter emergency fires.
|
||||
- Controlled RoseFirefighter emergency fires, including fire that naturally spreads during the incident.
|
||||
|
||||
Only players currently working the configured JobsReborn `Firefighter` job can receive the submitted JobsReborn rewards.
|
||||
Only players currently working the configured JobsReborn `Firefighter` job can receive submitted JobsReborn rewards.
|
||||
|
||||
## Anti-farming behavior
|
||||
|
||||
@@ -24,22 +24,20 @@ With `rewards.anti-farm: true`, RoseFirefighter rejects payment for player-creat
|
||||
- Fire caused by player-placed lava or tracked lava flow.
|
||||
- Campfires a player relights and then extinguishes.
|
||||
|
||||
Active player-created fire and active player-placed lava remain tainted for as long as those sources still exist; they do not become payable merely because a timer expires. Configurable retention timers remain after the active source disappears. A separate location cooldown prevents duplicate/delayed-event payouts and repeated same-location farming.
|
||||
|
||||
Creative and spectator players do not receive rewards by default. Creative earning can be enabled explicitly in `config.yml`.
|
||||
Active player-created fire and player-placed lava stay tainted while their sources remain active. Configurable retention timers remain afterward, and a separate location cooldown prevents duplicate payouts.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Paper 26.2
|
||||
- Java 25 for compilation/server runtime
|
||||
- Java 25
|
||||
- JobsReborn 5.2.6.x
|
||||
- CMILib, as required by JobsReborn
|
||||
- GriefPrevention 16.18.6+ (optional; enables protected-claim emergency building and forced entry)
|
||||
- GriefPrevention 16.18.6+ (optional, but recommended for claim-aware dispatch and protected-property response)
|
||||
- A JobsReborn-compatible economy if monetary payouts are desired
|
||||
|
||||
## Build
|
||||
|
||||
The repository includes the Gradle 9.6.1 Wrapper. A global Gradle installation is **not** required.
|
||||
The repository includes the Gradle 9.6.1 Wrapper. A global Gradle installation is not required.
|
||||
|
||||
Windows:
|
||||
|
||||
@@ -53,23 +51,27 @@ Linux/macOS:
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
The resulting plugin JAR is written to `build/libs/`.
|
||||
The resulting JAR is written to `build/libs/`.
|
||||
|
||||
## Install
|
||||
|
||||
1. Copy `jobs/Firefighter.yml` to:
|
||||
|
||||
`plugins/Jobs/jobs/Firefighter.yml`
|
||||
|
||||
1. Copy `jobs/Firefighter.yml` to `plugins/Jobs/jobs/Firefighter.yml`.
|
||||
2. Reload JobsReborn or restart the server.
|
||||
|
||||
3. Copy the RoseFirefighter JAR from `build/libs/` into the server's `plugins/` directory.
|
||||
|
||||
3. Copy the RoseFirefighter JAR into the server's `plugins/` directory.
|
||||
4. Restart Paper.
|
||||
5. Players can join with the normal JobsReborn command, for example `/jobs join Firefighter`.
|
||||
|
||||
5. Players can join the profession with the normal JobsReborn command, for example:
|
||||
## Duty controls
|
||||
|
||||
`/jobs join Firefighter`
|
||||
Firefighters are **on duty by default**. Duty state is persisted by UUID in `duty-status.yml`.
|
||||
|
||||
```text
|
||||
/firefighter onduty
|
||||
/firefighter offduty
|
||||
/firefighter status
|
||||
```
|
||||
|
||||
`/firefighter offduty` silences RoseFirefighter dispatch/lifecycle notifications for that Firefighter without forcing them to leave the JobsReborn job. Off-duty players retain emergency capability if they choose to respond manually, but they are not counted toward the minimum on-duty responders required to start an incident.
|
||||
|
||||
## JobsReborn payouts
|
||||
|
||||
@@ -82,113 +84,131 @@ The resulting plugin JAR is written to `build/libs/`.
|
||||
| `CAMPFIRE` | `COLLECT/CAMPFIRE` | 0.50 | 0.50 |
|
||||
| `SOUL_CAMPFIRE` | `COLLECT/SOUL_CAMPFIRE` | 0.75 | 0.75 |
|
||||
|
||||
Campfires intentionally use a synthetic JobsReborn `COLLECT` action. Putting campfires under the JobsReborn `Break` section would also reward players for simply mining the campfire block.
|
||||
Emergency fires additionally apply `emergencies.reward-multiplier`, which defaults to `5`.
|
||||
|
||||
The per-type `action-count` options in RoseFirefighter's `config.yml` control how many JobsReborn actions are submitted for a verified extinguish. Keep them at `1` for the normal base payout. Emergency fires additionally apply `emergencies.reward-multiplier`, which defaults to `5`.
|
||||
## Random incident dispatch
|
||||
|
||||
## Emergency sites
|
||||
Fixed sites are optional. Automatic incidents and `/firefighter emergency start random` can select random locations without any configured sites.
|
||||
|
||||
Stand at the center of an area where controlled emergencies may appear:
|
||||
When GriefPrevention is installed and `emergencies.random-locations.prefer-griefprevention-claims` is enabled, RoseFirefighter prefers a random top-level player-owned claim. The incident is labeled using the current/known claim owner, for example:
|
||||
|
||||
```text
|
||||
🔥 FIRE ALERT! Fire reported at The_Rose94's home in world near X:123 Y:68 Z:-245.
|
||||
```
|
||||
|
||||
On-duty Firefighters also receive a dispatch-detail line containing:
|
||||
|
||||
- Exact world and coordinates.
|
||||
- Nearby GriefPrevention claim owners.
|
||||
- Approximate distance to nearby claims, including `inside` when the incident is within the claim.
|
||||
|
||||
If no suitable player claim is available, RoseFirefighter can fall back to a random currently loaded world/chunk location and then to a configured fixed site. All of these behaviors are configurable.
|
||||
|
||||
## Fixed emergency sites
|
||||
|
||||
Fixed sites remain available for special locations or manual testing:
|
||||
|
||||
```text
|
||||
/firefighter site add RoseGarden 18
|
||||
```
|
||||
|
||||
Other management commands:
|
||||
|
||||
```text
|
||||
/firefighter status
|
||||
/firefighter site remove <name>
|
||||
/firefighter site list
|
||||
/firefighter emergency start <site>
|
||||
/firefighter emergency start random
|
||||
/firefighter emergency stop
|
||||
/firefighter reload
|
||||
```
|
||||
|
||||
`rosefirefighter.admin` is required for site, emergency, and reload management and defaults to server operators.
|
||||
|
||||
## Emergency behavior
|
||||
## Natural fire spread and instant rollback
|
||||
|
||||
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 is allowed to behave naturally **inside the active response radius** when `emergencies.fire-spread` and `emergencies.fire-damage` are enabled.
|
||||
|
||||
Emergency fire generation and response behavior:
|
||||
RoseFirefighter treats the incident as a temporary world transaction:
|
||||
|
||||
- Uses admin-defined named sites and radii.
|
||||
- Can be limited to configured world names with `emergencies.allowed-worlds`.
|
||||
- Places only temporary fire blocks on safe solid support blocks.
|
||||
- Does not replace the supporting block.
|
||||
- Cancels emergency-fire spread and spread ignition.
|
||||
- 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 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.
|
||||
- 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.
|
||||
- Initial emergency fires are tracked.
|
||||
- Every naturally spread fire becomes another tracked emergency fire.
|
||||
- Fire spread outside the incident boundary is cancelled.
|
||||
- Before emergency fire changes or burns a block, RoseFirefighter captures the first pre-incident `BlockState` for that location.
|
||||
- The first snapshot wins, so later changes cannot overwrite the original property state.
|
||||
- Tile/block-entity state is retained.
|
||||
- Container inventories are copied and protected from becoming duplicate fire loot before rollback.
|
||||
- Fire-triggered TNT is blocked by default to prevent explosion chains from escaping the controlled incident boundary.
|
||||
- Secondary physics damage is suppressed by default so falling blocks and support-dependent decorations do not create untracked damage or duplicate blocks.
|
||||
|
||||
Example alert:
|
||||
When the incident is contained, manually stopped, expires, the plugin disables, or the server performs a normal shutdown, RoseFirefighter immediately:
|
||||
|
||||
```text
|
||||
🔥 FIRE ALERT! A fire has been reported at RoseGarden near X:123 Y:64 Z:-245. Firefighters are needed immediately!
|
||||
```
|
||||
1. Removes all remaining tracked emergency fire, including spread fire.
|
||||
2. Removes/restores temporary Firefighter construction.
|
||||
3. Restores forced-entry damage.
|
||||
4. Restores all captured fire-damaged blocks to their first pre-incident state.
|
||||
5. Restores preserved container contents.
|
||||
6. Returns eligible temporary response-block items to the Firefighters who placed them.
|
||||
|
||||
## Protected-site integration
|
||||
Offline Firefighter item returns are saved in `pending-returns.yml` and delivered on the player's next join.
|
||||
|
||||
`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.
|
||||
## GriefPrevention integration and forced entry
|
||||
|
||||
With GriefPrevention installed, RoseFirefighter listens to `ClaimPermissionCheckEvent` and clears a denied `Build` check only when all of the following are true:
|
||||
RoseFirefighter never enables `/IgnoreClaims` and never gives Firefighters a global `griefprevention.adminclaims` bypass.
|
||||
|
||||
- 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 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.
|
||||
Instead, during an active incident it uses GriefPrevention's claim-permission API to allow the temporary response permission only when:
|
||||
|
||||
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.
|
||||
- The player is working the JobsReborn Firefighter job.
|
||||
- The emergency is active.
|
||||
- The affected block is inside the active incident radius.
|
||||
|
||||
`emergencies.cleanup-firefighter-blocks` controls whether placements made by Firefighters inside the active site are added to the temporary response-construction ledger.
|
||||
Inside that radius Firefighters can place response blocks, empty water buckets, extinguish fire, and—when `emergencies.forced-entry: true`—make temporary forced entry through protected doors, windows, walls, and other property.
|
||||
|
||||
## Configuration
|
||||
Forced-entry blocks:
|
||||
|
||||
- Produce no item drops.
|
||||
- Produce no XP.
|
||||
- Are captured in the same first-state rollback ledger.
|
||||
- Are automatically restored when the incident ends.
|
||||
|
||||
Temporary build access uses Bukkit runtime `PermissionAttachment` with `rosefirefighter.emergency.build`; it does not create permanent LuckPerms data.
|
||||
|
||||
## Emergency configuration highlights
|
||||
|
||||
`src/main/resources/config.yml` includes controls for:
|
||||
|
||||
- JobsReborn job name.
|
||||
- Normal fire, soul fire, campfire, and soul-campfire action submission.
|
||||
- Duplicate reward cooldown.
|
||||
- Player-created-fire and player-lava tracking retention.
|
||||
- Creative-mode earning.
|
||||
- Water attribution scan radius.
|
||||
- Emergency enable/disable and automatic scheduling.
|
||||
- Minimum online Firefighters.
|
||||
- Emergency duration and fire counts.
|
||||
- Emergency enable/disable and scheduling.
|
||||
- Minimum **on-duty** Firefighters.
|
||||
- Random claim/location dispatch.
|
||||
- Fixed-site fallback.
|
||||
- Nearby-claim reporting radius/count.
|
||||
- Fire spread and fire damage.
|
||||
- Instant fire-damage restoration.
|
||||
- Fire-triggered TNT prevention.
|
||||
- Secondary physics protection.
|
||||
- Protected-site building.
|
||||
- Forced entry.
|
||||
- Temporary response-block cleanup and item return.
|
||||
- Emergency duration and initial fire count.
|
||||
- Emergency reward multiplier.
|
||||
- Permitted worlds.
|
||||
- Announcement behavior.
|
||||
- Permitted emergency worlds.
|
||||
- Protected-site emergency building, temporary forced entry, and temporary Firefighter block cleanup.
|
||||
- Saved emergency sites.
|
||||
|
||||
## Development notes
|
||||
## Development dependencies
|
||||
|
||||
Paper 26.2 uses the current Paper dependency format and Java 25 toolchain:
|
||||
Paper 26.2:
|
||||
|
||||
```kotlin
|
||||
compileOnly("io.papermc.paper:paper-api:26.2.build.+")
|
||||
```
|
||||
|
||||
JobsReborn is referenced through its documented JitPack coordinate:
|
||||
JobsReborn:
|
||||
|
||||
```kotlin
|
||||
compileOnly("com.github.Zrips:Jobs:v5.2.6.2")
|
||||
compileOnly("com.github.Zrips:Jobs:v5.2.6.2") {
|
||||
isTransitive = false
|
||||
}
|
||||
```
|
||||
|
||||
GriefPrevention is an optional compile-time API dependency and a runtime soft dependency:
|
||||
GriefPrevention:
|
||||
|
||||
```kotlin
|
||||
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.6")
|
||||
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.6") {
|
||||
isTransitive = false
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.therosegarden.firefighter;
|
||||
|
||||
import io.papermc.paper.block.TileStateInventoryHolder;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,7 +31,7 @@ final class EmergencyConstructionManager implements Listener {
|
||||
private final EmergencyPermissionManager permissions;
|
||||
private final Map<UUID, Placement> placements = new HashMap<>();
|
||||
private final Map<BlockKey, UUID> byBlock = new HashMap<>();
|
||||
private final Map<BlockKey, ForcedEntry> forcedEntries = new HashMap<>();
|
||||
private final Map<BlockKey, IncidentSnapshot> incidentDamage = new HashMap<>();
|
||||
private final Map<UUID, List<ItemStack>> pendingReturns = new HashMap<>();
|
||||
private final File pendingFile;
|
||||
private EmergencySite activeSite;
|
||||
@@ -47,7 +48,7 @@ final class EmergencyConstructionManager implements Listener {
|
||||
activeSite = site;
|
||||
placements.clear();
|
||||
byBlock.clear();
|
||||
forcedEntries.clear();
|
||||
incidentDamage.clear();
|
||||
}
|
||||
|
||||
boolean isTemporaryBlock(Block block) {
|
||||
@@ -58,8 +59,65 @@ final class EmergencyConstructionManager implements Listener {
|
||||
return plugin.getConfig().getBoolean("emergencies.forced-entry", true);
|
||||
}
|
||||
|
||||
boolean isFireDamageRestoreEnabled() {
|
||||
return plugin.getConfig().getBoolean("emergencies.restore-fire-damage", true);
|
||||
}
|
||||
|
||||
void captureIncidentDamage(Block block) {
|
||||
captureDamage(block);
|
||||
}
|
||||
|
||||
void captureFireDamage(Block block) {
|
||||
captureDamage(block);
|
||||
}
|
||||
|
||||
private void captureDamage(Block block) {
|
||||
if (activeSite == null || block == null || !activeSite.contains(block.getLocation()) || isTemporaryBlock(block)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockKey key = BlockKey.of(block);
|
||||
if (incidentDamage.containsKey(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockState state = block.getState();
|
||||
ItemStack[] inventoryContents = null;
|
||||
if (state instanceof TileStateInventoryHolder holder) {
|
||||
inventoryContents = cloneContents(holder.getSnapshotInventory().getContents());
|
||||
}
|
||||
incidentDamage.put(key, new IncidentSnapshot(state, inventoryContents));
|
||||
}
|
||||
|
||||
void prepareFireBurn(Block block) {
|
||||
if (block == null || !incidentDamage.containsKey(BlockKey.of(block))) {
|
||||
return;
|
||||
}
|
||||
BlockState state = block.getState();
|
||||
if (state instanceof TileStateInventoryHolder holder) {
|
||||
holder.getInventory().clear();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isIncidentDamaged(Block block) {
|
||||
return block != null && incidentDamage.containsKey(BlockKey.of(block));
|
||||
}
|
||||
|
||||
boolean isNearIncidentDamage(Block block, int radius) {
|
||||
if (block == null) {
|
||||
return false;
|
||||
}
|
||||
BlockKey target = BlockKey.of(block);
|
||||
for (BlockKey damaged : incidentDamage.keySet()) {
|
||||
if (damaged.isWithin(target, radius)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void cleanupAndReturn() {
|
||||
if (placements.isEmpty() && forcedEntries.isEmpty()) {
|
||||
if (placements.isEmpty() && incidentDamage.isEmpty()) {
|
||||
activeSite = null;
|
||||
return;
|
||||
}
|
||||
@@ -69,7 +127,7 @@ final class EmergencyConstructionManager implements Listener {
|
||||
boolean restoredAny = false;
|
||||
for (PlacedBlock placed : placement.blocks()) {
|
||||
Block current = placed.key().block();
|
||||
if (current != null && current.getType() == placed.placedType()) {
|
||||
if (current != null) {
|
||||
placed.replacedState().update(true, false);
|
||||
restoredAny = true;
|
||||
}
|
||||
@@ -82,14 +140,18 @@ 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);
|
||||
// The first pre-incident snapshot wins. Restoring these after temporary
|
||||
// response construction ensures the original property state wins.
|
||||
for (IncidentSnapshot snapshot : new ArrayList<>(incidentDamage.values())) {
|
||||
snapshot.state().update(true, false);
|
||||
if (snapshot.inventoryContents() != null) {
|
||||
BlockState restored = snapshot.state().getBlock().getState();
|
||||
if (restored instanceof TileStateInventoryHolder holder) {
|
||||
holder.getInventory().setContents(cloneContents(snapshot.inventoryContents()));
|
||||
}
|
||||
}
|
||||
}
|
||||
forcedEntries.clear();
|
||||
incidentDamage.clear();
|
||||
activeSite = null;
|
||||
|
||||
for (Map.Entry<UUID, List<ItemStack>> entry : returns.entrySet()) {
|
||||
@@ -173,8 +235,9 @@ final class EmergencyConstructionManager implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
BlockKey primary = BlockKey.of(block);
|
||||
forcedEntries.putIfAbsent(primary, new ForcedEntry(player.getUniqueId(), List.copyOf(states)));
|
||||
for (BlockState state : states) {
|
||||
captureIncidentDamage(state.getBlock());
|
||||
}
|
||||
|
||||
// Forced entry is temporary incident damage, not resource harvesting.
|
||||
// Remove involved blocks without physics, item drops, or experience.
|
||||
@@ -224,6 +287,14 @@ final class EmergencyConstructionManager implements Listener {
|
||||
states.add(block.getState());
|
||||
}
|
||||
|
||||
private ItemStack[] cloneContents(ItemStack[] contents) {
|
||||
ItemStack[] copy = new ItemStack[contents.length];
|
||||
for (int i = 0; i < contents.length; i++) {
|
||||
copy[i] = contents[i] == null ? null : contents[i].clone();
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
List<ItemStack> items = pendingReturns.remove(event.getPlayer().getUniqueId());
|
||||
@@ -305,5 +376,5 @@ final class EmergencyConstructionManager implements Listener {
|
||||
|
||||
private record Placement(UUID id, UUID owner, ItemStack returnItem, List<PlacedBlock> blocks) {}
|
||||
private record PlacedBlock(BlockKey key, Material placedType, BlockState replacedState) {}
|
||||
private record ForcedEntry(UUID firefighter, List<BlockState> originalStates) {}
|
||||
private record IncidentSnapshot(BlockState state, ItemStack[] inventoryContents) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.therosegarden.firefighter;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.function.Predicate;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
||||
interface EmergencyLocationProvider {
|
||||
EmergencySite randomClaimSite(Random random, Predicate<World> worldAllowed, int maxRadius, int minClaimArea);
|
||||
|
||||
String nearbyClaims(Location location, int radius, int maxResults);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -29,6 +30,8 @@ final class EmergencyManager {
|
||||
private final Set<BlockKey> activeFires = new HashSet<>();
|
||||
private EmergencyPermissionManager permissions;
|
||||
private EmergencyConstructionManager construction;
|
||||
private FirefighterDutyManager duty;
|
||||
private EmergencyLocationProvider locationProvider;
|
||||
|
||||
private EmergencySite activeSite;
|
||||
private BukkitTask expiryTask;
|
||||
@@ -45,6 +48,11 @@ final class EmergencyManager {
|
||||
this.construction = construction;
|
||||
}
|
||||
|
||||
void setDispatchServices(FirefighterDutyManager duty, EmergencyLocationProvider locationProvider) {
|
||||
this.duty = duty;
|
||||
this.locationProvider = locationProvider;
|
||||
}
|
||||
|
||||
void reload() {
|
||||
loadSites();
|
||||
scheduleNextAutomaticEmergency();
|
||||
@@ -93,7 +101,7 @@ final class EmergencyManager {
|
||||
}
|
||||
|
||||
boolean addSite(String name, Location location, int radius) {
|
||||
if (name == null || !name.matches("[A-Za-z0-9_-]{1,32}") || location.getWorld() == null) {
|
||||
if (name == null || name.equalsIgnoreCase("random") || !name.matches("[A-Za-z0-9_-]{1,32}") || location.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -132,13 +140,22 @@ final class EmergencyManager {
|
||||
}
|
||||
|
||||
boolean siteExists(String name) {
|
||||
return name != null && sites.containsKey(name.toLowerCase(Locale.ROOT));
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
if (name.equalsIgnoreCase("random")) {
|
||||
return randomLocationsEnabled() || !sites.isEmpty();
|
||||
}
|
||||
return sites.containsKey(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
boolean isSitePermitted(String name) {
|
||||
if (name == null) {
|
||||
return false;
|
||||
}
|
||||
if (name.equalsIgnoreCase("random")) {
|
||||
return true;
|
||||
}
|
||||
EmergencySite site = sites.get(name.toLowerCase(Locale.ROOT));
|
||||
if (site == null) {
|
||||
return false;
|
||||
@@ -169,6 +186,10 @@ final class EmergencyManager {
|
||||
return plugin.getConfig().getBoolean("emergencies.enabled", true);
|
||||
}
|
||||
|
||||
boolean randomLocationsEnabled() {
|
||||
return plugin.getConfig().getBoolean("emergencies.random-locations.enabled", true);
|
||||
}
|
||||
|
||||
int requiredOnlineFirefighters() {
|
||||
if (!plugin.getConfig().getBoolean("emergencies.require-firefighters-online", true)) {
|
||||
return 0;
|
||||
@@ -185,10 +206,23 @@ final class EmergencyManager {
|
||||
}
|
||||
|
||||
boolean isEmergencyFire(Block block) {
|
||||
return isEmergencyFire(BlockKey.of(block));
|
||||
return block != null && isEmergencyFire(BlockKey.of(block));
|
||||
}
|
||||
|
||||
boolean isInsideActiveSite(Location location) {
|
||||
return activeSite != null && activeSite.contains(location);
|
||||
}
|
||||
|
||||
void registerEmergencyFire(Block block) {
|
||||
if (block != null && isInsideActiveSite(block.getLocation())) {
|
||||
activeFires.add(BlockKey.of(block));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isNearEmergencyFire(Block block, int radius) {
|
||||
if (block == null) {
|
||||
return false;
|
||||
}
|
||||
BlockKey target = BlockKey.of(block);
|
||||
for (BlockKey fire : activeFires) {
|
||||
if (fire.isWithin(target, radius)) {
|
||||
@@ -199,31 +233,50 @@ final class EmergencyManager {
|
||||
}
|
||||
|
||||
boolean startEmergency(String siteName) {
|
||||
if (!emergenciesEnabled() || activeSite != null || sites.isEmpty()) {
|
||||
if (!emergenciesEnabled() || activeSite != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onlineFirefighterCount() < requiredOnlineFirefighters()) {
|
||||
if (onlineOnDutyFirefighterCount() < requiredOnlineFirefighters()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EmergencySite site = selectSite(siteName);
|
||||
if (site == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Location center = site.center();
|
||||
if (center == null || center.getWorld() == null || !isWorldPermitted(center.getWorld())) {
|
||||
return false;
|
||||
}
|
||||
boolean randomRequested = siteName == null || siteName.equalsIgnoreCase("random");
|
||||
int candidateAttempts = randomRequested
|
||||
? Math.max(1, plugin.getConfig().getInt("emergencies.random-locations.attempts", 12))
|
||||
: 1;
|
||||
|
||||
int minimum = Math.max(1, plugin.getConfig().getInt("emergencies.fires-min", 6));
|
||||
int maximum = Math.max(minimum, plugin.getConfig().getInt("emergencies.fires-max", 12));
|
||||
int targetCount = minimum + random.nextInt(maximum - minimum + 1);
|
||||
|
||||
Set<BlockKey> spawned = generateFireLocations(site, targetCount);
|
||||
if (spawned.isEmpty()) {
|
||||
plugin.getLogger().warning("Could not find safe surfaces for a Firefighter emergency at site '" + site.name() + "'.");
|
||||
EmergencySite site = null;
|
||||
Set<BlockKey> spawned = Set.of();
|
||||
for (int attempt = 0; attempt < candidateAttempts; attempt++) {
|
||||
site = randomRequested ? selectAutomaticSite() : selectConfiguredSite(siteName);
|
||||
if (site == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
Location center = site.center();
|
||||
if (center == null || center.getWorld() == null || !isWorldPermitted(center.getWorld())) {
|
||||
if (!randomRequested) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
spawned = generateFireLocations(site, targetCount);
|
||||
if (!spawned.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
if (!randomRequested) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (site == null || spawned.isEmpty()) {
|
||||
plugin.getLogger().warning("Could not find a suitable location for a Firefighter emergency.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -240,25 +293,51 @@ final class EmergencyManager {
|
||||
expiryTask = Bukkit.getScheduler().runTaskLater(plugin, () -> endEmergency(false), durationMinutes * 60L * 20L);
|
||||
startIntegrityTask();
|
||||
|
||||
notifyResponders(plugin.message("messages.alert")
|
||||
.replace("%site%", site.name())
|
||||
.replace("%x%", Integer.toString(site.x()))
|
||||
.replace("%y%", Integer.toString(site.y()))
|
||||
.replace("%z%", Integer.toString(site.z())));
|
||||
|
||||
notifyEmergencyStart(site);
|
||||
return true;
|
||||
}
|
||||
|
||||
private EmergencySite selectSite(String siteName) {
|
||||
if (siteName != null) {
|
||||
EmergencySite requested = sites.get(siteName.toLowerCase(Locale.ROOT));
|
||||
if (requested == null) {
|
||||
return null;
|
||||
private EmergencySite selectConfiguredSite(String siteName) {
|
||||
if (siteName == null) {
|
||||
return randomConfiguredSite();
|
||||
}
|
||||
EmergencySite requested = sites.get(siteName.toLowerCase(Locale.ROOT));
|
||||
if (requested == null) {
|
||||
return null;
|
||||
}
|
||||
World world = Bukkit.getWorld(requested.worldName());
|
||||
return world != null && isWorldPermitted(world) ? requested : null;
|
||||
}
|
||||
|
||||
private EmergencySite selectAutomaticSite() {
|
||||
if (randomLocationsEnabled()) {
|
||||
if (locationProvider != null
|
||||
&& plugin.getConfig().getBoolean("emergencies.random-locations.prefer-griefprevention-claims", true)) {
|
||||
int maxRadius = Math.max(2, plugin.getConfig().getInt("emergencies.random-locations.claim-radius-max",
|
||||
plugin.getConfig().getInt("emergencies.default-site-radius", 18)));
|
||||
int minArea = Math.max(1, plugin.getConfig().getInt("emergencies.random-locations.min-claim-area", 25));
|
||||
EmergencySite claimSite = locationProvider.randomClaimSite(random, this::isWorldPermitted, maxRadius, minArea);
|
||||
if (claimSite != null) {
|
||||
return claimSite;
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("emergencies.random-locations.loaded-world-fallback", true)) {
|
||||
EmergencySite randomSite = randomLoadedLocation();
|
||||
if (randomSite != null) {
|
||||
return randomSite;
|
||||
}
|
||||
}
|
||||
World world = Bukkit.getWorld(requested.worldName());
|
||||
return world != null && isWorldPermitted(world) ? requested : null;
|
||||
}
|
||||
|
||||
if (plugin.getConfig().getBoolean("emergencies.random-locations.configured-sites-fallback", true)
|
||||
|| !randomLocationsEnabled()) {
|
||||
return randomConfiguredSite();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private EmergencySite randomConfiguredSite() {
|
||||
List<EmergencySite> eligible = new ArrayList<>();
|
||||
for (EmergencySite site : sites.values()) {
|
||||
World world = Bukkit.getWorld(site.worldName());
|
||||
@@ -269,6 +348,30 @@ final class EmergencyManager {
|
||||
return eligible.isEmpty() ? null : eligible.get(random.nextInt(eligible.size()));
|
||||
}
|
||||
|
||||
private EmergencySite randomLoadedLocation() {
|
||||
List<World> worlds = new ArrayList<>();
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
if (isWorldPermitted(world) && world.getLoadedChunks().length > 0) {
|
||||
worlds.add(world);
|
||||
}
|
||||
}
|
||||
if (worlds.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
World world = worlds.get(random.nextInt(worlds.size()));
|
||||
Chunk[] chunks = world.getLoadedChunks();
|
||||
if (chunks.length == 0) {
|
||||
return null;
|
||||
}
|
||||
Chunk chunk = chunks[random.nextInt(chunks.length)];
|
||||
int x = (chunk.getX() << 4) + random.nextInt(16);
|
||||
int z = (chunk.getZ() << 4) + random.nextInt(16);
|
||||
int y = world.getHighestBlockAt(x, z, HeightMap.MOTION_BLOCKING).getY() + 1;
|
||||
int radius = Math.max(3, plugin.getConfig().getInt("emergencies.default-site-radius", 18));
|
||||
return new EmergencySite("an area in " + world.getName(), world.getName(), x, y, z, radius);
|
||||
}
|
||||
|
||||
private Set<BlockKey> generateFireLocations(EmergencySite site, int targetCount) {
|
||||
Set<BlockKey> result = new HashSet<>();
|
||||
Location center = site.center();
|
||||
@@ -279,8 +382,8 @@ final class EmergencyManager {
|
||||
World world = center.getWorld();
|
||||
int attempts = Math.max(80, targetCount * 40);
|
||||
|
||||
// Prefer flammable surfaces so the fire remains visually stable without
|
||||
// changing the underlying block. BlockBurnEvent is cancelled separately.
|
||||
// Prefer flammable surfaces so the incident can develop naturally. The
|
||||
// response boundary and rollback ledger keep that damage temporary.
|
||||
for (int pass = 0; pass < 2 && result.size() < targetCount; pass++) {
|
||||
for (int attempt = 0; attempt < attempts && result.size() < targetCount; attempt++) {
|
||||
int dx = random.nextInt(site.radius() * 2 + 1) - site.radius();
|
||||
@@ -371,7 +474,7 @@ final class EmergencyManager {
|
||||
activeSite = null;
|
||||
|
||||
String key = cleared ? "messages.cleared" : "messages.expired";
|
||||
notifyResponders(plugin.message(key).replace("%site%", endedSite.name()));
|
||||
notifyResponders(formatMessage(plugin.message(key), endedSite));
|
||||
}
|
||||
|
||||
private void clearEmergencyFireBlocks() {
|
||||
@@ -383,6 +486,33 @@ final class EmergencyManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyEmergencyStart(EmergencySite site) {
|
||||
String alert = formatMessage(plugin.message("messages.alert"), site);
|
||||
String details = formatMessage(plugin.message("messages.dispatch-details"), site);
|
||||
boolean announcements = plugin.getConfig().getBoolean("emergencies.announcements.enabled", true);
|
||||
boolean everyone = plugin.getConfig().getBoolean("emergencies.announcements.notify-all-players",
|
||||
plugin.getConfig().getBoolean("emergencies.notify-all-players", true));
|
||||
|
||||
if (announcements) {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
boolean firefighter = jobs.isFirefighter(player);
|
||||
if (firefighter && duty != null && !duty.isOnDuty(player)) {
|
||||
continue;
|
||||
}
|
||||
if (everyone || firefighter) {
|
||||
player.sendMessage(plugin.prefix() + alert);
|
||||
}
|
||||
if (firefighter && !details.isEmpty()) {
|
||||
player.sendMessage(plugin.prefix() + details);
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLogger().info(plugin.stripColor(alert));
|
||||
if (!details.isEmpty()) {
|
||||
plugin.getLogger().info(plugin.stripColor(details));
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyResponders(String message) {
|
||||
boolean announcements = plugin.getConfig().getBoolean("emergencies.announcements.enabled", true);
|
||||
boolean everyone = plugin.getConfig().getBoolean("emergencies.announcements.notify-all-players",
|
||||
@@ -390,7 +520,11 @@ final class EmergencyManager {
|
||||
|
||||
if (announcements) {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (everyone || jobs.isFirefighter(player)) {
|
||||
boolean firefighter = jobs.isFirefighter(player);
|
||||
if (firefighter && duty != null && !duty.isOnDuty(player)) {
|
||||
continue;
|
||||
}
|
||||
if (everyone || firefighter) {
|
||||
player.sendMessage(plugin.prefix() + message);
|
||||
}
|
||||
}
|
||||
@@ -398,6 +532,26 @@ final class EmergencyManager {
|
||||
plugin.getLogger().info(plugin.stripColor(message));
|
||||
}
|
||||
|
||||
private String formatMessage(String message, EmergencySite site) {
|
||||
String nearby = nearbyClaims(site);
|
||||
return message
|
||||
.replace("%site%", site.name())
|
||||
.replace("%world%", site.worldName())
|
||||
.replace("%x%", Integer.toString(site.x()))
|
||||
.replace("%y%", Integer.toString(site.y()))
|
||||
.replace("%z%", Integer.toString(site.z()))
|
||||
.replace("%nearby_claims%", nearby);
|
||||
}
|
||||
|
||||
private String nearbyClaims(EmergencySite site) {
|
||||
if (locationProvider == null) {
|
||||
return "GriefPrevention unavailable";
|
||||
}
|
||||
int radius = Math.max(1, plugin.getConfig().getInt("emergencies.random-locations.nearby-claims-radius", 64));
|
||||
int max = Math.max(1, plugin.getConfig().getInt("emergencies.random-locations.nearby-claims-max", 5));
|
||||
return locationProvider.nearbyClaims(site.center(), radius, max);
|
||||
}
|
||||
|
||||
int onlineFirefighterCount() {
|
||||
int count = 0;
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
@@ -408,6 +562,19 @@ final class EmergencyManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
int onlineOnDutyFirefighterCount() {
|
||||
if (duty == null) {
|
||||
return onlineFirefighterCount();
|
||||
}
|
||||
int count = 0;
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (duty.isOnDuty(player)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void scheduleNextAutomaticEmergency() {
|
||||
cancelAutomaticTask();
|
||||
|
||||
|
||||
@@ -18,12 +18,17 @@ final class EmergencyPermissionManager implements Listener {
|
||||
private final JobsFacade jobs;
|
||||
private final Map<UUID, PermissionAttachment> attachments = new HashMap<>();
|
||||
private EmergencySite activeSite;
|
||||
private FirefighterDutyManager duty;
|
||||
|
||||
EmergencyPermissionManager(RoseFirefighterPlugin plugin, JobsFacade jobs) {
|
||||
this.plugin = plugin;
|
||||
this.jobs = jobs;
|
||||
}
|
||||
|
||||
void setDutyManager(FirefighterDutyManager duty) {
|
||||
this.duty = duty;
|
||||
}
|
||||
|
||||
void activate(EmergencySite site) {
|
||||
activeSite = site;
|
||||
syncResponders();
|
||||
@@ -56,6 +61,10 @@ final class EmergencyPermissionManager implements Listener {
|
||||
return activeSite != null && activeSite.contains(location);
|
||||
}
|
||||
|
||||
private boolean shouldNotify(Player player) {
|
||||
return duty == null || duty.isOnDuty(player);
|
||||
}
|
||||
|
||||
private void grant(Player player) {
|
||||
if (attachments.containsKey(player.getUniqueId())) {
|
||||
return;
|
||||
@@ -63,9 +72,11 @@ final class EmergencyPermissionManager implements Listener {
|
||||
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);
|
||||
if (shouldNotify(player)) {
|
||||
String message = plugin.message("messages.temporary-build-enabled");
|
||||
if (!message.isEmpty()) {
|
||||
player.sendMessage(plugin.prefix() + message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +86,7 @@ final class EmergencyPermissionManager implements Listener {
|
||||
return;
|
||||
}
|
||||
player.removeAttachment(attachment);
|
||||
if (notify) {
|
||||
if (notify && shouldNotify(player)) {
|
||||
String message = plugin.message("messages.temporary-build-disabled");
|
||||
if (!message.isEmpty()) {
|
||||
player.sendMessage(plugin.prefix() + message);
|
||||
|
||||
@@ -11,19 +11,25 @@ import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBurnEvent;
|
||||
import org.bukkit.event.block.BlockFromToEvent;
|
||||
import org.bukkit.event.block.BlockIgniteEvent;
|
||||
import org.bukkit.event.block.BlockPhysicsEvent;
|
||||
import org.bukkit.event.block.BlockSpreadEvent;
|
||||
import org.bukkit.event.block.TNTPrimeEvent;
|
||||
import org.bukkit.event.hanging.HangingBreakEvent;
|
||||
import org.bukkit.event.player.PlayerBucketEmptyEvent;
|
||||
|
||||
final class FireSafetyAndOriginListener implements Listener {
|
||||
private final RoseFirefighterPlugin plugin;
|
||||
private final FirefighterRewardService rewards;
|
||||
private final EmergencyManager emergencies;
|
||||
private final EmergencyConstructionManager construction;
|
||||
private final OriginTracker playerLava;
|
||||
|
||||
FireSafetyAndOriginListener(RoseFirefighterPlugin plugin, FirefighterRewardService rewards, EmergencyManager emergencies) {
|
||||
FireSafetyAndOriginListener(RoseFirefighterPlugin plugin, FirefighterRewardService rewards,
|
||||
EmergencyManager emergencies, EmergencyConstructionManager construction) {
|
||||
this.plugin = plugin;
|
||||
this.rewards = rewards;
|
||||
this.emergencies = emergencies;
|
||||
this.construction = construction;
|
||||
this.playerLava = new OriginTracker(playerLavaMemoryMillis());
|
||||
}
|
||||
|
||||
@@ -35,25 +41,119 @@ final class FireSafetyAndOriginListener implements Listener {
|
||||
return Math.max(1L, plugin.getConfig().getLong("rewards.player-placed-lava-memory-seconds", 600L)) * 1000L;
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void protectEmergencySpread(BlockSpreadEvent event) {
|
||||
if (emergencies.isEmergencyFire(event.getSource())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
private boolean emergencySpreadEnabled() {
|
||||
return plugin.getConfig().getBoolean("emergencies.fire-spread", true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void protectEmergencyIgnition(BlockIgniteEvent event) {
|
||||
private boolean emergencyFireDamageEnabled() {
|
||||
return plugin.getConfig().getBoolean("emergencies.fire-damage", true);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void controlEmergencySpread(BlockSpreadEvent event) {
|
||||
if (!emergencies.isEmergencyFire(event.getSource())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emergencySpreadEnabled() || !emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
construction.captureIncidentDamage(event.getBlock());
|
||||
event.setCancelled(false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void registerEmergencySpread(BlockSpreadEvent event) {
|
||||
if (emergencies.isEmergencyFire(event.getSource()) && emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
emergencies.registerEmergencyFire(event.getBlock());
|
||||
}
|
||||
rewards.propagatePlayerCreated(event.getSource(), event.getBlock());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void controlEmergencyIgnition(BlockIgniteEvent event) {
|
||||
Block source = event.getIgnitingBlock();
|
||||
if (event.getCause() == BlockIgniteEvent.IgniteCause.SPREAD
|
||||
&& source != null && emergencies.isEmergencyFire(source)) {
|
||||
if (event.getCause() != BlockIgniteEvent.IgniteCause.SPREAD || source == null || !emergencies.isEmergencyFire(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emergencySpreadEnabled() || !emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
construction.captureIncidentDamage(event.getBlock());
|
||||
event.setCancelled(false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void controlEmergencyBurn(BlockBurnEvent event) {
|
||||
Block source = event.getIgnitingBlock();
|
||||
boolean emergencyCaused = source != null
|
||||
? emergencies.isEmergencyFire(source)
|
||||
: emergencies.isNearEmergencyFire(event.getBlock(), 1);
|
||||
if (!emergencyCaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emergencyFireDamageEnabled() || !emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (construction.isFireDamageRestoreEnabled()) {
|
||||
construction.captureFireDamage(event.getBlock());
|
||||
}
|
||||
event.setCancelled(false);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void prepareEmergencyBurn(BlockBurnEvent event) {
|
||||
Block source = event.getIgnitingBlock();
|
||||
boolean emergencyCaused = source != null
|
||||
? emergencies.isEmergencyFire(source)
|
||||
: emergencies.isNearEmergencyFire(event.getBlock(), 1);
|
||||
if (emergencyCaused && emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
construction.prepareFireBurn(event.getBlock());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
|
||||
public void preventEmergencyTnt(TNTPrimeEvent event) {
|
||||
if (event.getCause() != TNTPrimeEvent.PrimeCause.FIRE) {
|
||||
return;
|
||||
}
|
||||
Block source = event.getPrimingBlock();
|
||||
if (source != null && emergencies.isEmergencyFire(source)
|
||||
&& plugin.getConfig().getBoolean("emergencies.prevent-fire-tnt", true)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void protectEmergencyBurn(BlockBurnEvent event) {
|
||||
if (emergencies.isNearEmergencyFire(event.getBlock(), 2)) {
|
||||
public void preventSecondaryPhysics(BlockPhysicsEvent event) {
|
||||
if (!plugin.getConfig().getBoolean("emergencies.prevent-secondary-physics-damage", true)
|
||||
|| !emergencies.isInsideActiveSite(event.getBlock().getLocation())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block source = event.getSourceBlock();
|
||||
if ((source != null && (emergencies.isEmergencyFire(source) || construction.isIncidentDamaged(source)))
|
||||
|| construction.isNearIncidentDamage(event.getBlock(), 1)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void protectHangingDecorations(HangingBreakEvent event) {
|
||||
if (event.getCause() != HangingBreakEvent.RemoveCause.PHYSICS
|
||||
|| !plugin.getConfig().getBoolean("emergencies.prevent-secondary-physics-damage", true)) {
|
||||
return;
|
||||
}
|
||||
Block block = event.getEntity().getLocation().getBlock();
|
||||
if (emergencies.isInsideActiveSite(block.getLocation()) && construction.isNearIncidentDamage(block, 1)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +161,14 @@ final class FireSafetyAndOriginListener implements Listener {
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void trackIgnition(BlockIgniteEvent event) {
|
||||
Block fire = event.getBlock();
|
||||
Block source = event.getIgnitingBlock();
|
||||
|
||||
if (event.getCause() == BlockIgniteEvent.IgniteCause.SPREAD
|
||||
&& source != null && emergencies.isEmergencyFire(source)
|
||||
&& emergencies.isInsideActiveSite(fire.getLocation())) {
|
||||
emergencies.registerEmergencyFire(fire);
|
||||
return;
|
||||
}
|
||||
|
||||
// Direct player ignition, including flint-and-steel and fire charges,
|
||||
// is never considered a legitimate payable fire.
|
||||
@@ -85,7 +193,6 @@ final class FireSafetyAndOriginListener implements Listener {
|
||||
return;
|
||||
}
|
||||
|
||||
Block source = event.getIgnitingBlock();
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
@@ -97,11 +204,6 @@ final class FireSafetyAndOriginListener implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void trackFireSpread(BlockSpreadEvent event) {
|
||||
rewards.propagatePlayerCreated(event.getSource(), event.getBlock());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void trackLavaBucket(PlayerBucketEmptyEvent event) {
|
||||
if (event.getBucket() == Material.LAVA_BUCKET) {
|
||||
|
||||
@@ -15,11 +15,13 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
private final RoseFirefighterPlugin plugin;
|
||||
private final JobsFacade jobs;
|
||||
private final EmergencyManager emergencies;
|
||||
private final FirefighterDutyManager duty;
|
||||
|
||||
FirefighterCommand(RoseFirefighterPlugin plugin, JobsFacade jobs, EmergencyManager emergencies) {
|
||||
FirefighterCommand(RoseFirefighterPlugin plugin, JobsFacade jobs, EmergencyManager emergencies, FirefighterDutyManager duty) {
|
||||
this.plugin = plugin;
|
||||
this.jobs = jobs;
|
||||
this.emergencies = emergencies;
|
||||
this.duty = duty;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,6 +40,14 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
showStatus(sender);
|
||||
return true;
|
||||
}
|
||||
if (root.equals("onduty")) {
|
||||
setDuty(sender, true);
|
||||
return true;
|
||||
}
|
||||
if (root.equals("offduty")) {
|
||||
setDuty(sender, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("rosefirefighter.admin")) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cYou do not have permission to manage the Fire Department."));
|
||||
@@ -53,6 +63,20 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setDuty(CommandSender sender, boolean onDuty) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cDuty status can only be changed by a player."));
|
||||
return;
|
||||
}
|
||||
if (!jobs.isFirefighter(player)) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.message("messages.joined-needed"));
|
||||
return;
|
||||
}
|
||||
duty.setOnDuty(player, onDuty);
|
||||
String key = onDuty ? "messages.on-duty" : "messages.off-duty";
|
||||
player.sendMessage(plugin.prefix() + plugin.message(key));
|
||||
}
|
||||
|
||||
private void reload(CommandSender sender) {
|
||||
plugin.reloadRuntimeConfiguration(true);
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aRoseFirefighter configuration reloaded."));
|
||||
@@ -67,7 +91,7 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
switch (args[1].toLowerCase(Locale.ROOT)) {
|
||||
case "list" -> {
|
||||
if (emergencies.sites().isEmpty()) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&7No response sites have been configured."));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&7No fixed response sites have been configured. Random dispatch may still be enabled."));
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&eConfigured response sites:"));
|
||||
@@ -101,7 +125,7 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
|
||||
if (!emergencies.addSite(args[2], player.getLocation(), radius)) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cSite names may only contain letters, numbers, _ and - (maximum 32 characters)."));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cSite names may only contain letters, numbers, _ and - (maximum 32 characters), and random is reserved."));
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aAdded response site &f" + args[2] + "&a with radius &f" + Math.max(3, radius) + "&a."));
|
||||
@@ -123,7 +147,7 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private void handleEmergency(CommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&eUsage: /firefighter emergency <start|stop> [site]"));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&eUsage: /firefighter emergency <start|stop> [site|random]"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,25 +167,25 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cUnknown response site: &f" + site));
|
||||
return;
|
||||
}
|
||||
if (site != null && !emergencies.isSitePermitted(site)) {
|
||||
if (site != null && !site.equalsIgnoreCase("random") && !emergencies.isSitePermitted(site)) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cThat response site's world is not permitted by config.yml."));
|
||||
return;
|
||||
}
|
||||
|
||||
int required = emergencies.requiredOnlineFirefighters();
|
||||
int online = emergencies.onlineFirefighterCount();
|
||||
int online = emergencies.onlineOnDutyFirefighterCount();
|
||||
if (online < required) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cAt least &f" + required + " &cFirefighter(s) must be online before an emergency can start."));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cAt least &f" + required + " &con-duty Firefighter(s) must be online before an emergency can start."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!emergencies.startEmergency(site)) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cCould not start an emergency. Add a permitted site and make sure it has suitable surfaces."));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cCould not start an emergency. No suitable permitted fire location was found."));
|
||||
return;
|
||||
}
|
||||
EmergencySite active = emergencies.activeSite();
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aEmergency started at &f" + active.name() + "&a with &f"
|
||||
+ emergencies.activeFireCount() + " &afires."));
|
||||
+ emergencies.activeFireCount() + " &afires near &f" + active.x() + ", " + active.y() + ", " + active.z() + "&a."));
|
||||
}
|
||||
case "stop" -> {
|
||||
if (!emergencies.hasActiveEmergency()) {
|
||||
@@ -169,40 +193,47 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
return;
|
||||
}
|
||||
emergencies.stopEmergency();
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aThe active emergency was stopped and remaining emergency fires were removed."));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aThe active emergency was stopped, remaining fire removed, and incident damage restored."));
|
||||
}
|
||||
default -> sender.sendMessage(plugin.prefix() + plugin.color("&eUsage: /firefighter emergency <start|stop> [site]"));
|
||||
default -> sender.sendMessage(plugin.prefix() + plugin.color("&eUsage: /firefighter emergency <start|stop> [site|random]"));
|
||||
}
|
||||
}
|
||||
|
||||
private void showStatus(CommandSender sender) {
|
||||
String jobName = plugin.getConfig().getString("job-name", "Firefighter");
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&fJob: &c" + jobName + " &8| &fFirefighters online: &c" + emergencies.onlineFirefighterCount()));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&fJob: &c" + jobName
|
||||
+ " &8| &fFirefighters online: &c" + emergencies.onlineFirefighterCount()
|
||||
+ " &8| &fOn duty: &c" + emergencies.onlineOnDutyFirefighterCount()));
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
sender.sendMessage(plugin.prefix() + (jobs.isFirefighter(player)
|
||||
? plugin.color("&aYou are currently on duty as a Firefighter.")
|
||||
: plugin.color("&7You are not currently working as a Firefighter.")));
|
||||
if (jobs.isFirefighter(player)) {
|
||||
String dutyText = duty.isOnDuty(player) ? "&aON DUTY" : "&7OFF DUTY";
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&aYou are currently working as a Firefighter. &8| " + dutyText));
|
||||
} else {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&7You are not currently working as a Firefighter."));
|
||||
}
|
||||
}
|
||||
|
||||
EmergencySite site = emergencies.activeSite();
|
||||
if (site == null) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&7No active fire emergency."));
|
||||
} else {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cACTIVE: &f" + site.name() + " &7near " + site.x() + ", " + site.y() + ", " + site.z()
|
||||
+ " &8| &e" + emergencies.activeFireCount() + " fires remaining"));
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&cACTIVE: &f" + site.name() + " &7in " + site.worldName() + " near "
|
||||
+ site.x() + ", " + site.y() + ", " + site.z() + " &8| &e" + emergencies.activeFireCount() + " fires remaining"));
|
||||
}
|
||||
}
|
||||
|
||||
private void showHelp(CommandSender sender) {
|
||||
sender.sendMessage(plugin.prefix() + plugin.color("&c&lRoseFirefighter &7commands"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter status &8- &7Show current response status"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter status &8- &7Show current response and duty status"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter onduty &8- &7Enable Fire Department dispatch alerts"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter offduty &8- &7Silence your Fire Department dispatch alerts"));
|
||||
if (sender.hasPermission("rosefirefighter.admin")) {
|
||||
sender.sendMessage(plugin.color("&e/firefighter site add <name> [radius] &8- &7Add a response site here"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter site remove <name> &8- &7Remove a response site"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter site list &8- &7List response sites"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter emergency start [site] &8- &7Start a controlled fire"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter emergency stop &8- &7Stop the active fire"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter site add <name> [radius] &8- &7Add a fixed response site"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter site remove <name> &8- &7Remove a fixed response site"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter site list &8- &7List fixed response sites"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter emergency start [site|random] &8- &7Start a fire response"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter emergency stop &8- &7Stop the active fire and restore damage"));
|
||||
sender.sendMessage(plugin.color("&e/firefighter reload &8- &7Reload the addon config"));
|
||||
}
|
||||
}
|
||||
@@ -213,6 +244,8 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
if (args.length == 1) {
|
||||
choices.add("status");
|
||||
choices.add("help");
|
||||
choices.add("onduty");
|
||||
choices.add("offduty");
|
||||
if (sender.hasPermission("rosefirefighter.admin")) {
|
||||
choices.add("site");
|
||||
choices.add("emergency");
|
||||
@@ -228,6 +261,9 @@ final class FirefighterCommand implements CommandExecutor, TabCompleter {
|
||||
} else if (sender.hasPermission("rosefirefighter.admin") && args.length == 3
|
||||
&& ((args[0].equalsIgnoreCase("site") && args[1].equalsIgnoreCase("remove"))
|
||||
|| (args[0].equalsIgnoreCase("emergency") && args[1].equalsIgnoreCase("start")))) {
|
||||
if (args[0].equalsIgnoreCase("emergency")) {
|
||||
choices.add("random");
|
||||
}
|
||||
for (EmergencySite site : emergencies.sites()) {
|
||||
choices.add(site.name());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.therosegarden.firefighter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class FirefighterDutyManager {
|
||||
private final RoseFirefighterPlugin plugin;
|
||||
private final JobsFacade jobs;
|
||||
private final File file;
|
||||
private final Set<UUID> offDuty = new HashSet<>();
|
||||
|
||||
FirefighterDutyManager(RoseFirefighterPlugin plugin, JobsFacade jobs) {
|
||||
this.plugin = plugin;
|
||||
this.jobs = jobs;
|
||||
this.file = new File(plugin.getDataFolder(), "duty-status.yml");
|
||||
load();
|
||||
}
|
||||
|
||||
boolean isOnDuty(Player player) {
|
||||
return player != null && jobs.isFirefighter(player) && !offDuty.contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
boolean setOnDuty(Player player, boolean onDuty) {
|
||||
if (player == null || !jobs.isFirefighter(player)) {
|
||||
return false;
|
||||
}
|
||||
if (onDuty) {
|
||||
offDuty.remove(player.getUniqueId());
|
||||
} else {
|
||||
offDuty.add(player.getUniqueId());
|
||||
}
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void load() {
|
||||
offDuty.clear();
|
||||
if (!file.isFile()) {
|
||||
return;
|
||||
}
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
|
||||
for (String value : yaml.getStringList("off-duty")) {
|
||||
try {
|
||||
offDuty.add(UUID.fromString(value));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
plugin.getLogger().warning("Ignoring invalid off-duty UUID: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
yaml.set("off-duty", offDuty.stream().map(UUID::toString).sorted().toList());
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists() && !plugin.getDataFolder().mkdirs()) {
|
||||
plugin.getLogger().warning("Could not create plugin data folder for duty status.");
|
||||
return;
|
||||
}
|
||||
yaml.save(file);
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().severe("Could not save Firefighter duty status: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package net.therosegarden.firefighter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
import me.ryanhamshire.GriefPrevention.Claim;
|
||||
import me.ryanhamshire.GriefPrevention.GriefPrevention;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.HeightMap;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.World;
|
||||
|
||||
final class GriefPreventionLocationProvider implements EmergencyLocationProvider {
|
||||
@Override
|
||||
public EmergencySite randomClaimSite(Random random, Predicate<World> worldAllowed, int maxRadius, int minClaimArea) {
|
||||
List<Claim> eligible = new ArrayList<>();
|
||||
for (Claim claim : GriefPrevention.instance.dataStore.getClaims()) {
|
||||
if (claim == null || claim.parent != null || claim.getOwnerID() == null || claim.getArea() < minClaimArea) {
|
||||
continue;
|
||||
}
|
||||
Location lesser = claim.getLesserBoundaryCorner();
|
||||
Location greater = claim.getGreaterBoundaryCorner();
|
||||
World world = lesser == null ? null : lesser.getWorld();
|
||||
if (world == null || greater == null || greater.getWorld() == null || !worldAllowed.test(world)) {
|
||||
continue;
|
||||
}
|
||||
eligible.add(claim);
|
||||
}
|
||||
|
||||
if (eligible.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Claim claim = eligible.get(random.nextInt(eligible.size()));
|
||||
Location lesser = claim.getLesserBoundaryCorner();
|
||||
Location greater = claim.getGreaterBoundaryCorner();
|
||||
World world = lesser.getWorld();
|
||||
if (world == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minX = Math.min(lesser.getBlockX(), greater.getBlockX());
|
||||
int maxX = Math.max(lesser.getBlockX(), greater.getBlockX());
|
||||
int minZ = Math.min(lesser.getBlockZ(), greater.getBlockZ());
|
||||
int maxZ = Math.max(lesser.getBlockZ(), greater.getBlockZ());
|
||||
int centerX = minX + ((maxX - minX) / 2);
|
||||
int centerZ = minZ + ((maxZ - minZ) / 2);
|
||||
int y = world.getHighestBlockAt(centerX, centerZ, HeightMap.MOTION_BLOCKING).getY() + 1;
|
||||
|
||||
int halfWidth = Math.max(2, (maxX - minX) / 2);
|
||||
int halfDepth = Math.max(2, (maxZ - minZ) / 2);
|
||||
int radius = Math.max(2, Math.min(Math.max(2, maxRadius), Math.min(halfWidth, halfDepth)));
|
||||
|
||||
String owner = ownerName(claim);
|
||||
return new EmergencySite(owner + "'s home", world.getName(), centerX, y, centerZ, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nearbyClaims(Location location, int radius, int maxResults) {
|
||||
if (location == null || location.getWorld() == null) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
List<NearbyClaim> nearby = new ArrayList<>();
|
||||
for (Claim claim : GriefPrevention.instance.dataStore.getClaims()) {
|
||||
if (claim == null || claim.parent != null) {
|
||||
continue;
|
||||
}
|
||||
Location lesser = claim.getLesserBoundaryCorner();
|
||||
Location greater = claim.getGreaterBoundaryCorner();
|
||||
if (lesser == null || greater == null || lesser.getWorld() == null
|
||||
|| !lesser.getWorld().equals(location.getWorld())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double distance = distanceToClaim(location, lesser, greater);
|
||||
if (distance <= radius) {
|
||||
nearby.add(new NearbyClaim(ownerName(claim), distance));
|
||||
}
|
||||
}
|
||||
|
||||
nearby.sort(Comparator.comparingDouble(NearbyClaim::distance));
|
||||
if (nearby.isEmpty()) {
|
||||
return "none within " + radius + " blocks";
|
||||
}
|
||||
|
||||
int limit = Math.max(1, maxResults);
|
||||
List<String> descriptions = new ArrayList<>();
|
||||
for (int i = 0; i < nearby.size() && i < limit; i++) {
|
||||
NearbyClaim entry = nearby.get(i);
|
||||
String distance = entry.distance() < 1.0 ? "inside" : Math.round(entry.distance()) + "m";
|
||||
descriptions.add(entry.owner() + " (" + distance + ")");
|
||||
}
|
||||
if (nearby.size() > limit) {
|
||||
descriptions.add("+" + (nearby.size() - limit) + " more");
|
||||
}
|
||||
return String.join(", ", descriptions);
|
||||
}
|
||||
|
||||
private String ownerName(Claim claim) {
|
||||
UUID ownerId = claim.getOwnerID();
|
||||
if (ownerId == null) {
|
||||
return "Admin claim";
|
||||
}
|
||||
OfflinePlayer offline = Bukkit.getOfflinePlayer(ownerId);
|
||||
String name = offline.getName();
|
||||
if (name == null || name.isBlank()) {
|
||||
name = claim.getOwnerName();
|
||||
}
|
||||
if (name == null || name.isBlank()) {
|
||||
name = ownerId.toString().substring(0, 8).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private double distanceToClaim(Location location, Location lesser, Location greater) {
|
||||
int minX = Math.min(lesser.getBlockX(), greater.getBlockX());
|
||||
int maxX = Math.max(lesser.getBlockX(), greater.getBlockX());
|
||||
int minZ = Math.min(lesser.getBlockZ(), greater.getBlockZ());
|
||||
int maxZ = Math.max(lesser.getBlockZ(), greater.getBlockZ());
|
||||
|
||||
double x = location.getX();
|
||||
double z = location.getZ();
|
||||
double dx = x < minX ? minX - x : (x > maxX ? x - maxX : 0.0);
|
||||
double dz = z < minZ ? minZ - z : (z > maxZ ? z - maxZ : 0.0);
|
||||
return Math.sqrt((dx * dx) + (dz * dz));
|
||||
}
|
||||
|
||||
private record NearbyClaim(String owner, double distance) {}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
|
||||
private FireSafetyAndOriginListener safetyListener;
|
||||
private EmergencyPermissionManager emergencyPermissions;
|
||||
private EmergencyConstructionManager emergencyConstruction;
|
||||
private FirefighterDutyManager duty;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
@@ -28,9 +29,11 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
|
||||
rewards = new FirefighterRewardService(this, jobs);
|
||||
emergencies = new EmergencyManager(this, jobs);
|
||||
rewards.setEmergencyManager(emergencies);
|
||||
safetyListener = new FireSafetyAndOriginListener(this, rewards, emergencies);
|
||||
emergencyPermissions = new EmergencyPermissionManager(this, jobs);
|
||||
emergencyConstruction = new EmergencyConstructionManager(this, jobs, emergencyPermissions);
|
||||
duty = new FirefighterDutyManager(this, jobs);
|
||||
emergencyPermissions.setDutyManager(duty);
|
||||
safetyListener = new FireSafetyAndOriginListener(this, rewards, emergencies, emergencyConstruction);
|
||||
emergencies.setEmergencyServices(emergencyPermissions, emergencyConstruction);
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(new FireInteractionListener(this, rewards, emergencies), this);
|
||||
@@ -38,16 +41,19 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
|
||||
Bukkit.getPluginManager().registerEvents(emergencyPermissions, this);
|
||||
Bukkit.getPluginManager().registerEvents(emergencyConstruction, this);
|
||||
|
||||
EmergencyLocationProvider locationProvider = null;
|
||||
Plugin griefPrevention = Bukkit.getPluginManager().getPlugin("GriefPrevention");
|
||||
if (griefPrevention != null && griefPrevention.isEnabled()) {
|
||||
locationProvider = new GriefPreventionLocationProvider();
|
||||
Bukkit.getPluginManager().registerEvents(
|
||||
new GriefPreventionBridge(emergencies, emergencyPermissions, emergencyConstruction), this);
|
||||
getLogger().info("GriefPrevention detected. Emergency-site build overrides are enabled.");
|
||||
getLogger().info("GriefPrevention detected. Protected response, random claim dispatch, and claim context are enabled.");
|
||||
} else {
|
||||
getLogger().info("GriefPrevention not detected. Emergency build permission will remain internal to RoseFirefighter.");
|
||||
getLogger().info("GriefPrevention not detected. Random dispatch will use loaded-world locations and configured sites.");
|
||||
}
|
||||
emergencies.setDispatchServices(duty, locationProvider);
|
||||
|
||||
FirefighterCommand commandHandler = new FirefighterCommand(this, jobs, emergencies);
|
||||
FirefighterCommand commandHandler = new FirefighterCommand(this, jobs, emergencies, duty);
|
||||
PluginCommand command = Objects.requireNonNull(getCommand("firefighter"), "firefighter command missing from plugin.yml");
|
||||
command.setExecutor(commandHandler);
|
||||
command.setTabCompleter(commandHandler);
|
||||
@@ -65,7 +71,7 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
|
||||
getLogger().warning("Copy jobs/Firefighter.yml from the RoseFirefighter package into plugins/Jobs/jobs/ and run /jobs reload.");
|
||||
}
|
||||
|
||||
getLogger().info("RoseFirefighter enabled. Controlled emergency spread protection is active.");
|
||||
getLogger().info("RoseFirefighter enabled. Emergency spread, rollback, dispatch, and response controls are active.");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,6 +79,9 @@ public final class RoseFirefighterPlugin extends JavaPlugin {
|
||||
if (emergencies != null) {
|
||||
emergencies.shutdown();
|
||||
}
|
||||
if (duty != null) {
|
||||
duty.save();
|
||||
}
|
||||
if (rewards != null) {
|
||||
rewards.clear();
|
||||
}
|
||||
|
||||
@@ -28,9 +28,6 @@ rewards:
|
||||
# are clamped to 8 to avoid an excessive per-bucket scan.
|
||||
water-bucket-scan-radius: 4
|
||||
|
||||
# These are JobsReborn action counts, not hard-coded currency amounts. Keep 1
|
||||
# for the normal payout configured in Firefighter.yml. Setting enabled false or
|
||||
# action-count 0 disables that extinguish reward type.
|
||||
normal-fire:
|
||||
enabled: true
|
||||
action-count: 1
|
||||
@@ -48,16 +45,50 @@ emergencies:
|
||||
enabled: true
|
||||
automatic: true
|
||||
|
||||
# Let incident fire behave naturally inside the response boundary. Every spread
|
||||
# fire is registered as part of the emergency, and spread outside the boundary
|
||||
# is blocked.
|
||||
fire-spread: true
|
||||
fire-damage: true
|
||||
|
||||
# Snapshot the first pre-incident state of blocks damaged by emergency fire and
|
||||
# restore them instantly when the incident clears, expires, is stopped, or the
|
||||
# plugin shuts down. Tile/container contents are preserved as part of rollback.
|
||||
restore-fire-damage: true
|
||||
|
||||
# Fire-triggered TNT is suppressed because explosion chains can escape the fire
|
||||
# boundary and cause damage that is not attributable to normal fire events.
|
||||
prevent-fire-tnt: true
|
||||
|
||||
# Prevent support-dependent secondary damage such as falling blocks or hanging
|
||||
# decorations popping off after a support block burns.
|
||||
prevent-secondary-physics-damage: true
|
||||
|
||||
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.
|
||||
# Allow Firefighters to make temporary forced entry through protected doors,
|
||||
# windows, walls, etc. inside the active response site. Broken property gives
|
||||
# no drops/XP and is restored by the same incident rollback ledger.
|
||||
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.
|
||||
# Automatic and `/firefighter emergency start random` dispatches use random
|
||||
# incident locations. With GriefPrevention installed, player-owned claims are
|
||||
# preferred so dispatches can identify the property owner.
|
||||
random-locations:
|
||||
enabled: true
|
||||
prefer-griefprevention-claims: true
|
||||
min-claim-area: 25
|
||||
claim-radius-max: 18
|
||||
attempts: 12
|
||||
loaded-world-fallback: true
|
||||
configured-sites-fallback: true
|
||||
nearby-claims-radius: 64
|
||||
nearby-claims-max: 5
|
||||
|
||||
# Automatic/manual incidents require this many ON-DUTY Firefighters. Players
|
||||
# working the JobsReborn Firefighter job are on duty by default and can silence
|
||||
# dispatch notifications with /firefighter offduty.
|
||||
require-firefighters-online: true
|
||||
min-firefighters-online: 1
|
||||
|
||||
@@ -84,13 +115,17 @@ emergencies:
|
||||
|
||||
messages:
|
||||
prefix: '&8[&cFire Dept&8] &r'
|
||||
alert: '&c&l🔥 FIRE ALERT! &eA fire has been reported at &f%site%&e near &fX:%x% Y:%y% Z:%z%&e. Firefighters are needed immediately!'
|
||||
cleared: '&aFire at &f%site% &ahas been brought under control.'
|
||||
expired: '&eThe fire response at &f%site% &ehas ended.'
|
||||
joined-needed: '&7You must be working as a &cFirefighter &7to receive firefighting pay.'
|
||||
alert: '&c&l🔥 FIRE ALERT! &eFire reported at &f%site% &ein &f%world% &enear &fX:%x% Y:%y% Z:%z%&e. Firefighters are needed immediately!'
|
||||
dispatch-details: '&6Dispatch details: &eCoordinates &f%world% %x%, %y%, %z% &8| &eNearby claims: &f%nearby_claims%'
|
||||
cleared: '&aFire at &f%site% &ahas been brought under control. Incident damage has been restored.'
|
||||
expired: '&eThe fire response at &f%site% &ehas ended. Incident damage has been restored.'
|
||||
joined-needed: '&7You must be working as a &cFirefighter &7to use Firefighter duty controls or receive firefighting pay.'
|
||||
on-duty: '&aYou are now ON DUTY. Fire dispatch alerts are enabled.'
|
||||
off-duty: '&7You are now OFF DUTY. Fire dispatch alerts are silenced until you use &f/firefighter onduty&7.'
|
||||
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]
|
||||
# Optional fixed sites managed with /firefighter site add <name> [radius]. Random
|
||||
# locations can operate without any fixed sites when enabled above.
|
||||
sites: {}
|
||||
|
||||
Reference in New Issue
Block a user