diff --git a/build.gradle.kts b/build.gradle.kts index 38f9d734..bb29b3ba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } group = "com.eternalcode" -version = "0.0.2-SNAPSHOT" +version = "0.1.0-SNAPSHOT" repositories { gradlePluginPortal() diff --git a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java index a5c75eba..1dfe9755 100644 --- a/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java +++ b/src/main/java/com/eternalcode/parcellockers/ParcelLockers.java @@ -155,7 +155,9 @@ public void onEnable() { lockerManager, userManager, itemStorageManager, - parcelDispatchService + parcelDispatchService, + parcelContentManager, + deliveryManager ); MainGui mainGUI = new MainGui( @@ -182,7 +184,7 @@ public void onEnable() { new ParcelCommand(mainGUI), new ParcelLockersCommand(configService, config, noticeService), new DebugCommand(parcelService, lockerManager, itemStorageManager, parcelContentManager, - noticeService) + noticeService, deliveryManager) )) .invalidUsage(new InvalidUsageHandlerImpl(noticeService)) .missingPermission(new MissingPermissionsHandlerImpl(noticeService)) diff --git a/src/main/java/com/eternalcode/parcellockers/command/debug/DebugCommand.java b/src/main/java/com/eternalcode/parcellockers/command/debug/DebugCommand.java index f5f5049d..7320269d 100644 --- a/src/main/java/com/eternalcode/parcellockers/command/debug/DebugCommand.java +++ b/src/main/java/com/eternalcode/parcellockers/command/debug/DebugCommand.java @@ -3,27 +3,25 @@ import com.eternalcode.commons.bukkit.ItemUtil; import com.eternalcode.multification.notice.Notice; import com.eternalcode.parcellockers.content.ParcelContentManager; +import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.itemstorage.ItemStorageManager; import com.eternalcode.parcellockers.locker.LockerManager; import com.eternalcode.parcellockers.notification.NoticeService; -import com.eternalcode.parcellockers.parcel.Parcel; import com.eternalcode.parcellockers.parcel.ParcelService; -import com.eternalcode.parcellockers.parcel.ParcelSize; -import com.eternalcode.parcellockers.parcel.ParcelStatus; import dev.rollczi.litecommands.annotations.argument.Arg; import dev.rollczi.litecommands.annotations.command.Command; import dev.rollczi.litecommands.annotations.context.Sender; import dev.rollczi.litecommands.annotations.execute.Execute; import dev.rollczi.litecommands.annotations.permission.Permission; -import java.util.Arrays; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import java.util.List; import java.util.Random; -import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; -import org.bukkit.Material; +import org.bukkit.Registry; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ItemType; @Command(name = "parcel debug") @Permission("parcellockers.debug") @@ -34,19 +32,21 @@ public class DebugCommand { private final ItemStorageManager itemStorageManager; private final ParcelContentManager contentManager; private final NoticeService noticeService; + private final DeliveryManager deliveryManager; public DebugCommand( ParcelService parcelService, LockerManager lockerManager, ItemStorageManager itemStorageManager, ParcelContentManager contentManager, - NoticeService noticeService + NoticeService noticeService, DeliveryManager deliveryManager ) { this.parcelService = parcelService; this.lockerManager = lockerManager; this.itemStorageManager = itemStorageManager; this.contentManager = contentManager; this.noticeService = noticeService; + this.deliveryManager = deliveryManager; } @Execute(name = "delete parcels") @@ -69,9 +69,15 @@ void deleteItems(@Sender CommandSender sender) { this.contentManager.deleteAll(sender, this.noticeService); } + @Execute(name = "delete delivieries") + void deleteDeliveries(@Sender CommandSender sender) { + this.deliveryManager.deleteAll(sender, this.noticeService); + } + @Execute(name = "delete all") void deleteAll(@Sender CommandSender sender) { this.deleteItemStorages(sender); + this.deleteDeliveries(sender); this.deleteLockers(sender); this.deleteParcels(sender); this.deleteItems(sender); @@ -84,45 +90,20 @@ void getRandomItem(@Sender Player player, @Arg int stacks) { return; } - List itemMaterials = Arrays.stream(Material.values()).filter(Material::isItem).toList(); + Registry itemTypeRegistry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM); + List types = itemTypeRegistry.keyStream().map(itemTypeRegistry::get).toList(); - if (itemMaterials.isEmpty()) { // should never happen + if (types.isEmpty()) { // should never happen this.noticeService.player(player.getUniqueId(), messages -> Notice.chat("&cNo valid item materials found.")); return; } Random random = ThreadLocalRandom.current(); - // Give player random items for (int i = 0; i < stacks; i++) { - Material randomMaterial = itemMaterials.get(random.nextInt(itemMaterials.size())); - int randomAmount = Math.min(random.nextInt(64) + 1, randomMaterial.getMaxStackSize()); - - ItemStack itemStack = new ItemStack(randomMaterial, randomAmount); - ItemUtil.giveItem(player, itemStack); - } - } - - @Execute(name = "send") - void send(@Sender Player player, @Arg int count) { - for (int i = 0; i < count; i++) { - UUID locker = UUID.randomUUID(); - this.parcelService.send( - player, - new Parcel( - UUID.randomUUID(), - player.getUniqueId(), - "test", - "test", - false, - player.getUniqueId(), - ParcelSize.MEDIUM, - locker, - locker, - ParcelStatus.DELIVERED - ), - List.of(new ItemStack(Material.DIRT, 1)) - ); + ItemType randomItem = types.get(random.nextInt(types.size())); + int randomAmount = Math.min(random.nextInt(64) + 1, randomItem.getMaxStackSize()); + ItemUtil.giveItem(player, randomItem.createItemStack(randomAmount)); } } } diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java index d6e7cfbb..4e13b8ed 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/MessageConfig.java @@ -4,9 +4,8 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; import eu.okaeri.configs.annotation.Header; -import org.bukkit.Sound; +import io.papermc.paper.registry.keys.SoundEventKeys; -@SuppressWarnings("ALL") @Header({ "# This file contains messages used by the ParcelLockers plugin.", "# You can customize these messages to fit your server's theme or language.", @@ -21,7 +20,7 @@ public class MessageConfig extends OkaeriConfig { public Notice playerNotFound = Notice.chat("&4✘ &cThe specified player could not be found!"); public Notice invalidUsage = Notice.builder() .chat("&4» &cCorrect usage: &6{USAGE}") - .sound(Sound.BLOCK_NOTE_BLOCK_PLING.key()) + .sound(SoundEventKeys.BLOCK_NOTE_BLOCK_PLING) .build(); public Notice reload = Notice.chat("&3❣ &bConfiguration has been successfully reloaded!"); @@ -41,82 +40,82 @@ public class MessageConfig extends OkaeriConfig { public static class ParcelMessages extends OkaeriConfig { public Notice sent = Notice.builder() .chat("&2✔ &aParcel sent successfully.") - .sound(Sound.ENTITY_ITEM_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_ITEM_PICKUP) .build(); public Notice cannotSend = Notice.builder() .chat("&4✘ &cAn error occurred while sending the parcel. Check the console for more information.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice nameCannotBeEmpty = Notice.builder() .chat("&4✘ &cThe parcel name cannot be empty!") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice nameSet = Notice.builder() .chat("&2✔ &aParcel name set successfully.") - .sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) .build(); public Notice cannotBeEmpty = Notice.builder() .chat("&4✘ &cThe parcel cannot be empty!") - .sound(Sound.ENTITY_ENDERMAN_AMBIENT.key()) + .sound(SoundEventKeys.ENTITY_ENDERMAN_AMBIENT) .build(); public Notice receiverSet = Notice.builder() .chat("&2✔ &aParcel receiver set successfully.") - .sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) .build(); public Notice descriptionSet = Notice.builder() - .chat("&2✔ &aParcel name set successfully.") - .sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key()) + .chat("&2✔ &aParcel description set successfully.") + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) .build(); public Notice destinationSet = Notice.builder() .chat("&2✔ &aParcel destination locker set successfully.") - .sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) .build(); public Notice cannotCollect = Notice.builder() .chat("&4✘ &cAn error occurred while collecting the parcel.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice noInventorySpace = Notice.builder() .chat("&4✘ &cYou don't have enough space in your inventory to collect the parcel!") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice collected = Notice.builder() .chat("&2✔ &aParcel collected successfully.") - .sound(Sound.ENTITY_PLAYER_LEVELUP.key()) + .sound(SoundEventKeys.ENTITY_PLAYER_LEVELUP) .build(); public Notice nameNotSet = Notice.builder() .chat("&4✘ &cThe parcel name is not set!") - .sound(Sound.ENTITY_ENDERMAN_AMBIENT.key()) + .sound(SoundEventKeys.ENTITY_ENDERMAN_AMBIENT) .build(); public Notice receiverNotSet = Notice.builder() .chat("&4✘ &cThe parcel receiver is not set!") - .sound(Sound.ENTITY_ENDERMAN_AMBIENT.key()) + .sound(SoundEventKeys.ENTITY_ENDERMAN_AMBIENT) .build(); public Notice destinationNotSet = Notice.builder() .chat("&4✘ &cThe parcel destination locker is not set!") - .sound(Sound.ENTITY_ENDERMAN_AMBIENT.key()) + .sound(SoundEventKeys.ENTITY_ENDERMAN_AMBIENT) .build(); public Notice lockerFull = Notice.builder() .chat("&4✘ &cThe destination locker is full! Please select another locker.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice illegalItem = Notice.builder() .chat("&4✘ &cThe parcel contains illegal items that cannot be sent. ({ITEMS})") .build(); public Notice cannotDelete = Notice.builder() .chat("&4✘ &cAn error occurred while deleting the parcel.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice deleted = Notice.builder() .chat("&2✔ &aParcel deleted successfully.") - .sound(Sound.ENTITY_ITEM_BREAK.key()) + .sound(SoundEventKeys.ENTITY_ITEM_BREAK) .build(); public Notice insufficientFunds = Notice.builder() .chat("&4✘ &cYou do not have enough funds to send this parcel! Required: &6${AMOUNT}&c.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice feeWithdrawn = Notice.builder() .chat("&2✔ &a${AMOUNT} has been withdrawn from your account to cover the parcel sending fee.") - .sound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP) .build(); @Comment({"", "# The parcel info message." }) @@ -131,36 +130,36 @@ public static class ParcelMessages extends OkaeriConfig { "&f• &6Priority: &e{PRIORITY}", "&f• &6Description: &e{DESCRIPTION}" ) - .sound(Sound.BLOCK_NOTE_BLOCK_CHIME.key()) + .sound(SoundEventKeys.BLOCK_NOTE_BLOCK_CHIME) .build(); } public static class LockerMessages extends OkaeriConfig { public Notice cannotCreate = Notice.builder() .chat("&4✘ &cCould not create the parcel locker.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice created = Notice.builder() .chat("&2✔ &aParcel locker created successfully.") - .sound(Sound.BLOCK_ANVIL_USE.key()) + .sound(SoundEventKeys.BLOCK_ANVIL_USE) .build(); public String descriptionPrompt = "&6Enter a name for the parcel locker:"; public Notice cannotBreak = Notice.builder() .chat("&4✘ &cYou have no permission to break the parcel locker.") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice deleted = Notice.builder() .chat("&2✔ &aParcel locker deleted successfully.") - .sound(Sound.BLOCK_ANVIL_BREAK.key()) + .sound(SoundEventKeys.BLOCK_ANVIL_BREAK) .build(); public Notice broadcastRemoved = Notice.chat("&4❣ &cThe parcel locker at &4{X} {Y} {Z} &cin &4{WORLD} &chas been removed by &4{PLAYER}!"); public Notice alreadyCreating = Notice.builder() .chat("&4✘ &cYou are already creating a parcel locker!") - .sound(Sound.ENTITY_VILLAGER_NO.key()) + .sound(SoundEventKeys.ENTITY_VILLAGER_NO) .build(); public Notice addedToInventory = Notice.builder() .chat("&2✔ &aParcel locker item added to your inventory.") - .sound(Sound.ENTITY_ITEM_PICKUP.key()) + .sound(SoundEventKeys.ENTITY_ITEM_PICKUP) .build(); } @@ -170,8 +169,5 @@ public static class AdminMessages extends OkaeriConfig { public Notice deletedItemStorages = Notice.chat("&4⚠ &cAll ({COUNT}) item storages have been deleted!"); public Notice deletedContents = Notice.chat("&4⚠ &cAll ({COUNT}) parcel contents have been deleted!"); public Notice deletedDeliveries = Notice.chat("&4⚠ &cAll ({COUNT}) deliveries have been deleted!"); - public Notice deletedUsers = Notice.chat("&4⚠ &cAll ({COUNT}) users have been deleted!"); - public Notice invalidNumberOfStacks = Notice.chat("&4✘ &cInvalid number of stacks. Must be between 1 and 36."); - public Notice invalidItemMaterials = Notice.chat("&4✘ &cItem materials to give are invalid (should never happen, if it does report this on our discord)."); } } diff --git a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java index fe0dd866..2132a377 100644 --- a/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java +++ b/src/main/java/com/eternalcode/parcellockers/configuration/implementation/PluginConfig.java @@ -160,7 +160,7 @@ public static class GuiSettings extends OkaeriConfig { public ConfigItem cornerItem = new ConfigItem() .name("") .lore(Collections.emptyList()) - .type(Material.ORANGE_STAINED_GLASS_PANE); + .type(Material.BLUE_STAINED_GLASS_PANE); @Comment({ "", "# The item of the parcel submit button" }) public ConfigItem submitParcelItem = new ConfigItem() @@ -216,6 +216,9 @@ public static class GuiSettings extends OkaeriConfig { ) .type(Material.CHEST_MINECART); + @Comment({ "", "# The lore line showing when the parcel will arrive. Placeholders: {DURATION} - time remaining, {DATE} - arrival date" }) + public String parcelArrivingLine = "&6Arriving in: &e{DURATION} &7({DATE})"; + @Comment({ "", "# The item of the parcel item storage button" }) public ConfigItem parcelStorageItem = new ConfigItem() .name("&6\uD83D\uDCBE Parcel storage") @@ -328,6 +331,12 @@ public static class GuiSettings extends OkaeriConfig { Material.END_PORTAL_FRAME ); + @Comment({ "", "# The first line of lore when the parcel contains items in the collection GUI."}) + public String parcelItemsCollectionGui = "&6Items:"; + + @Comment({ "", "# The line of lore containing the item name and amount when the parcel contains items in the collection GUI."}) + public String parcelItemCollectionFormat = "&6- {AMOUNT}x {ITEM}"; + @Comment({ "", "# The item of the parcel item in the collection GUI" }) public ConfigItem parcelCollectionItem = new ConfigItem() .name("&a{NAME}") @@ -344,5 +353,8 @@ public static class GuiSettings extends OkaeriConfig { .name("&4✘ &cNo parcels found") .lore(List.of("&cYou don't have any parcels to collect.")) .type(Material.STRUCTURE_VOID); + + @Comment({ "", "# The lore line showing when the parcel has arrived. Placeholders: {DATE} - arrival date" }) + public String parcelArrivedLine = "&aArrived on: &2{DATE}"; } } diff --git a/src/main/java/com/eternalcode/parcellockers/delivery/DeliveryManager.java b/src/main/java/com/eternalcode/parcellockers/delivery/DeliveryManager.java index e31412fe..99406c2e 100644 --- a/src/main/java/com/eternalcode/parcellockers/delivery/DeliveryManager.java +++ b/src/main/java/com/eternalcode/parcellockers/delivery/DeliveryManager.java @@ -1,12 +1,15 @@ package com.eternalcode.parcellockers.delivery; import com.eternalcode.parcellockers.delivery.repository.DeliveryRepository; +import com.eternalcode.parcellockers.notification.NoticeService; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import java.time.Duration; import java.time.Instant; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.bukkit.command.CommandSender; public class DeliveryManager { @@ -29,6 +32,17 @@ public Delivery getOrCreate(UUID parcel, Instant deliveryTimestamp) { return this.deliveryCache.get(parcel, key -> this.create(key, deliveryTimestamp)); } + public CompletableFuture> get(UUID parcel) { + Delivery cached = this.deliveryCache.getIfPresent(parcel); + if (cached != null) { + return CompletableFuture.completedFuture(Optional.of(cached)); + } + return this.deliveryRepository.fetch(parcel).thenApply(optional -> { + optional.ifPresent(delivery -> this.deliveryCache.put(parcel, delivery)); + return optional; + }); + } + public Delivery create(UUID parcel, Instant deliveryTimestamp) { Delivery delivery = new Delivery(parcel, deliveryTimestamp); if (this.deliveryCache.getIfPresent(parcel) != null) { @@ -46,6 +60,17 @@ public CompletableFuture delete(UUID parcel) { }); } + public CompletableFuture deleteAll(CommandSender sender, NoticeService noticeService) { + return this.deliveryRepository.deleteAll().thenAccept(deleted -> { + this.deliveryCache.invalidateAll(); + noticeService.create() + .viewer(sender) + .notice(messages -> messages.admin.deletedContents) + .placeholder("{COUNT}", deleted.toString()) + .send(); + }); + } + private void cacheAll() { this.deliveryRepository.fetchAll() .thenAccept(all -> all.ifPresent(list -> list.forEach(delivery -> this.deliveryCache.put(delivery.parcel(), delivery)))); diff --git a/src/main/java/com/eternalcode/parcellockers/gui/GuiManager.java b/src/main/java/com/eternalcode/parcellockers/gui/GuiManager.java index 44b790b5..e547b21b 100644 --- a/src/main/java/com/eternalcode/parcellockers/gui/GuiManager.java +++ b/src/main/java/com/eternalcode/parcellockers/gui/GuiManager.java @@ -1,5 +1,9 @@ package com.eternalcode.parcellockers.gui; +import com.eternalcode.parcellockers.content.ParcelContent; +import com.eternalcode.parcellockers.content.ParcelContentManager; +import com.eternalcode.parcellockers.delivery.Delivery; +import com.eternalcode.parcellockers.delivery.DeliveryManager; import com.eternalcode.parcellockers.itemstorage.ItemStorage; import com.eternalcode.parcellockers.itemstorage.ItemStorageManager; import com.eternalcode.parcellockers.locker.Locker; @@ -25,19 +29,25 @@ public class GuiManager { private final UserManager userManager; private final ItemStorageManager itemStorageManager; private final ParcelDispatchService parcelDispatchService; + private final ParcelContentManager parcelContentManager; + private final DeliveryManager deliveryManager; public GuiManager( ParcelService parcelService, LockerManager lockerManager, UserManager userManager, ItemStorageManager itemStorageManager, - ParcelDispatchService parcelDispatchService + ParcelDispatchService parcelDispatchService, + ParcelContentManager parcelContentManager, + DeliveryManager deliveryManager ) { this.parcelService = parcelService; this.lockerManager = lockerManager; this.userManager = userManager; this.itemStorageManager = itemStorageManager; this.parcelDispatchService = parcelDispatchService; + this.parcelContentManager = parcelContentManager; + this.deliveryManager = deliveryManager; } public void sendParcel(Player sender, Parcel parcel, List items) { @@ -84,4 +94,12 @@ public void saveItemStorage(UUID player, List items) { public CompletableFuture deleteItemStorage(UUID owner) { return this.itemStorageManager.delete(owner); } + + public CompletableFuture> getParcelContent(UUID parcelId) { + return this.parcelContentManager.get(parcelId); + } + + public CompletableFuture> getDelivery(UUID parcelId) { + return this.deliveryManager.get(parcelId); + } } diff --git a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/CollectionGui.java b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/CollectionGui.java index 8985f862..a683ee5b 100644 --- a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/CollectionGui.java +++ b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/CollectionGui.java @@ -10,15 +10,19 @@ import com.eternalcode.parcellockers.parcel.ParcelStatus; import com.eternalcode.parcellockers.parcel.util.PlaceholderUtil; import com.eternalcode.parcellockers.shared.Page; +import com.eternalcode.parcellockers.util.MaterialUtil; import com.spotify.futures.CompletableFutures; import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.GuiItem; import dev.triumphteam.gui.guis.PaginatedGui; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; public class CollectionGui implements GuiView { @@ -112,18 +116,32 @@ private CompletableFuture> createParcelItemAsync( Player player, PaginatedGuiRefresher refresher ) { + CompletableFuture> loreFuture = PlaceholderUtil.replaceParcelPlaceholdersAsync(parcel, parcelItem.lore(), this.guiManager); + CompletableFuture> contentFuture = this.guiManager.getParcelContent(parcel.uuid()) + .thenApply(optional -> optional.map(content -> content.items()).orElse(List.of())); + + return loreFuture.thenCombine(contentFuture, (processedLore, items) -> () -> { + ConfigItem item = parcelItem.clone(); + item.name(item.name().replace("{NAME}", parcel.name())); + + List loreWithItems = new ArrayList<>(processedLore); + if (!items.isEmpty()) { + loreWithItems.add(this.guiSettings.parcelItemsCollectionGui); + for (ItemStack itemStack : items) { + loreWithItems.add(this.guiSettings.parcelItemCollectionFormat + .replace("{ITEM}", MaterialUtil.format(itemStack.getType())) + .replace("{AMOUNT}", Integer.toString(itemStack.getAmount())) + ); + } + } - return PlaceholderUtil.replaceParcelPlaceholdersAsync(parcel, parcelItem.lore(), this.guiManager) - .thenApply(processedLore -> () -> { - ConfigItem item = parcelItem.clone(); - item.name(item.name().replace("{NAME}", parcel.name())); - item.lore(processedLore); - item.glow(true); + item.lore(loreWithItems); + item.glow(true); - return item.toGuiItem(event -> { - this.guiManager.collectParcel(player, parcel); - refresher.removeItemBySlot(event.getSlot()); - }); + return item.toGuiItem(event -> { + this.guiManager.collectParcel(player, parcel); + refresher.removeItemBySlot(event.getSlot()); }); + }); } } diff --git a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGui.java b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGui.java index f87e5a80..d96c6416 100644 --- a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGui.java +++ b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGui.java @@ -10,26 +10,29 @@ import com.eternalcode.parcellockers.notification.NoticeService; import com.eternalcode.parcellockers.parcel.Parcel; import com.eternalcode.parcellockers.parcel.ParcelSize; -import com.eternalcode.parcellockers.shared.ScheduledGuiAction; import com.eternalcode.parcellockers.util.MaterialUtil; -import de.rapha149.signgui.SignGUI; -import de.rapha149.signgui.exception.SignGUIVersionException; import dev.rollczi.liteskullapi.SkullAPI; import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.GuiItem; +import io.papermc.paper.dialog.Dialog; +import io.papermc.paper.dialog.DialogResponseView; +import io.papermc.paper.registry.data.dialog.ActionButton; +import io.papermc.paper.registry.data.dialog.DialogBase; +import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.type.DialogType; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; +import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickCallback; import net.kyori.adventure.text.minimessage.MiniMessage; -import org.bukkit.Bukkit; -import org.bukkit.DyeColor; -import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +@SuppressWarnings("UnstableApiUsage") public class SendingGui implements GuiView { private static final int RECEIVER_ITEM_SLOT = 23; @@ -91,77 +94,96 @@ public void show(Player player) { GuiItem cornerItem = this.guiSettings.cornerItem.toGuiItem(); ConfigItem nameItem = this.guiSettings.parcelNameItem.clone(); GuiItem nameGuiItem = nameItem.toGuiItem(event -> { - try { - SignGUI nameSignGui = SignGUI.builder() - .setColor(DyeColor.BLACK) - .setType(Material.OAK_SIGN) - .setLine(0, "Enter parcel name:") - .setHandler((p, result) -> { - String name = result.getLineWithoutColor(1); - if (name == null || name.isBlank()) { - this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.nameCannotBeEmpty); - return Collections.emptyList(); - } - - this.state.parcelName(name); - this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.nameSet); - - List lore = nameItem.lore(); - if (lore.size() > 1) { - lore.remove(1); - } - - lore.add( - this.guiSettings.parcelNameSetLine.replace("{NAME}", - this.state.parcelName() == null ? "None" : this.state.parcelName()) - ); - - this.gui.updateItem(NAME_ITEM_SLOT, nameItem - .lore(lore) - .toItemStack()); - return List.of(new ScheduledGuiAction(this.scheduler, () -> this.gui.open(player))); - }) - .build(); - nameSignGui.open(player); - } catch (SignGUIVersionException e) { - Bukkit.getLogger().severe("The server version is unsupported by SignGUI API!"); - } + Dialog nameDialog = Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(this.miniMessage.deserialize("Enter parcel name:")) + .inputs(List.of( + DialogInput.text("name", this.miniMessage.deserialize("Parcel name")) + .build() + )) + .build() + ) + .type(DialogType.confirmation( + ActionButton.create( + this.miniMessage.deserialize("Confirm"), + this.miniMessage.deserialize("Click to set the name"), + 200, + DialogAction.customClick((DialogResponseView view, Audience audience) -> { + String name = view.getText("name"); + if (name == null || name.isBlank()) { + this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.nameCannotBeEmpty); + return; + } + + this.state.parcelName(name); + this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.nameSet); + + this.updateNameItem(); + this.scheduler.run(() -> this.gui.open(player)); + }, ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ), + ActionButton.create( + this.miniMessage.deserialize("Cancel"), + this.miniMessage.deserialize("Click to cancel"), + 200, + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> + this.scheduler.run(() -> this.gui.open(player)), + ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ) + )) + ); + player.showDialog(nameDialog); }); ConfigItem descriptionItem = this.guiSettings.parcelDescriptionItem.clone(); GuiItem descriptionGuiItem = descriptionItem.toGuiItem(event -> { - SignGUI descriptionSignGui = null; - try { - descriptionSignGui = SignGUI.builder() - .setColor(DyeColor.BLACK) - .setType(Material.OAK_SIGN) - .setLine(0, "Enter parcel description:") - .setHandler((p, result) -> { - String description = result.getLineWithoutColor(1); - - this.state.parcelDescription(description); - this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.descriptionSet); - - List lore = descriptionItem.clone().lore(); - if (lore.size() > 1) { - lore.remove(1); - } - - lore.add(this.guiSettings.parcelDescriptionSetLine.replace("{DESCRIPTION}", description)); - - this.gui.updateItem(DESCRIPTION_ITEM_SLOT, descriptionItem - .lore(lore) - .toItemStack()); - return List.of(new ScheduledGuiAction(this.scheduler, () -> this.gui.open(player))); - }) - .build(); - } catch (SignGUIVersionException e) { - Bukkit.getLogger().severe("The server version is unsupported by SignGUI API!"); - } - - if (descriptionSignGui != null) { - descriptionSignGui.open(player); - } + Dialog descriptionDialog = Dialog.create(builder -> builder.empty() + .base(DialogBase.builder(this.miniMessage.deserialize("Enter parcel description:")) + .inputs(List.of( + DialogInput.text("description", this.miniMessage.deserialize("Parcel description")) + .build() + )) + .build() + ) + .type(DialogType.confirmation( + ActionButton.create( + this.miniMessage.deserialize("Confirm"), + this.miniMessage.deserialize("Click to set the description"), + 200, + DialogAction.customClick((DialogResponseView view, Audience audience) -> { + String description = view.getText("description"); + + this.state.parcelDescription(description); + this.noticeService.player(player.getUniqueId(), messages -> messages.parcel.descriptionSet); + + this.updateDescriptionItem(); + this.scheduler.run(() -> this.gui.open(player)); + }, ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ), + ActionButton.create( + this.miniMessage.deserialize("Cancel"), + this.miniMessage.deserialize("Click to cancel"), + 200, + DialogAction.customClick( + (DialogResponseView view, Audience audience) -> + this.scheduler.run(() -> this.gui.open(player)), + ClickCallback.Options.builder() + .uses(1) + .lifetime(ClickCallback.DEFAULT_LIFETIME) + .build()) + ) + )) + ); + player.showDialog(descriptionDialog); }); ConfigItem parcelStorageItem = this.guiSettings.parcelStorageItem.clone(); diff --git a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGuiState.java b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGuiState.java index 259c25a0..32c87bf7 100644 --- a/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGuiState.java +++ b/src/main/java/com/eternalcode/parcellockers/gui/implementation/locker/SendingGuiState.java @@ -29,7 +29,7 @@ public SendingGuiState() { this.priority = false; this.entryLocker = UUID.randomUUID(); this.destinationLocker = null; - this.status = ParcelStatus.IN_PROGRESS; + this.status = ParcelStatus.SENT; } } diff --git a/src/main/java/com/eternalcode/parcellockers/gui/implementation/remote/ParcelListGui.java b/src/main/java/com/eternalcode/parcellockers/gui/implementation/remote/ParcelListGui.java index 8ef90750..229c0de9 100644 --- a/src/main/java/com/eternalcode/parcellockers/gui/implementation/remote/ParcelListGui.java +++ b/src/main/java/com/eternalcode/parcellockers/gui/implementation/remote/ParcelListGui.java @@ -5,16 +5,25 @@ import com.eternalcode.commons.scheduler.Scheduler; import com.eternalcode.parcellockers.configuration.implementation.PluginConfig.GuiSettings; import com.eternalcode.parcellockers.configuration.serializable.ConfigItem; +import com.eternalcode.parcellockers.delivery.Delivery; import com.eternalcode.parcellockers.gui.GuiManager; import com.eternalcode.parcellockers.gui.GuiView; +import com.eternalcode.parcellockers.parcel.Parcel; import com.eternalcode.parcellockers.parcel.util.PlaceholderUtil; import com.eternalcode.parcellockers.shared.Page; +import com.eternalcode.parcellockers.util.DurationUtil; import com.spotify.futures.CompletableFutures; import dev.triumphteam.gui.builder.item.PaperItemBuilder; import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.GuiItem; import dev.triumphteam.gui.guis.PaginatedGui; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -25,6 +34,8 @@ public class ParcelListGui implements GuiView { private static final int WIDTH = 7; private static final int HEIGHT = 4; private static final Page FIRST_PAGE = new Page(0, WIDTH * HEIGHT); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); + private final Scheduler scheduler; private final MiniMessage miniMessage; private final GuiSettings guiSettings; @@ -75,19 +86,7 @@ public void show(Player player, Page page) { ConfigItem item = this.guiSettings.parcelItem; List> itemFutures = result.items().stream() - .map(parcel -> PlaceholderUtil.replaceParcelPlaceholdersAsync(parcel, item.lore(), this.guiManager) - .thenApply(processedLore -> { - PaperItemBuilder parcelItem = item.toBuilder(); - - List newLore = processedLore.stream() - .map(line -> resetItalic(this.miniMessage.deserialize(line))) - .toList(); - - parcelItem.lore(newLore); - parcelItem.name(resetItalic(this.miniMessage.deserialize(item.name().replace("{NAME}", parcel.name())))); - - return parcelItem.asGuiItem(); - })) + .map(parcel -> this.createParcelItemAsync(parcel, item)) .toList(); CompletableFutures.allAsList(itemFutures) @@ -119,4 +118,50 @@ private void setupStaticItems(Player player, PaginatedGui gui) { gui.setItem(49, closeItem); } + + // fixme: parcels not appearing when there are many deliveries? + private CompletableFuture createParcelItemAsync(Parcel parcel, ConfigItem item) { + CompletableFuture> loreFuture = PlaceholderUtil.replaceParcelPlaceholdersAsync( + parcel, item.lore(), this.guiManager + ); + CompletableFuture> deliveryFuture = this.guiManager.getDelivery(parcel.uuid()); + + return loreFuture.thenCombine(deliveryFuture, (processedLore, deliveryOptional) -> { + PaperItemBuilder parcelItem = item.toBuilder(); + + List loreWithArrival = new ArrayList<>(processedLore); + + if (deliveryOptional.isPresent()) { + Delivery delivery = deliveryOptional.get(); + Instant arrivalTime = delivery.deliveryTimestamp(); + Instant now = Instant.now(); + + if (arrivalTime.isAfter(now)) { + Duration remaining = Duration.between(now, arrivalTime); + String formattedDuration = DurationUtil.format(remaining); + String formattedDate = DATE_FORMATTER.format(arrivalTime.atZone(ZoneId.systemDefault())); + + String arrivingLine = this.guiSettings.parcelArrivingLine + .replace("{DURATION}", formattedDuration) + .replace("{DATE}", formattedDate); + + loreWithArrival.add(arrivingLine); + } else if (arrivalTime.isBefore(now)) { // not supported rn, because deliveries are deleted on arrival, so the if is always false + String arrivedLine = this.guiSettings.parcelArrivedLine + .replace("{DATE}", DATE_FORMATTER.format(arrivalTime.atZone(ZoneId.systemDefault()))); + loreWithArrival.add(arrivedLine); + } + } + + List newLore = loreWithArrival.stream() + .map(line -> resetItalic(this.miniMessage.deserialize(line))) + .toList(); + + parcelItem.lore(newLore); + parcelItem.name(resetItalic(this.miniMessage.deserialize(item.name().replace("{NAME}", parcel.name())))); + + return parcelItem.asGuiItem(); + }); + } + } diff --git a/src/main/java/com/eternalcode/parcellockers/parcel/ParcelDispatchService.java b/src/main/java/com/eternalcode/parcellockers/parcel/ParcelDispatchService.java index f131d17f..9423230b 100644 --- a/src/main/java/com/eternalcode/parcellockers/parcel/ParcelDispatchService.java +++ b/src/main/java/com/eternalcode/parcellockers/parcel/ParcelDispatchService.java @@ -55,26 +55,30 @@ public void dispatch(Player sender, Parcel parcel, List items) { : this.config.settings.parcelSendDuration; return this.parcelService.send(sender, parcel, items) - .thenAccept(success -> { + .thenCompose(success -> { if (!Boolean.TRUE.equals(success)) { - return; + this.noticeService.player(sender.getUniqueId(), messages -> messages.parcel.cannotSend); + return CompletableFuture.completedFuture(null); } - this.deliveryManager.create(parcel.uuid(), Instant.now().plus(delay)); + return this.itemStorageManager.delete(sender.getUniqueId()) + .thenAccept(deleted -> { + if (!Boolean.TRUE.equals(deleted)) { + this.parcelService.delete(parcel.uuid()); // Implement this method + this.noticeService.player(sender.getUniqueId(), messages -> messages.parcel.cannotSend); + return; + } - ParcelSendTask task = new ParcelSendTask( - parcel, - this.parcelService, - this.deliveryManager - ); + this.deliveryManager.create(parcel.uuid(), Instant.now().plus(delay)); - // FIXME: Items will remain in the storage if this fails and parcel will be sent - this.itemStorageManager.delete(sender.getUniqueId()).exceptionally(throwable -> { - throwable.printStackTrace(); - return false; - }); + ParcelSendTask task = new ParcelSendTask( + parcel, + this.parcelService, + this.deliveryManager + ); - this.scheduler.runLaterAsync(task, delay); + this.scheduler.runLaterAsync(task, delay); + }); }); }) .exceptionally(throwable -> { diff --git a/src/main/java/com/eternalcode/parcellockers/parcel/ParcelStatus.java b/src/main/java/com/eternalcode/parcellockers/parcel/ParcelStatus.java index 6e8a598f..8744073b 100644 --- a/src/main/java/com/eternalcode/parcellockers/parcel/ParcelStatus.java +++ b/src/main/java/com/eternalcode/parcellockers/parcel/ParcelStatus.java @@ -2,6 +2,6 @@ public enum ParcelStatus { - IN_PROGRESS, + SENT, DELIVERED } diff --git a/src/main/java/com/eternalcode/parcellockers/util/DurationUtil.java b/src/main/java/com/eternalcode/parcellockers/util/DurationUtil.java new file mode 100644 index 00000000..0d7f5d17 --- /dev/null +++ b/src/main/java/com/eternalcode/parcellockers/util/DurationUtil.java @@ -0,0 +1,36 @@ +package com.eternalcode.parcellockers.util; + +import com.eternalcode.commons.time.DurationParser; +import com.eternalcode.commons.time.TemporalAmountParser; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.regex.Pattern; + +public final class DurationUtil { + + private static final Duration ONE_SECOND = Duration.ofSeconds(1); + private static final Pattern REFORMAT_PATTERN = Pattern.compile("(\\d+)([dhms]+)"); + private static final String REFORMAT_REPLACEMENT = "$1$2 "; + + private static final TemporalAmountParser DURATION = new DurationParser() + .withUnit("s", ChronoUnit.SECONDS) + .withUnit("m", ChronoUnit.MINUTES) + .withUnit("h", ChronoUnit.HOURS) + .withUnit("d", ChronoUnit.DAYS); + + private DurationUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static String format(Duration duration) { + if (duration.compareTo(ONE_SECOND) < 0) { + return "0s"; + } + return reformat(DURATION.format(duration)); + } + + + private static String reformat(String input) { + return REFORMAT_PATTERN.matcher(input).replaceAll(REFORMAT_REPLACEMENT).trim(); + } +} diff --git a/src/main/java/com/eternalcode/parcellockers/util/InventoryUtil.java b/src/main/java/com/eternalcode/parcellockers/util/InventoryUtil.java index 3026473f..e469b6c0 100644 --- a/src/main/java/com/eternalcode/parcellockers/util/InventoryUtil.java +++ b/src/main/java/com/eternalcode/parcellockers/util/InventoryUtil.java @@ -5,6 +5,10 @@ public class InventoryUtil { + private InventoryUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + public static int freeSlotsInInventory(Player player) { int freeSlots = 0; for (ItemStack item : player.getInventory().getStorageContents()) { diff --git a/src/test/java/com/eternalcode/parcellockers/database/ParcelRepositoryIntegrationTest.java b/src/test/java/com/eternalcode/parcellockers/database/ParcelRepositoryIntegrationTest.java index 3a6a3763..d85e2514 100644 --- a/src/test/java/com/eternalcode/parcellockers/database/ParcelRepositoryIntegrationTest.java +++ b/src/test/java/com/eternalcode/parcellockers/database/ParcelRepositoryIntegrationTest.java @@ -54,7 +54,7 @@ void test() { UUID destinationLocker = UUID.randomUUID(); parcelRepository.save(new Parcel(uuid, sender, "name", "description", true, receiver, - ParcelSize.SMALL, entryLocker, destinationLocker, ParcelStatus.IN_PROGRESS)); + ParcelSize.SMALL, entryLocker, destinationLocker, ParcelStatus.SENT)); Optional parcel = this.await(parcelRepository.fetchById(uuid)); assertTrue(parcel.isPresent());