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
11 changes: 11 additions & 0 deletions src/main/java/world/bentobox/chunkblock/ChunkBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@
.type(Type.PROTECTION)
.defaultRank(RanksManager.COOP_RANK)
.build();
/**
* Flag to set who can spend the island's level credit on new chunks. Defaults to the
* owner alone, because a claim is irreversible until the levels are earned back.
*/
public final Flag CHUNKBLOCK_CLAIM_CHUNKS = new Flag.Builder("CHUNKBLOCK_CLAIM_CHUNKS", Material.OAK_FENCE_GATE)

Check warning on line 137 in src/main/java/world/bentobox/chunkblock/ChunkBlock.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this field "CHUNKBLOCK_CLAIM_CHUNKS" to match the regular expression '^[a-z][a-zA-Z0-9]*$'.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_ChunkBlock&issues=AZ_i6YanNUsYMSHZ0Rbn&open=AZ_i6YanNUsYMSHZ0Rbn&pullRequest=19
.mode(Mode.BASIC)
.type(Type.PROTECTION)
.defaultRank(RanksManager.OWNER_RANK)
.build();

@Override
public void onLoad() {
Expand Down Expand Up @@ -177,6 +186,8 @@
}
// Magic Block protection
getPlugin().getFlagsManager().registerFlag(this, this.MAGIC_BLOCK);
// Who may spend level credit on chunks
getPlugin().getFlagsManager().registerFlag(this, this.CHUNKBLOCK_CLAIM_CHUNKS);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@

import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.chunkblock.ChunkBlock;
import world.bentobox.chunkblock.chunks.ChunkManager;
import world.bentobox.chunkblock.chunks.ChunkManager.ClaimResult;

/**
* Lets the island owner spend level credit by hitting the border: when they punch (or
* right-click) toward the locked chunk blocking them, that chunk is claimed and opens up.
* Expansion is the owner's choice, in any direction, up to the island's protection range.
* Lets an island spend level credit by hitting the border: when a player punches (or
* right-clicks) toward the locked chunk blocking them, that chunk is claimed and opens up.
* Expansion goes in any direction, up to the island's protection range. Who is allowed to
* spend is the CHUNKBLOCK_CLAIM_CHUNKS island setting, owner-only unless an island opens it
* to lower ranks.
* <p>
* Levels are hard-won, so by default a claim takes two deliberate gestures: the first hit
* outlines the target chunk and quotes the price, and only a second hit made while sneaking
Expand Down Expand Up @@ -107,8 +110,10 @@ public void onBorderHit(PlayerInteractEvent e) {
return;
}
Island island = optionalIsland.get();
// Claiming is the owner's call
if (!player.getUniqueId().equals(island.getOwner())) {
User user = User.getInstance(player);
// Who may spend the island's credit is an island setting, owner-only by default
if (!island.isAllowed(user, addon.CHUNKBLOCK_CLAIM_CHUNKS)) {
denyClaim(user, island);
return;
}
ChunkManager cm = addon.getChunkManager();
Expand All @@ -133,7 +138,20 @@ public void onBorderHit(PlayerInteractEvent e) {
if (target == null) {
return;
}
attemptClaim(User.getInstance(player), island, target[0], target[1]);
attemptClaim(user, island, target[0], target[1]);
}

/**
* Tells a teammate whose rank is too low that expansion is not theirs to spend on.
* Visitors and passers-by are told nothing: they get the ordinary locked-chunk message
* from the guard listener instead, and have no business hearing about the island's
* credit.
*/
private void denyClaim(User user, Island island) {
if (island.getRank(user) > RanksManager.VISITOR_RANK && feedbackReady(user.getUniqueId())) {
user.notify(addon.CHUNKBLOCK_CLAIM_CHUNKS.getHintReference());
user.getPlayer().playSound(user.getLocation(), Sound.BLOCK_NOTE_BLOCK_BASS, 1F, 0.6F);
}
}

/**
Expand Down Expand Up @@ -185,7 +203,7 @@ private int[] findTargetLockedChunk(Player player, Island island) {
* enabled a claimable chunk is only previewed the first time round; the credit is spent
* on the confirming hit.
*
* @param user the island owner
* @param user the player spending the credit, already checked against the claim flag
* @param island the island
* @param chunkX target world chunk x
* @param chunkZ target world chunk z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ public void onPlayerMove(PlayerMoveEvent e) {
}

/**
* Tells a player bumping into the border what to do about it: owners aiming at a
* claimable chunk are invited to hit the border (or told how many levels they still
* need); everyone else just learns the chunk is locked.
* Tells a player bumping into the border what to do about it: those allowed to spend
* the island's credit on a claimable chunk are invited to hit the border (or told how
* many levels they still need); everyone else just learns the chunk is locked.
*/
private void sendBumpMessage(Player player, Location to) {
User user = User.getInstance(player);
Optional<Island> optionalIsland = islandAt(to);
if (optionalIsland.isPresent() && player.getUniqueId().equals(optionalIsland.get().getOwner())) {
if (optionalIsland.isPresent() && optionalIsland.get().isAllowed(user, addon.CHUNKBLOCK_CLAIM_CHUNKS)) {
Island island = optionalIsland.get();
ChunkManager cm = addon.getChunkManager();
if (cm.checkGeometry(island, to.getBlockX() >> 4, to.getBlockZ() >> 4) == ChunkManager.ClaimResult.OK) {
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ world:
CHORUS_FRUIT: 500
CONTAINER: 500
MAGIC_BLOCK: 200
CHUNKBLOCK_CLAIM_CHUNKS: 1000
JUKEBOX: 500
POTION_THROWING: 500
BARREL: 500
Expand Down
7 changes: 7 additions & 0 deletions src/main/resources/locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ protection:
&b Rank that can break the magic
&b block if they can break blocks.
hint: "&c Your rank cannot break the magic block!"
CHUNKBLOCK_CLAIM_CHUNKS:
name: Claim Chunks
description: |-
&b Rank that can spend the
&b island's level credit to
&b claim new chunks.
hint: "&c Your rank cannot claim chunks for this island!"
CHUNKBLOCK_START_SAFETY:
name: Starting Safety
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -25,6 +28,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.chunkblock.ChunkBlock;
import world.bentobox.chunkblock.CommonTestSetup;
import world.bentobox.chunkblock.Settings;
Expand All @@ -51,24 +56,25 @@ class ChunkClaimListenerTest extends CommonTestSetup {
@BeforeEach
public void setUp() throws Exception {
super.setUp();
addon = mock(ChunkBlock.class);
when(addon.getPlugin()).thenReturn(plugin);
when(addon.inWorld(world)).thenReturn(true);
when(addon.getIslands()).thenReturn(im);
// A spy on a real addon, so the CHUNKBLOCK_CLAIM_CHUNKS flag field is built
addon = spy(new ChunkBlock());
doReturn(plugin).when(addon).getPlugin();
doReturn(true).when(addon).inWorld(world);
doReturn(im).when(addon).getIslands();
settings = new Settings();
when(addon.getSettings()).thenReturn(settings);
addon.setSettings(settings);
borderDisplay = mock(BorderDisplay.class);
when(addon.getBorderDisplay()).thenReturn(borderDisplay);
doReturn(borderDisplay).when(addon).getBorderDisplay();
ChunkManager cm = new ChunkManager(addon);
when(addon.getChunkManager()).thenReturn(cm);
doReturn(cm).when(addon).getChunkManager();
data = new OneBlockIslands("test");
when(addon.getOneBlocksIsland(island)).thenReturn(data);
doReturn(data).when(addon).getOneBlocksIsland(island);
BlockListener blockListener = mock(BlockListener.class);
when(addon.getBlockListener()).thenReturn(blockListener);
doReturn(blockListener).when(addon).getBlockListener();
levelListener = mock(LevelListener.class);
when(addon.getLevelListener()).thenReturn(levelListener);
doReturn(levelListener).when(addon).getLevelListener();
level = 0;
when(addon.getIslandLevel(island)).thenAnswer(i -> level);
doAnswer(i -> level).when(addon).getIslandLevel(island);

// Island center chunk (0, 0)
when(island.getCenter()).thenReturn(location);
Expand All @@ -77,6 +83,8 @@ public void setUp() throws Exception {
when(island.getProtectionRange()).thenReturn(240);
when(island.getOwner()).thenReturn(uuid);
when(im.getIslandAt(any())).thenReturn(Optional.of(island));
// By default the player may claim: rank checks have their own tests
when(island.isAllowed(any(User.class), eq(addon.CHUNKBLOCK_CLAIM_CHUNKS))).thenReturn(true);

// Player stands near the east edge of the center chunk, looking east (+x):
// yaw -90 in Bukkit faces +x
Expand Down Expand Up @@ -133,11 +141,34 @@ void testNoCreditNoClaim() {
}

@Test
void testNonOwnerCannotClaim() {
void testPlayerBelowTheClaimRankCannotClaim() {
level = 100;
when(island.isAllowed(any(User.class), eq(addon.CHUNKBLOCK_CLAIM_CHUNKS))).thenReturn(false);
when(island.getRank(any(User.class))).thenReturn(RanksManager.MEMBER_RANK);
hitAndConfirm(Action.LEFT_CLICK_AIR);
assertFalse(data.isChunkUnlocked(1, 0));
// A teammate who cannot claim is told why rather than left wondering
verify(notifier).notify(any(), eq("protection.flags.CHUNKBLOCK_CLAIM_CHUNKS.hint"));
}

@Test
void testTeammateAtOrAboveTheClaimRankCanClaim() {
// The island has opened claiming up: a member who is not the owner may spend
level = 1;
when(island.getOwner()).thenReturn(UUID.randomUUID());
when(island.getRank(any(User.class))).thenReturn(RanksManager.MEMBER_RANK);
hitAndConfirm(Action.LEFT_CLICK_AIR);
assertTrue(data.isChunkUnlocked(1, 0));
}

@Test
void testVisitorIsNotToldAboutTheIslandsCredit() {
level = 100;
when(island.isAllowed(any(User.class), eq(addon.CHUNKBLOCK_CLAIM_CHUNKS))).thenReturn(false);
when(island.getRank(any(User.class))).thenReturn(RanksManager.VISITOR_RANK);
listener.onBorderHit(hit(Action.LEFT_CLICK_AIR));
assertFalse(data.isChunkUnlocked(1, 0));
verify(notifier, never()).notify(any(), any());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand All @@ -25,6 +28,7 @@

import com.google.common.collect.ImmutableSet;

import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.bentobox.util.Util;
import world.bentobox.chunkblock.ChunkBlock;
Expand All @@ -48,16 +52,17 @@ class ChunkGuardListenerTest extends CommonTestSetup {
@BeforeEach
public void setUp() throws Exception {
super.setUp();
addon = mock(ChunkBlock.class);
when(addon.getPlugin()).thenReturn(plugin);
when(addon.inWorld(world)).thenReturn(true);
when(addon.getIslands()).thenReturn(im);
// A spy on a real addon, so the CHUNKBLOCK_CLAIM_CHUNKS flag field is built
addon = spy(new ChunkBlock());
doReturn(plugin).when(addon).getPlugin();
doReturn(true).when(addon).inWorld(world);
doReturn(im).when(addon).getIslands();
Settings settings = new Settings();
when(addon.getSettings()).thenReturn(settings);
addon.setSettings(settings);
ChunkManager cm = new ChunkManager(addon);
when(addon.getChunkManager()).thenReturn(cm);
doReturn(cm).when(addon).getChunkManager();
data = new OneBlockIslands("test");
when(addon.getOneBlocksIsland(island)).thenReturn(data);
doReturn(data).when(addon).getOneBlocksIsland(island);

// Island centered at block (8, y, 8) → center chunk (0, 0); only that chunk unlocked
when(island.getCenter()).thenReturn(location);
Expand Down Expand Up @@ -95,6 +100,22 @@ void testMoveIntoLockedChunkCancelledAndTeleportedBack() {
mockedUtil.verify(() -> Util.teleportAsync(mockPlayer, location));
}

@Test
void testBumpInvitesAPlayerAllowedToClaim() {
when(island.isAllowed(any(User.class), eq(addon.CHUNKBLOCK_CLAIM_CHUNKS))).thenReturn(true);
doReturn(100L).when(addon).getIslandLevel(island);
listener.onPlayerMove(new PlayerMoveEvent(mockPlayer, location, lockedTo));
verify(notifier).notify(any(), eq("chunkblock.chunks.claim-hint"));
}

@Test
void testBumpJustSaysLockedToAPlayerWhoCannotClaim() {
// island.isAllowed is false by default in CommonTestSetup
doReturn(100L).when(addon).getIslandLevel(island);
listener.onPlayerMove(new PlayerMoveEvent(mockPlayer, location, lockedTo));
verify(notifier).notify(any(), eq("chunkblock.chunks.entry-denied"));
}

@Test
void testMoveWithinUnlockedChunkAllowed() {
PlayerMoveEvent e = new PlayerMoveEvent(mockPlayer, location, unlockedTo);
Expand Down
Loading