feat(quests): add expiration and held item claims
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.bukkit.entity.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
|
||||
final class BukkitQuestClaimInventory implements QuestClaimInventory {
|
||||
@Override
|
||||
public PreparedClaim prepare(List<EscrowItem> items) {
|
||||
List<ItemStack> stacks = items.stream().map(EscrowItem::toItemStack).toList();
|
||||
return player -> deliver(player, stacks);
|
||||
}
|
||||
|
||||
static Delivery deliver(Player player, List<ItemStack> stacks) {
|
||||
PlayerInventory inventory = player.getInventory();
|
||||
ItemStack[] snapshot = cloneContents(inventory.getStorageContents());
|
||||
List<Item> drops = new ArrayList<>();
|
||||
int overflow = 0;
|
||||
try {
|
||||
for (ItemStack stack : stacks) {
|
||||
Map<Integer, ItemStack> leftovers = inventory.addItem(stack.clone());
|
||||
for (ItemStack leftover : leftovers.values()) {
|
||||
Item drop = player.getWorld().dropItem(player.getLocation(), leftover.clone());
|
||||
drops.add(drop);
|
||||
drop.setOwner(player.getUniqueId());
|
||||
overflow += leftover.getAmount();
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
inventory.setStorageContents(cloneContents(snapshot));
|
||||
drops.forEach(Item::remove);
|
||||
throw exception;
|
||||
}
|
||||
int deliveredOverflow = overflow;
|
||||
return new Delivery() {
|
||||
private boolean rolledBack;
|
||||
|
||||
@Override
|
||||
public int overflowAmount() {
|
||||
return deliveredOverflow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
if (rolledBack) {
|
||||
return;
|
||||
}
|
||||
inventory.setStorageContents(cloneContents(snapshot));
|
||||
drops.forEach(Item::remove);
|
||||
rolledBack = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static ItemStack[] cloneContents(ItemStack[] contents) {
|
||||
ItemStack[] copy = new ItemStack[contents.length];
|
||||
for (int index = 0; index < contents.length; index++) {
|
||||
copy[index] = contents[index] == null ? null : contents[index].clone();
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
record ClaimCollectionResult(int claimsCollected, int overflowItems) {
|
||||
}
|
||||
@@ -22,10 +22,11 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
private final QuestBrowser browser;
|
||||
private final QuestCompletionGateway completer;
|
||||
private final QuestCancellationGateway canceller;
|
||||
private final QuestClaimGateway claimant;
|
||||
private final Clock clock;
|
||||
|
||||
QuestBoardDialogUi(QuestCreationGateway creator, QuestBrowser browser, Clock clock) {
|
||||
this(creator, browser, null, null, clock);
|
||||
this(creator, browser, null, null, null, clock);
|
||||
}
|
||||
|
||||
QuestBoardDialogUi(
|
||||
@@ -34,7 +35,7 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
QuestCompletionGateway completer,
|
||||
Clock clock
|
||||
) {
|
||||
this(creator, browser, completer, null, clock);
|
||||
this(creator, browser, completer, null, null, clock);
|
||||
}
|
||||
|
||||
QuestBoardDialogUi(
|
||||
@@ -43,11 +44,23 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
QuestCompletionGateway completer,
|
||||
QuestCancellationGateway canceller,
|
||||
Clock clock
|
||||
) {
|
||||
this(creator, browser, completer, canceller, null, clock);
|
||||
}
|
||||
|
||||
QuestBoardDialogUi(
|
||||
QuestCreationGateway creator,
|
||||
QuestBrowser browser,
|
||||
QuestCompletionGateway completer,
|
||||
QuestCancellationGateway canceller,
|
||||
QuestClaimGateway claimant,
|
||||
Clock clock
|
||||
) {
|
||||
this.creator = Objects.requireNonNull(creator, "creator");
|
||||
this.browser = Objects.requireNonNull(browser, "browser");
|
||||
this.completer = completer;
|
||||
this.canceller = canceller;
|
||||
this.claimant = claimant;
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
@@ -72,8 +85,23 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
java.time.Instant now = clock.instant();
|
||||
List<Quest> activeQuests = browser.activeQuests(now);
|
||||
String listing = QuestListingFormatter.formatAll(activeQuests, now);
|
||||
String claimListing = claimListingText(player);
|
||||
List<ActionButton> actions = new ArrayList<>();
|
||||
actions.add(create);
|
||||
if (claimant != null) {
|
||||
actions.add(ActionButton.builder(Component.text("Collect pending claims"))
|
||||
.tooltip(Component.text("Collect delivered blocks and returned rewards"))
|
||||
.width(250)
|
||||
.action(DialogAction.customClick((response, audience) -> {
|
||||
if (audience instanceof Player respondingPlayer) {
|
||||
submitClaim(respondingPlayer);
|
||||
}
|
||||
}, ClickCallback.Options.builder()
|
||||
.uses(1)
|
||||
.lifetime(Duration.ofMinutes(10))
|
||||
.build()))
|
||||
.build());
|
||||
}
|
||||
for (Quest quest : activeQuests) {
|
||||
if (completer != null) {
|
||||
actions.add(completionButton(quest));
|
||||
@@ -85,7 +113,8 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
DialogBase base = DialogBase.builder(Component.text("Quest Board"))
|
||||
.externalTitle(Component.text("Quest Board — Active quests and create"))
|
||||
.body(List.of(DialogBody.plainMessage(Component.text(
|
||||
"ACTIVE QUESTS\n" + listing + "\n\nCREATE A QUEST\n"
|
||||
"ACTIVE QUESTS\n" + listing + "\n\nYOUR PENDING CLAIMS\n" + claimListing
|
||||
+ "\n\nCREATE A QUEST\n"
|
||||
+ "Hold the reward in your main hand. The entire exact stack, including all item metadata, "
|
||||
+ "will be removed and held in escrow only if this quest saves successfully."
|
||||
), 800)))
|
||||
@@ -113,6 +142,25 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
return QuestListingFormatter.formatAll(browser.activeQuests(now), now);
|
||||
}
|
||||
|
||||
String claimListingText(Player player) {
|
||||
if (claimant == null) {
|
||||
return "Claim collection is unavailable.";
|
||||
}
|
||||
List<QuestClaim> claims = claimant.pendingClaims(player);
|
||||
if (claims.isEmpty()) {
|
||||
return "No pending claims.";
|
||||
}
|
||||
return claims.stream().map(claim -> {
|
||||
String kind = claim.type() == QuestClaimType.DELIVERED_BLOCKS
|
||||
? "DELIVERED BLOCKS" : "RETURNED REWARD (" + claim.source().name() + ")";
|
||||
String items = claim.items().stream()
|
||||
.map(item -> item.amount() + " " + item.material())
|
||||
.reduce((left, right) -> left + ", " + right)
|
||||
.orElseThrow();
|
||||
return kind + " — " + items + " — quest " + claim.questId();
|
||||
}).reduce((left, right) -> left + "\n" + right).orElseThrow();
|
||||
}
|
||||
|
||||
private ActionButton completionButton(Quest quest) {
|
||||
String id = quest.id().toString();
|
||||
return ActionButton.builder(Component.text(
|
||||
@@ -169,6 +217,20 @@ final class QuestBoardDialogUi implements QuestBoardUi {
|
||||
}
|
||||
}
|
||||
|
||||
void submitClaim(Player player) {
|
||||
if (claimant == null) {
|
||||
player.sendMessage("Quest claims are unavailable.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
claimant.collect(player);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
player.sendMessage(
|
||||
"Your claim could not be collected safely. It remains pending; please try again."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void submitCompletion(Player player, String questId) {
|
||||
if (completer == null) {
|
||||
player.sendMessage("Quest completion is unavailable.");
|
||||
|
||||
@@ -10,7 +10,8 @@ record QuestClaim(
|
||||
UUID questId,
|
||||
UUID ownerId,
|
||||
List<EscrowItem> items,
|
||||
Instant createdAt
|
||||
Instant createdAt,
|
||||
QuestClaimSource source
|
||||
) {
|
||||
QuestClaim {
|
||||
Objects.requireNonNull(id, "id");
|
||||
@@ -21,5 +22,17 @@ record QuestClaim(
|
||||
throw new IllegalArgumentException("Claim items must not be empty");
|
||||
}
|
||||
Objects.requireNonNull(createdAt, "createdAt");
|
||||
Objects.requireNonNull(source, "source");
|
||||
}
|
||||
|
||||
QuestClaim(
|
||||
UUID id, UUID questId, UUID ownerId, List<EscrowItem> items, Instant createdAt
|
||||
) {
|
||||
this(id, questId, ownerId, items, createdAt, QuestClaimSource.COMPLETION);
|
||||
}
|
||||
|
||||
QuestClaimType type() {
|
||||
return source == QuestClaimSource.COMPLETION
|
||||
? QuestClaimType.DELIVERED_BLOCKS : QuestClaimType.RETURNED_REWARD;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class QuestClaimController implements QuestClaimGateway {
|
||||
private final QuestService quests;
|
||||
private final QuestClaimInventory inventory;
|
||||
|
||||
QuestClaimController(QuestService quests, QuestClaimInventory inventory) {
|
||||
this.quests = Objects.requireNonNull(quests, "quests");
|
||||
this.inventory = Objects.requireNonNull(inventory, "inventory");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<QuestClaim> pendingClaims(Player player) {
|
||||
Objects.requireNonNull(player, "player");
|
||||
return quests.claimsFor(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized ClaimCollectionResult collect(Player player) throws IOException {
|
||||
Objects.requireNonNull(player, "player");
|
||||
List<QuestClaim> claims = quests.claimsFor(player.getUniqueId());
|
||||
if (claims.isEmpty()) {
|
||||
player.sendMessage("You have no pending quest claims.");
|
||||
return new ClaimCollectionResult(0, 0);
|
||||
}
|
||||
|
||||
int collected = 0;
|
||||
int overflow = 0;
|
||||
for (QuestClaim claim : claims) {
|
||||
QuestClaimInventory.PreparedClaim prepared = inventory.prepare(claim.items());
|
||||
QuestClaimInventory.Delivery delivery = prepared.deliver(player);
|
||||
try {
|
||||
quests.acknowledgeClaim(player.getUniqueId(), claim.id());
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
delivery.rollback();
|
||||
throw exception;
|
||||
}
|
||||
collected++;
|
||||
overflow += delivery.overflowAmount();
|
||||
player.sendMessage(description(claim));
|
||||
}
|
||||
player.sendMessage("Collected " + collected + " quest claim(s).");
|
||||
if (overflow > 0) {
|
||||
player.sendMessage(
|
||||
overflow + " item(s) did not fit and were dropped at your feet, protected for you."
|
||||
);
|
||||
}
|
||||
return new ClaimCollectionResult(collected, overflow);
|
||||
}
|
||||
|
||||
private static String description(QuestClaim claim) {
|
||||
if (claim.type() == QuestClaimType.DELIVERED_BLOCKS) {
|
||||
return "Collected delivered blocks from completed quest " + claim.questId() + ".";
|
||||
}
|
||||
String reason = claim.source() == QuestClaimSource.EXPIRATION
|
||||
? "expired" : "cancelled";
|
||||
return "Collected returned reward from " + reason + " quest " + claim.questId() + ".";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
interface QuestClaimGateway {
|
||||
ClaimCollectionResult collect(Player player) throws IOException;
|
||||
|
||||
default List<QuestClaim> pendingClaims(Player player) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
interface QuestClaimInventory {
|
||||
PreparedClaim prepare(List<EscrowItem> items);
|
||||
|
||||
interface PreparedClaim {
|
||||
Delivery deliver(Player player);
|
||||
}
|
||||
|
||||
interface Delivery {
|
||||
int overflowAmount();
|
||||
void rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
enum QuestClaimSource {
|
||||
COMPLETION,
|
||||
CANCELLATION,
|
||||
EXPIRATION
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
enum QuestClaimType {
|
||||
DELIVERED_BLOCKS,
|
||||
RETURNED_REWARD
|
||||
}
|
||||
@@ -18,10 +18,11 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
private final QuestBrowser browser;
|
||||
private final QuestCompletionGateway completer;
|
||||
private final QuestCancellationGateway canceller;
|
||||
private final QuestClaimGateway claimant;
|
||||
private final Clock clock;
|
||||
|
||||
QuestCommand(QuestCreationGateway creator, QuestBrowser browser, Clock clock) {
|
||||
this(creator, browser, null, null, clock);
|
||||
this(creator, browser, null, null, null, clock);
|
||||
}
|
||||
|
||||
QuestCommand(
|
||||
@@ -30,7 +31,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
QuestCompletionGateway completer,
|
||||
Clock clock
|
||||
) {
|
||||
this(creator, browser, completer, null, clock);
|
||||
this(creator, browser, completer, null, null, clock);
|
||||
}
|
||||
|
||||
QuestCommand(
|
||||
@@ -39,11 +40,23 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
QuestCompletionGateway completer,
|
||||
QuestCancellationGateway canceller,
|
||||
Clock clock
|
||||
) {
|
||||
this(creator, browser, completer, canceller, null, clock);
|
||||
}
|
||||
|
||||
QuestCommand(
|
||||
QuestCreationGateway creator,
|
||||
QuestBrowser browser,
|
||||
QuestCompletionGateway completer,
|
||||
QuestCancellationGateway canceller,
|
||||
QuestClaimGateway claimant,
|
||||
Clock clock
|
||||
) {
|
||||
this.creator = Objects.requireNonNull(creator, "creator");
|
||||
this.browser = Objects.requireNonNull(browser, "browser");
|
||||
this.completer = completer;
|
||||
this.canceller = canceller;
|
||||
this.claimant = claimant;
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
@@ -57,6 +70,24 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
sender.sendMessage(QuestListingFormatter.formatAll(browser.activeQuests(now), now));
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 1 && "claim".equalsIgnoreCase(arguments[0])) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage("Only players can collect quest claims.");
|
||||
return true;
|
||||
}
|
||||
if (claimant == null) {
|
||||
sender.sendMessage("Quest claims are unavailable.");
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
claimant.collect(player);
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
sender.sendMessage(
|
||||
"Your claim could not be collected safely. It remains pending; please try again."
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 2 && "complete".equalsIgnoreCase(arguments[0])) {
|
||||
if (!(sender instanceof Player player)) {
|
||||
sender.sendMessage("Only players can complete quests with inventory items.");
|
||||
@@ -131,7 +162,9 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
return List.of();
|
||||
}
|
||||
if (arguments.length == 1) {
|
||||
return startsWith(List.of("create", "list", "complete", "cancel"), arguments[0]);
|
||||
return startsWith(
|
||||
List.of("create", "list", "complete", "cancel", "claim"), arguments[0]
|
||||
);
|
||||
}
|
||||
if (arguments.length == 2 && "create".equalsIgnoreCase(arguments[0])) {
|
||||
return creator.suggestBlockMaterials(arguments[1]);
|
||||
@@ -161,7 +194,7 @@ final class QuestCommand implements CommandExecutor, TabCompleter {
|
||||
private static void usage(CommandSender sender) {
|
||||
sender.sendMessage(
|
||||
"Usage: /quests [list] | /quests create <block> <quantity> | "
|
||||
+ "/quests complete <quest> | /quests cancel <quest>"
|
||||
+ "/quests complete <quest> | /quests cancel <quest> | /quests claim"
|
||||
);
|
||||
sender.sendMessage("Hold the entire reward stack in your main hand; its exact metadata will be escrowed.");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package games.dmg.spigotquestboard;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
final class QuestExpiryTask implements Runnable {
|
||||
private final QuestService quests;
|
||||
private final IssuerNotifier notifier;
|
||||
private final Clock clock;
|
||||
private final Logger logger;
|
||||
|
||||
QuestExpiryTask(
|
||||
QuestService quests, IssuerNotifier notifier, Clock clock, Logger logger
|
||||
) {
|
||||
this.quests = quests;
|
||||
this.notifier = notifier;
|
||||
this.clock = clock;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final List<QuestClaim> claims;
|
||||
try {
|
||||
claims = quests.expire(clock.instant());
|
||||
} catch (IOException exception) {
|
||||
logger.log(Level.WARNING, "Could not persist expired quests; expiration will be retried", exception);
|
||||
return;
|
||||
}
|
||||
Set<java.util.UUID> owners = new LinkedHashSet<>();
|
||||
claims.forEach(claim -> owners.add(claim.ownerId()));
|
||||
owners.forEach(notifier::notifyIfOnline);
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,8 @@ final class QuestService implements QuestBrowser {
|
||||
QuestStatus.COMPLETED
|
||||
);
|
||||
QuestClaim claim = new QuestClaim(
|
||||
UUID.randomUUID(), active.id(), active.issuerId(), delivery, completedAt
|
||||
UUID.randomUUID(), active.id(), active.issuerId(), delivery, completedAt,
|
||||
QuestClaimSource.COMPLETION
|
||||
);
|
||||
IssuerNotification notification = new IssuerNotification(
|
||||
UUID.randomUUID(), active.id(), active.issuerId(),
|
||||
@@ -129,7 +130,8 @@ final class QuestService implements QuestBrowser {
|
||||
QuestStatus.CANCELLED
|
||||
);
|
||||
QuestClaim claim = new QuestClaim(
|
||||
UUID.randomUUID(), active.id(), active.issuerId(), active.reward(), cancelledAt
|
||||
UUID.randomUUID(), active.id(), active.issuerId(), active.reward(), cancelledAt,
|
||||
QuestClaimSource.CANCELLATION
|
||||
);
|
||||
Map<UUID, Quest> quests = new LinkedHashMap<>(state.quests());
|
||||
quests.put(active.id(), cancelled);
|
||||
@@ -143,6 +145,66 @@ final class QuestService implements QuestBrowser {
|
||||
return claim;
|
||||
}
|
||||
|
||||
synchronized List<QuestClaim> expire(Instant now) throws IOException {
|
||||
Objects.requireNonNull(now, "now");
|
||||
Map<UUID, Quest> quests = new LinkedHashMap<>(state.quests());
|
||||
Map<UUID, List<QuestClaim>> claims = mutableClaims(state.claims());
|
||||
Map<UUID, IssuerNotification> notifications = new LinkedHashMap<>(state.notifications());
|
||||
List<QuestClaim> expiredClaims = new ArrayList<>();
|
||||
|
||||
for (Quest active : state.quests().values()) {
|
||||
if (active.status() != QuestStatus.ACTIVE || active.expiresAt().isAfter(now)) {
|
||||
continue;
|
||||
}
|
||||
Quest expired = new Quest(
|
||||
active.id(), active.issuerId(), active.issuerName(), active.requestedMaterial(),
|
||||
active.requestedAmount(), active.reward(), active.createdAt(), active.expiresAt(),
|
||||
QuestStatus.EXPIRED
|
||||
);
|
||||
QuestClaim claim = new QuestClaim(
|
||||
UUID.randomUUID(), active.id(), active.issuerId(), active.reward(), active.expiresAt(),
|
||||
QuestClaimSource.EXPIRATION
|
||||
);
|
||||
IssuerNotification notification = new IssuerNotification(
|
||||
UUID.randomUUID(), active.id(), active.issuerId(),
|
||||
"Quest " + active.id() + " expired. Your returned reward can be claimed "
|
||||
+ "at any quest board or with /quests claim.",
|
||||
active.expiresAt()
|
||||
);
|
||||
quests.put(active.id(), expired);
|
||||
claims.computeIfAbsent(active.issuerId(), ignored -> new ArrayList<>()).add(claim);
|
||||
notifications.put(notification.id(), notification);
|
||||
expiredClaims.add(claim);
|
||||
}
|
||||
if (!expiredClaims.isEmpty()) {
|
||||
save(new QuestState(quests, claims, notifications));
|
||||
}
|
||||
return List.copyOf(expiredClaims);
|
||||
}
|
||||
|
||||
synchronized List<QuestClaim> claimsFor(UUID ownerId) {
|
||||
Objects.requireNonNull(ownerId, "ownerId");
|
||||
return state.claims().getOrDefault(ownerId, List.of());
|
||||
}
|
||||
|
||||
synchronized void acknowledgeClaim(UUID ownerId, UUID claimId) throws IOException {
|
||||
Objects.requireNonNull(ownerId, "ownerId");
|
||||
Objects.requireNonNull(claimId, "claimId");
|
||||
List<QuestClaim> existing = state.claims().getOrDefault(ownerId, List.of());
|
||||
if (existing.stream().noneMatch(claim -> claim.id().equals(claimId))) {
|
||||
return;
|
||||
}
|
||||
Map<UUID, List<QuestClaim>> claims = mutableClaims(state.claims());
|
||||
List<QuestClaim> remaining = new ArrayList<>(claims.get(ownerId));
|
||||
remaining.removeIf(claim -> claim.id().equals(claimId));
|
||||
if (remaining.isEmpty()) {
|
||||
claims.remove(ownerId);
|
||||
} else {
|
||||
claims.put(ownerId, remaining);
|
||||
}
|
||||
save(new QuestState(state.quests(), claims, state.notifications()));
|
||||
}
|
||||
|
||||
synchronized List<IssuerNotification> pendingNotifications(UUID recipientId) {
|
||||
Objects.requireNonNull(recipientId, "recipientId");
|
||||
return state.notifications().values().stream()
|
||||
@@ -172,6 +234,14 @@ final class QuestService implements QuestBrowser {
|
||||
return state;
|
||||
}
|
||||
|
||||
private static Map<UUID, List<QuestClaim>> mutableClaims(
|
||||
Map<UUID, List<QuestClaim>> source
|
||||
) {
|
||||
Map<UUID, List<QuestClaim>> copy = new LinkedHashMap<>();
|
||||
source.forEach((owner, claims) -> copy.put(owner, new ArrayList<>(claims)));
|
||||
return copy;
|
||||
}
|
||||
|
||||
private void save(QuestState candidate) throws IOException {
|
||||
repository.save(candidate);
|
||||
state = candidate;
|
||||
|
||||
@@ -39,18 +39,29 @@ public final class SpigotQuestBoardPlugin extends JavaPlugin {
|
||||
quests, new BukkitQuestCompletionInventory(), notifier
|
||||
);
|
||||
QuestCancellationGateway canceller = new QuestCancellationController(quests);
|
||||
QuestClaimGateway claimant = new QuestClaimController(
|
||||
quests, new BukkitQuestClaimInventory()
|
||||
);
|
||||
QuestCommand questCommand = new QuestCommand(
|
||||
creator, quests, completer, canceller, clock
|
||||
creator, quests, completer, canceller, claimant, clock
|
||||
);
|
||||
command("quests").setExecutor(questCommand);
|
||||
command("quests").setTabCompleter(questCommand);
|
||||
getServer().getPluginManager().registerEvents(notifier, this);
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new QuestBoardInteractionListener(
|
||||
boards, new QuestBoardDialogUi(creator, quests, completer, canceller, clock)
|
||||
boards, new QuestBoardDialogUi(
|
||||
creator, quests, completer, canceller, claimant, clock
|
||||
)
|
||||
),
|
||||
this
|
||||
);
|
||||
getServer().getScheduler().runTaskTimer(
|
||||
this,
|
||||
new QuestExpiryTask(quests, notifier, clock, getLogger()),
|
||||
0L,
|
||||
20L
|
||||
);
|
||||
getLogger().info(
|
||||
"Spigot Quest Board enabled with " + boards.size() + " boards and "
|
||||
+ quests.state().quests().size() + " quests."
|
||||
|
||||
@@ -60,7 +60,10 @@ final class YamlQuestRepository implements QuestRepository {
|
||||
UUID.fromString(requiredString(entry, "quest-id")),
|
||||
UUID.fromString(requiredString(entry, "owner-id")),
|
||||
readItems(entry.get("items"), "claim items"),
|
||||
Instant.parse(requiredString(entry, "created-at"))
|
||||
Instant.parse(requiredString(entry, "created-at")),
|
||||
readClaimSource(entry.get("source"), quests.get(
|
||||
UUID.fromString(requiredString(entry, "quest-id"))
|
||||
))
|
||||
);
|
||||
claims.computeIfAbsent(claim.ownerId(), ignored -> new ArrayList<>()).add(claim);
|
||||
}
|
||||
@@ -111,6 +114,7 @@ final class YamlQuestRepository implements QuestRepository {
|
||||
serialized.put("quest-id", claim.questId().toString());
|
||||
serialized.put("owner-id", claim.ownerId().toString());
|
||||
serialized.put("created-at", claim.createdAt().toString());
|
||||
serialized.put("source", claim.source().name());
|
||||
serialized.put("items", writeItems(claim.items()));
|
||||
serializedClaims.add(serialized);
|
||||
}
|
||||
@@ -182,6 +186,20 @@ final class YamlQuestRepository implements QuestRepository {
|
||||
return serializedItems;
|
||||
}
|
||||
|
||||
private static QuestClaimSource readClaimSource(Object value, Quest quest) {
|
||||
if (value instanceof String source) {
|
||||
return QuestClaimSource.valueOf(source);
|
||||
}
|
||||
if (value != null) {
|
||||
throw new IllegalArgumentException("Invalid claim source");
|
||||
}
|
||||
if (quest == null || quest.status() == QuestStatus.COMPLETED) {
|
||||
return QuestClaimSource.COMPLETION;
|
||||
}
|
||||
return quest.status() == QuestStatus.EXPIRED
|
||||
? QuestClaimSource.EXPIRATION : QuestClaimSource.CANCELLATION;
|
||||
}
|
||||
|
||||
private static QuestStatus readStatus(Object value) {
|
||||
if (value == null) {
|
||||
return QuestStatus.ACTIVE;
|
||||
|
||||
Reference in New Issue
Block a user