Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>1.1.0</build.version>
<build.version>1.1.1</build.version>
<!-- SonarCloud -->
<sonar.projectKey>BentoBoxWorld_ChunkBlock</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
Expand Down
28 changes: 22 additions & 6 deletions src/main/java/world/bentobox/chunkblock/ChunkBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public class ChunkBlock extends GameModeAddon {
/**
* Flag to set who can break the magic block.
*/
public final Flag MAGIC_BLOCK = new Flag.Builder("MAGIC_BLOCK", Material.GRASS_BLOCK)
public final Flag CHUNKBLOCK_MAGIC_BLOCK = new Flag.Builder("CHUNKBLOCK_MAGIC_BLOCK", Material.GRASS_BLOCK)
.mode(Mode.BASIC)
.type(Type.PROTECTION)
.defaultRank(RanksManager.COOP_RANK)
Expand Down Expand Up @@ -175,19 +175,35 @@ public void onLoad() {
adminCommand = new AdminCommand(this);
// Register flag with BentoBox
// Register protection flag with BentoBox
getPlugin().getFlagsManager().registerFlag(this, CHUNKBLOCK_START_SAFETY);
registerFlagOrWarn(CHUNKBLOCK_START_SAFETY);
// Bossbar
if (getSettings().isBossBar()) {
getPlugin().getFlagsManager().registerFlag(this, this.CHUNKBLOCK_BOSSBAR);
registerFlagOrWarn(this.CHUNKBLOCK_BOSSBAR);
}
// Actionbar
if (getSettings().isActionBar()) {
getPlugin().getFlagsManager().registerFlag(this, this.CHUNKBLOCK_ACTIONBAR);
registerFlagOrWarn(this.CHUNKBLOCK_ACTIONBAR);
}
// Magic Block protection
getPlugin().getFlagsManager().registerFlag(this, this.MAGIC_BLOCK);
registerFlagOrWarn(this.CHUNKBLOCK_MAGIC_BLOCK);
// Who may spend level credit on chunks
getPlugin().getFlagsManager().registerFlag(this, this.CHUNKBLOCK_CLAIM_CHUNKS);
registerFlagOrWarn(this.CHUNKBLOCK_CLAIM_CHUNKS);
}
}

/**
* Registers a flag and complains if it is refused. A flag whose ID is already taken by
* another addon is dropped silently by the flags manager, and this addon then runs
* against whichever definition won — so the only symptom would be settings that
* quietly do nothing. Every ID here is prefixed to avoid that, and this says so out
* loud if one ever collides anyway.
*
* @param flag the flag to register
*/
private void registerFlagOrWarn(Flag flag) {
if (!registerFlag(flag)) {
logError("Flag " + flag.getID() + " is already registered by another addon, so ChunkBlock's own "
+ "definition was dropped. Its island settings will behave as that addon defines them.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public ChunkBlockPlaceholders(ChunkBlock addon,
placeholdersManager.registerPlaceholder(addon, "island_next_chunk_level", this::getIslandNextChunkLevel);
placeholdersManager.registerPlaceholder(addon, "island_chunk_credit", this::getIslandChunkCredit);
placeholdersManager.registerPlaceholder(addon, "island_ring", this::getIslandRing);
placeholdersManager.registerPlaceholder(addon, "island_rings_complete", this::getIslandRingsComplete);
}

/**
Expand Down Expand Up @@ -128,6 +129,17 @@ public String getIslandRing(User user) {
return getUsersIsland(user).map(i -> String.valueOf(addon.getChunkManager().currentRing(i))).orElse("");
}

/**
* @param user user
* @return how many whole rings the user's island has closed around its center
*/
public String getIslandRingsComplete(User user) {
if (user == null || user.getUniqueId() == null) {
return "";
}
return getUsersIsland(user).map(i -> String.valueOf(addon.getChunkManager().completedRings(i))).orElse("");
}

/**
* Get the user's owned island. Returns the island owned by the user, not a team
* island they may be visiting as a member. If the user owns more than one island,
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/world/bentobox/chunkblock/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@ public class Settings implements WorldSettings {
@ConfigEntry(path = "chunkblock.claim.confirmation-timeout")
private int claimConfirmationTimeout = 15;

@ConfigComment("Announce ring milestones to the whole server, not just the island's members.")
@ConfigComment("A ring is the square of chunks at a fixed distance from the center chunk:")
@ConfigComment("ring 1 is the eight chunks around the center, ring 2 the sixteen around those.")
@ConfigEntry(path = "chunkblock.rings.broadcast")
private boolean ringBroadcast = false;

@ConfigComment("Console commands run once each time an island completes a whole ring.")
@ConfigComment("Placeholders: [ring] the completed ring, [chunks] the island's chunk count,")
@ConfigComment("[owner] the island owner's name.")
@ConfigComment("Rings are rewarded once per island — re-locking and re-claiming a ring pays")
@ConfigComment("nothing. Rewarding island levels here is not advised: levels buy chunks, so")
@ConfigComment("that makes each ring pay for the next one.")
@ConfigComment("Example: 'eco give [owner] 500'")
@ConfigEntry(path = "chunkblock.rings.commands")
private List<String> ringCommands = new ArrayList<>();

@ConfigComment("Console commands run once for every member of the island, including the")
@ConfigComment("owner and offline members. Placeholders: [player], [ring], [chunks].")
@ConfigComment("Example: 'give [player] diamond 1'")
@ConfigEntry(path = "chunkblock.rings.player-commands")
private List<String> ringPlayerCommands = new ArrayList<>();

@ConfigComment("If true, losing island levels below what has been spent re-locks chunks in")
@ConfigComment("reverse claim order (the most recently claimed chunks are lost first). Builds")
@ConfigComment("inside re-locked chunks are untouched but cannot be reached until the levels")
Expand Down Expand Up @@ -2607,6 +2629,48 @@ public void setMaxChunks(int maxChunks) {
this.maxChunks = maxChunks;
}

/**
* @return true if ring milestones are announced to the whole server
*/
public boolean isRingBroadcast() {
return ringBroadcast;
}

/**
* @param ringBroadcast the ringBroadcast to set
*/
public void setRingBroadcast(boolean ringBroadcast) {
this.ringBroadcast = ringBroadcast;
}

/**
* @return the console commands run once per completed ring, never null
*/
public List<String> getRingCommands() {
return ringCommands == null ? Collections.emptyList() : ringCommands;
}

/**
* @param ringCommands the ringCommands to set
*/
public void setRingCommands(List<String> ringCommands) {
this.ringCommands = ringCommands;
}

/**
* @return the console commands run for each island member per completed ring, never null
*/
public List<String> getRingPlayerCommands() {
return ringPlayerCommands == null ? Collections.emptyList() : ringPlayerCommands;
}

/**
* @param ringPlayerCommands the ringPlayerCommands to set
*/
public void setRingPlayerCommands(List<String> ringPlayerCommands) {
this.ringPlayerCommands = ringPlayerCommands;
}

/**
* @return true if a chunk must be previewed and confirmed before credit is spent
*/
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/world/bentobox/chunkblock/chunks/ChunkManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,48 @@ public int currentRing(Island island) {
return ring;
}

/**
* A ring is the square of chunks at a fixed Chebyshev distance from the center chunk:
* ring 1 is the eight chunks surrounding the center, ring 2 the sixteen around those.
* Ring 0 is the center chunk, which is always unlocked.
*
* @param island the island
* @param ring the ring radius in chunks
* @return true if every chunk in the ring is unlocked; false for rings that do not fit
* inside the island's protection range
*/
public boolean isRingComplete(Island island, int ring) {
if (ring <= 0) {
return true;
}
if (ring > maxRingRadius(island)) {
return false;
}
OneBlockIslands data = addon.getOneBlocksIsland(island);
for (int d = -ring; d <= ring; d++) {
// North and south edges cover the corners, so the east and west edges only
// need the same sweep to close the square
if (!data.isChunkUnlocked(d, -ring) || !data.isChunkUnlocked(d, ring)
|| !data.isChunkUnlocked(-ring, d) || !data.isChunkUnlocked(ring, d)) {
return false;
}
}
return true;
}

/**
* @param island the island
* @return the number of whole rings completed outwards from the center without a gap.
* A claimed chunk two rings out does not count while ring 1 has a hole in it.
*/
public int completedRings(Island island) {
int ring = 0;
while (isRingComplete(island, ring + 1)) {
ring++;
}
return ring;
}

/**
* @param island the island
* @return the island's unlocked chunk offsets in unlock order (x and z are chunk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;
import java.util.Optional;

import net.kyori.adventure.key.Key;
import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
Expand All @@ -23,6 +24,14 @@ public class IslandChunksCommand extends CompositeCommand {
/** Widest map that still fits comfortably in chat */
private static final int MAX_MAP_RADIUS = 7;

/**
* Minecraft's built-in fixed-width font. Chat's default font is proportional, so a
* grid built from mixed glyphs comes out ragged — a row's width depends on which
* chunks happen to be claimed. Only the map rows use it; the rest of the chat stays
* in the normal font.
*/
private static final Key MONOSPACE_FONT = Key.key("minecraft", "uniform");

private ChunkBlock addon;

public IslandChunksCommand(CompositeCommand islandCommand, String label, String[] aliases) {
Expand Down Expand Up @@ -61,6 +70,8 @@ public boolean execute(User user, String label, List<String> args) {
user.sendMessage("chunkblock.chunks.info", "[unlocked]", String.valueOf(unlocked), "[max]",
String.valueOf(max), "[credit]", String.valueOf(credit), "[cost]",
String.valueOf(cm.getChunkCost()));
user.sendMessage("chunkblock.chunks.rings", "[rings]", String.valueOf(cm.completedRings(island)), "[max]",
String.valueOf(cm.maxRingRadius(island)));
showMap(user, island, unlocked, max);
return true;
}
Expand All @@ -83,15 +94,20 @@ private void showMap(User user, Island island, int unlocked, int max) {
StringBuilder row = new StringBuilder();
for (int dx = -radius; dx <= radius; dx++) {
boolean here = dx == playerDx && dz == playerDz;
if (addon.getOneBlocksIsland(island).isChunkUnlocked(dx, dz)) {
if (dx == 0 && dz == 0) {
// The center chunk holds the magic block and can never lock, so it is
// marked in its own right — without it the grid has nothing to orient by
row.append(here ? "&b◉" : "&6◎");
} else if (addon.getOneBlocksIsland(island).isChunkUnlocked(dx, dz)) {
row.append(here ? "&b◆" : "&a■");
} else if (cm.checkGeometry(island, centerChunkX + dx, centerChunkZ + dz) == ClaimResult.OK) {
row.append(here ? "&b◆" : "&e▣");
} else {
row.append(here ? "&b◇" : "&7□");
}
}
user.sendMessage("chunkblock.chunks.map.row", "[row]", row.toString());
user.sendMessage(user.getTranslationAsComponent("chunkblock.chunks.map.row", "[row]", row.toString())
.font(MONOSPACE_FONT));
}
user.sendMessage("chunkblock.chunks.map.legend", "[cost]", String.valueOf(cm.getChunkCost()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public class OneBlockIslands implements DataObject {
@Expose
private long lastKnownLevel = 0;

/**
* The highest ring this island has already been rewarded for completing. Milestones
* are earned once and stay earned: re-locking a ring and claiming it back does not pay
* out again. Only an island create or reset clears it.
*/
@Expose
private int highestRingRewarded = 0;

/** Fast membership view of {@link #unlockedChunks}; rebuilt lazily after loads/edits */
private transient Set<Long> unlockedSet;

Expand Down Expand Up @@ -179,6 +187,20 @@ public void setLastKnownLevel(long lastKnownLevel) {
this.lastKnownLevel = lastKnownLevel;
}

/**
* @return the highest ring this island has already been rewarded for
*/
public int getHighestRingRewarded() {
return highestRingRewarded;
}

/**
* @param highestRingRewarded the highest rewarded ring
*/
public void setHighestRingRewarded(int highestRingRewarded) {
this.highestRingRewarded = highestRingRewarded;
}

/**
* @return the phaseName
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package world.bentobox.chunkblock.events;

import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.eclipse.jdt.annotation.NonNull;

import world.bentobox.bentobox.api.events.BentoBoxEvent;
import world.bentobox.bentobox.database.objects.Island;

/**
* Fired once when an island completes a whole ring of chunks around its center — every
* chunk at Chebyshev distance {@code ring} from the center chunk is unlocked. Rings are
* only ever rewarded once per island: re-locking and re-claiming the same chunks does not
* fire this again.
* <p>
* Cancelling suppresses the addon's own milestone handling (reward commands, messages and
* the celebration); the ring itself stays complete either way.
*
* @author tastybento
*/
public class RingCompleteEvent extends BentoBoxEvent implements Cancellable {

private static final HandlerList handlers = new HandlerList();

private final Island island;
private final int ring;
private final int unlockedChunkCount;
private boolean cancelled;

/**
* @param island the island that completed the ring
* @param ring the ring's radius in chunks, always &gt;= 1
* @param unlockedChunkCount how many chunks the island has unlocked in total
*/
public RingCompleteEvent(@NonNull Island island, int ring, int unlockedChunkCount) {
this.island = island;
this.ring = ring;
this.unlockedChunkCount = unlockedChunkCount;
}

@Override
public HandlerList getHandlers() {
return getHandlerList();
}

public static HandlerList getHandlerList() {
return handlers;
}

/**
* @return the island that completed the ring
*/
@NonNull
public Island getIsland() {
return island;
}

/**
* @return the completed ring's radius in chunks (ring 1 is the eight chunks around the
* center chunk)
*/
public int getRing() {
return ring;
}

/**
* @return the island's total unlocked chunk count including the center chunk
*/
public int getUnlockedChunkCount() {
return unlockedChunkCount;
}

@Override
public boolean isCancelled() {
return cancelled;
}

@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
Loading
Loading