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
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.2.1</build.version>
<build.version>1.3.0</build.version>
<!-- SonarCloud -->
<sonar.projectKey>BentoBoxWorld_ChunkBlock</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public ChunkBlockPlaceholders(ChunkBlock addon,
placeholdersManager.registerPlaceholder(addon, "island_chunk_credit", this::getIslandChunkCredit);
placeholdersManager.registerPlaceholder(addon, "island_ring", this::getIslandRing);
placeholdersManager.registerPlaceholder(addon, "island_rings_complete", this::getIslandRingsComplete);
placeholdersManager.registerPlaceholder(addon, "island_next_ring_remaining", this::getNextRingRemaining);
placeholdersManager.registerPlaceholder(addon, "island_title", this::getIslandTitle);
placeholdersManager.registerPlaceholder(addon, "island_trophies", this::getIslandTrophies);
}
Expand Down Expand Up @@ -167,6 +168,24 @@ public String getIslandRingsComplete(User user) {
return getUsersIsland(user).map(i -> String.valueOf(addon.getChunkManager().completedRings(i))).orElse("");
}

/**
* @param user user
* @return how many chunks remain to complete the user's island's next ring, or "0" if
* all rings are done
*/
public String getNextRingRemaining(User user) {
if (user == null || user.getUniqueId() == null) {
return "";
}
return getUsersIsland(user).map(i -> {
int next = addon.getChunkManager().completedRings(i) + 1;
if (next > addon.getChunkManager().maxRingRadius(i)) {
return "0";
}
return String.valueOf(addon.getChunkManager().chunksRemainingInRing(i, next));
}).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
3 changes: 2 additions & 1 deletion src/main/java/world/bentobox/chunkblock/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,9 @@ public class Settings implements WorldSettings {
@ConfigComment("Admins can change protection sizes for players individually using /chadmin range set <player> <new range>")
@ConfigComment("or set this permission: chunkblock.island.range.<number>")
@ConfigComment("ChunkBlock: this must cover the largest unlockable ring of chunks (see chunkblock.max-chunks).")
@ConfigComment("With max-chunks 441 (21x21, ring 10) the minimum needed is 168.")
@ConfigEntry(path = "world.protection-range")
private int islandProtectionRange = 240;
private int islandProtectionRange = 168;

@ConfigComment("Start islands at these coordinates. This is where new islands will start in the")
@ConfigComment("world. These must be a factor of your island distance, but the plugin will auto")
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/world/bentobox/chunkblock/chunks/ChunkManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,28 @@ public int completedRings(Island island) {
return ring;
}

/**
* @param island the island
* @param ring the ring radius to check
* @return how many chunks in the ring are still locked; 0 means the ring is complete
*/
public int chunksRemainingInRing(Island island, int ring) {
if (ring <= 0) {
return 0;
}
OneBlockIslands data = addon.getOneBlocksIsland(island);
int missing = 0;
for (int d = -ring; d <= ring; d++) {
if (!data.isChunkUnlocked(d, -ring)) missing++;
if (!data.isChunkUnlocked(d, ring)) missing++;
if (d != -ring && d != ring) {
if (!data.isChunkUnlocked(-ring, d)) missing++;
if (!data.isChunkUnlocked(ring, d)) missing++;
}
}
return missing;
}

/**
* @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 @@ -80,8 +80,17 @@ 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)));
int completedRings = cm.completedRings(island);
int maxRing = cm.maxRingRadius(island);
user.sendMessage("chunkblock.chunks.rings", "[rings]", String.valueOf(completedRings), "[max]",
String.valueOf(maxRing));
int nextRing = completedRings + 1;
if (nextRing <= maxRing) {
int remaining = cm.chunksRemainingInRing(island, nextRing);
int total = 8 * nextRing;
user.sendMessage("chunkblock.chunks.ring-progress", "[ring]", String.valueOf(nextRing),
"[remaining]", String.valueOf(remaining), "[total]", String.valueOf(total));
}
showMap(user, island, unlocked, max);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package world.bentobox.chunkblock.commands.island;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import world.bentobox.bentobox.api.commands.CompositeCommand;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
import world.bentobox.chunkblock.ChunkBlock;
import world.bentobox.chunkblock.activity.ActivityManager;
import world.bentobox.chunkblock.activity.CounterType;

/**
* /ch ledger — shows each team member's contributions to the island: blocks broken,
* chunks claimed, and rings completed. Reads from the activity counters foundation;
* the ledger itself adds no persistence.
*
* @author tastybento
*/
public class IslandLedgerCommand extends CompositeCommand {

private static final String REF = "chunkblock.commands.ledger.";

private ChunkBlock addon;

public IslandLedgerCommand(CompositeCommand islandCommand, String label, String[] aliases) {
super(islandCommand, label, aliases);
}

@Override
public void setup() {
setDescription(REF + "description");
setParametersHelp(REF + "parameters");
setOnlyPlayer(true);
setPermission("island.ledger");
addon = getAddon();
}

@Override
public boolean canExecute(User user, String label, List<String> args) {
if (!Util.sameWorld(getWorld(), user.getWorld())) {
user.sendMessage("general.errors.wrong-world");
return false;
}
return true;
}

@Override
public boolean execute(User user, String label, List<String> args) {
Optional<Island> optionalIsland = getIslands().getIslandAt(user.getLocation());
if (optionalIsland.isEmpty()) {
user.sendMessage("general.errors.not-on-island");
return false;
}
Island island = optionalIsland.get();
ActivityManager am = addon.getActivityManager();
if (am == null) {
return false;
}

int window = parseWindow(args);
String windowLabel = window <= 0
? user.getTranslation(REF + "all-time")
: user.getTranslation(REF + "last-days", "[days]", String.valueOf(window));

user.sendMessage(REF + "header", "[window]", windowLabel);

List<MemberRow> rows = buildRows(island, am, window);
if (rows.isEmpty()) {
user.sendMessage(REF + "no-activity");
return true;
}

for (MemberRow row : rows) {
user.sendMessage(REF + "row",
"[name]", row.name,
"[blocks]", String.valueOf(row.blocks),
"[chunks]", String.valueOf(row.chunks),
"[rings]", String.valueOf(row.rings));
}

long totalBlocks = rows.stream().mapToLong(r -> r.blocks).sum();
long totalChunks = rows.stream().mapToLong(r -> r.chunks).sum();
long totalRings = rows.stream().mapToLong(r -> r.rings).sum();
user.sendMessage(REF + "total",
"[blocks]", String.valueOf(totalBlocks),
"[chunks]", String.valueOf(totalChunks),
"[rings]", String.valueOf(totalRings));

return true;
}

private int parseWindow(List<String> args) {
if (args.isEmpty()) {
return 0;
}
try {
return Math.max(0, Integer.parseInt(args.get(0)));
} catch (NumberFormatException e) {
return 0;
}
}

private List<MemberRow> buildRows(Island island, ActivityManager am, int window) {
List<MemberRow> rows = new ArrayList<>();
for (UUID uuid : am.getContributors(island)) {
long blocks = am.getCount(island, uuid, CounterType.MAGIC_BLOCKS, window);
long chunks = am.getCount(island, uuid, CounterType.CHUNKS_CLAIMED, window)
+ am.getCount(island, uuid, CounterType.CHUNKS_RECLAIMED, window);
long rings = am.getCount(island, uuid, CounterType.RINGS_COMPLETED, window);
if (blocks > 0 || chunks > 0 || rings > 0) {
String name = addon.getPlayers().getName(uuid);
rows.add(new MemberRow(name == null || name.isEmpty() ? uuid.toString() : name,
blocks, chunks, rings));
}
}
rows.sort(Comparator.comparingLong(MemberRow::blocks).reversed());
return rows;
}

private record MemberRow(String name, long blocks, long chunks, long rings) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public boolean execute(User user, String label, List<String> args) {
return false;
}
if (args.isEmpty()) {
return toggleTitle(user, island);
}
if ("list".equalsIgnoreCase(args.get(0))) {
showTitles(user, island);
return true;
}
Expand Down Expand Up @@ -105,13 +108,33 @@ private void showTitles(User user, Island island) {
}
}

private boolean toggleTitle(User user, Island island) {
String active = addon.getTrophyManager().getActiveTitleText(island);
if (!active.isEmpty()) {
addon.getTrophyManager().setActiveTitle(island, null);
user.sendMessage("chunkblock.commands.title.toggled-off", TITLE_VAR, active);
return true;
}
// No active title — try to activate the first earned trophy that carries one
Optional<Trophy> first = addon.getTrophyManager().getEarned(island).stream()
.filter(t -> t.title() != null).findFirst();
if (first.isEmpty()) {
user.sendMessage("chunkblock.commands.title.none-earned-yet");
return false;
}
addon.getTrophyManager().setActiveTitle(island, first.get().id());
user.sendMessage("chunkblock.commands.title.toggled-on", TITLE_VAR, first.get().title());
return true;
}

@Override
public Optional<List<String>> tabComplete(User user, String alias, List<String> args) {
Island island = getIslands().getIsland(getWorld(), user);
if (island == null) {
return Optional.empty();
}
List<String> options = new ArrayList<>();
options.add("list");
options.add(CLEAR);
addon.getTrophyManager().getEarned(island).stream().filter(t -> t.title() != null)
.map(Trophy::id).forEach(options::add);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public void setup() {
settings.getSetCountCommand().split(" "));
// Chunk territory info and map
new IslandChunksCommand(this, "chunks", new String[] {"chunks"});
// Contribution ledger
new IslandLedgerCommand(this, "ledger", new String[] {"ledger"});
// Trophy titles
new IslandTitleCommand(this, "title", new String[] {"title"});
// Force block respawn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,19 @@ private void tryToShowBossBar(UUID uuid, Island island) {
int numBlocksToGo = addon.getOneBlockManager().getNextPhaseBlocks(obi);
int phaseBlocks = addon.getOneBlockManager().getPhaseBlocks(obi);
int done = phaseBlocks - numBlocksToGo;
String titlePrefix = "";
if (addon.getTrophyManager() != null) {
String activeTitle = addon.getTrophyManager().getActiveTitleText(island);
if (!activeTitle.isEmpty()) {
titlePrefix = user.getTranslationOrNothing("chunkblock.bossbar.title-prefix",
"[title]", activeTitle);
}
}
String translation = user.getTranslationOrNothing("chunkblock.bossbar.status", "[togo]",
String.valueOf(numBlocksToGo), "[total]", String.valueOf(phaseBlocks), "[done]", String.valueOf(done),
"[phase-name]", obi.getPhaseName(), "[percent-done]",
Math.round(addon.getOneBlockManager().getPercentageDone(obi)) + "%");
translation = translation.replace("[island-title]", titlePrefix);
bar.setTitle(translation);
// Add to user if they don't have it already
Player player = Bukkit.getPlayer(uuid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import world.bentobox.chunkblock.events.ChunkUnlockEvent;
import world.bentobox.chunkblock.events.RingCompleteEvent;
import world.bentobox.level.events.IslandLevelCalculatedEvent;
import world.bentobox.level.events.IslandPreLevelEvent;

/**
* Watches island level changes from the Level addon. Levels are chunk currency here:
Expand All @@ -44,6 +45,27 @@ public LevelListener(ChunkBlock addon) {
this.addon = addon;
}

/**
* Shrinks the island's protection range to cover only the unlocked chunks so the
* Level addon scans the playable area instead of the entire 240-block default.
* The calculator reads the range in the same tick; the original is restored on
* the next tick.
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onIslandPreLevel(IslandPreLevelEvent e) {
Island island = e.getIsland();
if (island == null || !addon.inWorld(island.getWorld())) {
return;
}
int ring = addon.getChunkManager().currentRing(island);
int needed = ring * 16 + ChunkManager.CHUNK_CENTER;
int stored = island.getProtectionRange();
if (needed < stored) {
island.setProtectionRange(needed);
Bukkit.getScheduler().runTask(addon.getPlugin(), () -> island.setProtectionRange(stored));
}
}

/**
* Fires after every island level calculation, before results are saved. Never
* cancelled here — we only read the level.
Expand Down Expand Up @@ -226,6 +248,10 @@ private void rewardRing(Island island, int ring) {
"[chunks]", chunkText));
}
celebrateRing(island, ring);
if (addon.getActivityManager() != null) {
addon.getActivityManager().recordActivity(island, null,
world.bentobox.chunkblock.activity.CounterType.RINGS_COMPLETED, 1);
}
List<String> ownerCommands = addon.getSettings().getRingCommands();
if (!ownerCommands.isEmpty()) {
runCommands(ownerCommands, ringText, chunkText, "[owner]", playerName(island.getOwner()));
Expand Down
Loading
Loading