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
19 changes: 18 additions & 1 deletion src/main/java/world/bentobox/chunkblock/ChunkBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import world.bentobox.chunkblock.chunks.BorderDisplay;
import world.bentobox.chunkblock.chunks.ChunkManager;
import world.bentobox.chunkblock.listeners.ActivityListener;
import world.bentobox.chunkblock.trophies.TrophyManager;
import world.bentobox.chunkblock.commands.admin.AdminCommand;
import world.bentobox.chunkblock.commands.island.PlayerCommand;
import world.bentobox.chunkblock.dataobjects.OneBlockIslands;
Expand Down Expand Up @@ -93,6 +94,8 @@ public class ChunkBlock extends GameModeAddon {
private ChunkManager chunkManager;
/** The per-member activity counters (the ledger/leaderboard/trophy substrate) */
private ActivityManager activityManager;
/** The config-defined island trophies and titles */
private TrophyManager trophyManager;
/** The placeholder manager for ChunkBlock */
private ChunkBlockPlaceholders phManager;
/** The listener for hologram-related events */
Expand Down Expand Up @@ -249,8 +252,10 @@ public void onEnable() {
oneBlockManager = new OneBlocksManager(this);
// Initialize the chunk lock manager
chunkManager = new ChunkManager(this);
// Initialize the activity counters
// Initialize the activity counters and the trophies that read them
activityManager = new ActivityManager(this);
trophyManager = new TrophyManager(this);
trophyManager.loadTrophies();
// Load phase data
if (loadData()) {
// Failed to load - don't register anything
Expand Down Expand Up @@ -345,6 +350,9 @@ public void onReload() {
log("Reloaded ChunkBlock settings");
loadData();
}
if (trophyManager != null) {
trophyManager.loadTrophies();
}
}

/**
Expand All @@ -368,6 +376,13 @@ public ActivityManager getActivityManager() {
return activityManager;
}

/**
* @return the trophy and title manager, or null before the addon is enabled
*/
public TrophyManager getTrophyManager() {
return trophyManager;
}

/**
* @return the chunk guard listener (containment and backtracking)
*/
Expand Down Expand Up @@ -503,6 +518,8 @@ public void saveDefaultConfig() {
super.saveDefaultConfig();
// Save default phases panel
this.saveResource("panels/phases_panel.yml", false);
// Save default trophy definitions
this.saveResource("trophies.yml", false);
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,33 @@ 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_title", this::getIslandTitle);
placeholdersManager.registerPlaceholder(addon, "island_trophies", this::getIslandTrophies);
}

/**
* @param user user
* @return the user's island's active trophy title as configured (MiniMessage text),
* or an empty string for no title
*/
public String getIslandTitle(User user) {
if (user == null || user.getUniqueId() == null || addon.getTrophyManager() == null) {
return "";
}
return getUsersIsland(user).map(i -> addon.getTrophyManager().getActiveTitleText(i)).orElse("");
}

/**
* @param user user
* @return how many trophies the user's island has earned
*/
public String getIslandTrophies(User user) {
if (user == null || user.getUniqueId() == null || addon.getTrophyManager() == null) {
return "";
}
return getUsersIsland(user)
.map(i -> String.valueOf(addon.getOneBlocksIsland(i).getEarnedTrophies().size()))
.orElse("");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package world.bentobox.chunkblock.activity;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand Down Expand Up @@ -43,8 +44,8 @@ public class ActivityManager {
/** Unsaved frequent-counter records per island, so block breaks don't save every hit */
private final Map<String, Integer> unsavedCounts = new HashMap<>();

/** Today as an epoch day; replaceable so tests can move time */
private LongSupplier daySupplier = () -> LocalDate.now().toEpochDay();
/** Today as an epoch day in the server's zone; replaceable so tests can move time */
private LongSupplier daySupplier = () -> LocalDate.now(ZoneId.systemDefault()).toEpochDay();

public ActivityManager(ChunkBlock addon) {
this.addon = addon;
Expand All @@ -69,7 +70,7 @@ public void setDaySupplier(LongSupplier daySupplier) {
* @param type the counter
* @param amount the amount to add, &gt; 0 (anything else is ignored)
*/
public void record(@NonNull Island island, @Nullable UUID member, @NonNull CounterType type, long amount) {
public void recordActivity(@NonNull Island island, @Nullable UUID member, @NonNull CounterType type, long amount) {
if (amount <= 0 || (member != null && !island.getMemberSet().contains(member))) {
return;
}
Expand All @@ -78,6 +79,9 @@ public void record(@NonNull Island island, @Nullable UUID member, @NonNull Count
data.add(member == null ? ISLAND_SCOPE : member.toString(), type.name(), today, amount);
data.prune(today - Math.max(1, addon.getSettings().getActivityRetentionDays()) + 1);
save(data, type == CounterType.MAGIC_BLOCKS);
if (addon.getTrophyManager() != null) {
addon.getTrophyManager().check(island);
}
}

/**
Expand All @@ -94,8 +98,8 @@ public void record(@NonNull Island island, @Nullable UUID member, @NonNull Count
public boolean recordClaim(@NonNull Island island, int dx, int dz, @Nullable UUID member) {
IslandActivity data = getActivity(island.getUniqueId());
boolean first = data.getClaimedEver().add(dx + "," + dz);
record(island, member, first ? CounterType.CHUNKS_CLAIMED : CounterType.CHUNKS_RECLAIMED, 1);
// record() may have skipped saving (non-member) but the claimedEver set changed
recordActivity(island, member, first ? CounterType.CHUNKS_CLAIMED : CounterType.CHUNKS_RECLAIMED, 1);
// recordActivity() may have skipped saving (non-member) but the claimedEver set changed
if (first) {
save(data, false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package world.bentobox.chunkblock.commands.island;

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

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.trophies.Trophy;

/**
* /ch title — shows the island's earned trophies and the titles they carry, and lets a
* member pick which title the island shows. One active title per island, chosen from the
* trophies it has earned; "none" clears it.
*
* @author tastybento
*/
public class IslandTitleCommand extends CompositeCommand {

private static final String CLEAR = "none";
private static final String TITLE_VAR = "[title]";

private ChunkBlock addon;

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

@Override
public void setup() {
setDescription("chunkblock.commands.title.description");
setParametersHelp("chunkblock.commands.title.parameters");
setOnlyPlayer(true);
setPermission("island.title");
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;
}
if (getIslands().getIsland(getWorld(), user) == null) {
user.sendMessage("general.errors.no-island");
return false;
}
return true;
}

@Override
public boolean execute(User user, String label, List<String> args) {
Island island = getIslands().getIsland(getWorld(), user);
if (island == null) {
// canExecute already refused this, but never spend credit on a null island
user.sendMessage("general.errors.no-island");
return false;
}
if (args.isEmpty()) {
showTitles(user, island);
return true;
}
String id = args.get(0);
if (CLEAR.equalsIgnoreCase(id)) {
addon.getTrophyManager().setActiveTitle(island, null);
user.sendMessage("chunkblock.commands.title.cleared");
return true;
}
if (!addon.getTrophyManager().setActiveTitle(island, id)) {
user.sendMessage("chunkblock.commands.title.not-earned");
return false;
}
addon.getTrophyManager().getTrophy(id).ifPresent(trophy -> user
.sendMessage("chunkblock.commands.title.set", TITLE_VAR, trophy.title()));
return true;
}

/**
* Lists the island's earned trophies, marking the ones that carry a title and which
* title is active.
*/
private void showTitles(User user, Island island) {
List<Trophy> earned = addon.getTrophyManager().getEarned(island);
if (earned.isEmpty()) {
user.sendMessage("chunkblock.commands.title.none-earned-yet");
return;
}
user.sendMessage("chunkblock.commands.title.header");
for (Trophy trophy : earned) {
if (trophy.title() == null) {
user.sendMessage("chunkblock.commands.title.trophy-entry", "[name]", trophy.name());
} else {
user.sendMessage("chunkblock.commands.title.title-entry", "[name]", trophy.name(),
TITLE_VAR, trophy.title(), "[id]", trophy.id());
}
}
String active = addon.getTrophyManager().getActiveTitleText(island);
if (active.isEmpty()) {
user.sendMessage("chunkblock.commands.title.no-active");
} else {
user.sendMessage("chunkblock.commands.title.active", TITLE_VAR, active);
}
}

@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(CLEAR);
addon.getTrophyManager().getEarned(island).stream().filter(t -> t.title() != null)
.map(Trophy::id).forEach(options::add);
String last = args.isEmpty() ? "" : args.get(args.size() - 1);
return Optional.of(Util.tabLimit(options, last));
}
}
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"});
// Trophy titles
new IslandTitleCommand(this, "title", new String[] {"title"});
// Force block respawn
new IslandRespawnBlockCommand(this,
settings.getRespawnBlockCommand().split(" ")[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ public class OneBlockIslands implements DataObject {
@Expose
private int highestRingRewarded = 0;

/**
* The trophies this island has earned, by trophy id. Earned once, stays earned:
* re-locking and re-claiming never re-awards, and only an island create or reset
* clears this.
*/
@Expose
private Set<String> earnedTrophies = new HashSet<>();

/**
* The id of the earned trophy whose title the island currently shows, or empty for
* no title.
*/
@Expose
private String activeTitle = "";

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

Expand Down Expand Up @@ -201,6 +216,39 @@ public void setHighestRingRewarded(int highestRingRewarded) {
this.highestRingRewarded = highestRingRewarded;
}

/**
* @return the earned trophy ids, never null. Mutable — callers add and clear in place.
*/
@NonNull
public Set<String> getEarnedTrophies() {
if (earnedTrophies == null) {
earnedTrophies = new HashSet<>();
}
return earnedTrophies;
}

/**
* @param earnedTrophies the earned trophy ids to set
*/
public void setEarnedTrophies(Set<String> earnedTrophies) {
this.earnedTrophies = earnedTrophies;
}

/**
* @return the id of the trophy whose title the island shows, or an empty string
*/
@NonNull
public String getActiveTitle() {
return activeTitle == null ? "" : activeTitle;
}

/**
* @param activeTitle the trophy id whose title to show, or an empty string for none
*/
public void setActiveTitle(String activeTitle) {
this.activeTitle = activeTitle;
}

/**
* @return the phaseName
*/
Expand Down
Loading
Loading