Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/main/java/world/bentobox/bentobox/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,16 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "island.obsidian-scooping-lava-tip-duration", since = "3.14.0")
private int obsidianScoopingLavaTipDuration = 5;

@ConfigComment("Maximum number of history (log) entries kept per island. The oldest entries")
@ConfigComment("are dropped first once the cap is reached, so long-lived islands do not grow")
@ConfigComment("without bound in memory and in the database.")
@ConfigComment("Set to 0 (or a negative value) for unlimited history. This is the default so")
@ConfigComment("existing servers keep their current behaviour.")
@ConfigComment("Note: capping can undercount the historical-members placeholder once old")
@ConfigComment("JOINED entries are dropped off the front.")
@ConfigEntry(path = "island.history.max-entries", since = "3.22.3")
private int islandHistoryMaxEntries = 0;

/* WORLD */
@ConfigComment("Vanilla structures disabled by default in EVERY BentoBox game mode world")
@ConfigComment("(overworld, nether and end). List the structure keys to stop generating and to")
Expand Down Expand Up @@ -1835,6 +1845,26 @@ public void setObsidianScoopingLavaTipDuration(int obsidianScoopingLavaTipDurati
this.obsidianScoopingLavaTipDuration = obsidianScoopingLavaTipDuration;
}

/**
* Gets the maximum number of history entries kept per island.
*
* @return the cap; 0 or less means unlimited
* @since 3.22.3
*/
public int getIslandHistoryMaxEntries() {
return islandHistoryMaxEntries;
}

/**
* Sets the maximum number of history entries kept per island.
*
* @param islandHistoryMaxEntries the cap; 0 or less means unlimited
* @since 3.22.3
*/
public void setIslandHistoryMaxEntries(int islandHistoryMaxEntries) {
this.islandHistoryMaxEntries = islandHistoryMaxEntries;
}

/**
* @return the islandNumber
* @since 2.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.scheduler.BukkitTask;

Expand Down Expand Up @@ -95,6 +96,20 @@ public void onPlayerTeleport(PlayerTeleportEvent e) {
}


/**
* Cancels and removes any pending delayed command when the player quits,
* otherwise the entry would remain in the monitor map forever.
*
* @param e Player quit event
*/
@EventHandler(priority = EventPriority.NORMAL)
public void onPlayerQuit(PlayerQuitEvent e) {
DelayedCommand delayed = toBeMonitored.remove(e.getPlayer().getUniqueId());
if (delayed != null) {
delayed.task().cancel();
}
}

/**
* Top level command
* @param addon - addon creating the command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Particle;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;

import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.commands.ConfirmableCommand;
Expand All @@ -18,7 +21,7 @@
import world.bentobox.bentobox.managers.BlueprintsManager;
import world.bentobox.bentobox.panels.BlueprintManagementPanel;

public class AdminBlueprintCommand extends ConfirmableCommand {
public class AdminBlueprintCommand extends ConfirmableCommand implements Listener {
// Clipboards
private Map<UUID, BlueprintClipboard> clipboards;

Expand All @@ -39,6 +42,7 @@ public void setup() {

clipboards = new HashMap<>();
displayClipboards = new HashMap<>();
Bukkit.getPluginManager().registerEvents(this, getPlugin());
Comment thread
tastybento marked this conversation as resolved.

// Sub commands
new AdminBlueprintLoadCommand(this);
Expand All @@ -64,6 +68,17 @@ protected Map<UUID, BlueprintClipboard> getClipboards() {
return clipboards;
}

/**
* Releases the quitting player's clipboard. A clipboard can hold a full copied
* island (tens of thousands of block objects), so it must not outlive the player.
*
* @param e Player quit event
*/
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) {
clipboards.remove(e.getPlayer().getUniqueId());
}


/**
* This method shows clipboard for requested user.
Expand Down
82 changes: 65 additions & 17 deletions src/main/java/world/bentobox/bentobox/api/flags/FlagListener.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package world.bentobox.bentobox.api.flags;

import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import org.bukkit.Bukkit;
import org.bukkit.Location;
Expand Down Expand Up @@ -34,6 +36,16 @@ public abstract class FlagListener implements Listener {

private static final String WHY = "Why: ";

/**
* Cache of per-world why-debug metadata keys to avoid rebuilding the string
* on every protection check.
*/
private static final Map<String, String> WHY_DEBUG_KEYS = new ConcurrentHashMap<>();

private static String whyDebugKey(String worldName) {
return worldName == null ? "null_why_debug" : WHY_DEBUG_KEYS.computeIfAbsent(worldName, n -> n + "_why_debug");
}

/**
* Reason for why flag was allowed or disallowed
* Used by admins for debugging player actions
Expand Down Expand Up @@ -150,7 +162,7 @@ public boolean checkIsland(@NonNull Event e, @Nullable Player player, @Nullable
* @return true if the check is okay, false if it was disallowed
*/
public boolean checkIsland(@NonNull Event e, @Nullable Player player, @Nullable Location loc, @NonNull Flag flag, boolean silent) {

// Set user
user = player == null ? null : User.getInstance(player);
if (loc == null) {
Expand All @@ -168,20 +180,19 @@ public boolean checkIsland(@NonNull Event e, @Nullable Player player, @Nullable

// Get the island and if present
Optional<Island> island = getIslands().getProtectedIslandAt(loc);

// Handle Settings Flag
if (flag.getType().equals(Flag.Type.SETTING)) {
return processSetting(flag, island, e, loc);
}

// Protection flag
// Ops or "bypass everywhere" moderators can do anything unless they have switched it off
if (hasBypassEverywhere(loc, flag)) {
report(user, e, loc, flag, user.isOp() ? Why.OP : Why.BYPASS_EVERYWHERE);
return true;
}
// Check if the island is deleted - if so, then nothing is allowed by default
if (isDeletedIsland(island)) {
// Ops or "bypass everywhere" moderators can do anything unless they have switched it off
if (checkBypassEverywhere(e, loc, flag)) {
return true;
}
report(user, e, loc, flag, Why.ISLAND_DELETED);
noGo(e, flag, silent, WORLD_PROTECTED);
return false;
Expand All @@ -203,19 +214,39 @@ public boolean checkIsland(@NonNull Event e, @Nullable Player player, @Nullable
report(user, e, loc, flag, Why.ALLOWED_IN_WORLD);
return true;
}
// Ops or "bypass everywhere" moderators can do anything unless they have switched it off
if (checkBypassEverywhere(e, loc, flag)) {
return true;
}
report(user, e, loc, flag, Why.NOT_ALLOWED_IN_WORLD);
noGo(e, flag, silent, WORLD_PROTECTED);
return false;
}

/**
* Checks the bypass-everywhere permissions and reports if they apply. Only called on
* the deny path so the common allow path skips the permission lookups entirely.
* @return true if the user may bypass protection everywhere
*/
private boolean checkBypassEverywhere(@NonNull Event e, @NonNull Location loc, @NonNull Flag flag) {
if (hasBypassEverywhere(loc, flag)) {
report(user, e, loc, flag, user.isOp() ? Why.OP : Why.BYPASS_EVERYWHERE);
return true;
}
return false;
}

private boolean isDeletedIsland(Optional<Island> island) {
return island.isPresent() && (island.get().isDeleted() || island.get().isDeletable());
}

private boolean hasBypassEverywhere(Location loc, Flag flag) {
return !user.getMetaData(AdminSwitchCommand.META_TAG).map(MetaDataValue::asBoolean).orElse(false)
&& (user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + "mod.bypassprotect")
|| user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + "mod.bypass." + flag.getID() + ".everywhere"));
if (user.getMetaData(AdminSwitchCommand.META_TAG).map(MetaDataValue::asBoolean).orElse(false)) {
return false;
}
String prefix = getIWM().getPermissionPrefix(loc.getWorld());
return user.hasPermission(prefix + "mod.bypassprotect")
|| user.hasPermission(prefix + "mod.bypass." + flag.getID() + ".everywhere");
}

private boolean processBypass(@NonNull Flag flag, Island island, @NonNull Event e, @NonNull Location loc, boolean silent) {
Expand All @@ -224,7 +255,12 @@ private boolean processBypass(@NonNull Flag flag, Island island, @NonNull Event
if (island.isAllowed(user, flag)) {
report(user, e, loc, flag, Why.RANK_ALLOWED);
return true;
} else if (!user.getMetaData(AdminSwitchCommand.META_TAG).map(MetaDataValue::asBoolean).orElse(false)
}
// Ops or "bypass everywhere" moderators can do anything unless they have switched it off
if (checkBypassEverywhere(e, loc, flag)) {
return true;
}
if (!user.getMetaData(AdminSwitchCommand.META_TAG).map(MetaDataValue::asBoolean).orElse(false)
&& (user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + "mod.bypass." + flag.getID() + ".island"))) {
report(user, e, loc, flag, Why.BYPASS_ISLAND);
return true;
Expand All @@ -239,6 +275,10 @@ private boolean processWorldSetting(@NonNull Flag flag, @NonNull Location loc, @
report(user, e, loc, flag, Why.ALLOWED_IN_WORLD);
return true;
}
// Ops or "bypass everywhere" moderators can do anything unless they have switched it off
if (checkBypassEverywhere(e, loc, flag)) {
return true;
}
report(user, e, loc, flag, Why.NOT_ALLOWED_IN_WORLD);
noGo(e, flag, silent, WORLD_PROTECTED);
return false;
Expand All @@ -247,11 +287,13 @@ private boolean processWorldSetting(@NonNull Flag flag, @NonNull Location loc, @
private boolean processSetting(@NonNull Flag flag, Optional<Island> island, @NonNull Event e, @NonNull Location loc) {
// If the island exists, return the setting, otherwise return the default setting for this flag
if (island.isPresent()) {
report(user, e, loc, flag, island.map(x -> x.isAllowed(flag)).orElse(false) ? Why.SETTING_ALLOWED_ON_ISLAND : Why.SETTING_NOT_ALLOWED_ON_ISLAND);
} else {
report(user, e, loc, flag, flag.isSetForWorld(loc.getWorld()) ? Why.SETTING_ALLOWED_IN_WORLD : Why.SETTING_NOT_ALLOWED_IN_WORLD);
boolean allowed = island.get().isAllowed(flag);
report(user, e, loc, flag, allowed ? Why.SETTING_ALLOWED_ON_ISLAND : Why.SETTING_NOT_ALLOWED_ON_ISLAND);
return allowed;
}
return island.map(x -> x.isAllowed(flag)).orElseGet(() -> flag.isSetForWorld(loc.getWorld()));
boolean allowed = flag.isSetForWorld(loc.getWorld());
report(user, e, loc, flag, allowed ? Why.SETTING_ALLOWED_IN_WORLD : Why.SETTING_NOT_ALLOWED_IN_WORLD);
return allowed;
}

/**
Expand All @@ -264,7 +306,12 @@ private boolean processSetting(@NonNull Flag flag, Optional<Island> island, @Non
*/
protected void report(@Nullable User user, @NonNull Event e, @NonNull Location loc, @NonNull Flag flag, @NonNull Why why) {
// A quick way to debug flag listener unit tests is to add this line here: System.out.println(why.name()); NOSONAR
if (user != null && user.isPlayer() && user.getPlayer().getMetadata(loc.getWorld().getName() + "_why_debug").stream()
if (user == null || !user.isPlayer()) {
return;
}
// hasMetadata is a cheap map hit - avoids the metadata list + stream allocations on every check
String whyDebugKey = whyDebugKey(loc.getWorld().getName());
if (user.getPlayer().hasMetadata(whyDebugKey) && user.getPlayer().getMetadata(whyDebugKey).stream()
Comment thread
reecepbcups marked this conversation as resolved.
.filter(p -> p.getOwningPlugin().equals(getPlugin())).findFirst().map(MetadataValue::asBoolean).orElse(false)) {
String whyEvent = WHY + e.getEventName() + " in world " + loc.getWorld().getName() + " at " + Util.xyz(loc.toVector());
String whyBypass = WHY + user.getName() + " " + flag.getID() + " - " + why.name();
Expand Down Expand Up @@ -327,8 +374,9 @@ public void report(@Nullable Addon addon, @NonNull Location loc, @NonNull String
String prefix = addon != null ? "[" + addon.getDescription().getName() + "] " : "";
String whyMessage = WHY + prefix + message + " - " + reason.name() + " in world "
+ loc.getWorld().getName() + " at " + Util.xyz(loc.toVector());
String whyDebugKey = whyDebugKey(loc.getWorld().getName());
Bukkit.getOnlinePlayers().stream()
.filter(p -> p.getMetadata(loc.getWorld().getName() + "_why_debug").stream()
.filter(p -> p.hasMetadata(whyDebugKey) && p.getMetadata(whyDebugKey).stream()
.filter(m -> m.getOwningPlugin().equals(getPlugin()))
.findFirst().map(MetadataValue::asBoolean).orElse(false))
.forEach(p -> {
Expand Down
Loading