Skip to content

Commit c9aa5c8

Browse files
tastybentoclaude
andcommitted
Add opt-in support for team members' limit permissions
Permission-based limits were applied from the island owner only, which surprises team servers where a member buys a rank with higher limits. New config option apply-member-limit-perms (default false). When enabled, a team member's login merges their limit permissions into the island's limits (highest value wins), and the owner's login still recalculates from scratch and then merges the permissions of all online team members. Coop and trusted players do not qualify. Limits recalculate on login, so revoking a member's permission takes effect at the owner's next login. Fixes #241 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015dbJyrv2uxHfTmiWkqGUJB
1 parent 840bbe7 commit c9aa5c8

4 files changed

Lines changed: 171 additions & 9 deletions

File tree

src/main/java/world/bentobox/limits/Settings.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ enum GeneralGroup {
5050
private final boolean logLimitsOnJoin;
5151
private final boolean asyncGolums;
5252
private final boolean showLimitMessages;
53+
private final boolean applyMemberLimitPerms;
5354
private static final List<EntityType> DISALLOWED = Arrays.asList(
5455
EntityType.TNT,
5556
EntityType.EVOKER_FANGS,
@@ -96,6 +97,8 @@ public Settings(Limits addon) {
9697
asyncGolums = addon.getConfig().getBoolean("async-golums", true);
9798
// Show or suppress the "hit the limit" player notifications
9899
showLimitMessages = addon.getConfig().getBoolean("show-limit-messages", true);
100+
// Apply team members' limit permissions, not just the owner's
101+
applyMemberLimitPerms = addon.getConfig().getBoolean("apply-member-limit-perms", false);
99102

100103
addon.log("Entity limits:");
101104
envLimits.forEach((env, m) -> m.entrySet().stream()
@@ -266,6 +269,13 @@ public boolean isShowLimitMessages() {
266269
return showLimitMessages;
267270
}
268271

272+
/**
273+
* @return true if team members' limit permissions are applied to the island, not just the owner's
274+
*/
275+
public boolean isApplyMemberLimitPerms() {
276+
return applyMemberLimitPerms;
277+
}
278+
269279
public Map<GeneralGroup, Integer> getGeneral() {
270280
return general;
271281
}

src/main/java/world/bentobox/limits/listeners/JoinListener.java

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.bukkit.permissions.PermissionAttachmentInfo;
2121
import org.eclipse.jdt.annotation.NonNull;
2222

23+
import world.bentobox.bentobox.api.addons.GameModeAddon;
2324
import world.bentobox.bentobox.api.events.island.IslandEvent;
2425
import world.bentobox.bentobox.api.events.island.IslandEvent.Reason;
2526
import world.bentobox.bentobox.api.events.team.TeamSetownerEvent;
@@ -34,6 +35,12 @@
3435
/**
3536
* Applies permission-based block, entity, and entity-group limits to islands.
3637
*
38+
* <p>By default only the island owner's permissions count. When
39+
* {@code apply-member-limit-perms} is enabled in the config, team members' permissions
40+
* are merged in as well: a member's login merges their limits on top (highest value
41+
* wins), and the owner's login recalculates from scratch and then merges the
42+
* permissions of all online members.
43+
*
3744
* <p>Permission format:
3845
* <ul>
3946
* <li>5-segment: {@code <gm>.island.limit.<KEY>.<N>} — same limit applied independently
@@ -64,6 +71,16 @@ public void checkPerms(Player player, String permissionPrefix, String islandId,
6471
islandBlockCount.clearAllEntityGroupLimits();
6572
islandBlockCount.clearAllBlockLimits();
6673
}
74+
mergePerms(player, permissionPrefix, islandId, gameMode);
75+
}
76+
77+
/**
78+
* Reads every limit-shaped permission the player has and merges it into the island's
79+
* existing permission-based limits — the highest value wins. Unlike
80+
* {@link #checkPerms}, nothing is cleared first.
81+
*/
82+
public void mergePerms(Player player, String permissionPrefix, String islandId, String gameMode) {
83+
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
6784
for (PermissionAttachmentInfo permissionInfo : player.getEffectivePermissions()) {
6885
if (!permissionInfo.getValue() || !permissionInfo.getPermission().startsWith(permissionPrefix)) {
6986
continue;
@@ -251,16 +268,47 @@ public void onOwnerChange(TeamSetownerEvent event) {
251268

252269
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
253270
public void onPlayerJoin(PlayerJoinEvent event) {
271+
UUID playerUUID = event.getPlayer().getUniqueId();
254272
addon.getGameModes().forEach(gameMode -> addon.getIslands()
255-
.getIslands(gameMode.getOverWorld(), event.getPlayer().getUniqueId()).stream()
256-
.filter(island -> event.getPlayer().getUniqueId().equals(island.getOwner()))
257-
.map(Island::getUniqueId).forEach(islandId -> {
258-
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
259-
if (!joinEventCheck(event.getPlayer(), islandId, islandBlockCount)) {
260-
checkPerms(event.getPlayer(), gameMode.getPermissionPrefix() + "island.limit.", islandId,
261-
gameMode.getDescription().getName());
262-
}
263-
}));
273+
.getIslands(gameMode.getOverWorld(), playerUUID).stream()
274+
.filter(island -> playerUUID.equals(island.getOwner())
275+
|| (addon.getSettings().isApplyMemberLimitPerms()
276+
&& island.getMemberSet().contains(playerUUID)))
277+
.forEach(island -> processJoin(event.getPlayer(), island, gameMode)));
278+
}
279+
280+
private void processJoin(Player player, Island island, GameModeAddon gameMode) {
281+
String islandId = island.getUniqueId();
282+
IslandBlockCount islandBlockCount = addon.getBlockLimitListener().getIsland(islandId);
283+
if (joinEventCheck(player, islandId, islandBlockCount)) {
284+
return;
285+
}
286+
String permissionPrefix = gameMode.getPermissionPrefix() + "island.limit.";
287+
String gameModeName = gameMode.getDescription().getName();
288+
if (player.getUniqueId().equals(island.getOwner())) {
289+
// Owner login recalculates the limits from scratch...
290+
checkPerms(player, permissionPrefix, islandId, gameModeName);
291+
// ...then merges in the perms of any online team members
292+
mergeOnlineMemberPerms(island, permissionPrefix, gameModeName);
293+
} else {
294+
// Member login merges their perms on top of whatever is set — highest wins
295+
mergePerms(player, permissionPrefix, islandId, gameModeName);
296+
}
297+
}
298+
299+
/**
300+
* Merges the limit permissions of every online team member (excluding the owner)
301+
* into the island's limits. No-op unless {@code apply-member-limit-perms} is enabled.
302+
*/
303+
private void mergeOnlineMemberPerms(Island island, String permissionPrefix, String gameModeName) {
304+
if (!addon.getSettings().isApplyMemberLimitPerms()) {
305+
return;
306+
}
307+
island.getMemberSet().stream()
308+
.filter(memberUUID -> !memberUUID.equals(island.getOwner()))
309+
.map(Bukkit::getPlayer)
310+
.filter(Objects::nonNull)
311+
.forEach(member -> mergePerms(member, permissionPrefix, island.getUniqueId(), gameModeName));
264312
}
265313

266314
private boolean joinEventCheck(Player player, String islandId, IslandBlockCount islandBlockCount) {
@@ -303,6 +351,7 @@ private void setOwnerPerms(Island island, UUID ownerUUID) {
303351
if (!permissionPrefix.isEmpty() && !gameModeName.isEmpty() && owner.getPlayer() != null) {
304352
checkPerms(Objects.requireNonNull(owner.getPlayer()), permissionPrefix + "island.limit.",
305353
island.getUniqueId(), gameModeName);
354+
mergeOnlineMemberPerms(island, permissionPrefix + "island.limit.", gameModeName);
306355
}
307356
}
308357
}

src/main/resources/config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ async-golums: true
3434
# silently - placements and spawns are still blocked, but no message is sent.
3535
show-limit-messages: true
3636

37+
# Apply limit permissions from team members as well as the island owner.
38+
# When enabled, a team member's <gamemode>.island.limit.* permissions are merged into
39+
# the island's limits when the member logs in (the highest value wins). The owner's
40+
# login still recalculates the limits from scratch and then merges in the permissions
41+
# of any online team members.
42+
# Limits only recalculate on login, so removing a member (or revoking their permission)
43+
# takes effect the next time the owner logs in.
44+
# Coop and trusted players are not team members and their permissions never apply.
45+
apply-member-limit-perms: false
46+
3747
# General block limiting
3848
# Use this section to limit how many blocks can be added to an island.
3949
# 0 means the item will be blocked from placement completely.

src/test/java/world/bentobox/limits/JoinListenerTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import org.mockito.junit.jupiter.MockitoSettings;
4747
import org.mockito.quality.Strictness;
4848

49+
import com.google.common.collect.ImmutableSet;
50+
4951
import world.bentobox.bentobox.api.addons.AddonDescription;
5052
import world.bentobox.bentobox.api.addons.GameModeAddon;
5153
import world.bentobox.bentobox.api.events.island.IslandEvent;
@@ -398,6 +400,97 @@ void testOnPlayerJoinWithPermLimitsMultiPerms() {
398400
verify(ibc).setEntityLimit(Environment.NORMAL, EntityType.CAVE_SPIDER, 4);
399401
}
400402

403+
// --- Team member limit perms (#241) ---
404+
405+
private PermissionAttachmentInfo mockPerm(String permission) {
406+
PermissionAttachmentInfo permAtt = mock(PermissionAttachmentInfo.class);
407+
when(permAtt.getPermission()).thenReturn(permission);
408+
when(permAtt.getValue()).thenReturn(true);
409+
return permAtt;
410+
}
411+
412+
@Test
413+
void testOnPlayerJoinMemberIgnoredWhenDisabled() {
414+
// Player is a team member, not the owner; feature off (default)
415+
when(island.getOwner()).thenReturn(UUID.randomUUID());
416+
when(settings.isApplyMemberLimitPerms()).thenReturn(false);
417+
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
418+
when(player.getEffectivePermissions()).thenReturn(perms);
419+
420+
jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));
421+
422+
verify(ibc, never()).setBlockLimit(any(), any(), anyInt());
423+
verify(bll, never()).setIsland(anyString(), any());
424+
}
425+
426+
@Test
427+
void testOnPlayerJoinMemberPermsMergedWhenEnabled() {
428+
// Player is a team member, not the owner; feature on
429+
when(island.getOwner()).thenReturn(UUID.randomUUID());
430+
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid));
431+
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
432+
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
433+
when(player.getEffectivePermissions()).thenReturn(perms);
434+
435+
jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));
436+
437+
verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24);
438+
// A member join merges — it must never clear the island's existing perm limits
439+
verify(ibc, never()).clearAllBlockLimits();
440+
verify(ibc, never()).clearAllEntityLimits();
441+
verify(ibc, never()).clearAllEntityGroupLimits();
442+
}
443+
444+
@Test
445+
void testOnPlayerJoinMemberMergeKeepsHigherExistingLimit() {
446+
// Island already has STONE at 30 (e.g. from the owner); member perm of 24 must not lower it
447+
when(island.getOwner()).thenReturn(UUID.randomUUID());
448+
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid));
449+
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
450+
when(ibc.getBlockLimit(any(Environment.class), any(NamespacedKey.class))).thenReturn(30);
451+
Set<PermissionAttachmentInfo> perms = Set.of(mockPerm("bskyblock.island.limit.STONE.24"));
452+
when(player.getEffectivePermissions()).thenReturn(perms);
453+
454+
jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));
455+
456+
verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 30);
457+
verify(ibc, never()).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 24);
458+
}
459+
460+
@Test
461+
void testOnPlayerJoinOwnerMergesOnlineMemberPerms() {
462+
// Owner joins with no perms of their own; an online member has STONE.44
463+
UUID memberUUID = UUID.randomUUID();
464+
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid, memberUUID));
465+
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
466+
Player member = mock(Player.class);
467+
when(member.getUniqueId()).thenReturn(memberUUID);
468+
when(member.getName()).thenReturn("member");
469+
Set<PermissionAttachmentInfo> memberPerms = Set.of(mockPerm("bskyblock.island.limit.STONE.44"));
470+
when(member.getEffectivePermissions()).thenReturn(memberPerms);
471+
mockedBukkit.when(() -> Bukkit.getPlayer(memberUUID)).thenReturn(member);
472+
473+
jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));
474+
475+
// Owner join recalculates from scratch, then the member's perm is merged in
476+
verify(ibc).clearAllBlockLimits();
477+
verify(ibc).setBlockLimit(Environment.NORMAL, Material.STONE.getKey(), 44);
478+
}
479+
480+
@Test
481+
void testOnPlayerJoinOwnerIgnoresOfflineMemberPerms() {
482+
// Owner joins; the only member is offline, so nothing extra is merged
483+
UUID memberUUID = UUID.randomUUID();
484+
when(island.getMemberSet()).thenReturn(ImmutableSet.of(uuid, memberUUID));
485+
when(settings.isApplyMemberLimitPerms()).thenReturn(true);
486+
mockedBukkit.when(() -> Bukkit.getPlayer(memberUUID)).thenReturn(null);
487+
488+
jl.onPlayerJoin(new PlayerJoinEvent(player, Component.text("welcome")));
489+
490+
verify(ibc).clearAllBlockLimits();
491+
verify(ibc, never()).setBlockLimit(any(), any(), anyInt());
492+
}
493+
401494
/**
402495
* Test method for {@link world.bentobox.limits.listeners.JoinListener#onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent)}.
403496
*/

0 commit comments

Comments
 (0)