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
10 changes: 10 additions & 0 deletions src/main/java/world/bentobox/limits/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ enum GeneralGroup {
private final boolean asyncGolums;
private final boolean showLimitMessages;
private final boolean stackedPlantsCountAsOne;
private final boolean applyMemberLimitPerms;
private static final List<EntityType> DISALLOWED = Arrays.asList(
EntityType.TNT,
EntityType.EVOKER_FANGS,
Expand Down Expand Up @@ -99,6 +100,8 @@ public Settings(Limits addon) {
showLimitMessages = addon.getConfig().getBoolean("show-limit-messages", true);
// Count a stackable plant column (sugar cane, bamboo) as a single plant
stackedPlantsCountAsOne = addon.getConfig().getBoolean("stacked-plants-count-as-one", false);
// Apply team members' limit permissions, not just the owner's
applyMemberLimitPerms = addon.getConfig().getBoolean("apply-member-limit-perms", false);

addon.log("Entity limits:");
envLimits.forEach((env, m) -> m.entrySet().stream()
Expand Down Expand Up @@ -277,6 +280,13 @@ public boolean isStackedPlantsCountAsOne() {
return stackedPlantsCountAsOne;
}

/**
* @return true if team members' limit permissions are applied to the island, not just the owner's
*/
public boolean isApplyMemberLimitPerms() {
return applyMemberLimitPerms;
}

public Map<GeneralGroup, Integer> getGeneral() {
return general;
}
Expand Down
67 changes: 58 additions & 9 deletions src/main/java/world/bentobox/limits/listeners/JoinListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.eclipse.jdt.annotation.NonNull;

import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.events.island.IslandEvent;
import world.bentobox.bentobox.api.events.island.IslandEvent.Reason;
import world.bentobox.bentobox.api.events.team.TeamSetownerEvent;
Expand All @@ -34,6 +35,12 @@
/**
* Applies permission-based block, entity, and entity-group limits to islands.
*
* <p>By default only the island owner's permissions count. When
* {@code apply-member-limit-perms} is enabled in the config, team members' permissions
* are merged in as well: a member's login merges their limits on top (highest value
* wins), and the owner's login recalculates from scratch and then merges the
* permissions of all online members.
*
* <p>Permission format:
* <ul>
* <li>5-segment: {@code <gm>.island.limit.<KEY>.<N>} — same limit applied independently
Expand Down Expand Up @@ -64,6 +71,16 @@
islandBlockCount.clearAllEntityGroupLimits();
islandBlockCount.clearAllBlockLimits();
}
mergePerms(player, permissionPrefix, islandId, gameMode);
}

/**
* Reads every limit-shaped permission the player has and merges it into the island's
* existing permission-based limits — the highest value wins. Unlike
* {@link #checkPerms}, nothing is cleared first.
*/
public void mergePerms(Player player, String permissionPrefix, String islandId, String gameMode) {
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
for (PermissionAttachmentInfo permissionInfo : player.getEffectivePermissions()) {
if (!permissionInfo.getValue() || !permissionInfo.getPermission().startsWith(permissionPrefix)) {
continue;
Expand Down Expand Up @@ -251,16 +268,47 @@

@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
UUID playerUUID = event.getPlayer().getUniqueId();
addon.getGameModes().forEach(gameMode -> addon.getIslands()
.getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream()
.filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner()))
.map(Island::getUniqueId).forEach(islandId -> {
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) {
checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId,
gameMode.getDescription().getName());
}
}));
.getIslands(gameMode.getOverWorld(), playerUUID).stream()
.filter(island -> playerUUID.equals(island.getOwner())
|| (addon.getSettings().isApplyMemberLimitPerms()
&& island.getMemberSet().contains(playerUUID)))
.forEach(island -> processJoin(event.getPlayer(), island, gameMode)));
}

private void processJoin(Player player, Island island, GameModeAddon gameMode) {
String islandId = island.getUniqueId();
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
if (joinEventCheck(player, islandId, islandBlockCount)) {
return;
}
String permissionPrefix = gameMode.getPermissionPrefix() + "island.limit.";

Check failure on line 286 in src/main/java/world/bentobox/limits/listeners/JoinListener.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "island.limit." 3 times.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Limits&issues=AZ9Mrb3SoE4wv4dolHxC&open=AZ9Mrb3SoE4wv4dolHxC&pullRequest=282
String gameModeName = gameMode.getDescription().getName();
if (player.getUniqueId().equals(island.getOwner())) {
// Owner login recalculates the limits from scratch...
checkPerms(player, permissionPrefix, islandId, gameModeName);
// ...then merges in the perms of any online team members
mergeOnlineMemberPerms(island, permissionPrefix, gameModeName);
} else {
// Member login merges their perms on top of whatever is set — highest wins
mergePerms(player, permissionPrefix, islandId, gameModeName);
}
}

/**
* Merges the limit permissions of every online team member (excluding the owner)
* into the island's limits. No-op unless {@code apply-member-limit-perms} is enabled.
*/
private void mergeOnlineMemberPerms(Island island, String permissionPrefix, String gameModeName) {
if (!addon.getSettings().isApplyMemberLimitPerms()) {
return;
}
island.getMemberSet().stream()
.filter(memberUUID -> !memberUUID.equals(island.getOwner()))
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.forEach(member -> mergePerms(member, permissionPrefix, island.getUniqueId(), gameModeName));
}

private boolean joinEventCheck(Player player, String islandId, IslandBlockCount islandBlockCount) {
Expand Down Expand Up @@ -303,6 +351,7 @@
if (!permissionPrefix.isEmpty() && !gameModeName.isEmpty() && owner.getPlayer() != null) {
checkPerms(Objects.requireNonNull(owner.getPlayer()), permissionPrefix + "island.limit.",
island.getUniqueId(), gameModeName);
mergeOnlineMemberPerms(island, permissionPrefix + "island.limit.", gameModeName);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ async-golums: true
# silently - placements and spawns are still blocked, but no message is sent.
show-limit-messages: true

# Apply limit permissions from team members as well as the island owner.
# When enabled, a team member's <gamemode>.island.limit.* permissions are merged into
# the island's limits when the member logs in (the highest value wins). The owner's
# login still recalculates the limits from scratch and then merges in the permissions
# of any online team members.
# Limits only recalculate on login, so removing a member (or revoking their permission)
# takes effect the next time the owner logs in.
# Coop and trusted players are not team members and their permissions never apply.
apply-member-limit-perms: false

# General block limiting
# Use this section to limit how many blocks can be added to an island.
# 0 means the item will be blocked from placement completely.
Expand Down
93 changes: 93 additions & 0 deletions src/test/java/world/bentobox/limits/JoinListenerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;

import com.google.common.collect.ImmutableSet;

import world.bentobox.bentobox.api.addons.AddonDescription;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.events.island.IslandEvent;
Expand Down Expand Up @@ -398,6 +400,97 @@ void testOnPlayerJoinWithPermLimitsMultiPerms() {
verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.CAVE_SPIDER, 4);
}

// --- Team member limit perms (#241) ---

private PermissionAttachmentInfo mockPerm(String permission) {
PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class);
when(permAtt.getPermission()).thenReturn(permission);
when(permAtt.getValue()).thenReturn(true);
return permAtt;
}

@Test
void testOnPlayerJoinMemberIgnoredWhenDisabled() {
// Player is a team member, not the owner; feature off (default)
when(island.getOwner()).thenReturn(UUID.randomUUID());
when(settings.isApplyMemberLimitPerms()).thenReturn(false);
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
when(player.getEffectivePermissions()).thenReturn(perms);

jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));

verify(ibc, never()).setBlockLimit(any(), any(), anyInt());
verify(bll, never()).setIsland(anyString(), any());
}

@Test
void testOnPlayerJoinMemberPermsMergedWhenEnabled() {
// Player is a team member, not the owner; feature on
when(island.getOwner()).thenReturn(UUID.randomUUID());
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid));
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
when(player.getEffectivePermissions()).thenReturn(perms);

jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));

verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24);
// A member join merges — it must never clear the island's existing perm limits
verify(ibc, never()).clearAllBlockLimits();
verify(ibc, never()).clearAllEntityLimits();
verify(ibc, never()).clearAllEntityGroupLimits();
}

@Test
void testOnPlayerJoinMemberMergeKeepsHigherExistingLimit() {
// Island already has STONE at 30 (e.g. from the owner); member perm of 24 must not lower it
when(island.getOwner()).thenReturn(UUID.randomUUID());
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid));
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
when(ibc.getBlockLimit(any(Environment.class), any(NamespacedKey.class))).thenReturn(30);
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
when(player.getEffectivePermissions()).thenReturn(perms);

jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));

verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 30);
verify(ibc, never()).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24);
}

@Test
void testOnPlayerJoinOwnerMergesOnlineMemberPerms() {
// Owner joins with no perms of their own; an online member has STONE.44
UUID memberUUID = UUID.randomUUID();
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid, memberUUID));
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
Player member = mock(Player.class);
when(member.getUniqueId()).thenReturn(memberUUID);
when(member.getName()).thenReturn("member");
Set<PermissionAttachmentInfo> memberPerms = Set.of(mockPerm("bskyblock.island.limit.STONE.44"));
when(member.getEffectivePermissions()).thenReturn(memberPerms);
mockedBukkit.when(() -> Bukkit.getPlayer(memberUUID)).thenReturn(member);

jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));

// Owner join recalculates from scratch, then the member's perm is merged in
verify(ibc).clearAllBlockLimits();
verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 44);
}

@Test
void testOnPlayerJoinOwnerIgnoresOfflineMemberPerms() {
// Owner joins; the only member is offline, so nothing extra is merged
UUID memberUUID = UUID.randomUUID();
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid, memberUUID));
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
mockedBukkit.when(() -> Bukkit.getPlayer(memberUUID)).thenReturn(null);

jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));

verify(ibc).clearAllBlockLimits();
verify(ibc, never()).setBlockLimit(any(), any(), anyInt());
}

/**
* Test method for {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}.
*/
Expand Down
Loading