diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index 6448b5cfe5..851dadb6e8 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -67,6 +67,35 @@ updater-source: "modrinth" This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) without core rewrites. +### New: Shop Tagging System + +A new player-driven tagging system has been introduced, allowing players to organize and track shops using custom tags. + +**Highlights** +- Players can add custom tags to shops they are looking at. +- Tags support letters, underscores (`_`), and dashes (`-`). +- Tags are normalized and limited in length to ensure consistency and performance. +- Reserved system tags power built-in features such as: + - `@fav` — Favorites + - `@watch` — Watchlist + - `@avoid` — Avoided shops +- Tags are stored per-player per-shop, allowing each player to maintain their own organization system. + +**Commands** +- `/qs tag ` — Add a tag to the shop you are looking at +- `/qs tag remove ` — Remove a tag from the current shop +- `/qs tag clear` — Remove all of your tags from the current shop +- `/qs tag tags` — List all tags you have created +- `/qs tag tagged ` — List shops you have tagged with a specific tag +- `/qs tag purge ` — Remove a tag from all shops +- `/qs favorite` - Add the current shop to your favorites +- `/qs watch` - Add the current shop to your watchlist +- `/qs avoid` - Add the current shop to your avoid list + +**Performance** +- Uses a dual-index caching system (`shop → tags` and `player → tag → shops`) for fast lookups. +- Tag filtering and listing operations are now **O(1)** instead of scanning all shops. + ## Minor Changes - Replaced startup parameter "playerDBFindUUIDTask" with config option "uuid-lookup.allow-playerdb-uuid" - Replaced startup parameter "playerDBFindNameTask" with config option "uuid-lookup.allow-playerdb-name" @@ -94,9 +123,29 @@ This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) - Replaced startup parameter "skipDatabaseVersionCheck" with config option "database.skip-version-check" - Bumped config version to 1037 - started including tax amount in shop failure message "you-cant-afford-to-buy" +- Update configuration file comments and standardize formatting(thanks to YuanYuanOwO) +- Split metrics into separate files and added FastStats metric tracking. +- Added toggle display icon to GUI. + +## Internals +- Deprecated FrozenType in favor of a new ShopState system. +- Added new ShopState system to handle shop state changes. +- Added new ShopPrice interface that accepts a generic type, which is an object for the shop's price. + - This new interface also contains methods for comparison purposes, such as price comparison for sorting in UIs. + - Added a format method to the ShopPrice interface to format the price for display. +- Added bukkitLocation() to the Locatable interface, which returns a BukkitLocation object. +- Replaced internal calls to shop.getLocation with shop.bukkitLocation +- Split the Shop interface into different sub interfaces to improve readability and maintainability. +- Started preloading all data for qs history command to improve performance.(reported by Warrior) + +## Deprecation Removals +- Removed ShopType enum and methods +- Removed deprecated tax methods. ## Fixes - Fix issue where disabling tax for unlimited shops wasn't properly working. - Fix issue where FAWE on 2.11.2 and lower wasn't working correctly. - Fix issue where FAWE would remove shops outside of selection zone. -- Fix issue where limits configs were not working properly(thanks to YuanYuanOwO) \ No newline at end of file +- Fix issue where limits configs were not working properly(thanks to YuanYuanOwO) +- Fix Folia thread violations in /qs info stock stats(thanks to GoodrichDev) +- Fix Towny Compat not working in Folia(thanks to GoodrichDev) \ No newline at end of file diff --git a/addon/betonquest/pom.xml b/addon/betonquest/pom.xml index 1c0e33e1da..068f08878d 100644 --- a/addon/betonquest/pom.xml +++ b/addon/betonquest/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/addon/bluemap/pom.xml b/addon/bluemap/pom.xml index d319c38495..e3dfa6ca25 100644 --- a/addon/bluemap/pom.xml +++ b/addon/bluemap/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java b/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java index 64e3c93c87..cc9cae45a7 100644 --- a/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java +++ b/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java @@ -82,7 +82,7 @@ private void updateAllMarkers() { public void updateShopMarker(final Shop shop) { - final Optional bWorld = blueMapAPI.getWorld(shop.getLocation().getWorld()); + final Optional bWorld = blueMapAPI.getWorld(shop.bukkitLocation().getWorld()); if(bWorld.isEmpty()) { return; } @@ -93,9 +93,9 @@ public void updateShopMarker(final Shop shop) { final POIMarker.Builder markerBuilder = POIMarker.builder() .label(markerName) - .position(shop.getLocation().getX(), - shop.getLocation().getY() + 1, - shop.getLocation().getZ()) + .position(shop.bukkitLocation().getX(), + shop.bukkitLocation().getY() + 1, + shop.bukkitLocation().getZ()) .maxDistance(getConfig().getDouble("max-distance")) .detail(desc); @@ -150,7 +150,7 @@ private String getShopColor(final Shop shop) { private String fillPlaceholders(String s, final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getX()); final String y = String.valueOf(loc.getY()); final String z = String.valueOf(loc.getZ()); diff --git a/addon/discordsrv/pom.xml b/addon/discordsrv/pom.xml index 7e6374b6d7..b2013a916a 100644 --- a/addon/discordsrv/pom.xml +++ b/addon/discordsrv/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java b/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java index eb6c232252..4e11cffdf6 100644 --- a/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java +++ b/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java @@ -55,10 +55,10 @@ public MessageEmbed modShopCreated(@NotNull final ShopCreateEvent event) { map.put("shop.name", ChatColor.stripColor(shop.getShopName())); map.put("shop.owner.name", wrap(shop.ownerName(plugin.text().findRelativeLanguages(langUser, false)))); - map.put("shop.location.world", shop.getLocation().getWorld().getName()); - map.put("shop.location.x", String.valueOf(shop.getLocation().getBlockX())); - map.put("shop.location.y", String.valueOf(shop.getLocation().getBlockY())); - map.put("shop.location.z", String.valueOf(shop.getLocation().getBlockZ())); + map.put("shop.location.world", shop.bukkitLocation().getWorld().getName()); + map.put("shop.location.x", String.valueOf(shop.bukkitLocation().getBlockX())); + map.put("shop.location.y", String.valueOf(shop.bukkitLocation().getBlockY())); + map.put("shop.location.z", String.valueOf(shop.bukkitLocation().getBlockZ())); map.put("shop.location.id", String.valueOf(shop.getShopId())); final Component customName = Util.getItemCustomName(shop.getItem()); if(customName != null) { @@ -77,7 +77,7 @@ public MessageEmbed modShopCreated(@NotNull final ShopCreateEvent event) { map.put("shop.display-name", shop.getShopName()); } else { // world, x, y, z - map.put("shop.display-name", String.format("%s %s, %s, %s", shop.getLocation().getWorld().getName(), shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ())); + map.put("shop.display-name", String.format("%s %s, %s, %s", shop.bukkitLocation().getWorld().getName(), shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ())); } map.put("shop.type", shop.shopType().identifier()); return map; @@ -126,7 +126,7 @@ private Map applyPlaceHoldersForPurchaseEvent(@NotNull final Map placeHolders.put("purchase.uuid", event.getPurchaser().toString()); placeHolders.put("purchase.name", getPlayerName(langUser)); //noinspection DataFlowIssue - placeHolders.put("purchase.world", shop.getLocation().getWorld().getName()); + placeHolders.put("purchase.world", shop.bukkitLocation().getWorld().getName()); placeHolders.put("purchase.amount", String.valueOf(event.getAmount())); placeHolders.put("purchase.balance", String.valueOf(event.getBalanceWithoutTax())); placeHolders.put("purchase.balance-formatted", purgeColors(plugin.getShopManager().format(event.getBalanceWithoutTax(), shop))); diff --git a/addon/discount/pom.xml b/addon/discount/pom.xml index 784360af8f..a23476cf5b 100644 --- a/addon/discount/pom.xml +++ b/addon/discount/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java b/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java index 2f6d4965bd..c1981d7fa4 100644 --- a/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java +++ b/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java @@ -48,7 +48,7 @@ public void onPrePurchase(final ShopInfoPanelEvent event) { case APPLICABLE -> quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code).send(); case APPLICABLE_WITH_THRESHOLD -> - quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); case NOT_APPLICABLE -> quickshop.text().of(purchaser, "addon.discount.discount-code-not-applicable", code).send(); case REACHED_THE_LIMIT -> @@ -84,16 +84,16 @@ public void onPurchase(final ShopPurchaseEvent event) { final double beforeDiscount = event.getTotal(); event.setTotal(codeInstalled.apply(purchaser, event.getTotal())); final double discounted = beforeDiscount - event.getTotal(); - quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } case APPLICABLE_WITH_THRESHOLD -> { if(event.getTotal() < codeInstalled.getThreshold()) { - quickshop.text().of(purchaser, "addon.discount.discount-code-under-threshold", quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-under-threshold", quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } else { final double beforeDiscount = event.getTotal(); event.setTotal(codeInstalled.apply(purchaser, event.getTotal())); final double discounted = beforeDiscount - event.getTotal(); - quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } } case NOT_APPLICABLE -> diff --git a/addon/displaycontrol/pom.xml b/addon/displaycontrol/pom.xml index 138df3f076..964003f8fe 100644 --- a/addon/displaycontrol/pom.xml +++ b/addon/displaycontrol/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/dynmap/pom.xml b/addon/dynmap/pom.xml index f07d10ab08..e955e1f5c0 100644 --- a/addon/dynmap/pom.xml +++ b/addon/dynmap/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java b/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java index da48fcadcd..302e9487d2 100644 --- a/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java +++ b/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java @@ -207,7 +207,7 @@ public void updateShopMarker(final Shop shop) { final MarkerSet markerSet = getMarkerSet(); String shopName = shop.getShopName(); - final String posStr = String.format("%s %s, %s, %s", shop.getLocation().getWorld().getName(), shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()); + final String posStr = String.format("%s %s, %s, %s", shop.bukkitLocation().getWorld().getName(), shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()); if(shopName == null) { shopName = posStr; } @@ -229,16 +229,16 @@ public void updateShopMarker(final Shop shop) { if(marker == null) { marker = markerSet.createMarker("quickshop-hikari-shop-" + shop.getShopId() , markerName - , shop.getLocation().getWorld().getName(), - shop.getLocation().getX(), - shop.getLocation().getY(), - shop.getLocation().getZ(), + , shop.bukkitLocation().getWorld().getName(), + shop.bukkitLocation().getX(), + shop.bukkitLocation().getY(), + shop.bukkitLocation().getZ(), getShopMarkerIcon(), false); } else { - marker.setLocation(shop.getLocation().getWorld().getName(), - shop.getLocation().getX(), - shop.getLocation().getY(), - shop.getLocation().getZ()); + marker.setLocation(shop.bukkitLocation().getWorld().getName(), + shop.bukkitLocation().getX(), + shop.bukkitLocation().getY(), + shop.bukkitLocation().getZ()); } final String desc = plain(text().of("addon.dynmap.marker-description", shopName, diff --git a/addon/limited/pom.xml b/addon/limited/pom.xml index eb2acf0863..397a651d63 100644 --- a/addon/limited/pom.xml +++ b/addon/limited/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/list/pom.xml b/addon/list/pom.xml index a75d014a05..f04f4a0cb6 100644 --- a/addon/list/pom.xml +++ b/addon/list/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java b/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java index eda8dcd9f4..5c58576f49 100644 --- a/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java +++ b/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java @@ -111,7 +111,7 @@ private void lookup(@NotNull final Player sender, @NotNull final UUID lookupUser continue; } String shopName = shop.getShopName(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String combineLocation = location.getWorld().getName() + " " + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ(); if(CommonUtil.isEmptyString(shopName)) { shopName = combineLocation; @@ -125,7 +125,7 @@ private void lookup(@NotNull final Player sender, @NotNull final UUID lookupUser } else { shopTypeComponent = quickshop.text().of(sender, "menu.this-shop-is-frozen").forLocale(); } - Component component = quickshop.text().of(sender, "addon.list.entry", counter, shopNameComponent, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()), shop.getShopStackingAmount(), Util.getItemStackName(shop.getItem()), shopTypeComponent).forLocale(); + Component component = quickshop.text().of(sender, "addon.list.entry", counter, shopNameComponent, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), shop.format(shop.bukkitLocation().getWorld().getName(), shop.getCurrency()), shop.getShopStackingAmount(), Util.getItemStackName(shop.getItem()), shopTypeComponent).forLocale(); component = component.clickEvent(ClickEvent.runCommand(MsgUtil.fillArgs("/{0} {1} {2}", quickshop.getMainCommand(), quickshop.getCommandPrefix("silentpreview"), shop.getRuntimeRandomUniqueId().toString()))); printer.printLine(component); loopCounter++; diff --git a/addon/pl3xmap/pom.xml b/addon/pl3xmap/pom.xml index cd4a36b255..a2ab8bc128 100644 --- a/addon/pl3xmap/pom.xml +++ b/addon/pl3xmap/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java b/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java index 8d606eeb45..7d6e23735b 100644 --- a/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java +++ b/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java @@ -54,11 +54,11 @@ public void addMarker(@NotNull final String key, @NotNull final Shop shop) { removeMarker(shop); - this.shopMarkers.get(shop.getLocation().getWorld().getName()).add(icon); + this.shopMarkers.get(shop.bukkitLocation().getWorld().getName()).add(icon); } public void removeMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); final String key = String.format("%s_%s_%s", Main.PL3X_KEY, worldName, shop.getShopId()); @@ -74,7 +74,7 @@ public void removeMarker(@NotNull final Shop shop) { } public Icon getIcon(@NotNull final String key, @NotNull final Shop shop) { - return Marker.icon(key, point(shop.getLocation()), Main.instance().icon()) + return Marker.icon(key, point(shop.bukkitLocation()), Main.instance().icon()) .setOptions(Options.builder() .tooltipDirection(Tooltip.Direction.TOP) .tooltipContent(fillPlaceholders(Main.instance().markerDetail(), shop)) @@ -89,7 +89,7 @@ public String plain(@NotNull final Component component) { private String fillPlaceholders(String s, final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getX()); final String y = String.valueOf(loc.getY()); final String z = String.valueOf(loc.getZ()); diff --git a/addon/plan/pom.xml b/addon/plan/pom.xml index 233ee1d498..7212ab1790 100644 --- a/addon/plan/pom.xml +++ b/addon/plan/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon @@ -85,7 +85,7 @@ com.ghostchu quickshop-platform-interface - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 provided diff --git a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java index bc98a78597..414dba3c75 100644 --- a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java +++ b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java @@ -120,7 +120,7 @@ public Table shopsTab() { final String item = dataUtil.getItemName(shop.getItem()) + " x" + shop.getShopStackingAmount(); String price = df.format(shop.getPrice()); if(main.getQuickShop().getEconomyManager().provider() != null) { - price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } final String type = switch(shop.shopType().identifier().toUpperCase(Locale.ROOT)) { @@ -128,7 +128,7 @@ public Table shopsTab() { case "FROZEN" -> "Frozen"; default -> "Selling"; }; - final String location = dataUtil.loc2String(shop.getLocation()); + final String location = dataUtil.loc2String(shop.bukkitLocation()); tableBuilder.addRow(owner, item, price, type, location); } return tableBuilder.build(); @@ -187,7 +187,7 @@ public Table shopsTabPlayer(final UUID playerUUID) { final String item = dataUtil.getItemName(shop.getItem()) + " x" + shop.getShopStackingAmount(); String price = df.format(shop.getPrice()); if(main.getQuickShop().getEconomyManager().provider() != null) { - price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + price = shop.format(shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } final String type = switch(shop.shopType().identifier().toUpperCase(Locale.ROOT)) { @@ -195,7 +195,7 @@ public Table shopsTabPlayer(final UUID playerUUID) { case "FROZEN" -> "Frozen"; default -> "Selling"; }; - final String location = dataUtil.loc2String(shop.getLocation()); + final String location = dataUtil.loc2String(shop.bukkitLocation()); tableBuilder.addRow(item, price, type, location); } return tableBuilder.build(); diff --git a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java index 0357a302e2..2afcc57a94 100644 --- a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java +++ b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java @@ -32,7 +32,7 @@ public String formatEconomy(@NotNull final ShopMetricRecord record) { final DecimalFormat df = new DecimalFormat("#.00"); return df.format(record.getTotal()); } - return main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(record.getTotal()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + return main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(record.getTotal()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } @NotNull @@ -73,7 +73,7 @@ public String getShopName(@NotNull final ShopMetricRecord record, @NotNull final nameBuilder.append(ChatColor.stripColor(shopName)); } else { if(shop != null) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String template = "%s %s,%s,%s"; nameBuilder.append(String.format(template, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); } else { diff --git a/addon/quests/pom.xml b/addon/quests/pom.xml index 2d84f1e479..2b612f3d13 100644 --- a/addon/quests/pom.xml +++ b/addon/quests/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/addon/reremake-migrator/pom.xml b/addon/reremake-migrator/pom.xml index 8bb2fb5e42..f7fe69236a 100644 --- a/addon/reremake-migrator/pom.xml +++ b/addon/reremake-migrator/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java b/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java index b45a4a73c2..41d7d97bf5 100644 --- a/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java +++ b/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java @@ -80,6 +80,7 @@ public boolean migrate() { QUserImpl.createSync(getHikari().getPlayerFinder(), reremakeShop.getOwner()), reremakeShop.isUnlimited(), QuickShop.getInstance().getShopManager().shopTypeOrDefault(reremakeShop.getShopType().toID()), + QuickShop.getInstance().getShopManager().shopStateOrDefault("active"), getReremakeShopExtra(reremakeShop), reremakeShop.getCurrency(), reremakeShop.isDisableDisplay(), diff --git a/addon/shopitemonly/pom.xml b/addon/shopitemonly/pom.xml index 6aa2bfaa77..03a704191c 100644 --- a/addon/shopitemonly/pom.xml +++ b/addon/shopitemonly/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/squaremap/pom.xml b/addon/squaremap/pom.xml index 365ed534f3..2dda92d9fe 100644 --- a/addon/squaremap/pom.xml +++ b/addon/squaremap/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.addon diff --git a/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java b/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java index 501fe8b00c..d8a24940e3 100644 --- a/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java +++ b/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java @@ -70,7 +70,7 @@ public void updateMarkers() { } public void updateShopMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); if(!registeredWorlds.containsKey(worldName)) { return; } @@ -86,14 +86,14 @@ public void updateShopMarker(@NotNull final Shop shop) { } public void removeShopMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); final Key markerKey = Key.of(String.format("%s_%s_%s", Main.SQUAREMAP_KEY, worldName, shop.getShopId())); provider.removeMarker(markerKey); } @NotNull private Icon createShopIcon(@NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final Point point = Point.of(loc.getBlockX(), loc.getBlockZ()); final String tooltip = fillPlaceholders(Main.instance().markerTooltip(), shop); @@ -117,7 +117,7 @@ private String plain(@NotNull final Component component) { @NotNull private String fillPlaceholders(@NotNull final String text, @NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getBlockX()); final String y = String.valueOf(loc.getBlockY()); final String z = String.valueOf(loc.getBlockZ()); diff --git a/compatibility/advancedregionmarket/pom.xml b/compatibility/advancedregionmarket/pom.xml index 519a32fe04..a46966dc32 100644 --- a/compatibility/advancedregionmarket/pom.xml +++ b/compatibility/advancedregionmarket/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility @@ -36,11 +36,11 @@ alex9849 - https://nexus.alex9849.net/repository/maven-releases/ + https://nexus.liggesmeyer.net/repository/maven-releases/ alex9849-snapshots - https://nexus.alex9849.net/repository/maven-snapshots/ + https://nexus.liggesmeyer.net/repository/maven-snapshots/ jitpack diff --git a/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java b/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java index a6440132b4..5d46acb7a3 100644 --- a/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java +++ b/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java @@ -87,7 +87,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } @@ -132,7 +132,7 @@ private void handleDeletion(final Region region) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - final Location shopLocation = shop.getLocation(); + final Location shopLocation = shop.bukkitLocation(); if(region.getRegion().contains(shopLocation.getBlockX(), shopLocation.getBlockY(), shopLocation.getBlockZ())) { getApi().getShopManager().deleteShop(shop); diff --git a/compatibility/angelchest/pom.xml b/compatibility/angelchest/pom.xml index 8e2f5c53f5..a200780d94 100644 --- a/compatibility/angelchest/pom.xml +++ b/compatibility/angelchest/pom.xml @@ -6,7 +6,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/bentobox/pom.xml b/compatibility/bentobox/pom.xml index 6b33e22cc3..c59aa494ad 100644 --- a/compatibility/bentobox/pom.xml +++ b/compatibility/bentobox/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java b/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java index 884ef3ff15..c90109a68a 100644 --- a/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java +++ b/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java @@ -96,7 +96,7 @@ public void onIslandResetted(final IslandResettedEvent event) { @EventHandler(ignoreCancelled = true) public void permissionOverride(final ShopPermissionCheckEvent event) { - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); BentoBox.getInstance().getIslandsManager().getIslandAt(shopLoc).ifPresent(island->{ if(event.playerUUID().equals(island.getOwner())) { if(event.pluginNamespace().equals(QuickShop.getInstance().getJavaPlugin().getName()) && event.permissionNode().equals(BuiltInShopPermission.DELETE.getRawNode())) { diff --git a/compatibility/bungeecord-geyser/pom.xml b/compatibility/bungeecord-geyser/pom.xml index 56dee39056..156611eeb4 100644 --- a/compatibility/bungeecord-geyser/pom.xml +++ b/compatibility/bungeecord-geyser/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/bungeecord/pom.xml b/compatibility/bungeecord/pom.xml index 97a9308fbc..588996adab 100644 --- a/compatibility/bungeecord/pom.xml +++ b/compatibility/bungeecord/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/chestprotect/pom.xml b/compatibility/chestprotect/pom.xml index 96626cde62..20e4e68a8d 100644 --- a/compatibility/chestprotect/pom.xml +++ b/compatibility/chestprotect/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/clearlag/pom.xml b/compatibility/clearlag/pom.xml index ef4210db2c..5b874599fd 100644 --- a/compatibility/clearlag/pom.xml +++ b/compatibility/clearlag/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/common/pom.xml b/compatibility/common/pom.xml index 3a28487f1a..6ee31f6f13 100644 --- a/compatibility/common/pom.xml +++ b/compatibility/common/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java b/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java index ee658060d1..9745874bc7 100644 --- a/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java +++ b/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java @@ -44,7 +44,7 @@ public List getShops(@NotNull final String worldName, final int minX, fina } } final BoundingBox boundingBox = new BoundingBox(minX, Integer.MIN_VALUE, minZ, maxX, Integer.MAX_VALUE, maxZ); - return shopsList.stream().filter(s->boundingBox.contains(s.getLocation().toVector())).toList(); + return shopsList.stream().filter(s->boundingBox.contains(s.bukkitLocation().toVector())).toList(); } public List getShops(@NotNull final String worldName, final int chunkX, final int chunkZ) { diff --git a/compatibility/dominion/pom.xml b/compatibility/dominion/pom.xml index 18531b8e3e..4c0ca9993e 100644 --- a/compatibility/dominion/pom.xml +++ b/compatibility/dominion/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java b/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java index 85b33a0675..4f069edfd6 100644 --- a/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java +++ b/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java @@ -145,7 +145,7 @@ public void onLandsDeleted(final DominionDeleteEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - final Location loc = event.getShop().getLocation(); + final Location loc = event.getShop().bukkitLocation(); if(loc == null) { return; } @@ -163,7 +163,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location loc = event.shop().get().getLocation(); + final Location loc = event.shop().get().bukkitLocation(); if(loc.getWorld() == null) { return; } diff --git a/compatibility/ecoenchants/pom.xml b/compatibility/ecoenchants/pom.xml index 96c7d019cd..9e19c69847 100644 --- a/compatibility/ecoenchants/pom.xml +++ b/compatibility/ecoenchants/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/elitemobs/pom.xml b/compatibility/elitemobs/pom.xml index 3cffc6c76c..5b8925e8fd 100644 --- a/compatibility/elitemobs/pom.xml +++ b/compatibility/elitemobs/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/fabledskyblock/pom.xml b/compatibility/fabledskyblock/pom.xml index a212b5216f..d22a16afb8 100644 --- a/compatibility/fabledskyblock/pom.xml +++ b/compatibility/fabledskyblock/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java b/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java index 3c8dce5622..dcb2cf8f5a 100644 --- a/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java +++ b/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java @@ -119,7 +119,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location loc = event.shop().get().getLocation(); + final Location loc = event.shop().get().bukkitLocation(); final Island island = SkyBlockAPI.getIslandManager().getIslandAtLocation(loc); if(island == null) { @@ -183,6 +183,6 @@ public List getShops(@NotNull final String worldName, final int minX, fina } } final BoundingBox boundingBox = new BoundingBox(minX, Integer.MIN_VALUE, minZ, maxX, Integer.MAX_VALUE, maxZ); - return shopsList.stream().filter(s->boundingBox.contains(s.getLocation().toVector())).toList(); + return shopsList.stream().filter(s->boundingBox.contains(s.bukkitLocation().toVector())).toList(); } } diff --git a/compatibility/griefprevention/pom.xml b/compatibility/griefprevention/pom.xml index bc9f54480d..058cd3bbdf 100644 --- a/compatibility/griefprevention/pom.xml +++ b/compatibility/griefprevention/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java b/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java index 04a789c4a4..15bb0d44f6 100644 --- a/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java +++ b/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java @@ -114,7 +114,7 @@ private void handleMainClaimUnclaimedOrExpired(final Claim claim, final String l final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(claim.contains(shop.getLocation(), false, false)) { + if(claim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [System] Claim/SubClaim Unclaimed/Expired: " + logMessage, this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -128,8 +128,8 @@ private void handleMainClaimResized(final Claim oldClaim, final Claim newClaim) final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(oldClaim.contains(shop.getLocation(), false, false) && - !newClaim.contains(shop.getLocation(), false, false)) { + if(oldClaim.contains(shop.bukkitLocation(), false, false) && + !newClaim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] Claim Resized: ", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -151,8 +151,8 @@ private void handleSubClaimResizedHelper(final Claim claimVerifyChunks, final Cl final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!claimVerifyChunks.getOwnerID().equals(shop.getOwner().getUniqueId()) && - claimVerifyChunks.contains(shop.getLocation(), false, false) && - !claimVerifyShop.contains(shop.getLocation(), false, false)) { + claimVerifyChunks.contains(shop.bukkitLocation(), false, false) && + !claimVerifyShop.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Resized: ", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -194,7 +194,7 @@ public void onTransfer(final ClaimTransferEvent event) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(event.getClaim().contains(shop.getLocation(), false, false)) { + if(event.getClaim().contains(shop.bukkitLocation(), false, false)) { continue; } @@ -271,7 +271,7 @@ private void handleSubClaimUnclaimed(final Claim subClaim) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!subClaim.getOwnerID().equals(shop.getOwner().getUniqueId()) && - subClaim.contains(shop.getLocation(), false, false)) { + subClaim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Unclaimed", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -332,7 +332,7 @@ public void onSubClaimCreated(final ClaimCreatedEvent event) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!event.getClaim().getOwnerID().equals(shop.getOwner().getUniqueId()) && - event.getClaim().contains(shop.getLocation(), false, false)) { + event.getClaim().contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Created", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -348,7 +348,7 @@ public void onTrading(final ShopPurchaseEvent event) { return; } - if(checkPermission(p, event.getShop().getLocation(), tradeLimits)) { + if(checkPermission(p, event.getShop().bukkitLocation(), tradeLimits)) { return; } event.setCancelled(true, getApi().getTextManager().of(event.getPurchaser(), "addon.griefprevention.trade-denied").forLocale()); @@ -363,7 +363,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { } Log.debug("GP-Compat: Starting override permission..."); - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(!griefPrevention.claimsEnabledForWorld(shopLoc.getWorld())) { final String worldName = shopLoc.getWorld() == null? "Null World" : shopLoc.getWorld().getName(); Log.debug("GP-Compat: World " + worldName + " not enabled for claims"); diff --git a/compatibility/husktowns/pom.xml b/compatibility/husktowns/pom.xml index 884256f69e..5d1890ec18 100644 --- a/compatibility/husktowns/pom.xml +++ b/compatibility/husktowns/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java b/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java index 47b0148c08..228aee8fe7 100644 --- a/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java +++ b/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java @@ -175,8 +175,8 @@ public void onUnClaimAll(final UnClaimAllEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(event.getShop().getLocation().getWorld() == null - || huskAPI.getClaimWorld(huskAPI.getWorld(event.getShop().getLocation().getWorld().getName())).isEmpty()) { + if(event.getShop().bukkitLocation().getWorld() == null + || huskAPI.getClaimWorld(huskAPI.getWorld(event.getShop().bukkitLocation().getWorld().getName())).isEmpty()) { if(ignoreDisabledWorlds) { return; @@ -192,7 +192,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/iridiumskyblock/pom.xml b/compatibility/iridiumskyblock/pom.xml index d3c97fd6dd..354471ee50 100644 --- a/compatibility/iridiumskyblock/pom.xml +++ b/compatibility/iridiumskyblock/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java b/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java index 2aab6e3eb8..b86b8b42e3 100644 --- a/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java +++ b/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java @@ -101,7 +101,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final Optional island = IridiumSkyblockAPI.getInstance().getIslandViaLocation(shopLoc); if(island.isEmpty()) { return; diff --git a/compatibility/itemsadder/pom.xml b/compatibility/itemsadder/pom.xml index a50ff44857..1b7253cf48 100644 --- a/compatibility/itemsadder/pom.xml +++ b/compatibility/itemsadder/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/lands/pom.xml b/compatibility/lands/pom.xml index 06570bd07d..127a89ec70 100644 --- a/compatibility/lands/pom.xml +++ b/compatibility/lands/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java b/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java index 0ce42fcce6..23264dc709 100644 --- a/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java +++ b/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java @@ -114,7 +114,7 @@ private void deleteShopInLand(final Land land, final UUID target) { if(target.equals(owner)) { recordDeletion(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "Lands", false), shop, "Lands: shop deleted because owner lost permission"); // Use regionThread for Folia/Canvas compatibility - must run on the region thread for this location - Util.regionThread(shop.getLocation(), ()->getApi().getShopManager().deleteShop(shop)); + Util.regionThread(shop.bukkitLocation(), ()->getApi().getShopManager().deleteShop(shop)); } } } @@ -135,8 +135,8 @@ public void onLandsPermissionChanges(final LandUntrustPlayerEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(event.getShop().getLocation().getWorld() == null - || landsIntegration.getWorld(event.getShop().getLocation().getWorld()) == null) { + if(event.getShop().bukkitLocation().getWorld() == null + || landsIntegration.getWorld(event.getShop().bukkitLocation().getWorld()) == null) { if(ignoreDisabledWorlds) { return; } @@ -151,7 +151,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/matcherplus/pom.xml b/compatibility/matcherplus/pom.xml index e3244fe184..622b37a869 100644 --- a/compatibility/matcherplus/pom.xml +++ b/compatibility/matcherplus/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/openinv/pom.xml b/compatibility/openinv/pom.xml index 6dfc3f00a8..3681988756 100644 --- a/compatibility/openinv/pom.xml +++ b/compatibility/openinv/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java b/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java index 38c367aba4..4467dedc99 100644 --- a/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java +++ b/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java @@ -42,7 +42,7 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ return; } if(shop.getInventory() instanceof EnderChestWrapper) { - shop.setInventory(new BukkitInventoryWrapper((((InventoryHolder)shop.getLocation().getBlock().getState()).getInventory())), plugin.getApi().getInventoryWrapperRegistry().get("QuickShop-Hikari")); + shop.setInventory(new BukkitInventoryWrapper((((InventoryHolder)shop.bukkitLocation().getBlock().getState()).getInventory())), plugin.getApi().getInventoryWrapperRegistry().get("QuickShop-Hikari")); sender.sendMessage(ChatColor.translateAlternateColorCodes('&', plugin.getConfig().getString("messages.to-chest"))); } else { shop.setInventory(new EnderChestWrapper(shop.getOwner().getUniqueIdIfRealPlayer().orElse(CommonUtil.getNilUniqueId()), plugin.getOpenInv(), plugin), plugin.getManager()); diff --git a/compatibility/plotsquared/pom.xml b/compatibility/plotsquared/pom.xml index 6b999e7501..ad766e0a8e 100644 --- a/compatibility/plotsquared/pom.xml +++ b/compatibility/plotsquared/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java b/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java index 1f09b706fd..4b4a7be33d 100644 --- a/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java +++ b/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java @@ -82,7 +82,7 @@ public void canCreateShopHere(final ShopCreateEvent event) { @EventHandler(ignoreCancelled = true) public void canTradeShopHere(final ShopPurchaseEvent event) { - final Location location = event.getShop().getLocation(); + final Location location = event.getShop().bukkitLocation(); final com.plotsquared.core.location.Location pLocation = com.plotsquared.core.location.Location.at( location.getWorld().getName(), location.getBlockX(), @@ -182,7 +182,7 @@ public void onPlotPlayerUntrusted(final com.plotsquared.core.events.PlayerPlotTr @EventHandler(ignoreCancelled = true) public void onShopTrading(final ShopPurchaseEvent event) { - final Location location = event.getShop().getLocation(); + final Location location = event.getShop().bukkitLocation(); final com.plotsquared.core.location.Location pLocation = com.plotsquared.core.location.Location.at( location.getWorld().getName(), location.getBlockX(), @@ -207,7 +207,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/reforges/pom.xml b/compatibility/reforges/pom.xml index 70980cb5b5..7cd3495969 100644 --- a/compatibility/reforges/pom.xml +++ b/compatibility/reforges/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/residence/pom.xml b/compatibility/residence/pom.xml index d7c76e71a9..abffad968b 100644 --- a/compatibility/residence/pom.xml +++ b/compatibility/residence/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java b/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java index 4445132c0b..9c7cf31c42 100644 --- a/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java +++ b/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java @@ -83,7 +83,7 @@ private boolean playerHas(final FlagPermissions permissions, final Player player @EventHandler(ignoreCancelled = true) public void onPurchase(final ShopPurchaseEvent event) { - final Location shopLoc = event.getShop().getLocation(); + final Location shopLoc = event.getShop().bukkitLocation(); final ClaimedResidence residence = Residence.getInstance().getResidenceManager().getByLoc(shopLoc); if(residence == null) { return; @@ -103,7 +103,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { if(!getConfig().getBoolean("allow-permission-override") || event.shop().isEmpty()) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final ClaimedResidence residence = ResidenceApi.getResidenceManager().getByLoc(shopLoc); diff --git a/compatibility/simpleclaimsystem/pom.xml b/compatibility/simpleclaimsystem/pom.xml index 89ee4376a7..5f25cee09d 100644 --- a/compatibility/simpleclaimsystem/pom.xml +++ b/compatibility/simpleclaimsystem/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java b/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java index 6acd3146af..3dc6e1da71 100644 --- a/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java +++ b/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java @@ -159,7 +159,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/slimefun/pom.xml b/compatibility/slimefun/pom.xml index 0f96381a76..047019d99a 100644 --- a/compatibility/slimefun/pom.xml +++ b/compatibility/slimefun/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/superiorskyblock/pom.xml b/compatibility/superiorskyblock/pom.xml index afaf395e7f..8a3dd792cf 100644 --- a/compatibility/superiorskyblock/pom.xml +++ b/compatibility/superiorskyblock/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java b/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java index e20858ee6b..6db7053baa 100644 --- a/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java +++ b/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java @@ -161,7 +161,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final Island island = SuperiorSkyblockAPI.getIslandAt(shopLoc); if(island == null) { return; diff --git a/compatibility/towny/pom.xml b/compatibility/towny/pom.xml index babafa2f77..108c01c3a2 100644 --- a/compatibility/towny/pom.xml +++ b/compatibility/towny/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java index 2df3e16265..67a7cbc6cf 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java @@ -273,11 +273,11 @@ private void purgeShops(@NotNull final Set worldCoords, @Nullable fi final QUser actor = QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "Towny", false); //Getting all shop with world-chunk-shop mapping for(final Shop shop : api.getShopManager().getAllShops()) { - if(!worldCoords.contains(WorldCoord.parseWorldCoord(shop.getLocation()))) { + if(!worldCoords.contains(WorldCoord.parseWorldCoord(shop.bukkitLocation()))) { continue; } if(overrideOwner || owner != null && owner.equals(shop.getOwner().getUniqueId())) { - Util.regionThread(shop.getLocation(), ()->{ + Util.regionThread(shop.bukkitLocation(), ()->{ recordDeletion(actor, shop, reason); getApi().getShopManager().deleteShop(shop); }); @@ -349,11 +349,11 @@ public void onPreCreation(final ShopCreateEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(isWorldIgnored(event.getShop().getLocation().getWorld())) { + if(isWorldIgnored(event.getShop().bukkitLocation().getWorld())) { return; } event.getPurchaser().getBukkitPlayer().ifPresent(player->{ - final Optional component = checkFlags(player, event.getShop().getLocation(), this.tradeFlags); + final Optional component = checkFlags(player, event.getShop().bukkitLocation(), this.tradeFlags); component.ifPresent(value->event.setCancelled(true, value)); }); } @@ -391,7 +391,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(isWorldIgnored(shopLoc.getWorld())) { return; } @@ -507,7 +507,7 @@ public void taxesAccountOverride(final ShopTaxAccountEvent event) { return; } // Modify tax account to town account if they aren't town shop or nation shop but inside town or nation - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town != null) { UUID uuid = QuickShop.getInstance().getPlayerFinder().name2Uuid(town.getAccount().getName()); if(uuid == null) { diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java index f9b05f8f68..143db2ac09 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java @@ -28,7 +28,7 @@ public static Nation getShopNation(@NotNull final Shop shop) { return null; } final Nation nation = TownyAPI.getInstance().getNation(UUID.fromString(uuid)); - Log.debug("Nation finding for shop " + shop.getLocation() + " => nation uuid: " + uuid + " nation: " + nation); + Log.debug("Nation finding for shop " + shop.bukkitLocation() + " => nation uuid: " + uuid + " nation: " + nation); return nation; } @@ -56,7 +56,7 @@ public static Town getShopTown(@NotNull final Shop shop) { return null; } final Town town = TownyAPI.getInstance().getTown(UUID.fromString(uuid)); - Log.debug("Town finding for shop " + shop.getLocation() + " => town uuid: " + uuid + " town: " + town); + Log.debug("Town finding for shop " + shop.bukkitLocation() + " => town uuid: " + uuid + " town: " + town); return town; } diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java index 4da40ffe47..e08ef02e3c 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java @@ -66,7 +66,7 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ } // Set as a nation shop - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); return; diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java index 7979167e2e..4b39783650 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java @@ -60,16 +60,16 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ } } // Set as a town shop - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); return; } if(plugin.getConfig().getBoolean("bank-mode.bank-plot-only", false)) { - final TownBlock townBlock = TownyAPI.getInstance().getTownBlock(shop.getLocation()); + final TownBlock townBlock = TownyAPI.getInstance().getTownBlock(shop.bukkitLocation()); if(townBlock == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); - plugin.getLogger().warning("Failed to get townBlock at " + shop.getLocation() + " maybe a bug?"); + plugin.getLogger().warning("Failed to get townBlock at " + shop.bukkitLocation() + " maybe a bug?"); return; } if(townBlock.getType() != TownBlockType.BANK) { diff --git a/compatibility/ultimateclaims/pom.xml b/compatibility/ultimateclaims/pom.xml index 73b0f1fa9a..b0fadcabcd 100644 --- a/compatibility/ultimateclaims/pom.xml +++ b/compatibility/ultimateclaims/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java b/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java index 17c12b44b5..7b6367b121 100644 --- a/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java +++ b/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java @@ -194,7 +194,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/velocity/pom.xml b/compatibility/velocity/pom.xml index 2e0bdbc7d2..85cd8889c1 100644 --- a/compatibility/velocity/pom.xml +++ b/compatibility/velocity/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/voidchest/pom.xml b/compatibility/voidchest/pom.xml index 3cc0b1fc61..03c6e4cb25 100644 --- a/compatibility/voidchest/pom.xml +++ b/compatibility/voidchest/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/worldguard/pom.xml b/compatibility/worldguard/pom.xml index ce197c1bca..b768e9c5ec 100644 --- a/compatibility/worldguard/pom.xml +++ b/compatibility/worldguard/pom.xml @@ -7,7 +7,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml com.ghostchu.quickshop.compatibility diff --git a/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java b/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java index fea0dc0348..cfd8449a16 100644 --- a/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java +++ b/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java @@ -89,7 +89,7 @@ public void onPermissionCheck(final ShopPermissionCheckEvent event) { if(event.shop().isEmpty()) return; - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final World world = shopLoc.getWorld(); if(world == null) return; @@ -121,7 +121,7 @@ public void onShopCreate(final ShopCreateEvent event) { final RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer(); final RegionQuery query = container.createQuery(); - final Location shopLocation = event.shop().get().getLocation(); + final Location shopLocation = event.shop().get().bukkitLocation(); final ApplicableRegionSet regions = query.getApplicableRegions(BukkitAdapter.adapt(shopLocation)); final RegionManager manager = container.get(BukkitAdapter.adapt(shopLocation.getWorld())); @@ -191,7 +191,7 @@ public void onShopPurchase(final ShopPurchaseEvent event) { event.getPurchaser().getBukkitPlayer().ifPresent(player->{ final RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer(); final RegionQuery query = container.createQuery(); - final Location shopLocation = event.getShop().getLocation(); + final Location shopLocation = event.getShop().bukkitLocation(); final ApplicableRegionSet regions = query.getApplicableRegions(BukkitAdapter.adapt(shopLocation)); final RegionManager manager = container.get(BukkitAdapter.adapt(shopLocation.getWorld())); diff --git a/platform/quickshop-platform-interface/pom.xml b/platform/quickshop-platform-interface/pom.xml index 7cd83b15db..5acfdf51cf 100644 --- a/platform/quickshop-platform-interface/pom.xml +++ b/platform/quickshop-platform-interface/pom.xml @@ -8,7 +8,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml diff --git a/platform/quickshop-platform-paper/pom.xml b/platform/quickshop-platform-paper/pom.xml index 67315051ac..6f807888c3 100644 --- a/platform/quickshop-platform-paper/pom.xml +++ b/platform/quickshop-platform-paper/pom.xml @@ -8,7 +8,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 ../../pom.xml diff --git a/pom.xml b/pom.xml index b9021944d1..4fea5fa514 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.ghostchu quickshop-hikari - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6 pom quickshop-hikari Another QuickShop fork @@ -39,6 +39,7 @@ 0.4.7 1.3.0 1.3.0 + 0.17.2 2.15.0 2.9.0-SNAPSHOT 1.21-R0.1-SNAPSHOT @@ -219,6 +220,7 @@ org.slf4j:slf4j-jdk14:* cc.carm.lib:* com.rollbar:* + dev.faststats.metrics:* net.kyori:adventure-api @@ -280,6 +282,10 @@ com.rollbar. ${qs.relocation}.com.rollbar. + + dev.faststats. + ${qs.relocation}.faststats + - - com.github.MilkBowl - VaultAPI - ${depend.vault} - provided - - - * - * - - - true - me.clip @@ -337,6 +329,11 @@ compile 0.4.7 + + dev.faststats.metrics + bukkit + ${depend.faststats} + org.apache.commons commons-compress diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java index ac7d54bef8..6cbc61cadb 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java @@ -27,11 +27,13 @@ import com.ghostchu.quickshop.api.shop.ShopItemBlackList; import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.display.DisplayType; +import com.ghostchu.quickshop.api.shop.tag.TagManager; import com.ghostchu.quickshop.command.QuickShopCommand; import com.ghostchu.quickshop.command.SimpleCommandManager; import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.common.util.QuickExecutor; +import com.ghostchu.quickshop.config.EffectsConfig; import com.ghostchu.quickshop.config.GuiConfig; import com.ghostchu.quickshop.config.MainConfig; import com.ghostchu.quickshop.database.DatabaseIOUtil; @@ -81,6 +83,7 @@ import com.ghostchu.quickshop.shop.interaction.QuickShopInteractionManager; import com.ghostchu.quickshop.shop.inventory.BukkitInventoryWrapperManager; import com.ghostchu.quickshop.shop.sign.SignHooker; +import com.ghostchu.quickshop.shop.tag.QuickShopTagManager; import com.ghostchu.quickshop.util.FastPlayerFinder; import com.ghostchu.quickshop.util.ItemMarker; import com.ghostchu.quickshop.util.MsgUtil; @@ -97,7 +100,6 @@ import com.ghostchu.quickshop.util.matcher.item.BukkitItemMatcherImpl; import com.ghostchu.quickshop.util.matcher.item.ModernCustomMatcher; import com.ghostchu.quickshop.util.matcher.item.QuickShopItemMatcherImpl; -import com.ghostchu.quickshop.util.matcher.item.TNEItemMatcherImpl; import com.ghostchu.quickshop.util.metric.MetricManager; import com.ghostchu.quickshop.util.paste.PasteManager; import com.ghostchu.quickshop.util.performance.PerfMonitor; @@ -147,6 +149,7 @@ import net.tnemc.menu.paper.listener.PaperInventoryClickListener; import net.tnemc.menu.paper.listener.PaperInventoryCloseListener; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.PluginCommand; @@ -199,6 +202,7 @@ public class QuickShop implements QuickShopAPI, Reloadable { @ApiStatus.Internal private static QuickShop instance; private MainConfig config; + private EffectsConfig effectsConfig; /** * The manager to check permissions. */ @@ -218,6 +222,7 @@ public class QuickShop implements QuickShopAPI, Reloadable { @Getter private final EconomyLoader economyLoader = new EconomyLoader(this); private final EconomyManager economyManager = new QSEconomyManager(); + private final QuickShopTagManager tagManager; private UpdateManager updateManager; @Getter private final PasteManager pasteManager = new PasteManager(); @@ -349,6 +354,7 @@ public QuickShop(final QuickShopBukkit javaPlugin, final Logger logger, final Pl this.logger = logger; this.platform = platform; this.helperMethods = new BukkitHelper(); + this.tagManager = new QuickShopTagManager(this); } /** @@ -417,7 +423,8 @@ public final void onLoad() { this.registry = new SimpleRegistryManager(); this.registry.registerRegistry(BuiltInRegistry.ITEM_EXPRESSION.getName(), new SimpleItemExpressionRegistry(this)); logger.info("Setting up metrics manager..."); - this.metricManager = new MetricManager(this); + this.metricManager = new MetricManager(); + this.metricManager.initPlatforms(); logger.info("Loading player name and unique id mapping..."); this.playerFinder = new FastPlayerFinder(this); loadTextManager(); @@ -486,6 +493,11 @@ private void initConfiguration() { logger.error("Failed to load config.yml, The binary file of QuickShop may be corrupted. Please re-download from our website."); } + this.effectsConfig = new EffectsConfig(this); + if(!this.effectsConfig.load()) { + logger.error("Failed to load effects.yml, The binary file of QuickShop may be corrupted. Please re-download from our website."); + } + /*try { javaPlugin.saveDefaultConfig(); } catch(final IllegalArgumentException resourceNotFoundException) { @@ -576,6 +588,16 @@ public YamlDocument getConfig() { return this.config.getYaml(); } + public EffectsConfig effectsConfig() { + return this.effectsConfig; + } + + @NotNull + public YamlDocument getEffects() { + + return this.effectsConfig.getYaml(); + } + private void updateConfig() { } @@ -634,6 +656,19 @@ public EconomyManager getEconomyManager() { return economyManager; } + /** + * Retrieves the TagManager associated with the QuickShop system. The TagManager is responsible + * for handling operations related to managing tags, which may include inventory item metadata and + * custom item tags used by the system. + * + * @return The TagManager instance that provides functionality for tag-related operations within + * the QuickShop system. + */ + @Override + public TagManager tagManager() { + return tagManager; + } + public UpdateManager updateManager() { return this.updateManager; } @@ -825,6 +860,10 @@ public final void onEnable() { shopLoader = new ShopLoader(this); shopLoader.loadShops(); QuickExecutor.getCommonExecutor().submit(this::bakeShopsOwnerCache); + + logger.info("Loading shop tags..."); + this.tagManager.loadAllFromDB(); + logger.info("Registering listeners..."); this.interactionManager = new QuickShopInteractionManager(this); // Register events @@ -1017,12 +1056,12 @@ private void registerDisplayItem() { } logger.info("Registering DisplayCheck task...."); folia.getScheduler().runTimerAsync(()->{ - for(final Shop shop : getShopManager().getLoadedShops()) { + for(final Shop shop : getShopManager().getLoadedShops()) { //Shop may be deleted or unloaded when iterating if(!shop.isLoaded()) { continue; } - folia.getScheduler().runAtLocationLater(shop.getLocation(), shop::checkDisplay, 1L); + folia.getScheduler().runAtLocationLater((Location)shop.bukkitLocation(), shop::checkDisplay, 1L); } }, 1L, getDisplayItemCheckTicks()); } else if(getDisplayItemCheckTicks() == 0) { @@ -1214,7 +1253,7 @@ public final void onDisable() { } if(getShopManager() != null) { logger.info("Saving all in-memory changed shops..."); - final List> futures = getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); + final List<@NotNull CompletableFuture> futures = getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); logger.info("Shops needed saved: " + futures.size()); final CompletableFuture[] completableFutures = futures.toArray(new CompletableFuture[0]); @@ -1236,7 +1275,7 @@ public final void onDisable() { shop.updateSync(); } catch(final RuntimeException re) { - logger.warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.getLocation(), re); + logger.warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.bukkitLocation(), re); } } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java index 27e6407db3..69d88e58d1 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java @@ -5,6 +5,7 @@ import com.ghostchu.quickshop.api.command.CommandManager; import com.ghostchu.quickshop.command.subcommand.SubCommand_About; import com.ghostchu.quickshop.command.subcommand.SubCommand_Amount; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Avoid; import com.ghostchu.quickshop.command.subcommand.SubCommand_Benefit; import com.ghostchu.quickshop.command.subcommand.SubCommand_Browse; import com.ghostchu.quickshop.command.subcommand.SubCommand_Buy; @@ -16,6 +17,7 @@ import com.ghostchu.quickshop.command.subcommand.SubCommand_Debug; import com.ghostchu.quickshop.command.subcommand.SubCommand_Empty; import com.ghostchu.quickshop.command.subcommand.SubCommand_Export; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Favorite; import com.ghostchu.quickshop.command.subcommand.SubCommand_FetchMessage; import com.ghostchu.quickshop.command.subcommand.SubCommand_Find; import com.ghostchu.quickshop.command.subcommand.SubCommand_Freeze; @@ -45,12 +47,14 @@ import com.ghostchu.quickshop.command.subcommand.SubCommand_StaffAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_SuggestPrice; import com.ghostchu.quickshop.command.subcommand.SubCommand_SuperCreate; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Tag; import com.ghostchu.quickshop.command.subcommand.SubCommand_TaxAccount; import com.ghostchu.quickshop.command.subcommand.SubCommand_ToggleDisplay; import com.ghostchu.quickshop.command.subcommand.SubCommand_ToggleDisplayAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_TransferAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_TransferOwnership; import com.ghostchu.quickshop.command.subcommand.SubCommand_Unlimited; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Watch; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentBuy; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentEmpty; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentFreeze; @@ -470,6 +474,59 @@ public SimpleCommandManager(final QuickShop plugin) { .permission("quickshop.suggestprice") .executor(new SubCommand_SuggestPrice(plugin)) .build()); + + final SubCommand_Avoid avoidCommand = new SubCommand_Avoid(plugin); + final SubCommand_Favorite favoriteCommand = new SubCommand_Favorite(plugin); + final SubCommand_Tag tagCommand = new SubCommand_Tag(plugin); + final SubCommand_Watch watchCommand = new SubCommand_Watch(plugin); + + registerCmd( + CommandContainer.builder() + .prefix("avoid") + .selectivePermission("quickshop.avoid") + .selectivePermission("quickshop.avoid.list") + .description((locale)->plugin.text().of("tags.commands.avoid").forLocale(locale)) + .disabledSupplier(()->!plugin.getConfig().getBoolean("shop-tag.enabled") || !plugin.getConfig().getBoolean("shop-tag.avoid-enabled")) + .executor(avoidCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("favorite") + .selectivePermission("quickshop.favorite") + .selectivePermission("quickshop.favorite.list") + .description((locale)->plugin.text().of("tags.commands.favorite").forLocale(locale)) + .disabledSupplier(()->!plugin.getConfig().getBoolean("shop-tag.enabled") || !plugin.getConfig().getBoolean("shop-tag.favorite-enabled")) + .executor(favoriteCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("tag") + .selectivePermission("quickshop.tag") + .selectivePermission("quickshop.tag.add") + .selectivePermission("quickshop.tag.delete") + .selectivePermission("quickshop.tag.list") + .selectivePermission("quickshop.tag.purge") + .selectivePermission("quickshop.tag.shops") + .selectivePermission("quickshop.tag.tagged") + .selectivePermission("quickshop.tag.tagged.teleport") + .selectivePermission("quickshop.tag.clear") + .selectivePermission("quickshop.tag.clearall") + .description((locale)->plugin.text().of("tags.commands.tag").forLocale(locale)) + .disabledSupplier(()->!plugin.getConfig().getBoolean("shop-tag.enabled")) + .executor(tagCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("watch") + .selectivePermission("quickshop.watch") + .selectivePermission("quickshop.watch.list") + .description((locale)->plugin.text().of("tags.commands.watch").forLocale(locale)) + .disabledSupplier(()->!plugin.getConfig().getBoolean("shop-tag.enabled") || !plugin.getConfig().getBoolean("shop-tag.watch-enabled")) + .executor(watchCommand) + .build()); init(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java new file mode 100644 index 0000000000..2a3d46a82a --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java @@ -0,0 +1,136 @@ +package com.ghostchu.quickshop.command.subcommand; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static com.ghostchu.quickshop.api.shop.tag.TagService.SYS_AVOID; +import static com.ghostchu.quickshop.shop.tag.QuickShopTagService.MAX_TAG_LENGTH; + +/** + * SubCommand_Avoid + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SubCommand_Avoid implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Avoid(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.avoid")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final String tag = plugin.tagManager().service().normalizeTag(SYS_AVOID, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "tags.general.invalid", MAX_TAG_LENGTH).send(); + return; + } + + if(parser.getArgs().isEmpty()) { + avoidShop(sender, parser, tag); + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { + + if(!plugin.perm().hasPermission(sender, "quickshop.avoid.list")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final int page = (parser.getArgs().size() >= 2)? Integer.parseInt(parser.getArgs().get(1)) : 1; + + plugin.tagManager().listShopsByFilter(sender, commandLabel, page, + new ArrayList<>(List.of(tag)), + "avoid", + "tags.tag.title-avoided-shops", + "tags.avoid.none"); + } + default -> avoidShop(sender, parser, tag); + } + } + + private void avoidShop(final Player sender, final CommandParser parser, final String tag) { + + final Shop shop = findShop(sender, parser, 0); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.avoid.added").send(); + } else { + plugin.text().of(sender, "tags.avoid.removed").send(); + } + } + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.avoid.unable").send(); + } + } + + @NotNull + @Override + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + + return List.of(plugin.text().of(sender, "tabcomplete.list").legacy(), + plugin.text().of(sender, "tabcomplete.shop-id").legacy()); + } else if(parser.getArgs().size() == 2) { + + return Collections.singletonList(plugin.text().of(sender, "tabcomplete.page").legacy()); + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java index c33ae74748..eca4c4e29a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java @@ -15,7 +15,6 @@ import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.obj.QUserImpl; import com.ghostchu.quickshop.util.MsgUtil; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java index 1fbe91e90a..e1450897ac 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java @@ -55,12 +55,12 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(world) { shops.addAll(plugin.getShopManager().getAllShops().stream().filter(shop->{ - if(shop.getLocation().getWorld() == null + if(shop.bukkitLocation().getWorld() == null || sender.getLocation().getWorld() == null) { return false; } - return shop.getLocation().getWorld().getUID().equals(sender.getLocation().getWorld().getUID()); + return shop.bukkitLocation().getWorld().getUID().equals(sender.getLocation().getWorld().getUID()); }).toList()); } else { shops.addAll(plugin.getShopManager().getAllShops()); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Buy.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Buy.java index dee65c9811..adf6b70021 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Buy.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Buy.java @@ -30,6 +30,9 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman shop.shopType(BUYING_TYPE); shop.setSignText(plugin.text().findRelativeLanguages(sender)); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); + + Util.playSound(sender, "effect.sound.shop.mode-change"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.mode-change"); } else { plugin.text().of(sender, "not-managed-shop").send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java index 4707fc5d6b..ddd3735d4f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java @@ -33,7 +33,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String for(final Shop shop : plugin.getShopManager().getAllShops()) { try { - if(Util.isLoaded(shop.getLocation()) + if(Util.isLoaded(shop.bukkitLocation()) && shop.isSelling() && shop.getRemainingStock() == 0) { pendingRemoval.add( @@ -50,7 +50,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } for(final Shop shop : pendingRemoval) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), ()->{ plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop clean", shop.saveToInfoStorage())); plugin.getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java index 1a3f172b5b..1557a1e36b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java @@ -44,7 +44,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final List> pendingTasks = new CopyOnWriteArrayList<>(); for(final Shop shop : plugin.getShopManager().getAllShops()) { - final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc) -> { + final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (loc)->{ if(shop == null) { return; // WTF } @@ -69,26 +69,25 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); return; } - if(!shop.getLocation().isWorldLoaded()) { + if(!shop.bukkitLocation().isWorldLoaded()) { plugin.text().of(sender, "cleanghost-deleting", shop.getShopId(), "unloaded world").send(); plugin.getShopManager().deleteShop(shop); deletionCounter.incrementAndGet(); plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); return; } - if(!Util.canBeShop(shop.getLocation().getBlock())) { + if(!Util.canBeShop(shop.bukkitLocation().getBlock())) { plugin.text().of(sender, "cleanghost-deleting", shop.getShopId(), "invalid shop block").send(); plugin.getShopManager().deleteShop(shop); deletionCounter.incrementAndGet(); plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); - return; } }); pendingTasks.add(task); } CompletableFuture.allOf(pendingTasks.toArray(new CompletableFuture[0])) - .whenComplete((v, t) -> plugin.text().of(sender, "cleanghost-deleted", deletionCounter.get()).send()); + .whenComplete((v, t)->plugin.text().of(sender, "cleanghost-deleted", deletionCounter.get()).send()); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java index 2881f5fd78..31398cae56 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java @@ -65,7 +65,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman plugin.text().of(sender, "currency-not-support").send(); return; } - if(!plugin.getEconomyManager().provider().supportsCurrency(Objects.requireNonNull(shop.getLocation().getWorld()).getName(), parser.getArgs().getFirst())) { + if(!plugin.getEconomyManager().provider().supportsCurrency(Objects.requireNonNull(shop.bukkitLocation().getWorld()).getName(), parser.getArgs().getFirst())) { plugin.text().of(sender, "currency-not-exists").send(); return; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java index 13e1f0e1a0..a20702f970 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java @@ -83,7 +83,7 @@ private void handleTrim(@NotNull final CommandSender sender, @NotNull final List private void saveShops(final CommandSender sender, @NotNull final List subParams) { plugin.logger().info("Saving all in-memory changed shops..."); - final List> futures = plugin.getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); + final List futures = plugin.getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); plugin.logger().info("Shops needed saved: " + futures.size()); final CompletableFuture[] completableFutures = futures.toArray(new CompletableFuture[0]); @@ -105,7 +105,7 @@ private void saveShops(final CommandSender sender, @NotNull final List s shop.updateSync(); } catch(final RuntimeException re) { - plugin.logger().warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.getLocation(), re); + plugin.logger().warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.bukkitLocation(), re); } } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java index 6c1fc18a79..88520ac6a3 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java @@ -135,8 +135,8 @@ private void handleItemInfo(final CommandSender sender, final List subPa return; } if(player.getInventory().getItemInMainHand().getType().isAir()) { - plugin.text().of(sender, "no-anythings-in-your-hand").send(); - return; + plugin.text().of(sender, "no-anythings-in-your-hand").send(); + return; } final String hand = player.getInventory().getItemInMainHand().getItemMeta().getAsString(); plugin.text().of(sender, "debug.item-info-hand-as-string", hand, Hashing.crc32().hashString(hand, StandardCharsets.UTF_8).toString()).send(); @@ -186,7 +186,7 @@ private void handleDbConnectionTest(final CommandSender sender, final List subParams) { - final long stopped = plugin.getSqlManager().getActiveQuery().values().size(); + final long stopped = plugin.getSqlManager().getActiveQuery().size(); plugin.getSqlManager().getActiveQuery().values().forEach(SQLQuery::close); plugin.text().of(sender, "debug.queries-stopped", stopped).send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java new file mode 100644 index 0000000000..9e78483b97 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java @@ -0,0 +1,136 @@ +package com.ghostchu.quickshop.command.subcommand; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static com.ghostchu.quickshop.api.shop.tag.TagService.SYS_FAV; +import static com.ghostchu.quickshop.shop.tag.QuickShopTagService.MAX_TAG_LENGTH; + +/** + * SubCommand_Favorite + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SubCommand_Favorite implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Favorite(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.favorite")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final String tag = plugin.tagManager().service().normalizeTag(SYS_FAV, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "tags.general.invalid", MAX_TAG_LENGTH).send(); + return; + } + + if(parser.getArgs().isEmpty()) { + favoriteShop(sender, parser, tag); + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { + + if(!plugin.perm().hasPermission(sender, "quickshop.favorite.list")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final int page = (parser.getArgs().size() >= 2)? Integer.parseInt(parser.getArgs().get(1)) : 1; + + plugin.tagManager().listShopsByFilter(sender, commandLabel, page, + new ArrayList<>(List.of(tag)), + "favorite", + "tags.tag.title-favorited-shops", + "tags.favorite.none"); + } + default -> favoriteShop(sender, parser, tag); + } + } + + private void favoriteShop(final Player sender, final CommandParser parser, final String tag) { + + final Shop shop = findShop(sender, parser, 0); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.favorite.added").send(); + } else { + plugin.text().of(sender, "tags.favorite.removed").send(); + } + } + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.favorite.unable").send(); + } + } + + @NotNull + @Override + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + + return List.of(plugin.text().of(sender, "tabcomplete.list").legacy(), + plugin.text().of(sender, "tabcomplete.shop-id").legacy()); + } else if(parser.getArgs().size() == 2) { + + return Collections.singletonList(plugin.text().of(sender, "tabcomplete.page").legacy()); + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java index 0c1c1877d0..1e6c16dfb5 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java @@ -87,7 +87,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } //Calc distance between player and shop for(final Shop shop : scanPool) { - if(!Objects.equals(shop.getLocation().getWorld(), loc.getWorld())) { + if(!Objects.equals(shop.bukkitLocation().getWorld(), loc.getWorld())) { continue; } if(aroundShops.size() == shopLimit) { @@ -97,7 +97,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman && !plugin.perm().hasPermission(sender, "quickshop.other.search")) { continue; } - final Vector shopVector = shop.getLocation().toVector(); + final Vector shopVector = shop.bukkitLocation().toVector(); final double distance = shopVector.distance(playerVector); //Check distance if(distance <= maxDistance || global) { @@ -123,13 +123,13 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } //Okay now all shops is our wanted shop in Map - final List> sortedShops = aroundShops.entrySet().stream().sorted(Map.Entry.comparingByValue(Double::compare).reversed()).toList(); + final List> sortedShops = aroundShops.entrySet().stream().sorted(Map.Entry. comparingByValue(Double::compare).reversed()).toList(); //Function if(usingOldLogic) { final Map.Entry closest = sortedShops.getFirst(); - final Location lookAt = closest.getKey().getLocation().clone().add(0.5, 0.5, 0.5); + final Location lookAt = closest.getKey().bukkitLocation().clone().add(0.5, 0.5, 0.5); sender.teleportAsync(Util.lookAt(sender.getEyeLocation(), lookAt).add(0, -1.62, 0), PlayerTeleportEvent.TeleportCause.UNKNOWN); plugin.text().of(sender, "nearby-shop-this-way", closest.getValue().intValue()).send(); @@ -138,7 +138,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman for(final Map.Entry shopDoubleEntry : sortedShops) { final Shop shop = shopDoubleEntry.getKey(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); ItemStack previewItemStack = shop.getItem().clone(); final ItemPreviewComponentPrePopulateEvent previewComponentPrePopulateEvent = new ItemPreviewComponentPrePopulateEvent(previewItemStack, sender); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java index 10deef0682..3e4529f112 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java @@ -12,7 +12,9 @@ import java.util.Collections; import java.util.List; +import static com.ghostchu.quickshop.shop.SimpleShopManager.ACTIVE_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.BUYING_TYPE; +import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_TYPE; public class SubCommand_Freeze implements CommandHandler { @@ -29,19 +31,22 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman final Shop shop = getLookingShop(sender); if(shop != null) { - if(shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_SHOPTYPE) + if(shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_SHOP_STATE) || plugin.perm().hasPermission(sender, "quickshop.other.freeze")) { - if(shop.shopType().isTradingBlocked() && shop.shopType().identifier().equalsIgnoreCase("FROZEN")) { + if(!shop.shopState().isTradingAllowed() && shop.shopState().identifier().equalsIgnoreCase("FROZEN")) { - shop.shopType(BUYING_TYPE); + shop.shopState(ACTIVE_STATE); plugin.text().of(sender, "shop-nolonger-freezed", Util.getItemStackName(shop.getItem())).send(); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); } else { - shop.shopType(FROZEN_TYPE); + shop.shopState(FROZEN_STATE); plugin.text().of(sender, "shop-now-freezed", Util.getItemStackName(shop.getItem())).send(); } + + Util.playSound(sender, "effect.sound.shop.freeze-toggle"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.freeze-toggle"); } else { plugin.text().of(sender, "not-managed-shop").send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_History.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_History.java index 6d805d81ca..530d03ab3f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_History.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_History.java @@ -3,6 +3,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.database.bean.DataRecord; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.shop.history.ShopHistory; @@ -19,7 +20,11 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_DATA_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_SUMMARY; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.SHOPS_DATA; @@ -82,6 +87,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } } } + final MenuViewer viewer = new MenuViewer(sender.getUniqueId()); MenuManager.instance().addViewer(viewer); @@ -99,8 +105,36 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } + final Map dataRecords = new ConcurrentHashMap<>(); + final List> futures = new ArrayList<>(); + + for(final ShopHistory.ShopHistoryRecord record : queryResult) { + final long id = record.dataId(); + + futures.add(QuickShop.getInstance() + .getDatabaseHelper() + .getDataRecord(id) + .thenAccept(data->{ + if(data != null) { + + dataRecords.put(id, data); + } + }) + .exceptionally(ex->{ + + QuickShop.getInstance().logger().warn( + "Failed to load DataRecord for id {}", + id, + ex); + return null; + })); + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + viewer.addData(SHOPS_DATA, shops); viewer.addData(HISTORY_RECORDS, queryResult); + viewer.addData(HISTORY_DATA_RECORDS, dataRecords); viewer.addData(HISTORY_SUMMARY, summary); Util.mainThreadRun(()->{ MenuManager.instance().open("qs:history", 1, menuPlayer); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java index f7f196705e..562a9b07cf 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java @@ -74,7 +74,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final AtomicInteger noStockCounter = new AtomicInteger(0); final List> futures = new ArrayList<>(outOfStockCheckQueue.size()); for(final Shop shop : outOfStockCheckQueue) { - final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), task -> { + final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), task->{ try { if(shop.getRemainingStock() == 0) { noStockCounter.incrementAndGet(); @@ -91,7 +91,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final int worldsFinal = worlds; CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .whenComplete((unused, throwable)->QuickShop.folia().getScheduler().runLater(()-> - sendStats(sender, buyingFinal, sellingFinal, chunksFinal, worldsFinal, noStockCounter.get()), 1)); + sendStats(sender, buyingFinal, sellingFinal, chunksFinal, worldsFinal, noStockCounter.get()), 1)); } private void sendStats(@NotNull final CommandSender sender, final int buying, final int selling, final int chunks, final int worlds, final int nostock) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java index 8d84117d8e..49a3d063ba 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java @@ -76,7 +76,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(fee > 0) { if(!plugin.perm().hasPermission(sender, "quickshop.bypass.namefee")) { transaction = QSEconomyTransaction.builder() - .world(shop.getLocation().getWorld().getName()) + .world(shop.bukkitLocation().getWorld().getName()) .from(QUserImpl.createFullFilled(sender)) .to(shop.getTaxAccount()) .currency(plugin.getCurrency()) @@ -85,7 +85,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman .amount(BigDecimal.valueOf(fee)) .build(); if(!transaction.completable()) { - plugin.text().of(sender, "you-cant-afford-shop-naming", plugin.getShopManager().format(fee, shop.getLocation().getWorld(), plugin.getCurrency())).send(); + plugin.text().of(sender, "you-cant-afford-shop-naming", plugin.getShopManager().format(fee, shop.bukkitLocation().getWorld(), plugin.getCurrency())).send(); return; } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java index 99b1214f78..df84c37966 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java @@ -72,13 +72,13 @@ private boolean pasteToLocalFile(@NotNull final CommandSender sender) { try { final boolean createResult = file.createNewFile(); Log.debug("Create paste file: " + file.getCanonicalPath() + " " + createResult); - try(FileWriter fwriter = new FileWriter(file)) { + try(final FileWriter fwriter = new FileWriter(file)) { fwriter.write(string); fwriter.flush(); } plugin.text().of(sender, "paste-created-local", file.getAbsolutePath()).send(); return true; - } catch(IOException e) { + } catch(final IOException e) { if(plugin.getSentryErrorReporter() != null) { plugin.getSentryErrorReporter().ignoreThrow(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Price.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Price.java index 2f62144061..764d8ccf40 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Price.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Price.java @@ -67,7 +67,12 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } - ShopUtil.setPrice(plugin, QUserImpl.createFullFilled(sender), priceDouble, shop); + final boolean changed = ShopUtil.setPrice(plugin, QUserImpl.createFullFilled(sender), priceDouble, shop); + if(changed) { + + Util.playSound(sender, "effect.sound.shop.price-change"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.price-change"); + } } @NotNull diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Remove.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Remove.java index f60d249f6f..89b9e1970c 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Remove.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Remove.java @@ -30,7 +30,10 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } if(shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.DELETE) || plugin.perm().hasPermission(sender, "quickshop.other.destroy")) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { + + Util.playSound(sender, "effect.sound.shop.remove"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.remove"); plugin.getShopManager().deleteShop(shop); plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(sender), "/quickshop remove command", shop.saveToInfoStorage())); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveAll.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveAll.java index 77a6f8b34b..d535c80e0f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveAll.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveAll.java @@ -9,6 +9,7 @@ import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logging.container.ShopRemoveLog; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,9 +61,14 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } pendingRemoval.forEach(shop->{ plugin.logEvent(new ShopRemoveLog(qUser, "Deleting shop " + shop + " as requested by the /quickshop removeall command.", shop.saveToInfoStorage())); - Util.regionThread(shop.getLocation(), () -> plugin.getShopManager().deleteShop(shop)); + Util.regionThread(shop.bukkitLocation(), () -> plugin.getShopManager().deleteShop(shop)); }); plugin.text().of(sender, "command.some-shops-removed", pendingRemoval.size()).send(); + + if(sender instanceof final Player player) { + Util.playSound(player, "effect.sound.shop.remove"); + Util.playParticle(player, "effect.particle.shop.remove"); + } }) .exceptionally(err->{ plugin.text().of(sender, "internal-error", err.getMessage()).send(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java index 5a62ad5b00..a6e9afebeb 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java @@ -9,6 +9,7 @@ import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -36,13 +37,19 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } int shopsDeleted = 0; for(final Shop shop : plugin.getShopManager().getAllShops()) { - if(Objects.equals(shop.getLocation().getWorld(), world)) { - Util.regionThread(shop.getLocation(), () -> plugin.getShopManager().deleteShop(shop)); + + if(Objects.equals(shop.bukkitLocation().getWorld(), world)) { + Util.regionThread(shop.bukkitLocation(), () -> plugin.getShopManager().deleteShop(shop)); shopsDeleted++; } } Log.debug("Successfully deleted all shops in world " + parser.getArgs().getFirst() + "!"); plugin.text().of(sender, "shops-removed-in-world", shopsDeleted, world.getName()).send(); + + if(sender instanceof final Player player) { + Util.playSound(player, "effect.sound.shop.remove"); + Util.playParticle(player, "effect.particle.shop.remove"); + } } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sell.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sell.java index 445a8bb31a..2fc4e40c93 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sell.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sell.java @@ -30,6 +30,9 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman shop.shopType(SELLING_TYPE); shop.setSignText(plugin.text().findRelativeLanguages(sender)); plugin.text().of(sender, "command.now-selling", Util.getItemStackName(shop.getItem())).send(); + + Util.playSound(sender, "effect.sound.shop.mode-change"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.mode-change"); } else { plugin.text().of(sender, "not-managed-shop").send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java index 5dce12cbeb..7fadafbc7f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java @@ -49,7 +49,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } for(final Sign sign : shop.getSigns()) { - plugin.getShopManager().makeShopSign(shop.getLocation().getBlock(), sign.getBlock(), material); + plugin.getShopManager().makeShopSign(shop.bukkitLocation().getBlock(), sign.getBlock(), material); shop.claimShopSign(sign); } shop.setSignText(plugin.text().findRelativeLanguages(sender)); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java new file mode 100644 index 0000000000..fbb5ebd436 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java @@ -0,0 +1,422 @@ +package com.ghostchu.quickshop.command.subcommand; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; +import com.ghostchu.quickshop.util.pagination.Pagination; +import com.ghostchu.quickshop.util.pagination.PaginationOptions; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Consumer; + +import static com.ghostchu.quickshop.api.shop.tag.TagService.TOTAL_INDEX; +import static com.ghostchu.quickshop.shop.tag.QuickShopTagManager.MAX_PER_PAGE; + +public class SubCommand_Tag implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Tag(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + if(!plugin.perm().hasPermission(sender, "quickshop.tag")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final String sub = parser.getArgs().getFirst().toLowerCase(Locale.ROOT); + + switch(sub) { + case "shops" -> handleShopsTagged(sender, commandLabel, parser); + case "tagged" -> handleTaggedList(sender, commandLabel, parser); + case "purge", "removefromall", "untagall" -> handleRemoveTagFromAllShops(sender, parser); + case "add" -> handleAdd(sender, parser, 1); + case "remove", "del", "delete" -> handleRemove(sender, parser); + case "clear" -> handleClear(sender, parser); + case "clearall" -> handleClearAll(sender, parser); //todo: confirmation maybe for clear and clearall? + case "list" -> handleTags(sender, commandLabel, parser); + default -> handleAdd(sender, parser, 0); + } + + System.out.println("Tag command: " + sub + " Parser Args: " + parser.getArgs().size() + ""); + + int i = 0; + for(final String arg : parser.getArgs()) { + System.out.println("Arg " + i + ": " + arg); + i++; + } + } + + private void handleAdd(final Player sender, final CommandParser parser, final int tagIndex) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.add")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + if(parser.getArgs().size() <= tagIndex) { + sendUsage(sender); + return; + } + + final Shop shop = findShop(sender, parser, tagIndex + 1); + if(shop == null) { + + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(tagIndex), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } + + final TaggingResult result = plugin.tagManager().addTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "tags.tag.added", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case ALREADY_EXISTS -> + plugin.text().of(sender, "tags.tag.duplicate", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case INVALID_TAG -> plugin.text().of(sender, "tags.general.invalid").send(); + case DATABASE_ERROR -> plugin.text().of(sender, "tags.general.database-error").send(); + default -> + plugin.text().of(sender, "tags.tag.failed-add", plugin.tagManager().service().displayTag(sender, normalized)).send(); + } + } + + private void handleRemove(final Player sender, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.delete")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final Shop shop = findShop(sender, parser, 2); + if(shop == null) { + + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } + + final TaggingResult result = plugin.tagManager().removeTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "tags.tag.removed", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case NOT_FOUND -> + plugin.text().of(sender, "tags.tag.does-not-exist", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case DATABASE_ERROR -> plugin.text().of(sender, "tags.general.database-error").send(); + default -> + plugin.text().of(sender, "tags.tag.failed-remove", plugin.tagManager().service().displayTag(sender, normalized)).send(); + } + } + + private void handleClear(final Player sender, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.clear")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final Shop shop = findShop(sender, parser, 1); + if(shop == null) { + + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final boolean removed = plugin.tagManager().removeAllShopTagsBy(shop.getShopId(), sender.getUniqueId()); + if(removed) { + plugin.text().of(sender, "tags.tag.cleared", shop.getShopId()).send(); + } else { + plugin.text().of(sender, "tags.tag.no-tags", shop.getShopId()).send(); + } + } + + private void handleClearAll(final Player sender, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.clearall")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final boolean removed = plugin.tagManager().removeAllTags(); + if(removed) { + plugin.text().of(sender, "tags.tag.cleared-all").send(); + } else { + plugin.text().of(sender, "tags.tag.no-tags-all").send(); + } + } + + private void handleShopsTagged(final Player sender, @NotNull final String commandLabel, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.shops")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final TreeMap count = plugin.tagManager().tagsCount(sender.getUniqueId()); + if(count.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops").send(); + return; + } + + final int page = (parser.getArgs().size() >= 2)? Integer.parseInt(parser.getArgs().get(1)) : 1; + + final PaginationOptions options = PaginationOptions + .builder() + .setCommand(commandLabel + " tag shops") + .setCurrentPage(page) + .setEntries(List.copyOf(count.keySet())) + .setMaxPerPage(MAX_PER_PAGE) + .setEntryConsumer((pos, entry)->{ + + if(entry.equals(TOTAL_INDEX)) { + return; + } + + plugin.text().of(sender, "tags.tag.list-player-shop-entry", + pos, + entry, + count.getOrDefault((Long)entry, -1), + commandLabel + " tag list 1 " + entry).send(); + }) + .setHeaderLanguageKey("pagination.header") + .setFooterLanguageKey("pagination.footer").build(); + + final Pagination shops = new Pagination<>(options); + + final Component titleComponent = plugin.text().of(sender, "tags.tag.list-player-shops-title", count.size() - 1, count.get(TOTAL_INDEX)).forLocale(); + + shops.printHeader(sender, titleComponent, shops.page(), shops.totalPages()); + shops.printEntries(sender); + shops.printFooter(sender, options.command() + " " + shops.previousPage(), + shops.page(), shops.totalPages(), + options.command() + " " + shops.nextPage()); + } + + private void handleTags(final Player sender, @NotNull final String commandLabel, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.list")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final Shop shop = findShop(sender, parser, 2); + if(shop == null) { + + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final List tags = List.copyOf(plugin.tagManager().tagsFilteredByShop(sender.getUniqueId(), shop.getShopId())); + if(tags.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops-player", shop.getShopId()).send(); + return; + } + + final int page = (parser.getArgs().size() >= 2)? Integer.parseInt(parser.getArgs().get(1)) : 1; + + final PaginationOptions options = PaginationOptions + .builder() + .setCommand(commandLabel + " tag list") + .setCurrentPage(page) + .setEntries(tags) + .setMaxPerPage(MAX_PER_PAGE) + .setEntryConsumer((pos, entry)->{ + plugin.text().of(sender, "tags.general.list-entry", + pos, + entry, + commandLabel + " " + plugin.tagManager().service().commandFromTag((String)entry, true), + commandLabel + " " + plugin.tagManager().service().commandFromTag((String)entry, false) + " " + shop.getShopId()).send(); + }) + .setHeaderLanguageKey("pagination.header") + .setFooterLanguageKey("pagination.footer").build(); + + final Pagination shops = new Pagination<>(options); + + final Component titleComponent = plugin.text().of(sender, "tags.tag.list-player-shop-title", tags.size()).forLocale(); + + shops.printHeader(sender, titleComponent, shops.page(), shops.totalPages()); + shops.printEntries(sender); + shops.printFooter(sender, options.command() + " " + shops.previousPage(), + shops.page(), shops.totalPages(), + options.command() + " " + shops.nextPage()); + } + + private void handleTaggedList(final Player sender, @NotNull final String commandLabel, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.tagged")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final int page = (parser.getArgs().size() >= 3)? Integer.parseInt(parser.getArgs().get(2)) : 1; + + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } + + final List shopIds = plugin.tagManager().shopsFilteredByTag(sender.getUniqueId(), normalized); + if(shopIds.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops-tag", normalized).send(); + return; + } + + final PaginationOptions options = PaginationOptions + .builder() + .setCommand(commandLabel + " tag tagged " + normalized + "") + .setCurrentPage(page) + .setEntries(shopIds) + .setMaxPerPage(MAX_PER_PAGE) + .setEntryConsumer((pos, entry)->{ + + final Shop shop = plugin.getShopManager().getShop((Long)entry); + if(shop == null) { + return; + } + + //TODO: Option to teleport through list? Maybe add a /qs teleport command for this? + //final boolean canTeleport = plugin.perm().hasPermission(sender, "quickshop.tagged.teleport"); + //final Component tpComponent = plugin.text().of(sender, "tags.tag.list-tag-shop-entry-tp", commandLabel + " tp ").forLocale(); + + plugin.text().of(sender, "tags.tag.list-tag-shop-entry", pos, entry, shop.getOwner().getDisplay(), shop.getItem().displayName(), "", commandLabel + " tag remove " + normalized).send(); + }) + .setHeaderLanguageKey("pagination.header") + .setFooterLanguageKey("pagination.footer").build(); + + final Pagination shops = new Pagination<>(options); + + + final Component titleComponent = plugin.text().of(sender, "tags.tag.list-tag-title", shopIds.size()).forLocale(); + + shops.printHeader(sender, titleComponent, shops.page(), shops.totalPages()); + shops.printEntries(sender); + shops.printFooter(sender, options.command() + " " + shops.previousPage(), + shops.page(), shops.totalPages(), + options.command() + " " + shops.nextPage()); + } + + private void handleRemoveTagFromAllShops(final Player sender, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.purge")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } + + final boolean cleared = plugin.tagManager().removeTag(sender.getUniqueId(), normalized); + if(!cleared) { + plugin.text().of(sender, "tags.general.database-error", normalized).send(); + return; + } + + plugin.text().of(sender, "tags.tag.cleared-tag", normalized).send(); + } + + @NotNull + @Override + public List onTabComplete(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + + return List.of(plugin.text().of(sender, "tabcomplete.add").legacy(), + plugin.text().of(sender, "tabcomplete.remove").legacy(), + plugin.text().of(sender, "tabcomplete.clear").legacy(), + plugin.text().of(sender, "tabcomplete.clearall").legacy(), + plugin.text().of(sender, "tabcomplete.list").legacy(), + plugin.text().of(sender, "tabcomplete.shops").legacy(), + plugin.text().of(sender, "tabcomplete.tagged").legacy(), + plugin.text().of(sender, "tabcomplete.purge").legacy(), + plugin.text().of(sender, "tabcomplete.tag").legacy()); + } + + if(parser.getArgs().size() == 2) { + switch(parser.getArgs().getFirst().toLowerCase(Locale.ROOT)) { + case "add", "remove": + return List.of(plugin.text().of(sender, "tabcomplete.tag").legacy()); + case "clear": + return List.of(plugin.text().of(sender, "tabcomplete.shop-id").legacy()); + case "shops": + case "list": + return List.of(plugin.text().of(sender, "tabcomplete.page").legacy()); + case "tagged": + return List.of(plugin.text().of(sender, "tabcomplete.tag").legacy()); + case "purge": + return List.of(plugin.text().of(sender, "tabcomplete.tag").legacy()); + default: + return Collections.emptyList(); + } + } + + if(parser.getArgs().size() == 3) { + switch(parser.getArgs().getFirst().toLowerCase(Locale.ROOT)) { + case "list": + case "shops": + case "remove": + return List.of(plugin.text().of(sender, "tabcomplete.shop-id").legacy()); + case "tagged": + return List.of(plugin.text().of(sender, "tabcomplete.page").legacy()); + default: + } + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + + plugin.text().of(sender, "command-incorrect", + "/quickshop tag [tag]").send(); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplay.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplay.java index eb42e19315..74d576cc1b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplay.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplay.java @@ -7,6 +7,7 @@ import com.ghostchu.quickshop.api.event.settings.type.ShopDisplayEvent; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -51,6 +52,8 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman shop.setDisableDisplay(event.updated()); final String message = (event.updated())? "display-turn-off" : "display-turn-on"; + Util.playSound(sender, "effect.sound.shop.toggle-display"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.toggle-display"); plugin.text().of(sender, message).send(); event = event.clone(Phase.POST); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplayAll.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplayAll.java index 2bc50dd844..71219c1788 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplayAll.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_ToggleDisplayAll.java @@ -26,6 +26,7 @@ import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.obj.QUserImpl; +import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -93,6 +94,9 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman event = event.clone(Phase.POST); event.callEvent(); + + Util.playSound(sender, "effect.sound.shop.toggle-display"); + Util.playParticle(sender, "effect.particle.shop.toggle-display"); } final String message = (server)? ((off)? "display-turn-off-all" : "display-turn-on-all") : ((off)? "display-turn-off-owned" : "display-turn-on-owned"); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java index e06658220e..efe27d44b1 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java @@ -77,8 +77,8 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman plugin.text().of(sender, "not-looking-at-shop").send(); return; } - if (!targetShop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.OWNERSHIP_TRANSFER) - && !plugin.perm().hasPermission(sender, "quickshop.transferownership.other")) { + if(!targetShop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.OWNERSHIP_TRANSFER) + && !plugin.perm().hasPermission(sender, "quickshop.transferownership.other")) { plugin.text().of(sender, "no-permission").send(); return; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java new file mode 100644 index 0000000000..a1ec494eb5 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java @@ -0,0 +1,112 @@ +package com.ghostchu.quickshop.command.subcommand; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TagService; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static com.ghostchu.quickshop.shop.tag.QuickShopTagService.MAX_TAG_LENGTH; + + +public class SubCommand_Watch implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Watch(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.watch")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final String tag = plugin.tagManager().service().normalizeTag(TagService.SYS_WATCH, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "tags.general.invalid", MAX_TAG_LENGTH).send(); + return; + } + + if(parser.getArgs().isEmpty()) { + watchShop(sender, parser, tag); + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { + + if(!plugin.perm().hasPermission(sender, "quickshop.watch.list")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final int page = (parser.getArgs().size() >= 2)? Integer.parseInt(parser.getArgs().get(1)) : 1; + + plugin.tagManager().listShopsByFilter(sender, commandLabel, page, + new ArrayList<>(List.of(tag)), + "watch", + "tags.tag.title-watched-shops", + "tags.watch.none"); + } + default -> watchShop(sender, parser, tag); + } + } + + public void watchShop(final Player sender, final CommandParser parser, final String tag) { + final Shop shop = findShop(sender, parser, 0); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.watch.added").send(); + } else { + plugin.text().of(sender, "tags.watch.removed").send(); + } + } + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.watch.unable").send(); + } + } + + @NotNull + @Override + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + + return List.of(plugin.text().of(sender, "tabcomplete.list").legacy(), + plugin.text().of(sender, "tabcomplete.shop-id").legacy()); + } else if(parser.getArgs().size() == 2) { + + return Collections.singletonList(plugin.text().of(sender, "tabcomplete.page").legacy()); + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java index b006b23163..8d8204b90b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java @@ -32,6 +32,8 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N shop.setSignText(plugin.text().findRelativeLanguages(sender)); MsgUtil.sendControlPanelInfo(sender, shop); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); - } -} + Util.playSound(sender, "effect.sound.shop.mode-change"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.mode-change"); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java index d19d593573..467e4bc375 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java @@ -9,7 +9,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import static com.ghostchu.quickshop.shop.SimpleShopManager.ACTIVE_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.BUYING_TYPE; +import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_TYPE; @@ -29,17 +31,20 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N return; } - if(shop.shopType().isTradingBlocked() && shop.shopType().identifier().equalsIgnoreCase("FROZEN")) { - shop.shopType(BUYING_TYPE); + if(!shop.shopState().isTradingAllowed() && shop.shopState().identifier().equalsIgnoreCase("FROZEN")) { + shop.shopState(ACTIVE_STATE); plugin.text().of(sender, "shop-nolonger-freezed", Util.getItemStackName(shop.getItem())).send(); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); } else { - shop.shopType(FROZEN_TYPE); + shop.shopState(FROZEN_STATE); plugin.text().of(sender, "shop-now-freezed", Util.getItemStackName(shop.getItem())).send(); } MsgUtil.sendControlPanelInfo(sender, shop); shop.setSignText(plugin.text().findRelativeLanguages(sender)); + + Util.playSound(sender, "effect.sound.shop.freeze-toggle"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.freeze-toggle"); } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentHistory.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentHistory.java index 09f609e06e..c78620b580 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentHistory.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentHistory.java @@ -2,6 +2,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.database.bean.DataRecord; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.shop.history.ShopHistory; @@ -15,7 +16,11 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_DATA_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_SUMMARY; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.SHOPS_DATA; @@ -55,8 +60,36 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N return; } + final Map dataRecords = new ConcurrentHashMap<>(); + final List> futures = new ArrayList<>(); + + for(final ShopHistory.ShopHistoryRecord record : queryResult) { + final long id = record.dataId(); + + futures.add(QuickShop.getInstance() + .getDatabaseHelper() + .getDataRecord(id) + .thenAccept(data->{ + if(data != null) { + + dataRecords.put(id, data); + } + }) + .exceptionally(ex->{ + + QuickShop.getInstance().logger().warn( + "Failed to load DataRecord for id {}", + id, + ex); + return null; + })); + } + + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + viewer.addData(SHOPS_DATA, shops); viewer.addData(HISTORY_RECORDS, queryResult); + viewer.addData(HISTORY_DATA_RECORDS, dataRecords); viewer.addData(HISTORY_SUMMARY, summary); Util.mainThreadRun(()->{ MenuManager.instance().open("qs:history", 1, menuPlayer); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java index 45204da69c..211dd520cd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java @@ -33,7 +33,7 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N Log.debug("Inventory is empty! " + cs); return; } - + if(plugin.getConfig().getBoolean("shop.lock") && !shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.ACCESS_INVENTORY)) { if(plugin.perm().hasPermission(sender, "quickshop.other.open")) { if(LockListener.lockCoolDown.getIfPresent(sender.getUniqueId()) == null) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java index 98acf30ae9..509514e290 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java @@ -38,7 +38,11 @@ protected void doSilentCommand(@NotNull final Player sender, @NotNull final Shop } final boolean skipConfirmation = plugin.getConfig().getBoolean("shop.skip-command-confirmation", false); if(sender.getUniqueId().equals(deleteConfirmation.getIfPresent(shop.getRuntimeRandomUniqueId())) || skipConfirmation) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { + + Util.playSound(sender, "effect.sound.shop.remove"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.remove"); + plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(sender), "/quickshop silentremove command", shop.saveToInfoStorage())); plugin.getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentSell.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentSell.java index 2761653046..80e49459ab 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentSell.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentSell.java @@ -37,5 +37,8 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N plugin.text().of(sender, "command.now-selling", Util.getItemStackName(shop.getItem())).send(); + Util.playSound(sender, "effect.sound.shop.mode-change"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.mode-change"); + } } \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentToggleDisplay.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentToggleDisplay.java index c42679c254..9153ec2f09 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentToggleDisplay.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentToggleDisplay.java @@ -4,6 +4,7 @@ import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.util.MsgUtil; +import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -19,6 +20,8 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N shop.setDisableDisplay(!shop.isDisableDisplay()); shop.setSignText(plugin.text().findRelativeLanguages(sender)); + Util.playSound(sender, "effect.sound.shop.toggle-display"); + Util.playParticle(sender, shop.bukkitLocation(), "effect.particle.shop.toggle-display"); MsgUtil.sendControlPanelInfo(sender, shop); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/config/EffectsConfig.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/config/EffectsConfig.java new file mode 100644 index 0000000000..a66cbb8bb4 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/config/EffectsConfig.java @@ -0,0 +1,56 @@ +package com.ghostchu.quickshop.config; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.config.QSConfig; +import dev.dejvokep.boostedyaml.YamlDocument; +import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning; +import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings; +import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings; + +import java.util.Collections; + +/** + * EffectsConfig + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class EffectsConfig extends QSConfig { + + private static EffectsConfig instance; + + public EffectsConfig(final QuickShop plugin) { + + super("effects.yml", "effects.yml", Collections.emptyList(), + LoaderSettings.builder().setAutoUpdate(true).build(), + UpdaterSettings.builder().setAutoSave(true) + .setVersioning(new BasicVersioning("config-version")) + .build()); + + instance = this; + plugin.getReloadManager().register(this); + } + + public static YamlDocument yaml() { + + return instance.getYaml(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java index 0f2aa99d41..aabe478f64 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java @@ -31,6 +31,7 @@ public enum DataTables { table.addColumn("name", "TEXT"); // SHOP NAME table.addColumn("type", "INT NOT NULL DEFAULT 0"); // SHOP TYPE (see ShopType enum) + table.addColumn("shop_state", "VARCHAR(64)"); // shop state table.addColumn("currency", "VARCHAR(64)"); // CURRENCY (NULL means use the default currency) table.addColumn("price", "DECIMAL(32,2) NOT NULL"); // SHOP ITEM PRICE diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index 3af198a584..a1bd927cb3 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -21,7 +21,6 @@ import com.ghostchu.quickshop.database.bean.SimpleDataRecord; import com.ghostchu.quickshop.shop.ContainerShop; import com.ghostchu.quickshop.shop.cache.SimpleShopInventoryCountCache; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.performance.PerfMonitor; import org.apache.commons.lang3.tuple.Triple; @@ -58,7 +57,7 @@ public class SimpleDatabaseHelperV2 implements DatabaseHelper { @NotNull private final String prefix; - private final int LATEST_DATABASE_VERSION = 19; + private final int LATEST_DATABASE_VERSION = 20; public SimpleDatabaseHelperV2(@NotNull final QuickShop plugin, @NotNull final SQLManager manager, @NotNull final String prefix) throws Exception { @@ -212,6 +211,33 @@ private void upgradeBenefit() { return manager; } + private void addStateColumn() { + + fastBackup(); + try { + Log.debug("Adding state column to " + DataTables.DATA.getName()); + getManager().alterTable(DataTables.DATA.getName()) + .addColumn("shop_state", "VARCHAR(64)") + .execute(); + + Log.debug("Converting old frozen type shops to new frozen state."); + getManager().createUpdate(DataTables.SHOPS.getName()) + .setColumnValues("shop_state", "frozen") + .addCondition("type", 2) + .build().execute(); + + Log.debug("Converting old frozen type shops to buy type."); + getManager().createUpdate(DataTables.SHOPS.getName()) + .setColumnValues("type", 1) + .addCondition("type", 2) + .build().execute(); + + } catch(final SQLException e) { + + Log.debug("Failed to add state " + DataTables.DATA.getName() + "! Err:" + e.getMessage()); + } + } + private void addEncodedColumn() { fastBackup(); @@ -526,6 +552,40 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina return shopRecords; } + @Override + public void loadAllTags() { + try(final SQLQuery query = DataTables.TAGS.createQuery().build().execute()) { + + final ResultSet set = query.getResultSet(); + while(set.next()) { + + final String tagger = set.getString("tagger"); + final long shopID = set.getLong("shop"); + final String tag = set.getString("tag"); + + plugin.tagManager().addTag(shopID, UUID.fromString(tagger), tag, false); + } + + } catch(final SQLException e) { + plugin.logger().error("Failed to load all tags", e); + } + } + + @Override + public @NotNull List listShopsByTag(@NotNull final String tag) { + + final List shopIds = new ArrayList<>(); + try(final SQLQuery query = DataTables.TAGS.createQuery() + .addCondition("tag", tag) + .build().execute()) { + final ResultSet set = query.getResultSet(); + shopIds.add(set.getLong("shop")); + } catch(final SQLException e) { + plugin.logger().error("Failed to list shops by with tag " + tag, e); + } + return shopIds; + } + @Override public @NotNull List listShopsTaggedBy(@NotNull final UUID tagger, @NotNull final String tag) { @@ -557,6 +617,47 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina return tags; } + @Override + public @NotNull List listTags(@NotNull final UUID tagger, @NotNull final Long shopId) { + + final List tags = new ArrayList<>(); + try(final SQLQuery query = DataTables.TAGS.createQuery() + .addCondition("tagger", tagger.toString()) + .addCondition("shop", shopId) + .build().execute()) { + final ResultSet set = query.getResultSet(); + tags.add(set.getString("tag")); + } catch(final SQLException e) { + plugin.logger().error("Failed to list tags by " + tagger, e); + } + return tags; + } + + @Override + public @NotNull CompletableFuture<@Nullable Integer> tagShop(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { + + return DataTables.TAGS.createInsert() + .setColumnNames("tagger", "shop", "tag") + .setParams(tagger.toString(), shopId, tag) + .executeFuture(i->i); + } + + @Override + public CompletableFuture<@Nullable Integer> removeAllShopTags(@NotNull final Long shopId) { + + return DataTables.TAGS.createDelete() + .addCondition("shop", shopId) + .build().executeFuture(i->i); + } + + @Override + public CompletableFuture<@Nullable Integer> removeAllTagsBy(@NotNull final UUID tagger) { + + return DataTables.TAGS.createDelete() + .addCondition("tagger", tagger.toString()) + .build().executeFuture(i->i); + } + @Override public CompletableFuture<@Nullable Integer> removeShopTag(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { @@ -567,7 +668,7 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina } @Override - public CompletableFuture<@Nullable Integer> removeShopAllTag(@NotNull final UUID tagger, @NotNull final Long shopId) { + public CompletableFuture<@Nullable Integer> removeAllShopTagsBy(@NotNull final UUID tagger, @NotNull final Long shopId) { return DataTables.TAGS.createDelete() .addCondition("tagger", tagger.toString()) @@ -584,15 +685,6 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina .build().executeFuture(i->i); } - @Override - public @NotNull CompletableFuture<@Nullable Integer> tagShop(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { - - return DataTables.TAGS.createInsert() - .setColumnNames("tagger", "shop", "tag") - .setParams(tagger.toString(), shopId, tag) - .executeFuture(i->i); - } - @Override public @NotNull CompletableFuture<@Nullable Long> locateShopDataId(final long shopId) { @@ -780,7 +872,7 @@ public CompletableFuture updatePlayerProfileInBatch(final List updateShop(@NotNull final Shop shop) { final SimpleDataRecord simpleDataRecord = ((ContainerShop)shop).createDataRecord(); - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); // check if datarecord exists final long shopId = shop.getShopId(); if(shopId < 1) { @@ -978,7 +1070,7 @@ public void upgrade() { int currentDatabaseVersion = parent.getDatabaseVersion(); if(currentDatabaseVersion == -1) { - currentDatabaseVersion = 19; + currentDatabaseVersion = 20; } logger.info("Database upgrade script running... Current Database Version: " + currentDatabaseVersion); @@ -1043,6 +1135,12 @@ public void upgrade() { currentDatabaseVersion = 19; } + if(currentDatabaseVersion == 16 || currentDatabaseVersion == 17 || currentDatabaseVersion == 18 || currentDatabaseVersion == 19) { + logger.info("Data upgrading: Creating a new column... shop_state for the new shop states system."); + parent.addStateColumn(); + currentDatabaseVersion = 20; + } + parent.setDatabaseVersion(currentDatabaseVersion).join(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java index 8fac3576ad..c51fc43915 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java @@ -23,6 +23,7 @@ public class SimpleDataRecord implements DataRecord { private final String encoded; private final String name; private final int type; + private final String state; private final String currency; private final double price; private final boolean unlimited; @@ -37,7 +38,7 @@ public class SimpleDataRecord implements DataRecord { private final String benefit; public SimpleDataRecord(final QUser owner, final String item, final String encoded, final String name, - final int type, final String currency, final double price, final boolean unlimited, + final int type, final String state, final String currency, final double price, final boolean unlimited, final boolean hologram, final QUser taxAccount, final String permissions, final String extra, final String inventoryWrapper, final String inventorySymbolLink, final Date createTime, final String benefit) { @@ -47,6 +48,7 @@ public SimpleDataRecord(final QUser owner, final String item, final String encod this.encoded = encoded; this.name = name; this.type = type; + this.state = state; this.currency = currency; this.price = price; this.unlimited = unlimited; @@ -74,6 +76,7 @@ public SimpleDataRecord(final PlayerFinder finder, final ResultSet set) throws S this.name = set.getString("name"); this.type = set.getInt("type"); + this.state = set.getString("shop_state"); this.currency = set.getString("currency"); this.price = set.getDouble("price"); this.unlimited = set.getBoolean("unlimited"); @@ -105,6 +108,7 @@ public Map generateParams() { map.put("encoded", encoded); map.put("name", name); map.put("type", type); + map.put("shop_state", state); map.put("currency", currency); map.put("price", price); map.put("unlimited", unlimited); @@ -201,6 +205,12 @@ public int getType() { return type; } + @Override + public String getState() { + + return state; + } + @Override public boolean isHologram() { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java index 89900705f2..165b2d13d9 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java @@ -19,6 +19,7 @@ import com.ghostchu.quickshop.api.economy.EconomyManager; import com.ghostchu.quickshop.api.economy.EconomyProvider; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java index 87149f64bd..fce7078d6a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java @@ -264,28 +264,6 @@ public void taxer(final @Nullable QUser user) { this.taxer = user; } - /** - * Retrieves the tax amount associated with this transaction. - * - * @return a BigDecimal value representing the tax amount of the transaction - */ - @Override - public @NotNull BigDecimal tax() { - - return tax; - } - - /** - * Sets the tax for the transaction. - * - * @param tax the amount of tax to be set for the transaction - */ - @Override - public void tax(final @NotNull BigDecimal tax) { - - this.tax = tax; - } - /** * Calculates and retrieves the tax amount associated with this transaction based on the defined * tax rules or system configuration. diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java index 19c78cb9b5..d2c3b2e35a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java @@ -85,7 +85,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java index 098be6188a..6ea2523457 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java @@ -80,7 +80,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java index 8f6d7e3d23..cd49f27ee6 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java @@ -54,7 +54,7 @@ public > boolean setBlock(final BlockVector3 posit final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java index 52099e36fa..e7b6518c75 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java @@ -79,13 +79,13 @@ public void shopCreate(final ShopCreateEvent event) { return; } - if(isForbidden(event.shop().get().getLocation().getBlock().getType(), event.shop().get().getItem().getType())) { + if(isForbidden(event.shop().get().bukkitLocation().getBlock().getType(), event.shop().get().getItem().getType())) { event.setCancelled(true, plugin.text().of(event.user(), "forbidden-vanilla-behavior").forLocale()); return; } if(loggingAction) { - plugin.logEvent(new ShopCreationLog(event.user(), event.shop().get().saveToInfoStorage(), new BlockPos(event.shop().get().getLocation()))); + plugin.logEvent(new ShopCreationLog(event.user(), event.shop().get().saveToInfoStorage(), new BlockPos(event.shop().get().bukkitLocation()))); } } @@ -131,13 +131,13 @@ public void shopInventoryCalc(final ShopInventoryCalculateEvent event) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void shopPrePurchase(final ShopPurchaseEvent event) { - if(isForbidden(event.getShop().getLocation().getBlock().getType(), event.getShop().getItem().getType())) { + if(isForbidden(event.getShop().bukkitLocation().getBlock().getType(), event.getShop().getItem().getType())) { event.setCancelled(true, plugin.text().of(event.getPurchaser(), "forbidden-vanilla-behavior").forLocale()); return; } if(loggingBalance) { - plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); - plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); } } @@ -167,8 +167,8 @@ public void shopPurchase(final ShopSuccessPurchaseEvent event) { event.getTax())); } if(loggingBalance) { - plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); - plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); } if(event.getPurchaser().equals(event.getShop().getOwner())) { plugin.text().of(event.getPurchaser(), "shop-owner-self-trade").send(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java index 39b7ec39c9..1e0db26d5e 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java @@ -54,8 +54,8 @@ public void onWorldLoad(final WorldLoadEvent e) { for(final Entry entry : oldInChunk.getValue().entrySet()) { final Shop shop = entry.getValue(); - shop.getLocation().setWorld(world); - inChunk.put(shop.getLocation(), shop); + shop.bukkitLocation().setWorld(world); + inChunk.put(shop.bukkitLocation(), shop); } } // Done - Now we can store the new world dataz! diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/MiniMessageFiller.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/MiniMessageFiller.java new file mode 100644 index 0000000000..23cb03fd39 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/MiniMessageFiller.java @@ -0,0 +1,64 @@ +package com.ghostchu.quickshop.localization.text; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * MiniMessageFiller + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public final class MiniMessageFiller { + + private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + + private MiniMessageFiller() { } + + @NotNull + public static String fillRaw(@NotNull final String message, @Nullable final Component... args) { + String filled = message; + + if(args == null || args.length == 0) { + return filled; + } + + for(int i = 0; i < args.length; i++) { + final String placeholder = "{" + i + "}"; + final String replacement = serializeForMiniMessage(args[i]); + filled = filled.replace(placeholder, replacement); + } + + return filled; + } + + @NotNull + private static String serializeForMiniMessage(@Nullable final Component component) { + if(component == null) { + return ""; + } + + // Preserves formatting if the arg is already a styled Adventure component. + return MINI_MESSAGE.serialize(component); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/SimpleTextManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/SimpleTextManager.java index 1d95ca5ae2..31c208dc69 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/SimpleTextManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/localization/text/SimpleTextManager.java @@ -833,7 +833,10 @@ public List forLocale(@NotNull final String locale) { Log.debug("Fallback Missing Language Key: " + path + ", report to QuickShop!"); return Collections.singletonList(LegacyComponentSerializer.legacySection().deserialize(path)); } - final List components = str.stream().map(s->manager.plugin.platform().miniMessage().deserialize(s, tagResolvers)).toList(); + final List components = str.stream() + .map(s -> MiniMessageFiller.fillRaw(s, args)) + .map(s -> manager.plugin.platform().miniMessage().deserialize(s, tagResolvers)) + .toList(); return postProcess(components); } } @@ -994,7 +997,9 @@ public Component forLocale(@NotNull final String locale) { } return LegacyComponentSerializer.legacySection().deserialize(path); } - final Component component = manager.plugin.platform().miniMessage().deserialize(str, tagResolvers); + + final String filled = MiniMessageFiller.fillRaw(str, args); + final Component component = manager.plugin.platform().miniMessage().deserialize(filled, tagResolvers); return postProcess(component); } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/ShopHistoryMenu.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/ShopHistoryMenu.java index 59dd7ec036..f0c625ea57 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/ShopHistoryMenu.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/ShopHistoryMenu.java @@ -33,6 +33,7 @@ public class ShopHistoryMenu extends Menu { public static final String SHOPS_DATA = "SHOPS_LIST"; public static final String HISTORY_RECORDS = "HISTORY_RECORDS"; + public static final String HISTORY_DATA_RECORDS = "HISTORY_DATA_RECORDS"; public static final String HISTORY_SUMMARY = "HISTORY_SUMMARY"; public static final String SHOPS_PAGE = "SHOPS_PAGE_ID"; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java index 49d0591f18..c04187048f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java @@ -167,8 +167,8 @@ public void handle(final PageOpenCallback callback) { if(i >= (start + items)) break; - final String world = (shop.getLocation().getWorld() != null)? shop.getLocation().getWorld().getName() : "World"; - final String location = world + " " + shop.getLocation().getBlockX() + ", " + shop.getLocation().getBlockY() + ", " + shop.getLocation().getBlockZ(); + final String world = (shop.bukkitLocation().getWorld() != null)? shop.bukkitLocation().getWorld().getName() : "World"; + final String location = world + " " + shop.bukkitLocation().getBlockX() + ", " + shop.bukkitLocation().getBlockY() + ", " + shop.bukkitLocation().getBlockZ(); final QUser owner = shop.getOwner(); SkullProfile ownerProfile = null; if(owner.isRealPlayer() && owner.getUniqueId() != null) { @@ -177,8 +177,7 @@ public void handle(final PageOpenCallback callback) { ownerProfile.setUuid(owner.getUniqueId()); } - final EconomyProvider eco = QuickShop.getInstance().getEconomyManager().provider(); - final String priceFormatted = eco.format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + final String priceFormatted = shop.format(shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); final AbstractItemStack stack = new BukkitItemStack().of(shop.getItem().getType().key().asString(), shop.getShopStackingAmount()) .lore(getConfigLore(id, shopItemConfig, shop.getOwner().getDisplay(), location, diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java index ee8aebe02f..42ed2f8b28 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java @@ -213,8 +213,11 @@ public static List sortShops(@NotNull final List shops, final List sorted = new ArrayList<>(shops); switch(sortMode) { - case PRICE_ASC -> sorted.sort(Comparator.comparingDouble(Shop::getPrice)); - case PRICE_DESC -> sorted.sort(Comparator.comparingDouble(Shop::getPrice).reversed()); + case PRICE_ASC -> { + + sorted.sort((a, b) -> a.comparePrice(b.price(), false)); + } + case PRICE_DESC -> sorted.sort((a, b) -> a.comparePrice(b.price(), true)); case STOCK -> sorted.sort(Comparator.comparingInt(MarketUtils::getStockFromCache).reversed()); case NAME -> sorted.sort(Comparator.comparing(shop-> CommonUtil.prettifyText(shop.getItem().getType().name()))); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java index 60303a8d07..fb4394c992 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java @@ -269,7 +269,7 @@ public void handle(final PageOpenCallback callback) { // Get teleport location - use shop location + 1 block up // Note: We avoid calling shop.getSigns() here as it requires block access // which can fail on Folia when the shop is in a different region - final Location teleportTarget = shop.getLocation().clone().add(0.5, 1, 0.5); + final Location teleportTarget = shop.bukkitLocation().clone().add(0.5, 1, 0.5); final IconBuilder iconBuilder = new IconBuilder(stack) .withSlot(listStartSlot + (i - start)); @@ -278,7 +278,7 @@ public void handle(final PageOpenCallback callback) { if(canTeleport) { // Capture for lambda final Location finalTeleportTarget = teleportTarget; - final Location shopLoc = shop.getLocation().clone().add(0.5, 0.5, 0.5); + final Location shopLoc = shop.bukkitLocation().clone().add(0.5, 0.5, 0.5); iconBuilder.withActions(new RunnableAction((click)->{ final Player p = Bukkit.getPlayer(click.player().identifier()); @@ -339,11 +339,11 @@ private List buildShopLore(final Shop shop, final double avgPrice, fi } // Location - final String world = shop.getLocation().getWorld() != null? - shop.getLocation().getWorld().getName() : "Unknown"; - final String coords = shop.getLocation().getBlockX() + ", " + - shop.getLocation().getBlockY() + ", " + - shop.getLocation().getBlockZ(); + final String world = shop.bukkitLocation().getWorld() != null? + shop.bukkitLocation().getWorld().getName() : "Unknown"; + final String coords = shop.bukkitLocation().getBlockX() + ", " + + shop.bukkitLocation().getBlockY() + ", " + + shop.bukkitLocation().getBlockZ(); lore.add(mm.deserialize("Location: " + world + "")); lore.add(mm.deserialize("" + coords + "")); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java index a370b8c6e3..089676aba2 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.UUID; +import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_DATA_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_RECORDS; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_SUMMARY; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.SHOPS_DATA; @@ -98,6 +99,7 @@ public void handle(final PageOpenCallback callback) { final Optional shopsData = viewer.get().findData(SHOPS_DATA); final Optional historyData = viewer.get().findData(HISTORY_RECORDS); + final Map dataRecords = (Map) viewer.get().findData(HISTORY_DATA_RECORDS).orElse(Map.of()); final Optional summaryData = viewer.get().findData(HISTORY_SUMMARY); final Player player = Bukkit.getPlayer(viewer.get().uuid()); if(shopsData.isPresent() && historyData.isPresent() && summaryData.isPresent() && player != null) { @@ -145,14 +147,14 @@ public void handle(final PageOpenCallback callback) { //header icon final Shop shop = shops.getFirst(); - final String world = (shop.getLocation().getWorld() != null)? shop.getLocation().getWorld().getName() : "World"; + final String world = (shop.bukkitLocation().getWorld() != null)? shop.bukkitLocation().getWorld().getName() : "World"; final Component shopName; if(shop.getShopName() != null) { shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-name", shop.getShopName()).forLocale(); } else { - shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-empty-name", world, shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()).forLocale(); + shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-empty-name", world, shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()).forLocale(); } final Component shopType = QuickShop.getInstance().text().of(shop.shopType().translationKey()).forLocale(); @@ -179,10 +181,10 @@ public void handle(final PageOpenCallback callback) { shopType, shop.getOwner().getDisplay(), Util.getItemStackName(shop.getItem()), - shop.getPrice(), shop.getShopStackingAmount(), - shop.getLocation().getWorld().getName() + " " + shop.getLocation().getBlockX() - + ", " + shop.getLocation().getBlockY() + ", " - + shop.getLocation().getBlockZ())) + shop.price(), shop.getShopStackingAmount(), + shop.bukkitLocation().getWorld().getName() + " " + shop.bukkitLocation().getBlockX() + + ", " + shop.bukkitLocation().getBlockY() + ", " + + shop.bukkitLocation().getBlockZ())) .profile(ownerProfile)) .withActions(new SwitchPageAction(returnMenu, returnPage)) .withSlot(shopInfoSlot) @@ -257,7 +259,7 @@ public void handle(final PageOpenCallback callback) { int i = 0; for(final ShopHistory.ShopHistoryRecord record : queryResult) { final String userName = QUserImpl.createSync(QuickShop.getInstance().getPlayerFinder(), record.buyer()).getDisplay(); - final DataRecord dataRecord = QuickShop.getInstance().getDatabaseHelper().getDataRecord(record.dataId()).join(); + final DataRecord dataRecord = dataRecords.get(record.dataId()); if(i < start) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java index 26003b0c36..7c55513566 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java @@ -56,7 +56,9 @@ import static com.ghostchu.quickshop.menu.ShopHistoryMenu.HISTORY_SUMMARY; import static com.ghostchu.quickshop.menu.ShopHistoryMenu.SHOPS_DATA; import static com.ghostchu.quickshop.menu.ShopKeeperMenu.KEEPER_MAIN; +import static com.ghostchu.quickshop.shop.SimpleShopManager.ACTIVE_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.BUYING_TYPE; +import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_TYPE; import static com.ghostchu.quickshop.shop.SimpleShopManager.SELLING_TYPE; @@ -94,6 +96,8 @@ public void open(final PageOpenCallback open) { final GuiConfig.IconConfig borderConfig = menuConfig != null? menuConfig.getIcon("border") : null; final GuiConfig.IconConfig shopItemConfig = menuConfig != null? menuConfig.getIcon("shop-item") : null; final GuiConfig.IconConfig changePriceConfig = menuConfig != null? menuConfig.getIcon("change-price") : null; + final GuiConfig.IconConfig displayToggleConfig = menuConfig != null? menuConfig.getIcon("display-toggle") : null; + final GuiConfig.IconConfig freezeToggleConfig = menuConfig != null? menuConfig.getIcon("freeze-toggle") : null; final GuiConfig.IconConfig modeToggleConfig = menuConfig != null? menuConfig.getIcon("mode-toggle") : null; final GuiConfig.IconConfig inventoryConfig = menuConfig != null? menuConfig.getIcon("inventory") : null; final GuiConfig.IconConfig staffConfig = menuConfig != null? menuConfig.getIcon("staff") : null; @@ -119,6 +123,47 @@ public void open(final PageOpenCallback open) { // Always read price directly from shop to get the latest value final double currentPrice = shop.get().getPrice(); + + + final GuiConfig.IconConfig activeConfig = (displayToggleConfig != null)? displayToggleConfig.getSubIcon("active") : null; + final GuiConfig.IconConfig inactiveConfig = (displayToggleConfig != null)? displayToggleConfig.getSubIcon("inactive") : null; + final String activeMaterial = (activeConfig != null)? activeConfig.getMaterial() : "GLOW_ITEM_FRAME"; + final String inactiveMaterial = (inactiveConfig != null)? inactiveConfig.getMaterial() : "ITEM_FRAME"; + final int displayToggleSlot = (displayToggleConfig != null)? displayToggleConfig.getSlot() : 18; + + if(shop.get().playerAuthorize(id, BuiltInShopPermission.TOGGLE_DISPLAY) + || QuickShop.getInstance().perm().hasPermission(player, "quickshop.other.toggledisplay")) { + + final String enabledText = QuickShop.getInstance().text().of("shop-display.enabled").plain(); + final String disabledText = QuickShop.getInstance().text().of("shop-display.disabled").plain(); + + final AbstractItemStack activeStack = QuickShop.getInstance().stack().of(activeMaterial, 1) + .display(getConfigDisplay(id, displayToggleConfig, "Toggle Preview")) + .lore(getConfigLore(id, displayToggleConfig, enabledText)); + + final AbstractItemStack inactiveStack = QuickShop.getInstance().stack().of(inactiveMaterial, 1) + .display(getConfigDisplay(id, displayToggleConfig, "Toggle Preview")) + .lore(getConfigLore(id, displayToggleConfig, disabledText)); + + final String modeState = (!shop.get().isDisableDisplay())? "ACTIVE" : "INACTIVE"; + + final StateIcon changeIcon = new StateIcon(activeStack, null, "SHOP_DISPLAY", modeState, (currentState)->{ + if(currentState.toUpperCase(Locale.ROOT).equals("ACTIVE")) { + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().setDisableDisplay(true)); + return "INACTIVE"; + } else if(currentState.toUpperCase(Locale.ROOT).equals("INACTIVE")) { + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().setDisableDisplay(false)); + return "ACTIVE"; + } + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().setDisableDisplay(false)); + return "ACTIVE"; + }); + changeIcon.setSlot(displayToggleSlot); + changeIcon.addState("ACTIVE", activeStack); + changeIcon.addState("INACTIVE", inactiveStack); + open.getPage().addIcon(changeIcon); + } + // Change price icon from config (GOLD_NUGGET for "price") final String changePriceMaterial = changePriceConfig != null? changePriceConfig.getMaterial() : "GOLD_NUGGET"; final int changePriceSlot = changePriceConfig != null? changePriceConfig.getSlot() : 19; @@ -133,7 +178,7 @@ public void open(final PageOpenCallback open) { try { final BigDecimal price = new BigDecimal(message); // Update price and reopen menu in the same region thread to ensure price is updated before GUI shows - Util.regionThread(shop.get().getLocation(), ()->{ + Util.regionThread(shop.get().bukkitLocation(), ()->{ ShopUtil.setPrice(QuickShop.getInstance(), QUserImpl.createFullFilled(player), price.doubleValue(), shop.get()); // Reopen menu after price is set final MenuPlayer menuPlayer = QuickShop.getInstance().createMenuPlayer(player); @@ -147,23 +192,65 @@ public void open(final PageOpenCallback open) { .withSlot(changePriceSlot).build()); } + final GuiConfig.IconConfig freezeConfig = (freezeToggleConfig != null)? freezeToggleConfig.getSubIcon("freeze") : null; + final GuiConfig.IconConfig unfreezeConfig = (freezeToggleConfig != null)? freezeToggleConfig.getSubIcon("unfreeze") : null; + final String freezeMaterial = (freezeConfig != null)? freezeConfig.getMaterial() : "LIGHT_BLUE_CONCRETE"; + final String unfreezeMaterial = (unfreezeConfig != null)? unfreezeConfig.getMaterial() : "RED_CONCRETE"; + final int freezeToggleSlot = (freezeToggleConfig != null)? freezeToggleConfig.getSlot() : 20; + + if(shop.get().playerAuthorize(id, BuiltInShopPermission.SET_SHOP_STATE) + && QuickShop.getInstance().perm().hasPermission(player, "quickshop.freeze") + || QuickShop.getInstance().perm().hasPermission(player, "quickshop.other.freeze")) { + + final String freezeText = QuickShop.getInstance().text().of("shop-state.freeze").plain(); + final String unfreezeText = QuickShop.getInstance().text().of("shop-state.unfreeze").plain(); + + final String frozenText = QuickShop.getInstance().text().of("shop-state.frozen").plain(); + final String unfrozenText = QuickShop.getInstance().text().of("shop-state.unfrozen").plain(); + + final AbstractItemStack freezeStack = QuickShop.getInstance().stack().of(freezeMaterial, 1) + .display(getConfigDisplay(id, freezeToggleConfig, "Toggle Freeze")) + .lore(getConfigLore(id, freezeToggleConfig, frozenText, unfreezeText)); + + final AbstractItemStack unfreezeStack = QuickShop.getInstance().stack().of(unfreezeMaterial, 1) + .display(getConfigDisplay(id, freezeToggleConfig, "Toggle Freeze")) + .lore(getConfigLore(id, freezeToggleConfig, unfrozenText, freezeText)); + + final String modeState = shop.get().shopState().identifier().toUpperCase(Locale.ROOT); + + final StateIcon changeIcon = new StateIcon(freezeStack, null, "SHOP_STATE", modeState, (currentState)->{ + if(currentState.toUpperCase(Locale.ROOT).equals("ACTIVE")) { + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopState(FROZEN_STATE)); + return "FROZEN"; + } else if(currentState.toUpperCase(Locale.ROOT).equals("FROZEN")) { + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopState(ACTIVE_STATE)); + return "ACTIVE"; + } + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopState(ACTIVE_STATE)); + return "ACTIVE"; + }); + changeIcon.setSlot(freezeToggleSlot); + changeIcon.addState("FROZEN", freezeStack); + changeIcon.addState("ACTIVE", unfreezeStack); + open.getPage().addIcon(changeIcon); + } + // Mode Toggle Icon from config (concrete for clean look) final GuiConfig.IconConfig sellingConfig = (modeToggleConfig != null)? modeToggleConfig.getSubIcon("selling") : null; final GuiConfig.IconConfig buyingConfig = (modeToggleConfig != null)? modeToggleConfig.getSubIcon("buying") : null; - final GuiConfig.IconConfig frozenConfig = (modeToggleConfig != null)? modeToggleConfig.getSubIcon("frozen") : null; final String sellingMaterial = (sellingConfig != null)? sellingConfig.getMaterial() : "LIME_CONCRETE"; final String buyingMaterial = (buyingConfig != null)? buyingConfig.getMaterial() : "ORANGE_CONCRETE"; - final String frozenMaterial = (frozenConfig != null)? frozenConfig.getMaterial() : "LIGHT_BLUE_CONCRETE"; final int modeToggleSlot = (modeToggleConfig != null)? modeToggleConfig.getSlot() : 21; if(shop.get().playerAuthorize(id, BuiltInShopPermission.SET_SHOPTYPE) + && QuickShop.getInstance().perm().hasPermission(player, "quickshop.create.buy") + && QuickShop.getInstance().perm().hasPermission(player, "quickshop.create.sell") || QuickShop.getInstance().perm().hasPermission(player, "quickshop.other.freeze") && QuickShop.getInstance().perm().hasPermission(player, "quickshop.other.sell") && QuickShop.getInstance().perm().hasPermission(player, "quickshop.other.buy")) { final String sellingText = QuickShop.getInstance().text().of("shop-type.selling").plain(); final String buyingText = QuickShop.getInstance().text().of("shop-type.buying").plain(); - final String frozenText = QuickShop.getInstance().text().of("shop-type.frozen").plain(); final AbstractItemStack buyingStack = QuickShop.getInstance().stack().of(sellingMaterial, 1) .display(getConfigDisplay(id, modeToggleConfig, "Change Mode")) @@ -171,29 +258,24 @@ public void open(final PageOpenCallback open) { final AbstractItemStack sellingStack = QuickShop.getInstance().stack().of(buyingMaterial, 1) .display(getConfigDisplay(id, modeToggleConfig, "Change Mode")) - .lore(getConfigLore(id, modeToggleConfig, buyingText, frozenText)); - - final AbstractItemStack frozenStack = QuickShop.getInstance().stack().of(frozenMaterial, 1) - .display(getConfigDisplay(id, modeToggleConfig, "Change Mode")) - .lore(getConfigLore(id, modeToggleConfig, frozenText, sellingText)); + .lore(getConfigLore(id, modeToggleConfig, buyingText, sellingText)); final String modeState = shop.get().shopType().identifier().toUpperCase(Locale.ROOT); final StateIcon changeIcon = new StateIcon(buyingStack, null, "SHOP_TYPE", modeState, (currentState)->{ if(currentState.toUpperCase(Locale.ROOT).equals("SELLING")) { - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(BUYING_TYPE)); + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(BUYING_TYPE)); return "BUYING"; - } else if(currentState.toUpperCase(Locale.ROOT).equals("FROZEN")) { - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(SELLING_TYPE)); + } else if(currentState.toUpperCase(Locale.ROOT).equals("BUYING")) { + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(SELLING_TYPE)); return "SELLING"; } - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(FROZEN_TYPE)); - return "FROZEN"; + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(SELLING_TYPE)); + return "SELLING"; }); changeIcon.setSlot(modeToggleSlot); changeIcon.addState("SELLING", buyingStack); changeIcon.addState("BUYING", sellingStack); - changeIcon.addState("FROZEN", frozenStack); open.getPage().addIcon(changeIcon); } @@ -221,7 +303,7 @@ public void open(final PageOpenCallback open) { } viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); - Util.regionThread(shop.get().getLocation(), ()->{ + Util.regionThread(shop.get().bukkitLocation(), ()->{ player.openInventory(inventory.getHolder().getInventory()); QuickShop.inShop.add(player.getUniqueId()); @@ -318,7 +400,7 @@ public void open(final PageOpenCallback open) { .withActions(new GuiChatAction((message)->{ if(!message.isEmpty()) { if(message.equalsIgnoreCase("confirm")) { - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().deleteShop(shop.get())); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().deleteShop(shop.get())); QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(player), "/quickshop remove command", shop.get().saveToInfoStorage())); return true; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java index a629295eac..bb58f7dffc 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java @@ -266,7 +266,7 @@ public void handle(final PageOpenCallback callback) { if(!message.isEmpty()) { if(message.equalsIgnoreCase("confirm")) { if(shop.get().playerAuthorize(id, BuiltInShopPermission.OWNERSHIP_TRANSFER)) { - Util.regionThread(shop.get().getLocation(), ()->ShopUtil.transferRequest(id, uuid, name, shop.get())); + Util.regionThread(shop.get().bukkitLocation(), ()->ShopUtil.transferRequest(id, uuid, name, shop.get())); } else { QuickShop.getInstance().text().of(id, "no-permission").send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java index a622b10aa2..3dca7d1d2c 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java @@ -101,9 +101,7 @@ public void handle(final PageOpenCallback open) { // Use cache to avoid Folia cross-region block access issues final int stock = (shop.get().isBuying())? -1 : MarketUtils.getStockFromCache(shop.get()); final String stockString = (shop.get().isUnlimited())? "Unlimited" : stock + ""; - final String priceFormatted = eco.format(BigDecimal.valueOf(shop.get().getPrice()), - shop.get().getLocation().getWorld().getName(), - shop.get().getCurrency()); + final String priceFormatted = shop.get().format(shop.get().bukkitLocation().getWorld().getName(), shop.get().getCurrency()); // Shop item display slot from config (centered in row 2) final int shopItemSlot = (shopItemConfig != null)? shopItemConfig.getSlot() : 13; @@ -167,11 +165,11 @@ public void handle(final PageOpenCallback open) { return true; } if(shop.get().isBuying()) { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); } else { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); } return true; } catch(final NumberFormatException ignore) { } @@ -196,9 +194,8 @@ public void handle(final PageOpenCallback open) { final int quantity = configQuantities.get(i); final int slot = configSlots.get(i); final int adjustedAmount = (amount * quantity); - final String totalPrice = eco.format(BigDecimal.valueOf((quantity * shop.get().getPrice())), - shop.get().getLocation().getWorld().getName(), - shop.get().getCurrency()); + final String totalPrice = shop.get().format(shop.get().bukkitLocation().getWorld().getName(), + shop.get().getCurrency(), quantity); final String displayText = (shop.get().isSelling())? "Buy x" + adjustedAmount + "" : "Sell x" + adjustedAmount + ""; open.getPage().addIcon(new IconBuilder(QuickShop.getInstance().stack().of(quantityMaterial, Math.min(adjustedAmount, 64)) @@ -207,13 +204,13 @@ public void handle(final PageOpenCallback open) { .withActions(new RunnableAction((click->{ if(shop.get().isBuying()) { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); } else { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); } }))) diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java index fa5ed410f4..a6f2a3c45a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java @@ -60,8 +60,8 @@ private String compileUniqueKey(@NotNull final UUID player, @NotNull final Strin private long getShopsInWorld(@NotNull final String world, final boolean loadedOnly) { return plugin.getShopManager().getAllShops().stream() - .filter(shop->shop.getLocation().getWorld() != null) - .filter(shop->shop.getLocation().getWorld().getName().equals(world)) + .filter(shop->shop.bukkitLocation().getWorld() != null) + .filter(shop->shop.bukkitLocation().getWorld().getName().equals(world)) .filter(shop->!loadedOnly || shop.isLoaded()) .count(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java index b1de4d5e5f..1092b96295 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java @@ -10,6 +10,7 @@ import com.ghostchu.quickshop.api.shop.cache.ShopCache; import com.ghostchu.quickshop.api.shop.cache.ShopCacheNamespacedKey; import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import com.ghostchu.quickshop.common.util.QuickExecutor; import com.ghostchu.quickshop.shop.cache.BoxedShop; import com.ghostchu.quickshop.shop.cache.SimpleShopCache; @@ -65,6 +66,7 @@ public abstract class AbstractShopManager implements ShopManager { .initialCapacity(50) .build(); protected final QuickShop plugin; + protected final TradeService tradeService; protected final EconomyFormatter formatter; protected final Map>> shops = Maps.newConcurrentMap(); protected final Set loadedShops = Sets.newConcurrentHashSet(); // Handle it by collection to reduce @@ -77,6 +79,7 @@ public AbstractShopManager(@NotNull final QuickShop plugin) { Util.ensureThread(false); this.plugin = plugin; this.formatter = new EconomyFormatter(plugin); + this.tradeService = new SimpleTradeService(plugin); } public void init() { @@ -96,14 +99,14 @@ public void init() { */ protected void addShopToLookupTable(@NotNull final Shop shop) { - final String world = shop.getLocation().getWorld().getName(); + final String world = shop.bukkitLocation().getWorld().getName(); final Map> inWorld = shops.computeIfAbsent(world, k->new MapMaker().initialCapacity(3).makeMap()); // There's no world storage yet. We need to create that map. // Put it in the data universe // Calculate the chunks coordinates. These are 1,2,3 for each chunk, NOT // location rounded to the nearest 16. - final int x = (int)Math.floor((shop.getLocation().getBlockX()) / 16.0); - final int z = (int)Math.floor((shop.getLocation().getBlockZ()) / 16.0); + final int x = (int)Math.floor((shop.bukkitLocation().getBlockX()) / 16.0); + final int z = (int)Math.floor((shop.bukkitLocation().getBlockZ()) / 16.0); // Get the chunk set from the world info final ShopChunk shopChunk = new SimpleShopChunk(world, x, z); final Map inChunk = @@ -111,8 +114,8 @@ protected void addShopToLookupTable(@NotNull final Shop shop) { // That chunk data hasn't been created yet - Create it! // Put it in the world // Put the shop in its location in the chunk list. - inChunk.put(shop.getLocation(), shop); - shopCache.invalidate(null, shop.getLocation()); + inChunk.put(shop.bukkitLocation(), shop); + shopCache.invalidate(null, shop.bukkitLocation()); } @Override @@ -184,7 +187,7 @@ public void unloadShop(@NotNull final Shop shop, final boolean dontTouchWorld) { */ private void removeShopFromLookupTable(@NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String world = Objects.requireNonNull(loc.getWorld()).getName(); final Map> inWorld = this.getShops().get(world); if(inWorld == null) { @@ -198,7 +201,7 @@ private void removeShopFromLookupTable(@NotNull final Shop shop) { return; } inChunk.remove(loc); - shopCache.invalidate(null, shop.getLocation()); + shopCache.invalidate(null, shop.bukkitLocation()); shopRuntimeUUIDCaching.invalidate(shop.getRuntimeRandomUniqueId()); } @@ -207,7 +210,7 @@ protected void processCreationFail(@NotNull final Shop shop, @NotNull final QUse plugin.logger().error("Shop create failed, auto fix failed, the changes may won't commit to database.", e2); plugin.text().of(owner, "shop-creation-failed").send(); - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { deleteShop(shop); unloadShop(shop); unregisterShop(shop, true); @@ -219,7 +222,7 @@ protected void processCreationFail(@NotNull final Shop shop, @NotNull final QUse @Override public CompletableFuture<@Nullable Integer> clearShopTags(@NotNull final UUID tagger, @NotNull final Shop shop) { - return plugin.getDatabaseHelper().removeShopAllTag(tagger, shop.getShopId()); + return plugin.getDatabaseHelper().removeAllShopTagsBy(tagger, shop.getShopId()); } @Override @@ -259,7 +262,7 @@ public CompletableFuture unregisterShop(@NotNull final Shop shop, final boole removeShopFromLookupTable(shop); if(!persist) return CompletableFuture.completedFuture(null); - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); return plugin.getDatabaseHelper().removeShopMap(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()) .thenCombine(plugin.getDatabaseHelper().removeShop(shop.getShopId()), (a, b)->null) .exceptionally(throwable->{ @@ -476,11 +479,11 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean Log.debug("DEBUG: Setting shop id"); shop.setShopId(id); Log.debug("DEBUG: Creating shop map"); - plugin.getDatabaseHelper().createShopMap(id, shop.getLocation()).join(); + plugin.getDatabaseHelper().createShopMap(id, shop.bukkitLocation()).join(); Log.debug("DEBUG: Creating shop successfully"); shop.setDirty(); - new ShopCreateEvent(Phase.POST, shop, shop.getOwner(), shop.getLocation()).callEvent(); + new ShopCreateEvent(Phase.POST, shop, shop.getOwner(), shop.bukkitLocation()).callEvent(); }) .exceptionally(err->{ processCreationFail(shop, shop.getOwner(), err); @@ -559,7 +562,7 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean final List worldShops = new ArrayList<>(); for(final Shop shop : getAllShops()) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(location.isWorldLoaded() && Objects.equals(location.getWorld(), world)) { worldShops.add(shop); } @@ -572,7 +575,7 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean final List worldShops = new ArrayList<>(); for(final Shop shop : getAllShops()) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(location.isWorldLoaded() && com.ghostchu.quickshop.common.util.CommonUtil.strEquals(worldName, location.getWorld().getName())) { worldShops.add(shop); } @@ -589,63 +592,15 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean return plugin.getDatabaseHelper().queryInventoryCache(shop.getShopId()); } + /** + * Retrieves the TradeService associated with the EconomyManager. + * + * @return A non-null instance of TradeService, which provides functionality for executing and + * previewing trade operations such as buying from and selling to shops. + */ + @Override + public @NotNull TradeService tradeService() { - static class TagParser { - - private final List tags; - private final ShopManager shopManager; - private final UUID tagger; - private final Map> singleCaching = new HashMap<>(); - - public TagParser(final UUID tagger, final ShopManager shopManager, final List tags) { - - Util.ensureThread(true); - this.shopManager = shopManager; - this.tags = tags; - this.tagger = tagger; - } - - public List parseTags() { - - final List finalShop = new ArrayList<>(); - for(final String tag : tags) { - final ParseResult result = parseSingleTag(tag); - if(result.getBehavior() == Behavior.INCLUDE) { - finalShop.addAll(result.getShops()); - } else if(result.getBehavior() == Behavior.EXCLUDE) { - finalShop.removeAll(result.getShops()); - } - } - return finalShop; - } - - public ParseResult parseSingleTag(final String tag) throws IllegalArgumentException { - - Util.ensureThread(true); - Behavior behavior = Behavior.INCLUDE; - if(tag.startsWith("-")) { - behavior = Behavior.EXCLUDE; - } - final String tagName = tag.substring(1); - if(tagName.isEmpty()) { - throw new IllegalArgumentException("Tag name can't be empty"); - } - final List shops = singleCaching.computeIfAbsent(tag, (t)->shopManager.queryTaggedShops(tagger, t).join()); - return new ParseResult(behavior, shops); - } - - enum Behavior { - INCLUDE, - EXCLUDE - } - - @AllArgsConstructor - @Data - static class ParseResult { - - private final Behavior behavior; - private final List shops; - } + return tradeService; } - -} +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java index fd45c56225..dc4b0c56ec 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java @@ -3,6 +3,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.ServiceInjector; +import com.ghostchu.quickshop.api.economy.EconomyProvider; import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; import com.ghostchu.quickshop.api.event.Phase; import com.ghostchu.quickshop.api.event.general.ShopSignUpdateEvent; @@ -19,6 +20,7 @@ import com.ghostchu.quickshop.api.event.settings.type.ShopOwnerNameEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopPlayerGroupEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopSignLinesEvent; +import com.ghostchu.quickshop.api.event.settings.type.ShopStateEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopTaxAccountEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopTypeEnhancedEvent; import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitEvent; @@ -30,10 +32,11 @@ import com.ghostchu.quickshop.api.shop.IShopType; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.ShopInfoStorage; -import com.ghostchu.quickshop.api.shop.ShopType; import com.ghostchu.quickshop.api.shop.display.DisplayType; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import com.ghostchu.quickshop.api.shop.trading.TradeResult; import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.database.bean.SimpleDataRecord; @@ -41,7 +44,6 @@ import com.ghostchu.quickshop.shop.datatype.ShopSignPersistentDataType; import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; import com.ghostchu.quickshop.util.MsgUtil; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.logging.container.ShopRemoveLog; @@ -70,10 +72,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.math.BigDecimal; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -88,12 +92,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import static com.ghostchu.quickshop.util.Util.waitForFuture; +import static java.math.BigDecimal.ZERO; /** * ChestShop core */ @EqualsAndHashCode -public class ContainerShop implements Shop, Reloadable { +public class ContainerShop implements Shop, Reloadable { // We use deprecated method to create a fake quickshop-reremake namespace to trick bukkit to access legacy data. private static final NamespacedKey LEGACY_SHOP_NAMESPACED_KEY = new NamespacedKey("quickshop", "shopsign"); @@ -113,6 +118,7 @@ public class ContainerShop implements Shop, Reloadable { private QUser owner; private double price; private IShopType shopType; + private ShopState shopState; private boolean unlimited; @NotNull private ItemStack item; @@ -172,6 +178,7 @@ public ContainerShop( @NotNull final QUser owner, final boolean unlimited, @NotNull final IShopType type, + @NotNull final ShopState state, @Nullable final YamlConfiguration extra, @Nullable final String currency, final boolean disableDisplay, @@ -214,6 +221,7 @@ public ContainerShop( } } this.shopType = type; + this.shopState = state; this.unlimited = unlimited; this.extra = extra; this.currency = currency; @@ -283,52 +291,12 @@ public void add(@NotNull ItemStack item, final int amount) { * @param amount The amount to buy */ @Override - public void buy(@NotNull final QUser buyer, @NotNull final InventoryWrapper buyerInventory, - @NotNull final Location loc2Drop, int amount) throws Exception { + public TradeResult buy(@NotNull final QUser buyer, + @NotNull final InventoryWrapper buyerInventory, + @NotNull final Location loc2Drop, + final int amount) { - Util.ensureThread(false); - amount = amount * item.getAmount(); - if(amount < 0) { - this.sell(buyer, buyerInventory, loc2Drop, -amount); - return; - } - if(this.isUnlimited()) { - final SimpleInventoryTransaction transaction = SimpleInventoryTransaction - .builder() - .from(buyerInventory) - .to(null) // To void - .item(this.getItem()) - .amount(amount) - .build(); - if(!transaction.failSafeCommit()) { - if(plugin.getSentryErrorReporter() != null) { - plugin.getSentryErrorReporter().ignoreThrow(); - } - throw new IllegalStateException("Failed to commit transaction! Economy Error Response:" + transaction.getLastError()); - } - } else { - final InventoryWrapper chestInv = this.getInventory(); - if(chestInv == null) { - plugin.logger().warn("Failed to process buy, reason: {} x{} to shop {}: Inventory null.", item, amount, this); - Log.debug("Failed to process buy, reason: " + item + " x" + amount + " to shop " + this + ": Inventory null."); - return; - } - final SimpleInventoryTransaction transaction = SimpleInventoryTransaction - .builder() - .from(buyerInventory) - .to(chestInv) - .item(this.getItem()) - .amount(amount) - .build(); - if(!transaction.failSafeCommit()) { - if(plugin.getSentryErrorReporter() != null) { - plugin.getSentryErrorReporter().ignoreThrow(); - } - throw new IllegalStateException("Failed to commit transaction! Economy Error Response:" + transaction.getLastError()); - } - } - //Update sign - this.setSignText(plugin.text().findRelativeLanguages(buyer, false)); + return plugin.getShopManager().tradeService().executeSellToShop(this, buyer, buyerInventory, loc2Drop, amount); } @Override @@ -558,6 +526,17 @@ public void setItem(@NotNull final ItemStack item) { return this.location; } + /** + * Converts the location associated with this object to a Bukkit-compatible {@link Location}. + * + * @return the Bukkit {@link Location} representation of this object's location. + */ + @Override + public Location bukkitLocation() { + + return this.location; + } + /** * @return The name of the player who owns the shop. */ @@ -628,6 +607,7 @@ public void setOwner(@NotNull final QUser owner) { /** * @return The price per item this shop is selling */ + @SuppressWarnings("removal") @Override public double getPrice() { @@ -639,6 +619,7 @@ public double getPrice() { * * @param price The new price of the shop. */ + @SuppressWarnings("removal") @Override public void setPrice(final double price) { @@ -648,6 +629,146 @@ public void setPrice(final double price) { setSignText(); } + /** + * Retrieves the price of the shop. + * + * @return the price of the shop as an instance of type U, where U represents a generic type. + */ + @Override + public Double price() { + + return price; + } + + /** + * Sets the price for a shop. + * + * @param price the price to set for the shop; must be of type U and should not be null + */ + @Override + public void price(final Double price) { + this.price = price; + } + + /** + * Formats a string representation based on the provided world and optional currency. + * + * @param world the name of the world for which the string is being formatted; must not be + * null + * @param currency the optional currency to include in the formatted string; can be null + * + * @return a formatted string combining the world and currency information; never null + */ + @Override + public @NotNull String format(final @NotNull String world, final @Nullable String currency) { + + return plugin.getEconomyManager().provider().format(BigDecimal.valueOf(price()), world, currency); + } + + /** + * Formats a string representation based on the provided world, optional currency, and quantity. + * + * @param world the name of the world for which the string is being formatted; must not be + * null + * @param currency the optional currency to include in the formatted string; can be null + * @param quantity the quantity to include in the formatted string; represents a non-negative + * integer + * + * @return a formatted string combining the world, currency, and quantity information; never null + */ + @Override + public @NotNull String format(final @NotNull String world, final @Nullable String currency, final int quantity) { + + return plugin.getEconomyManager().provider().format(BigDecimal.valueOf(price() * quantity), world, currency); + } + + /** + * Provides a comparator for comparing instances of the generic type U used in the shop's + * pricing. + * + * @return a {@link Comparator} for comparing values of type U + */ + @Override + public Comparator priceComparator() { + + return Comparator.comparingDouble(Double::doubleValue); + } + + /** + * Retrieves the maximum number of items that can currently be purchased or acquired based on the + * shop's available balance and the price of the items. + * + * @return the maximum number of items that can be afforded; always a non-negative integer. + */ + @Override + public int getMaxAffordable() { + if(this.isUnlimited() || this.isFreeShop()) { + return Integer.MAX_VALUE; + } + + final BigDecimal unitPrice = BigDecimal.valueOf(this.price()); + if(unitPrice == null || unitPrice.compareTo(ZERO) <= 0) { + + return 0; + } + + final EconomyProvider eco = QuickShop.getInstance().getEconomyManager().provider(); + if(eco == null) { + + return 0; + } + + final BigDecimal balance = eco.balance(owner, location.getWorld().getName(), currency); + + if(balance == null || balance.compareTo(ZERO) <= 0) { + + return 0; + } + + final BigDecimal affordable = balance.divideToIntegralValue(unitPrice); + if(affordable.compareTo(BigDecimal.valueOf(Integer.MAX_VALUE)) > 0) { + return Integer.MAX_VALUE; + } + return Math.max(0, affordable.intValue()); + } + + /** + * Determines whether the current shop can afford the transaction of a specified quantity of + * items. + * + * @param itemAmount the number of items involved in the transaction; must be a non-negative + * integer + * + * @return true if the shop can afford the specified number of items, false otherwise + */ + @Override + public boolean canAfford(final int itemAmount) { + if(itemAmount <= 0) { + return false; + } + if(this.isUnlimited() || this.isFreeShop()) { + return true; + } + + final BigDecimal unitPrice = BigDecimal.valueOf(this.price()); + if(unitPrice == null || unitPrice.compareTo(ZERO) <= 0) { + return false; + } + + final EconomyProvider eco = QuickShop.getInstance().getEconomyManager().provider(); + if(eco == null) { + return false; + } + + final BigDecimal balance = eco.balance(owner, location.getWorld().getName(), currency); + if(balance == null || balance.compareTo(ZERO) <= 0) { + return false; + } + + final BigDecimal total = unitPrice.multiply(BigDecimal.valueOf(itemAmount)); + return balance.compareTo(total) >= 0; + } + /** * Returns the number of free spots in the chest for the particular item. * @@ -789,11 +910,65 @@ public int getShopStackingAmount() { return 1; } - @SuppressWarnings("removal") + /** + * Retrieves the current state of the shop. + * + * @return the current state of the shop as a ShopState object + */ + @Override + public ShopState shopState() { + + final ShopStateEvent event = new ShopStateEvent(Phase.RETRIEVE, this, this.shopState); + event.callEvent(); + + return event.updated(); + } + + /** + * Updates the current state of the shop based on the provided {@code ShopState}. + * + * @param newState the new state to set for the shop; must not be null + */ + @Override + public void shopState(@NotNull final ShopState newState) { + + Util.ensureThread(false); + + if(this.shopState.identifier().equalsIgnoreCase(newState.identifier())) { + + return; + } + + ShopStateEvent event = new ShopStateEvent(Phase.PRE, this, this.shopState, newState); + event.callEvent(); + + event = event.clone(Phase.MAIN); + + if(event.callCancellableEvent()) { + + Log.debug("Some addon cancelled shop state changes, target shop: " + this); + return; + } + + this.shopState = event.updated(); + + event = event.clone(Phase.POST); + event.callEvent(); + + this.setSignText(); + setDirty(); + } + + /** + * Updates or processes the state of a shop based on the provided identifier. + * + * @param shopStateIdentifier a non-null string representing the unique identifier for the shop + * state to be updated or processed. + */ @Override - public @NotNull ShopType getShopType() { + public void shopState(@NotNull final String shopStateIdentifier) { - return ShopType.fromID(shopType.id()); + shopState(QuickShop.getInstance().getShopManager().shopStateOrDefault(shopStateIdentifier)); } /** @@ -856,18 +1031,6 @@ public void shopType(@NotNull final String shopTypeIdentifier) { shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(shopTypeIdentifier)); } - /** - * Changes a shop type to Buying or Selling. Also updates the signs nearby. - * - * @param newShopType The new type (ShopType.BUYING or ShopType.SELLING) - */ - @SuppressWarnings("removal") - @Override - public void setShopType(@NotNull final ShopType newShopType) { - - shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(newShopType.name())); - } - @Override public List getSignText(@NotNull final ProxiedLocale locale) { @@ -891,7 +1054,7 @@ public List getSignText(@NotNull final ProxiedLocale locale) { Util.ensureThread(false); final List signs = new ArrayList<>(4); - if(this.getLocation().getWorld() == null) { + if(this.bukkitLocation().getWorld() == null) { return Collections.emptyList(); } final Block[] blocks = new Block[4]; @@ -993,7 +1156,7 @@ public boolean inventoryAvailable() { public boolean isAttached(@NotNull final Block b) { Util.ensureThread(false); - return this.getLocation().getBlock().equals(Util.getAttached(b)); + return this.bukkitLocation().getBlock().equals(Util.getAttached(b)); } @Override @@ -1005,7 +1168,7 @@ public boolean isBuying() { @Override public boolean isFrozen() { - return this.shopType.isTradingBlocked(); + return this.shopType.isTradingBlocked() || !this.shopState.isTradingAllowed(); } private boolean isDeleted() { @@ -1156,7 +1319,7 @@ public boolean isShopSign(@NotNull final Sign sign) { } } if(shopSignStorage != null) { - return shopSignStorage.equals(getLocation().getWorld().getName(), getLocation().getBlockX(), getLocation().getBlockY(), getLocation().getBlockZ()); + return shopSignStorage.equals(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); } return false; } @@ -1202,7 +1365,7 @@ public boolean isValid() { if(this.isDeleted) { return false; } - return Util.canBeShop(this.getLocation().getBlock()); + return Util.canBeShop(this.bukkitLocation().getBlock()); } /** @@ -1472,8 +1635,8 @@ public void remove(@NotNull ItemStack item, final int amount) { @Override public ShopInfoStorage saveToInfoStorage() { - return new ShopInfoStorage(getLocation().getWorld().getName(), - new BlockPos(getLocation()), this.owner, this.price, + return new ShopInfoStorage(this.bukkitLocation().getWorld().getName(), + new BlockPos(this.bukkitLocation()), this.owner, this.price, QuickShop.getInstance().platform().encodeStack(this.originalItem), isUnlimited()? 1 : 0 , shopType().id(), saveExtraToYaml(), this.currency, this.disableDisplay, @@ -1497,51 +1660,11 @@ public String saveToSymbolLink() { * @param amount The amount to sell */ @Override - public void sell(@NotNull final QUser seller, @NotNull final InventoryWrapper sellerInventory, - @NotNull final Location loc2Drop, int amount) throws Exception { - - Util.ensureThread(false); - amount = item.getAmount() * amount; - if(amount < 0) { - this.buy(seller, sellerInventory, loc2Drop, -amount); - return; - } - // Items to drop on floor - if(this.isUnlimited()) { - final SimpleInventoryTransaction transaction = SimpleInventoryTransaction - .builder() - .from(null) - .to(sellerInventory) // To void - .item(this.getItem()) - .amount(amount) - .build(); - if(!transaction.failSafeCommit()) { - if(plugin.getSentryErrorReporter() != null) { - plugin.getSentryErrorReporter().ignoreThrow(); - } - throw new IllegalStateException("Failed to commit transaction! Economy Error Response:" + transaction.getLastError()); - } - } else { - final InventoryWrapper chestInv = this.getInventory(); - if(chestInv == null) { - plugin.logger().warn("Failed to process sell, reason: {} to shop {}: Inventory null.", item, amount); - return; - } - final SimpleInventoryTransaction transactionTake = SimpleInventoryTransaction - .builder() - .from(chestInv) - .to(sellerInventory) // To void - .item(this.getItem()) - .amount(amount) - .build(); - if(!transactionTake.failSafeCommit()) { - if(plugin.getSentryErrorReporter() != null) { - plugin.getSentryErrorReporter().ignoreThrow(); - } - throw new IllegalStateException("Failed to commit transaction! Economy Error Response:" + transactionTake.getLastError()); - } - this.setSignText(plugin.getTextManager().findRelativeLanguages(seller, false)); - } + public TradeResult sell(@NotNull final QUser seller, + @NotNull final InventoryWrapper sellerInventory, + @NotNull final Location loc2Drop, + final int amount) { + return plugin.getShopManager().tradeService().executeBuyFromShop(this, seller, sellerInventory, loc2Drop, amount); } @Override @@ -1690,7 +1813,7 @@ public void setSignText(@NotNull final List lines) { } if(plugin.getSignHooker() != null) { Log.debug("Start sign broadcast..."); - plugin.getSignHooker().updatePerPlayerShopSignBroadcast(getLocation(), this); + plugin.getSignHooker().updatePerPlayerShopSignBroadcast(this.bukkitLocation(), this); Log.debug("Sign broadcast completed."); } } @@ -1792,6 +1915,7 @@ public void setShopBenefit(@NotNull final BenefitProvider benefit) { plugin.platform().encodeStack(getItem()), getShopName(), shopType().id(), + shopState().identifier(), getCurrency(), getPrice(), isUnlimited(), @@ -1866,7 +1990,7 @@ public ReloadResult reloadModule() throws Exception { private ShopSignStorage saveToShopSignStorage() { - return new ShopSignStorage(getLocation().getWorld().getName(), getLocation().getBlockX(), getLocation().getBlockY(), getLocation().getBlockZ()); + return new ShopSignStorage(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); } @Override diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java index 4f702939db..d49959cfbd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java @@ -8,11 +8,11 @@ import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.IShopType; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.state.ShopState; import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.common.util.Timer; import com.ghostchu.quickshop.economy.QSBenefitProvider; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.paste.item.SubPasteItem; @@ -182,6 +182,7 @@ private ShopLoadResult loadSingleShop(final InfoRecord infoRecord, final DataRec rawInfo.getOwner(), rawInfo.isUnlimited(), rawInfo.getType(), + rawInfo.getState(), rawInfo.getExtra(), rawInfo.getCurrency(), rawInfo.isHologram(), @@ -265,7 +266,7 @@ private boolean shopNullCheck(@Nullable final Shop shop) { Log.debug("Shop itemStack amount can't be 0"); return true; } - if(shop.getLocation() == null) { + if(shop.bukkitLocation() == null) { Log.debug("Shop location is null"); return true; } @@ -302,6 +303,7 @@ public static class DataRawDatabaseInfo { private QUser owner; private String name; private IShopType type; + private ShopState state; private String currency; private double price; private boolean unlimited; @@ -324,6 +326,7 @@ public static class DataRawDatabaseInfo { this.owner = dataRecord.getOwner(); this.price = dataRecord.getPrice(); this.type = QuickShop.getInstance().getShopManager().shopTypeOrDefault(dataRecord.getType()); + this.state = QuickShop.getInstance().getShopManager().shopStateOrDefault(dataRecord.getState()); this.unlimited = dataRecord.isUnlimited(); final String extraStr = dataRecord.getExtra(); this.name = dataRecord.getName(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopPurger.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopPurger.java index 603e6e2f36..a0a8eac362 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopPurger.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopPurger.java @@ -96,7 +96,7 @@ private void run() { final List> deletionFutures = new CopyOnWriteArrayList<>(); for(final Shop shop : pendingRemovalShops) { - final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc) -> { + final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (loc) -> { try { plugin.getShopManager().deleteShop(shop); purgedCount.incrementAndGet(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java index a7e9703424..7fd9c83c7b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java @@ -140,15 +140,17 @@ public Component renderTrading(final @NotNull Shop shop, final @NotNull ProxiedL final String noRemainingStringKey = shop.shopType().outOfStockTranslationKey(); final int shopRemaining = shop.shopType().remainingStock(shop); + final String finalTradingStringKey = (shop.shopState().overrideShopTypeText())? shop.shopState().translationKey() : tradingStringKey; + final Component trading = switch(shopRemaining) { //Unlimited case -1 -> - plugin.text().of(tradingStringKey, plugin.text().of("signs.unlimited").forLocale(locale.getLocale())).forLocale(locale.getLocale()); + plugin.text().of(finalTradingStringKey, plugin.text().of("signs.unlimited").forLocale(locale.getLocale())).forLocale(locale.getLocale()); //No remaining case 0 -> plugin.text().of(noRemainingStringKey).forLocale(locale.getLocale()); //Has remaining default -> - plugin.text().of(tradingStringKey, Component.text(shopRemaining)).forLocale(locale.getLocale()); + plugin.text().of(finalTradingStringKey, Component.text(shopRemaining)).forLocale(locale.getLocale()); }; return trading; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java index 04c411c3c1..1bc70833de 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java @@ -26,8 +26,13 @@ import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.cache.ShopCacheNamespacedKey; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import com.ghostchu.quickshop.api.shop.state.impl.ActiveState; +import com.ghostchu.quickshop.api.shop.state.impl.FrozenState; import com.ghostchu.quickshop.api.shop.tax.TaxManager; import com.ghostchu.quickshop.api.shop.tax.TaxRates; +import com.ghostchu.quickshop.api.shop.trading.TradeFailureReason; +import com.ghostchu.quickshop.api.shop.trading.TradeResult; import com.ghostchu.quickshop.api.shop.type.BuyingType; import com.ghostchu.quickshop.api.shop.type.FrozenType; import com.ghostchu.quickshop.api.shop.type.SellingType; @@ -106,6 +111,7 @@ public class SimpleShopManager extends AbstractShopManager implements ShopManage protected final Map cooldowns = Maps.newConcurrentMap(); protected final Map shopTypes = Maps.newConcurrentMap(); + protected final Map shopStates = Maps.newConcurrentMap(); protected final ConcurrentLinkedQueue inDeletion = new ConcurrentLinkedQueue<>(); protected final InteractiveManager interactiveManager; @@ -137,6 +143,10 @@ public class SimpleShopManager extends AbstractShopManager implements ShopManage public static final SellingType SELLING_TYPE = new SellingType(); public static final FrozenType FROZEN_TYPE = new FrozenType(); + //Initialize our shop states + public static final ActiveState ACTIVE_STATE = new ActiveState(); + public static final FrozenState FROZEN_STATE = new FrozenState(); + public SimpleShopManager(@NotNull final QuickShop plugin) { super(plugin); @@ -165,6 +175,9 @@ public void init() { addShopType(SELLING_TYPE); addShopType(FROZEN_TYPE); + addShopState(ACTIVE_STATE); + addShopState(FROZEN_STATE); + Log.debug("Loading caching tax account..."); final String taxAccount = taxManager().taxAccount(); if(!taxAccount.isEmpty()) { @@ -287,6 +300,38 @@ public Map shopTypes() { return type.orElse(SELLING_TYPE); } + /** + * Retrieves a map of shop states where the keys are shop identifiers and the values are the + * corresponding shop states. + * + * @return a non-null map containing shop identifiers as keys and their corresponding + * {@link ShopState} objects as values. + */ + @Override + public @NotNull Map shopStates() { + + return shopStates; + } + + /** + * Retrieves the ShopState associated with the specified identifier. If no ShopState is found for + * the identifier, a default ShopState is returned. + * + * @param identifier the unique identifier for the shop state to retrieve + * + * @return the ShopState associated with the identifier, or a default ShopState if not found + */ + @Override + public @NotNull ShopState shopStateOrDefault(final String identifier) { + + if(identifier == null) { + return ACTIVE_STATE; + } + + final Optional type = shopState(identifier); + return type.orElse(ACTIVE_STATE); + } + @Override public boolean actionBuying(@NotNull final Player buyer, @NotNull final InventoryWrapper buyerInventory, @NotNull final EconomyProvider eco, @NotNull final Info info, @NotNull final Shop shop, final int amount) { @@ -301,32 +346,14 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor return false; } - if(shop.isFrozen()) { - plugin.text().of(buyer, "shop-cannot-trade-when-freezing").send(); - return false; - } - if(shopIsNotValid(buyerQUser, info, shop)) { return false; } + int space = shop.getRemainingSpace(); + if(space == -1) { - space = 10000; - } - if(space < amount) { - plugin.text().of(buyer, "shop-has-no-space", Component.text(space), Util.getItemStackName(shop.getItem())).send(); - return false; - } - final int count = Util.countItems(buyerInventory, shop); - // Not enough items - if(amount > count) { - plugin.text().of(buyer, "you-dont-have-that-many-items", Component.text(count), Util.getItemStackName(shop.getItem())).send(); - return false; - } - if(amount < 1) { - // & Dumber - plugin.text().of(buyer, "negative-amount").send(); - return false; + space = Integer.MAX_VALUE; } // Money handling @@ -355,7 +382,7 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor } BigDecimal fromTax = BigDecimal.ZERO; final QSEconomyTransaction transaction; - final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().amount(BigDecimal.valueOf(total)).toTax(new BigDecimal(taxEvent.getTax().interactorRate())).taxer(taxAccount).currency(shop.getCurrency()).world(shop.getLocation().getWorld().getName()).to(buyerQUser); + final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().amount(BigDecimal.valueOf(total)).toTax(new BigDecimal(taxEvent.getTax().interactorRate())).taxer(taxAccount).currency(shop.getCurrency()).world(shop.bukkitLocation().getWorld().getName()).to(buyerQUser); if(!shop.isUnlimited() || (plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners") && shop.isUnlimited())) { fromTax = new BigDecimal(taxEvent.getTax().shopRate()); @@ -364,10 +391,49 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor transaction = builder.from(null).build(); } + //run our trade service stuff + final TradeResult result = shop.buy(buyerQUser, buyerInventory, buyer.getLocation(), amount); + + if(!result.success()) { + + switch(result.failureReason()) { + case INSUFFICIENT_FUNDS-> { + plugin.text().of(buyer, "the-owner-cant-afford-to-buy-from-you", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); + return false; + } + case SHOP_NO_SPACE -> { + + plugin.text().of(buyer, "shop-has-no-space", Component.text(space), Util.getItemStackName(shop.getItem())).send(); + return false; + } + case SHOP_FROZEN -> { + + plugin.text().of(buyer, "shop-cannot-trade-when-freezing").send(); + return false; + } + case INVALID_AMOUNT -> { + + plugin.text().of(buyer, "negative-amount").send(); + return false; + } + case ITEM_NOT_ENOUGH -> { + + final int count = Util.countItems(buyerInventory, shop); + plugin.text().of(buyer, "you-dont-have-that-many-items", Component.text(count), Util.getItemStackName(shop.getItem())).send(); + return false; + } + } + + plugin.logger().warn("Failed to processing purchase, rolling back..."); + plugin.text().of(buyer, "shop-transaction-failed", result.failureReason() + " (" + result.debugMessage() + ")").send(); + return false; + } + if(!transaction.completable()) { - plugin.text().of(buyer, "the-owner-cant-afford-to-buy-from-you", format((total + fromTax.doubleValue()), shop.getLocation().getWorld(), shop.getCurrency()), format(eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(buyer, "the-owner-cant-afford-to-buy-from-you", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return false; } + if(!transaction.safeCommit()) { plugin.text().of(buyer, "economy-transaction-failed", transaction.lastError()).send(); plugin.logger().error("EconomyTransaction Failed, last error: {}", transaction.lastError()); @@ -375,14 +441,6 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor return false; } - try { - shop.buy(buyerQUser, buyerInventory, buyer.getLocation(), amount); - } catch(final Exception shopError) { - plugin.logger().warn("Failed to processing purchase, rolling back...", shopError); - transaction.rollback(true); - plugin.text().of(buyer, "shop-transaction-failed", shopError.getMessage()).send(); - return false; - } sendSellSuccess(buyerQUser, shop, amount, total, transaction.toTax().doubleValue()); new ShopSuccessPurchaseEvent(shop, buyerQUser, buyerInventory, amount, total, transaction.toTax().doubleValue()).callEvent(); shop.setSignText(plugin.text().findRelativeLanguages(buyer)); // Update the signs count @@ -401,7 +459,7 @@ private void notifySold(@NotNull final QUser buyerQUser, @NotNull final Shop sho if(space == amount) { Component spaceWarn; if(shop.getShopName() == null) { - spaceWarn = plugin.text().of("shop-out-of-space", shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()).forLocale(langCode); + spaceWarn = plugin.text().of("shop-out-of-space", shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()).forLocale(langCode); } else { spaceWarn = plugin.text().of("shop-out-of-space-name", shop.getShopName(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } @@ -496,7 +554,7 @@ public void actionCreate(@NotNull final Player p, final Info info, @NotNull fina } final ContainerShop shop = new ContainerShop(plugin, -1, info.getLocation(), priceDouble, info.getItem(), createQUser, false, - SELLING_TYPE, new YamlConfiguration(), null, !plugin.getConfig().getBoolean("shop.display-default", true), + SELLING_TYPE, ACTIVE_STATE, new YamlConfiguration(), null, !plugin.getConfig().getBoolean("shop.display-default", true), null, plugin.getJavaPlugin().getName(), symbolLink, null, Collections.emptyMap(), new QSBenefitProvider()); @@ -527,39 +585,16 @@ public boolean actionSelling(@NotNull final Player seller, @NotNull final Invent return false; } - if(shop.isFrozen()) { - plugin.text().of(seller, "shop-cannot-trade-when-freezing").send(); - return false; - } - int stock = shop.getRemainingStock(); if(stock == -1) { - stock = 10000; + stock = Integer.MAX_VALUE; } /*if(shop.isStackingShop()) { stock = stock * shop.getItem().getAmount(); }*/ - if(stock < amount) { - plugin.text().of(seller, "shop-stock-too-low", Component.text(stock), Util.getItemStackName(shop.getItem())).send(); - return false; - } final int playerSpace = Util.countSpace(sellerInventory, shop); - if(playerSpace < amount) { - plugin.text().of(seller, "inventory-space-full", amount, playerSpace).send(); - return false; - } - if(amount < 1) { - // & Dumber - plugin.text().of(seller, "negative-amount").send(); - return false; - } - final int pSpace = Util.countSpace(sellerInventory, shop); - if(amount > pSpace) { - plugin.text().of(seller, "not-enough-space", Component.text(pSpace)).send(); - return false; - } final TaxRates taxRates = taxManager.provider().calculateTax(shop, sellerQUser); @@ -586,7 +621,7 @@ public boolean actionSelling(@NotNull final Player seller, @NotNull final Invent } } final BigDecimal fromTax = new BigDecimal(taxEvent.getTax().interactorRate()); - final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().from(sellerQUser).amount(BigDecimal.valueOf(total)).fromTax(fromTax).taxer(taxAccount).benefitManager(shop.getShopBenefit()).world(shop.getLocation().getWorld().getName()).currency(shop.getCurrency()); + final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().from(sellerQUser).amount(BigDecimal.valueOf(total)).fromTax(fromTax).taxer(taxAccount).benefitManager(shop.getShopBenefit()).world(shop.bukkitLocation().getWorld().getName()).currency(shop.getCurrency()); if(!shop.isUnlimited() || (plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners") && shop.isUnlimited())) { transaction = builder.to(shop.getOwner()).toTax(new BigDecimal(taxEvent.getTax().shopRate())).build(); @@ -594,24 +629,52 @@ public boolean actionSelling(@NotNull final Player seller, @NotNull final Invent transaction = builder.to(null).build(); } + final TradeResult result = shop.sell(sellerQUser, sellerInventory, seller.getLocation(), amount); + if(!result.success()) { + + switch(result.failureReason()) { + case INSUFFICIENT_FUNDS-> { + plugin.text().of(seller, "you-cant-afford-to-buy", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(sellerQUser, shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); + return false; + } + case SHOP_FROZEN -> { + + plugin.text().of(seller, "shop-cannot-trade-when-freezing").send(); + return false; + } + case INVALID_AMOUNT -> { + + plugin.text().of(seller, "negative-amount").send(); + return false; + } + case STOCK_TOO_LOW -> { + + plugin.text().of(seller, "shop-stock-too-low", Component.text(stock), Util.getItemStackName(shop.getItem())).send(); + return false; + } + case INVENTORY_FULL -> { + + plugin.text().of(seller, "inventory-space-full", amount, playerSpace).send(); + return false; + } + } + + plugin.logger().warn("Failed to processing purchase, rolling back..."); + plugin.text().of(seller, "shop-transaction-failed", result.failureReason() + " (" + result.debugMessage() + ")").send(); + return false; + } + if(!transaction.completable()) { - plugin.text().of(seller, "you-cant-afford-to-buy", format((total + fromTax.doubleValue()), shop.getLocation().getWorld(), shop.getCurrency()), format(eco.balance(sellerQUser, shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(seller, "you-cant-afford-to-buy", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(sellerQUser, shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return false; } + if(!transaction.safeCommit()) { plugin.text().of(seller, "economy-transaction-failed", transaction.lastError()).send(); plugin.logger().error("EconomyTransaction Failed, last error: {}", transaction.lastError()); return false; } - try { - shop.sell(sellerQUser, sellerInventory, seller.getLocation(), amount); - } catch(final Exception shopError) { - plugin.logger().warn("Failed to processing purchase, rolling back...", shopError); - transaction.rollback(true); - plugin.text().of(seller, "shop-transaction-failed", shopError.getMessage()).send(); - return false; - } sendPurchaseSuccess(sellerQUser, shop, amount, total, transaction.fromTax().doubleValue()); new ShopSuccessPurchaseEvent(shop, sellerQUser, sellerInventory, amount, total, transaction.fromTax().doubleValue()).callEvent(); notifyBought(sellerQUser, shop, amount, stock, transaction); @@ -677,7 +740,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock return; } // Check if target block is allowed shop-block - if(!Util.canBeShop(shop.getLocation().getBlock())) { + if(!Util.canBeShop(shop.bukkitLocation().getBlock())) { plugin.text().of(p, "chest-was-removed").send(); return; } @@ -697,7 +760,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock // Protection check if(!bypassProtectionCheck) { - final Result result = plugin.getPermissionChecker().canBuild(p, shop.getLocation()); + final Result result = plugin.getPermissionChecker().canBuild(p, shop.bukkitLocation()); if(!result.isSuccess()) { plugin.text().of(p, "3rd-plugin-build-check-failed", result.getMessage()).send(); if(plugin.perm().hasPermission(p, "quickshop.alerts")) { @@ -709,13 +772,13 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock } // Check if the shop is already created - if(plugin.getShopManager().getShop(shop.getLocation()) != null) { + if(plugin.getShopManager().getShop(shop.bukkitLocation()) != null) { plugin.text().of(p, "shop-already-owned").send(); return; } // Check if player and server allow double chest shop - if(Util.isDoubleChest(shop.getLocation().getBlock().getBlockData()) && !plugin.perm().hasPermission(p, "quickshop.create.double")) { + if(Util.isDoubleChest(shop.bukkitLocation().getBlock().getBlockData()) && !plugin.perm().hasPermission(p, "quickshop.create.double")) { plugin.text().of(p, "no-double-chests").send(); return; } @@ -750,7 +813,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock case PASS -> { // Calling ShopCreateEvent - ShopCreateEvent event = new ShopCreateEvent(Phase.PRE_CANCELLABLE, shop, shop.getOwner(), shop.getLocation()); + ShopCreateEvent event = new ShopCreateEvent(Phase.PRE_CANCELLABLE, shop, shop.getOwner(), shop.bukkitLocation()); if(event.callCancellableEvent()) { @@ -766,9 +829,9 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock createCost = 0; } if(createCost > 0) { - final QSEconomyTransaction economyTransaction = QSEconomyTransaction.builder().taxer(cacheTaxAccount).tax(BigDecimal.ZERO).from(QUserImpl.createFullFilled(p)).to(null).amount(BigDecimal.valueOf(createCost)).currency(plugin.getCurrency()).world(shop.getLocation().getWorld().getName()).build(); + final QSEconomyTransaction economyTransaction = QSEconomyTransaction.builder().taxer(cacheTaxAccount).tax(BigDecimal.ZERO).from(QUserImpl.createFullFilled(p)).to(null).amount(BigDecimal.valueOf(createCost)).currency(plugin.getCurrency()).world(shop.bukkitLocation().getWorld().getName()).build(); if(!economyTransaction.completable()) { - plugin.text().of(p, "you-cant-afford-a-new-shop", format(createCost, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "you-cant-afford-a-new-shop", format(createCost, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return; } if(!economyTransaction.safeCommit()) { @@ -787,7 +850,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock // Shop info sign check if(signBlock != null && autoSign) { if(signBlock.getType().isAir() || signBlock.getType() == Material.WATER) { - final BlockState signState = this.makeShopSign(shop.getLocation().getBlock(), signBlock, null); + final BlockState signState = this.makeShopSign(shop.bukkitLocation().getBlock(), signBlock, null); if(signState instanceof final Sign puttedSign) { try { @@ -802,6 +865,9 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock loadShop(shop); shop.setSignText(plugin.getTextManager().findRelativeLanguages(p)); + Util.playSound(p, "effect.sound.shop.create"); + Util.playParticle(p, shop.bukkitLocation(), "effect.particle.shop.create"); + event = event.clone(Phase.MAIN); event.callEvent(); } @@ -895,7 +961,7 @@ public void handleChat(@NotNull final Player p, @NotNull final String msg) { private void refundShop(final Shop shop) { - final World world = shop.getLocation().getWorld(); + final World world = shop.bukkitLocation().getWorld(); if(plugin.getConfig().getBoolean("shop.refund")) { double cost = plugin.getConfig().getDouble("shop.cost"); final QSEconomyTransaction transaction; @@ -1135,7 +1201,7 @@ private void notifyBought(@NotNull final QUser seller, @NotNull final Shop shop, if(stock == amount) { Component stockWarn; if(shop.getShopName() == null) { - stockWarn = plugin.text().of("shop-out-of-stock", shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ(), Util.getItemStackName(shop.getItem())).forLocale(langCode); + stockWarn = plugin.text().of("shop-out-of-stock", shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } else { stockWarn = plugin.text().of("shop-out-of-stock-name", shop.getShopName(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } @@ -1187,7 +1253,7 @@ private int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull final final int shopHaveSpaces = Util.countSpace(shop.getInventory(), shop); final int invHaveItems = Util.countItems(new BukkitInventoryWrapper(p.getInventory()), shop); // Check if shop owner has enough money - final double ownerBalance = eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double ownerBalance = eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int ownerCanAfford; if(shop.getPrice() != 0) { ownerCanAfford = (int)(ownerBalance / shop.getPrice()); @@ -1215,7 +1281,7 @@ private int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull final if(ownerCanAfford == 0 && (!shop.isUnlimited() || payUnlimitedShopOwner)) { // when typed 'all' but the shop owner doesn't have enough money to buy at least 1 // item (and shop isn't unlimited or pay-unlimited is true) - plugin.text().of(p, "the-owner-cant-afford-to-buy-from-you", plugin.getShopManager().format(shop.getPrice(), shop.getLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(ownerBalance, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "the-owner-cant-afford-to-buy-from-you", plugin.getShopManager().format(shop.getPrice(), shop.bukkitLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(ownerBalance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return 0; } // when typed 'all' but player doesn't have any items to sell @@ -1364,7 +1430,7 @@ private void actionTrade(@NotNull final Player p, final Info info, @NotNull fina actionSelling(p, new BukkitInventoryWrapper(p.getInventory()), eco, info, shop, amount); } else { plugin.text().of(p, "shop-purchase-cancelled").send(); - plugin.logger().warn("Shop data broken? Loc: {}", shop.getLocation()); + plugin.logger().warn("Shop data broken? Loc: {}", shop.bukkitLocation()); } } @@ -1390,7 +1456,7 @@ private int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull fina } // typed 'all', check if player has enough money than price * amount final double price = shop.getPrice(); - final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); amount = Math.min(amount, (int)Math.floor(balance / price)); if(amount < 1) { // typed 'all' but the auto set amount is 0 // when typed 'all' but player can't buy any items @@ -1404,7 +1470,7 @@ private int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull fina plugin.text().of(p, "not-enough-space", Component.text(invHaveSpaces)).send(); return 0; } - plugin.text().of(p, "you-cant-afford-to-buy", plugin.getShopManager().format(price, shop.getLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(balance, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "you-cant-afford-to-buy", plugin.getShopManager().format(price, shop.bukkitLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(balance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); } return 0; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleTradeService.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleTradeService.java new file mode 100644 index 0000000000..149a158cc5 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleTradeService.java @@ -0,0 +1,493 @@ +package com.ghostchu.quickshop.shop; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.inventory.InventoryWrapper; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.trading.TradeFailureReason; +import com.ghostchu.quickshop.api.shop.trading.TradeOptions; +import com.ghostchu.quickshop.api.shop.trading.TradePreview; +import com.ghostchu.quickshop.api.shop.trading.TradeResult; +import com.ghostchu.quickshop.api.shop.trading.TradeService; +import com.ghostchu.quickshop.api.shop.trading.TradeType; +import com.ghostchu.quickshop.util.Util; +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.math.BigDecimal; + +/** + * SimpleTradeService + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SimpleTradeService implements TradeService { + + private static final BigDecimal ZERO = BigDecimal.ZERO; + + private final QuickShop plugin; + + public SimpleTradeService(@NotNull final QuickShop plugin) { + this.plugin = plugin; + } + + @Override + public @NotNull TradeResult executeBuyFromShop(@NotNull final Shop shop, + @NotNull final QUser buyer, + @NotNull final InventoryWrapper buyerInventory, + @NotNull final Location dropLocation, + final int amount) { + return executeBuyFromShop(shop, buyer, buyerInventory, dropLocation, amount, TradeOptions.DEFAULT); + } + + @Override + public @NotNull TradeResult executeBuyFromShop(@NotNull final Shop shop, + @NotNull final QUser buyer, + @NotNull final InventoryWrapper buyerInventory, + @NotNull final Location dropLocation, + final int amount, + @NotNull final TradeOptions options) { + Util.ensureThread(false); + + final int normalizedAmount = normalizeAmount(shop, amount); + if(normalizedAmount < 0) { + return executeSellToShop(shop, buyer, buyerInventory, dropLocation, -amount, options); + } + + final TradePreview preview = previewBuyFromShop(shop, buyer, buyerInventory, amount); + if(!preview.allowed()) { + return failedResult( + TradeType.BUY_FROM_SHOP, + amount, + preview.allowedAmount(), + preview.unitPrice(), + preview.totalPrice(), + preview.limitingReason(), + preview.debugMessage() + ); + } + + try { + final ItemStack item = shop.getItem(); + final SimpleInventoryTransaction transaction; + + if(shop.isUnlimited()) { + transaction = SimpleInventoryTransaction.builder() + .from(null) + .to(buyerInventory) + .item(item) + .amount(normalizedAmount) + .build(); + } else { + final InventoryWrapper chestInv = shop.getInventory(); + if(chestInv == null) { + return failedResult( + TradeType.BUY_FROM_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.SHOP_TRANSACTION_FAILED, + "Shop inventory is null." + ); + } + + transaction = SimpleInventoryTransaction.builder() + .from(chestInv) + .to(buyerInventory) + .item(item) + .amount(normalizedAmount) + .build(); + } + + if(options.commit() && !transaction.failSafeCommit()) { + if(plugin.getSentryErrorReporter() != null) { + plugin.getSentryErrorReporter().ignoreThrow(); + } + return failedResult( + TradeType.BUY_FROM_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.INVENTORY_TRANSACTION_FAILED, + "Inventory transaction failed: " + transaction.getLastError() + ); + } + + if(options.updateSigns() && !shop.isUnlimited()) { + shop.setSignText(plugin.text().findRelativeLanguages(buyer, false)); + } + + return successResult( + TradeType.BUY_FROM_SHOP, + amount, + amount, + unitPrice(shop) + ); + } catch(final Exception e) { + return failedResult( + TradeType.BUY_FROM_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.INTERNAL_ERROR, + e.getMessage() + ); + } + } + + @Override + public @NotNull TradeResult executeSellToShop(@NotNull final Shop shop, + @NotNull final QUser seller, + @NotNull final InventoryWrapper sellerInventory, + @NotNull final Location dropLocation, + final int amount) { + return executeSellToShop(shop, seller, sellerInventory, dropLocation, amount, TradeOptions.DEFAULT); + } + + @Override + public @NotNull TradeResult executeSellToShop(@NotNull final Shop shop, + @NotNull final QUser seller, + @NotNull final InventoryWrapper sellerInventory, + @NotNull final Location dropLocation, + final int amount, + @NotNull final TradeOptions options) { + Util.ensureThread(false); + + final int normalizedAmount = normalizeAmount(shop, amount); + if(normalizedAmount < 0) { + return executeBuyFromShop(shop, seller, sellerInventory, dropLocation, -amount, options); + } + + final TradePreview preview = previewSellToShop(shop, seller, sellerInventory, amount); + if(!preview.allowed()) { + return failedResult( + TradeType.SELL_TO_SHOP, + amount, + preview.allowedAmount(), + preview.unitPrice(), + preview.totalPrice(), + preview.limitingReason(), + preview.debugMessage() + ); + } + + try { + final ItemStack item = shop.getItem(); + final SimpleInventoryTransaction transaction; + + if(shop.isUnlimited()) { + transaction = SimpleInventoryTransaction.builder() + .from(sellerInventory) + .to(null) + .item(item) + .amount(normalizedAmount) + .build(); + } else { + final InventoryWrapper chestInv = shop.getInventory(); + if(chestInv == null) { + return failedResult( + TradeType.SELL_TO_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.SHOP_TRANSACTION_FAILED, + "Shop inventory is null." + ); + } + + transaction = SimpleInventoryTransaction.builder() + .from(sellerInventory) + .to(chestInv) + .item(item) + .amount(normalizedAmount) + .build(); + } + + if(options.commit() && !transaction.failSafeCommit()) { + if(plugin.getSentryErrorReporter() != null) { + plugin.getSentryErrorReporter().ignoreThrow(); + } + return failedResult( + TradeType.SELL_TO_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.INVENTORY_TRANSACTION_FAILED, + "Inventory transaction failed: " + transaction.getLastError() + ); + } + + if(options.updateSigns()) { + shop.setSignText(plugin.text().findRelativeLanguages(seller, false)); + } + + return successResult( + TradeType.SELL_TO_SHOP, + amount, + amount, + unitPrice(shop) + ); + } catch(final Exception e) { + return failedResult( + TradeType.SELL_TO_SHOP, + amount, + 0, + unitPrice(shop), + totalPrice(shop, 0), + TradeFailureReason.INTERNAL_ERROR, + e.getMessage() + ); + } + } + + @Override + public @NotNull TradePreview previewBuyFromShop(@NotNull final Shop shop, + @NotNull final QUser buyer, + @NotNull final InventoryWrapper buyerInventory, + final int amount) { + if(amount <= 0) { + return previewFailure(TradeType.BUY_FROM_SHOP, amount, TradeFailureReason.INVALID_AMOUNT, "Amount must be > 0."); + } + if(!shop.isValid()) { + return previewFailure(TradeType.BUY_FROM_SHOP, amount, TradeFailureReason.SHOP_INVALID, "Shop is invalid."); + } + if(shop.isFrozen()) { + return previewFailure(TradeType.BUY_FROM_SHOP, amount, TradeFailureReason.SHOP_FROZEN, "Shop is frozen."); + } + if(!shop.isSelling()) { + return previewFailure(TradeType.BUY_FROM_SHOP, amount, TradeFailureReason.SHOP_DISABLED, "Shop is not selling items."); + } + + final int requestedUnits = normalizeAmount(shop, amount); + + if(!shop.isUnlimited()) { + final InventoryWrapper chestInv = shop.getInventory(); + if(chestInv == null) { + return previewFailure(TradeType.BUY_FROM_SHOP, amount, TradeFailureReason.SHOP_TRANSACTION_FAILED, "Shop inventory is null."); + } + + final int stock = Util.countItems(chestInv, shop); + if(stock < requestedUnits) { + final int stackSize = Math.max(1, shop.getItem().getAmount()); + final int allowedTrades = stock / stackSize; + return new TradePreview( + TradeType.BUY_FROM_SHOP, + amount, + Math.max(0, allowedTrades), + unitPrice(shop), + totalPrice(shop, Math.max(0, allowedTrades)), + false, + TradeFailureReason.STOCK_TOO_LOW, + "Not enough stock in shop." + ); + } + } + + final int buyerSpace = Util.countSpace(buyerInventory, shop); + if(buyerSpace < amount) { + return new TradePreview( + TradeType.BUY_FROM_SHOP, + amount, + Math.max(0, buyerSpace), + unitPrice(shop), + totalPrice(shop, Math.max(0, buyerSpace)), + false, + TradeFailureReason.INVENTORY_FULL, + "Buyer does not have enough inventory space." + ); + } + + return new TradePreview( + TradeType.BUY_FROM_SHOP, + amount, + amount, + unitPrice(shop), + totalPrice(shop, amount), + true, + null, + null + ); + } + + @Override + public @NotNull TradePreview previewSellToShop(@NotNull final Shop shop, + @NotNull final QUser seller, + @NotNull final InventoryWrapper sellerInventory, + final int amount) { + if(amount <= 0) { + return previewFailure(TradeType.SELL_TO_SHOP, amount, TradeFailureReason.INVALID_AMOUNT, "Amount must be > 0."); + } + if(!shop.isValid()) { + return previewFailure(TradeType.SELL_TO_SHOP, amount, TradeFailureReason.SHOP_INVALID, "Shop is invalid."); + } + if(shop.isFrozen()) { + return previewFailure(TradeType.SELL_TO_SHOP, amount, TradeFailureReason.SHOP_FROZEN, "Shop is frozen."); + } + if(!shop.isBuying()) { + return previewFailure(TradeType.SELL_TO_SHOP, amount, TradeFailureReason.SHOP_DISABLED, "Shop is not buying items."); + } + + final int requestedUnits = normalizeAmount(shop, amount); + + final int sellerStock = Util.countItems(sellerInventory, shop); + if(sellerStock < requestedUnits) { + final int stackSize = Math.max(1, shop.getItem().getAmount()); + final int allowedTrades = sellerStock / stackSize; + return new TradePreview( + TradeType.SELL_TO_SHOP, + amount, + Math.max(0, allowedTrades), + unitPrice(shop), + totalPrice(shop, Math.max(0, allowedTrades)), + false, + TradeFailureReason.ITEM_NOT_ENOUGH, + "Seller does not have enough items." + ); + } + + if(!shop.isUnlimited()) { + final int maxAffordable = shop.getMaxAffordable(); + if(amount > maxAffordable) { + return new TradePreview( + TradeType.SELL_TO_SHOP, + amount, + Math.max(0, maxAffordable), + unitPrice(shop), + totalPrice(shop, Math.max(0, maxAffordable)), + false, + TradeFailureReason.INSUFFICIENT_FUNDS, + "Shop cannot afford that many items." + ); + } + + int space = shop.getRemainingSpace(); + + if(space == -1) { + space = Integer.MAX_VALUE; + } + + final int stackSize = Math.max(1, shop.getItem().getAmount()); + final int maxTrades = space / stackSize; + + if(amount > maxTrades) { + return new TradePreview( + TradeType.SELL_TO_SHOP, + amount, + Math.max(0, maxTrades), + unitPrice(shop), + totalPrice(shop, Math.max(0, maxTrades)), + false, + TradeFailureReason.SHOP_NO_SPACE, + "Shop does not have enough space." + ); + } + } + + return new TradePreview( + TradeType.SELL_TO_SHOP, + amount, + amount, + unitPrice(shop), + totalPrice(shop, amount), + true, + null, + null + ); + } + + private int normalizeAmount(@NotNull final Shop shop, final int tradeAmount) { + return shop.getItem().getAmount() * tradeAmount; + } + + @SuppressWarnings("unchecked") + private BigDecimal unitPrice(@NotNull final Shop shop) { + + final Object price = shop.price(); + if(price instanceof final BigDecimal decimal) { + return decimal; + } + + if(price instanceof final Double doublePrice) { + return BigDecimal.valueOf(doublePrice); + } + + throw new IllegalStateException("DefaultTradeService expects Shop pricing."); + } + + private BigDecimal totalPrice(@NotNull final Shop shop, final int tradeAmount) { + return unitPrice(shop).multiply(BigDecimal.valueOf(Math.max(0, tradeAmount))); + } + + private TradePreview previewFailure(@NotNull final TradeType type, + final int requestedAmount, + @NotNull final TradeFailureReason reason, + @NotNull final String debug) { + return new TradePreview(type, requestedAmount, 0, ZERO, ZERO, false, reason, debug); + } + + private TradeResult successResult(@NotNull final TradeType type, + final int requestedAmount, + final int tradedAmount, + @NotNull final BigDecimal unitPrice) { + return new TradeResult( + true, + type, + requestedAmount, + tradedAmount, + unitPrice, + unitPrice.multiply(BigDecimal.valueOf(tradedAmount)), + ZERO, + ZERO, + null, + null, + null + ); + } + + private TradeResult failedResult(@NotNull final TradeType type, + final int requestedAmount, + final int tradedAmount, + @NotNull final BigDecimal unitPrice, + @NotNull final BigDecimal totalPrice, + @NotNull final TradeFailureReason reason, + final String debugMessage) { + return new TradeResult( + false, + type, + requestedAmount, + tradedAmount, + unitPrice, + totalPrice, + ZERO, + ZERO, + reason, + null, + debugMessage + ); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java index 93e29d7777..7f4ecbb7cd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java @@ -149,7 +149,7 @@ public static ItemStack createGuardItemStack(@NotNull ItemStack itemStack, @NotN public static ShopProtectionFlag createShopProtectionFlag( @NotNull final ItemStack itemStack, @NotNull final Shop shop) { - return new ShopProtectionFlag(shop.getLocation().toString(), Util.serialize(itemStack)); + return new ShopProtectionFlag(shop.bukkitLocation().toString(), Util.serialize(itemStack)); } public static boolean isVirtualDisplayDoesntWork() { @@ -226,7 +226,7 @@ protected void init() { final double y = PLUGIN.getConfig().getDouble("shop.display-coords.y", 0.8); final double z = PLUGIN.getConfig().getDouble("shop.display-coords.z", 0.5); - return this.shop.getLocation().clone().add(x, y, z); + return this.shop.bukkitLocation().clone().add(x, y, z); } /** diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java index 2a4674c68d..5a5a03a568 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java @@ -231,15 +231,15 @@ private void load() { Util.ensureThread(false); //some time shop can be loaded when world isn't loaded - chunkLocation = SimpleShopChunk.fromLocation(shop.getLocation()); + chunkLocation = SimpleShopChunk.fromLocation(shop.bukkitLocation()); manager.put(chunkLocation, this); //Let nearby player can saw fake item final List onlinePlayers = new ArrayList<>(Bukkit.getOnlinePlayers()); - onlinePlayers.removeIf(p->!p.getWorld().equals(shop.getLocation().getWorld())); + onlinePlayers.removeIf(p->!p.getWorld().equals(shop.bukkitLocation().getWorld())); for(final Player onlinePlayer : onlinePlayers) { - final double distance = onlinePlayer.getLocation().distance(shop.getLocation()); + final double distance = onlinePlayer.getLocation().distance(shop.bukkitLocation()); if(Math.abs(distance) > Bukkit.getViewDistance() * 16) { continue; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/interaction/QuickShopInteractionManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/interaction/QuickShopInteractionManager.java index fcd2e1cb32..83cfd47382 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/interaction/QuickShopInteractionManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/interaction/QuickShopInteractionManager.java @@ -248,15 +248,6 @@ public void loadConfig() { plugin.logger().warn("Failed to copy interaction.yml to plugin folder!"); } - /*final File configFile = new File(plugin.getDataFolder(), "interaction.yml"); - if(!configFile.exists()) { - try { - Files.copy(plugin.getJavaPlugin().getResource("interaction.yml"), configFile.toPath()); - } catch(final IOException e) { - plugin.logger().warn("Failed to copy interaction.yml to plugin folder!", e); - } - } - final FileConfiguration config = YamlConfiguration.loadConfiguration(configFile);*/ behaviorMapping.clear(); for(final String interaction : interactions.keySet()) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java index bbef4d6a00..ffe1965c3e 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java @@ -105,12 +105,12 @@ public void unload() { public void updatePerPlayerShopSignBroadcast(final Location location, final Shop shop) { - final World world = shop.getLocation().getWorld(); + final World world = shop.bukkitLocation().getWorld(); if(world == null) { return; } - QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc)->{ - final Collection nearbyPlayers = world.getNearbyEntities(shop.getLocation(), Bukkit.getViewDistance() * 16, shop.getLocation().getWorld().getMaxHeight(), Bukkit.getViewDistance() * 16); + QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (loc)->{ + final Collection nearbyPlayers = world.getNearbyEntities(shop.bukkitLocation(), Bukkit.getViewDistance() * 16, shop.bukkitLocation().getWorld().getMaxHeight(), Bukkit.getViewDistance() * 16); for(final Entity nearbyPlayer : nearbyPlayers) { if(nearbyPlayer instanceof final Player player) { updatePerPlayerShopSign(player, location, shop); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java new file mode 100644 index 0000000000..944f178aac --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java @@ -0,0 +1,73 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * PlayerTags + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class PlayerTags { + + private final Set tags = ConcurrentHashMap.newKeySet(); + + private final UUID player; + + public PlayerTags(final UUID player) { + + this.player = player; + } + + public boolean hasTag(final String tag) { + + return tags.contains(tag); + } + + public boolean hasTags(final List filterTags) { + + return tags.containsAll(filterTags); + } + + public boolean addTag(final String tag) { + + return tags.add(tag); + } + + public boolean removeTag(final String tag) { + + return tags.remove(tag); + } + + public Set getTags() { + + return Collections.unmodifiableSet(tags); + } + + public UUID player() { + + return player; + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java new file mode 100644 index 0000000000..bf85fa30fb --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java @@ -0,0 +1,120 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.api.shop.tag.PlayerTagIndex; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Default QuickShop implementation of {@link PlayerTagIndex}. + * + *

This implementation maintains two concurrent indexes:

+ *
    + *
  • shopId → tags
  • + *
  • tag → shopIds
  • + *
+ * + *

This allows fast lookups in both directions.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class QuickShopPlayerTagIndex implements PlayerTagIndex { + + private final Map> shopToTags = new ConcurrentHashMap<>(); + private final Map> tagToShops = new ConcurrentHashMap<>(); + + @Override + public void addTag(final long shopId, final String tag) { + + shopToTags + .computeIfAbsent(shopId, id->ConcurrentHashMap.newKeySet()) + .add(tag); + + tagToShops + .computeIfAbsent(tag, t->ConcurrentHashMap.newKeySet()) + .add(shopId); + } + + @Override + public void removeTag(final long shopId, final String tag) { + + final Set tags = shopToTags.get(shopId); + if(tags != null) { + tags.remove(tag); + if(tags.isEmpty()) { + shopToTags.remove(shopId); + } + } + + final Set shops = tagToShops.get(tag); + if(shops != null) { + shops.remove(shopId); + if(shops.isEmpty()) { + tagToShops.remove(tag); + } + } + } + + @Override + public Set getTags(final long shopId) { + + return shopToTags.getOrDefault(shopId, Collections.emptySet()); + } + + @Override + public Set getShops(final String tag) { + + return tagToShops.getOrDefault(tag, Collections.emptySet()); + } + + @Override + public Set shops() { + + return Collections.unmodifiableSet(shopToTags.keySet()); + } + + @Override + public int totalTags() { + + int total = 0; + for(final Set tags : shopToTags.values()) { + total += tags.size(); + } + return total; + } + + @Override + public boolean hasTag(final long shopId, final String tag) { + + final Set tags = shopToTags.get(shopId); + return tags != null && tags.contains(tag); + } + + @Override + public void clear() { + + shopToTags.clear(); + tagToShops.clear(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java new file mode 100644 index 0000000000..a1a68c6e49 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java @@ -0,0 +1,438 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.database.DatabaseHelper; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.PlayerTagIndex; +import com.ghostchu.quickshop.api.shop.tag.TagManager; +import com.ghostchu.quickshop.api.shop.tag.TagService; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; +import com.ghostchu.quickshop.util.pagination.Pagination; +import com.ghostchu.quickshop.util.pagination.PaginationOptions; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +import static com.ghostchu.quickshop.api.shop.tag.TagService.TOTAL_INDEX; +import static com.ghostchu.quickshop.api.shop.tag.TaggingResult.NOT_FOUND; + +/** + * The TagManager class handles the management of tags associated with shops and players. It allows + * adding, removing, and querying tags for shops, as well as filtering shops by tags. The data is + * stored in memory with support for persistent operations via the TagService. + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class QuickShopTagManager implements TagManager { + + public static final int MAX_PER_PAGE = 5; + + private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); + private final ConcurrentHashMap playerIndexes = new ConcurrentHashMap<>(); + + private final QuickShop plugin; + private final TagService service; + + public QuickShopTagManager(final QuickShop plugin) { + + this.plugin = plugin; + this.service = new QuickShopTagService(plugin); + } + + private PlayerTagIndex playerIndex(final UUID player) { + + return playerIndexes.computeIfAbsent(player, id->new QuickShopPlayerTagIndex()); + } + + private void cleanupPlayerIndex(final UUID player) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return; + } + + if(index.totalTags() <= 0) { + playerIndexes.remove(player); + } + } + + /** + * Loads all tag-related data from the database into memory. + * + * This method initializes or refreshes the in-memory representation of tags and their + * associations by retrieving the complete tag data set from the underlying database. It should be + * called during application startup or when a full reload of tag data is needed. + */ + @Override + public void loadAllFromDB() { + + CompletableFuture.runAsync(() -> { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + db.loadAllTags(); + }); + } + + @Override + public TaggingResult addTag(final long shopId, final UUID player, final String tag, final boolean db) { + + final ShopTags shopTags = tags.computeIfAbsent(shopId, id->new ShopTags()); + if(shopTags.hasTag(player, tag)) { + return TaggingResult.ALREADY_EXISTS; + } + + shopTags.addTag(player, tag); + //update our index service + playerIndex(player).addTag(shopId, tag); + //update our database + if(db) { + service.addShopTag(player, shopId, tag); + } + return TaggingResult.SUCCESS; + } + + @Override + public TaggingResult toggleTag(final long shopId, final UUID player, final String tag) { + + if(hasTag(shopId, player, tag)) { + return removeTag(shopId, player, tag); + } + return addTag(shopId, player, tag); + } + + @Override + public boolean removeAllTags() { + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + removeAllShopTags(shopId); + } + return true; + } + + @Override + public boolean removeAllShopTags(final long shopId) { + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return false; + } + + for(final PlayerTags playerTags : shopTags.getTags()) { + final UUID player = playerTags.player(); + + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + for(final String tag : playerTags.getTags()) { + index.removeTag(shopId, tag); + } + cleanupPlayerIndex(player); + } + } + + shopTags.clear(); + service.removeAllShopTags(shopId); + tags.remove(shopId); + return true; + } + + @Override + public boolean removeAllShopTagsBy(final long shopId, final UUID player) { + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return false; + } + + final PlayerTags playerTags = shopTags.getTags(player); + if(playerTags == null || playerTags.getTags().isEmpty()) { + return false; + } + + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + for(final String tag : playerTags.getTags()) { + index.removeTag(shopId, tag); + } + cleanupPlayerIndex(player); + } + + shopTags.removeAllTags(player); + //remove our shop object if empty to save memory + if(shopTags.isEmpty()) { + tags.remove(shopId); + } + + service.removeAllShopTagsBy(shopId, player); + return true; + } + + @Override + public boolean removeAllPlayerTags(final UUID player) { + + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null || index.totalTags() <= 0) { + return false; + } + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + removeAllShopTagsBy(shopId, player); + } + + //update our index service + playerIndexes.remove(player); + return true; + } + + @Override + public int totalTags() { + + int total = 0; + for(final PlayerTagIndex index : playerIndexes.values()) { + total += index.totalTags(); + } + return total; + } + + @Override + public int totalTagsByPlayer(final UUID player) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return 0; + } + return index.totalTags(); + } + + @Override + public TreeMap tagsCount(final UUID player) { + + final TreeMap count = new TreeMap<>(); + final PlayerTagIndex index = playerIndexes.get(player); + //check out index service + if(index == null) { + count.put(TOTAL_INDEX, 0); + return count; + } + + int total = 0; + for(final Long shopId : index.shops()) { + final int size = index.getTags(shopId).size(); + if(size <= 0) { + continue; + } + + count.put(shopId, size); + total += size; + } + + //our total count + count.put(TOTAL_INDEX, total); + return count; + } + + @Override + public Set tagsFilteredByShop(final UUID player, final long shopId) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + return index.getTags(shopId); + } + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return Collections.emptySet(); + } + + final PlayerTags playerTags = shopTags.getTags(player); + if(playerTags == null) { + return Collections.emptySet(); + } + + return playerTags.getTags(); + } + + @Override + public List shopsFilteredByTag(final UUID player, final String tag) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return Collections.emptyList(); + } + + return new ArrayList<>(index.getShops(tag)); + } + + @Override + public List shopsFilteredByTags(final UUID player, final List filterTags) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null || filterTags.isEmpty()) { + return Collections.emptyList(); + } + + Set result = null; + + for(final String tag : filterTags) { + final Set shops = index.getShops(tag); + if(shops.isEmpty()) { + return Collections.emptyList(); + } + + if(result == null) { + result = ConcurrentHashMap.newKeySet(); + result.addAll(shops); + continue; + } + + result.retainAll(shops); + if(result.isEmpty()) { + return Collections.emptyList(); + } + } + + return result == null? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(result)); + } + + @Override + public void listShopsByFilter(final Player player, + final String commandLabel, + final int page, + final List filterTags, + final String type, + final String titleNode, + final String noEntries) { + + final List shopIds = shopsFilteredByTags(player.getUniqueId(), filterTags); + if(shopIds.isEmpty()) { + plugin.text().of(player, noEntries).send(); + return; + } + + final PaginationOptions options = PaginationOptions + .builder() + .setCommand(commandLabel + " " + type + " list") + .setCurrentPage(page) + .setEntries(shopIds) + .setMaxPerPage(MAX_PER_PAGE) + .setEntryConsumer((pos, entry)->{ + + final Shop shop = plugin.getShopManager().getShop((Long)entry); + if(shop == null) { + return; + } + + //TODO: Option to teleport through list? Maybe add a /qs teleport command for this? + //final boolean canTeleport = plugin.perm().hasPermission(sender, "quickshop.tagged.teleport"); + //final Component tpComponent = plugin.text().of(sender, "tags.tag.list-tag-shop-entry-tp", commandLabel + " tp ").forLocale(); + + plugin.text().of(player, "tags.tag.list-tag-shop-entry", pos, entry, shop.getOwner().getDisplay(), shop.getItem().displayName(), "", commandLabel + " " + type + " " + entry).send(); + }) + .setHeaderLanguageKey("pagination.header") + .setFooterLanguageKey("pagination.footer").build(); + + final Pagination shops = new Pagination<>(options); + + + final Component titleComponent = plugin.text().of(player, titleNode, shopIds.size()).forLocale(); + + shops.printHeader(player, titleComponent, shops.page(), shops.totalPages()); + shops.printEntries(player); + shops.printFooter(player, options.command() + " " + shops.previousPage(), + shops.page(), shops.totalPages(), + options.command() + " " + shops.nextPage()); + } + + @Override + public boolean hasTag(final long shopId, final UUID player, final String tag) { + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return false; + } + return shopTags.hasTag(player, tag); + } + + @Override + public boolean removeTag(final UUID player, final String tag) { + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + final Long shopId = iterator.next(); + removeTag(shopId, player, tag); + } + return true; + } + + @Override + public TaggingResult removeTag(final long shopId, final UUID player, final String tag) { + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return NOT_FOUND; + } + + if(!shopTags.hasTag(player, tag)) { + return NOT_FOUND; + } + + shopTags.removeTag(player, tag); + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + + index.removeTag(shopId, tag); + } + + //update our database + service.removeShopTag(player, shopId, tag); + + //remove our shop object if empty to save memory + if(shopTags.isEmpty()) { + tags.remove(shopId); + } + + return TaggingResult.SUCCESS; + } + + @Override + public TagService service() { + + return service; + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java new file mode 100644 index 0000000000..c3a189d698 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java @@ -0,0 +1,220 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.database.DatabaseHelper; +import com.ghostchu.quickshop.api.shop.tag.TagService; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +/** + * Default QuickShop implementation of {@link TagService}. + * + *

This implementation handles normalization, formatting, and persistence + * operations for tags using the QuickShop database layer.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class QuickShopTagService implements TagService { + + public static final int MAX_TAG_LENGTH = 32; + private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); + + private static QuickShopTagService instance; + + private final QuickShop plugin; + + /** + * Creates a new tag service implementation. + * + * @param plugin the QuickShop plugin instance + * + * @since 6.3.0.0 + */ + public QuickShopTagService(final QuickShop plugin) { + + this.plugin = plugin; + instance = this; + } + + /** + * Returns the active tag service implementation instance. + * + * @return the active tag service implementation + * + * @throws IllegalStateException if the tag service has not been initialized + * @since 6.3.0.0 + */ + public static QuickShopTagService instance() { + + if(instance == null) { + throw new IllegalStateException("QuickShopTagService has not been initialized."); + } + return instance; + } + + /** + * Generates a command based on the provided tag value and list mode. + * + * @param stored The tag value as a {@code String}. If {@code null}, the fallback "tag list" command is returned. + * @param list A {@code boolean} indicating whether the command should be in list format or not. + * If {@code true}, the method returns commands in the "list" format (e.g., "favorite list"). + * If {@code false}, it returns commands in single format (e.g., "favorite"). + * @return A {@code String} representing the command based on the input {@code stored} and {@code list} values. + * Possible outputs include: + * - "tag list" if {@code stored} is {@code null}. + * - "favorite", "watch", or "avoid" (and their respective list forms) for predefined system tags. + * - "tag remove " for custom tags if {@code list} is {@code false}. + * @since 6.3.0.0 + */ + @Override + public String commandFromTag(final String stored, final boolean list) { + + if(stored == null) { + return "tag list"; + } + + return switch(stored) { + case SYS_FAV -> (list)? "favorite list" : "favorite"; + case SYS_WATCH -> (list)? "watch list" : "watch"; + case SYS_AVOID -> (list)? "avoid list" : "avoid"; + default -> (list)? "tag tagged " + stored : "tag remove " + stored; + }; + } + + @Override + public String displayTag(final Player sender, final String stored) { + + if(stored == null) { + return ""; + } + + return switch(stored) { + case SYS_FAV -> "Favorite"; + case SYS_WATCH -> "Watch"; + case SYS_AVOID -> "Avoid"; + default -> "#" + stored; + }; + } + + @Override + public String displayTagOrHash(final String stored) { + + if(stored == null) { + return ""; + } + + if(stored.startsWith("@")) { + return stored; + } + return "#" + stored; + } + + @Override + public @Nullable String normalizeTag(@Nullable String input, final boolean allowSystem) { + + if(input == null) { + return null; + } + + if(input.startsWith("#")) { + input = input.substring(1); + } + + input = input.trim().toLowerCase(Locale.ROOT); + + if(input.isEmpty()) { + return null; + } + + if(input.length() > MAX_TAG_LENGTH) { + return null; + } + + final boolean system = input.startsWith("@"); + + if(!allowSystem && system) { + return null; + } + + if(system) { + + input = input.substring(1); + } + + if(!VALID_TAG_PATTERN.matcher(input).matches()) { + return null; + } + + if(system) { + return "@" + input; + } + + return input; + } + + @Override + public CompletableFuture addShopTag(final UUID player, final long shopId, final String tag) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.tagShop(player, shopId, tag).thenApply(result->result != null && result > 0); + } + + @Override + public CompletableFuture removeShopTag(final UUID player, final long shopId, final String tag) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.removeShopTag(player, shopId, tag).thenApply(result->result != null && result > 0); + } + + @Override + public CompletableFuture removeAllShopTags(final long shopId) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.removeAllShopTags(shopId).thenApply(result->result != null && result > 0); + } + + @Override + public CompletableFuture removeAllShopTagsBy(final long shopId, final UUID player) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.removeAllShopTagsBy(player, shopId).thenApply(result->result != null && result > 0); + } + + @Override + public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + + return db.tagShop(player, shopId, tag).thenCompose(result->{ + if(result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return db.removeShopTag(player, shopId, tag).thenApply(r->false); + }); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java new file mode 100644 index 0000000000..9d51926544 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java @@ -0,0 +1,104 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * ShopTags + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopTags { + + private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); + + public void addTag(final UUID player, final String tag) { + + if(!tags.containsKey(player)) { + tags.put(player, new PlayerTags(player)); + } + tags.get(player).addTag(tag); + } + + public void removeTag(final UUID player, final String tag) { + + final PlayerTags playerTags = tags.get(player); + if(playerTags == null) { + return; + } + + playerTags.removeTag(tag); + //remove our player object if empty to save memory + if(playerTags.getTags().isEmpty()) { + tags.remove(player); + } + } + + public boolean removeAllTags(final UUID player) { + + if(!tags.containsKey(player)) { + return false; + } + tags.remove(player); + return true; + } + + public boolean hasTag(final UUID player, final String tag) { + + final PlayerTags playerTags = tags.get(player); + return playerTags != null && playerTags.hasTag(tag); + } + + public boolean hasTags(final UUID player, final List filterTags) { + + final PlayerTags playerTags = tags.get(player); + return playerTags != null && playerTags.hasTags(filterTags); + } + + public PlayerTags getTags(final UUID player) { + + return tags.get(player); + } + + public void setTags(final UUID player, final PlayerTags tags) { + + this.tags.put(player, tags); + } + + public boolean isEmpty() { + + return tags.isEmpty(); + } + + public void clear() { + + tags.clear(); + } + + public Collection getTags() { + + return Collections.unmodifiableCollection(tags.values()); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/BasicProvider.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/BasicProvider.java index 163bc9fa36..ca1843cd74 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/BasicProvider.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/BasicProvider.java @@ -70,8 +70,10 @@ public String identifier() { public TaxRates calculateTax(final Shop shop, final QUser player) { final double interactorRate = (appliesTo.equalsIgnoreCase("player") + || appliesTo.equalsIgnoreCase("payee") && shop.isBuying() || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, player) : 0.0; final double ownerRate = (appliesTo.equalsIgnoreCase("shop") + || appliesTo.equalsIgnoreCase("payee") && shop.isSelling() || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, shop.getOwner()) : 0.0; return new TaxRates(interactorRate, ownerRate); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java index a6d14371b9..07bd9f84f4 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java @@ -75,9 +75,11 @@ public String identifier() { public TaxRates calculateTax(final Shop shop, final QUser player) { final double interactorRate = (appliesTo.equalsIgnoreCase("player") - || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, player, getRate(player, shop.getLocation().getWorld().getName(), shop.getCurrency())) : 0.0; + || appliesTo.equalsIgnoreCase("payee") && shop.isBuying() + || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, player, getRate(player, shop.bukkitLocation().getWorld().getName(), shop.getCurrency())) : 0.0; final double ownerRate = (appliesTo.equalsIgnoreCase("shop") - || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, shop.getOwner(), getRate(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency())) : 0.0; + || appliesTo.equalsIgnoreCase("payee") && shop.isSelling() + || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, shop.getOwner(), getRate(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())) : 0.0; return new TaxRates(interactorRate, ownerRate); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java index 1fd2589d69..2d534e4f05 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java @@ -77,7 +77,7 @@ public String getShopName(@NotNull final ShopMetricRecord record, @NotNull final nameBuilder.append(ChatColor.stripColor(shopName)); } else { if(shop != null) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String template = "%s %s,%s,%s"; nameBuilder.append(String.format(template, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); } else { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MsgUtil.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MsgUtil.java index 672ae3b5e8..3617902a45 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MsgUtil.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MsgUtil.java @@ -32,6 +32,7 @@ import java.math.BigDecimal; import java.text.DecimalFormat; import java.util.AbstractMap; +import java.util.Arrays; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -171,6 +172,7 @@ public static String fillArgs(@Nullable String raw, @Nullable final String... ar public static Component fillArgs(@NotNull Component origin, @Nullable final Component... args) { for(int i = 0; i < args.length; i++) { + origin = origin.replaceText(TextReplacementConfig.builder() .matchLiteral("{" + i + "}") .replacement(args[i] == null? Component.empty() : args[i]) diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java index 6eae2e1c6a..e9eacd7a4b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java @@ -65,9 +65,9 @@ public class ShopUtil { public static boolean allowed(final Shop shop, final ItemStack itemStack) { - if(shop.getLocation().getWorld() != null) { + if(shop.bukkitLocation().getWorld() != null) { - final Block block = shop.getLocation().getBlock(); + final Block block = shop.bukkitLocation().getBlock(); return allowed(block, itemStack); } @@ -113,10 +113,10 @@ public static void transferRequest(@NotNull final UUID sender, @NotNull final UU QuickShop.getInstance().text().of(receiver, "transfer-single-ask", 60).send(); } - public static void setPrice(final QuickShop plugin, @NotNull final QUser user, final double price, @NotNull final Shop shop) { + public static boolean setPrice(final QuickShop plugin, @NotNull final QUser user, final double price, @NotNull final Shop shop) { if(user.getUniqueId() == null || user.getBukkitPlayer().isEmpty()) { - return; + return false; } final boolean format = plugin.getConfig().getBoolean("use-decimal-format"); @@ -132,13 +132,13 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f if(!shop.playerAuthorize(user.getUniqueId(), BuiltInShopPermission.SET_PRICE) && !plugin.perm().hasPermission(user, "quickshop.other.price")) { plugin.text().of(user.getUniqueId(), "not-managed-shop").send(); - return; + return false; } if(shop.getPrice() == price) { // Stop here if there isn't a price change plugin.text().of(user.getUniqueId(), "no-price-change").send(); - return; + return false; } final int maximumDigitsInPrice = plugin.getConfig().getInt("shop.maximum-digits-in-price", -1); @@ -146,7 +146,7 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f final int inputScale = Math.max(BigDecimal.valueOf(price).stripTrailingZeros().scale(), 0); if(inputScale > maximumDigitsInPrice) { plugin.text().of(user, "digits-reach-the-limit", Component.text(maximumDigitsInPrice)).send(); - return; + return false; } } @@ -157,19 +157,19 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f plugin.text().of(user.getUniqueId(), "restricted-prices", Util.getItemStackName(shop.getItem()), Component.text(checkResult.getMin()), Component.text(checkResult.getMax())).send(); - return; + return false; } case REACHED_PRICE_MIN_LIMIT -> { plugin.text().of(user, "price-too-cheap", (format)? MsgUtil.decimalFormat(checkResult.getMin()) : Double.toString(checkResult.getMin())).send(); - return; + return false; } case REACHED_PRICE_MAX_LIMIT -> { plugin.text().of(user, "price-too-high", (format)? MsgUtil.decimalFormat(checkResult.getMax()) : Double.toString(checkResult.getMax())).send(); - return; + return false; } case NOT_A_WHOLE_NUMBER -> { plugin.text().of(user, "not-a-integer", price).send(); - return; + return false; } } @@ -180,23 +180,23 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f if(event.callCancellableEvent()) { Log.debug("A plugin cancelled the price change event."); - return; + return false; } if(fee > 0) { final QSEconomyTransaction transaction = QSEconomyTransaction.builder() .from(QUserImpl.createFullFilled(user.getBukkitPlayer().get())) .amount(BigDecimal.valueOf(fee)) - .world(Objects.requireNonNull(shop.getLocation().getWorld()).getName()) + .world(Objects.requireNonNull(shop.bukkitLocation().getWorld()).getName()) .currency(plugin.getCurrency()) .build(); if(!transaction.completable()) { plugin.text().of(user, "you-cant-afford-to-change-price", plugin.getShopManager().format(fee, shop)).send(); - return; + return false; } if(!transaction.safeCommit()) { plugin.text().of(user, "economy-transaction-failed", transaction.lastError()).send(); - return; + return false; } } @@ -212,6 +212,8 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f event = event.clone(Phase.POST); event.callEvent(); + + return true; } public static boolean sellToShop(@NotNull final Player p, @Nullable final Shop shop, final boolean direct, final boolean all) { @@ -237,12 +239,12 @@ public static boolean sellToShop(@NotNull final Player p, @Nullable final Shop s final double price = shop.getPrice(); final Inventory playerInventory = p.getInventory(); final String tradeAllWord = QuickShop.getInstance().getConfig().getString("shop.word-for-trade-all-items", "all"); - final double ownerBalance = eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double ownerBalance = eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int items = getPlayerCanSell(shop, ownerBalance, price, new BukkitInventoryWrapper(playerInventory)); final ShopManager.InteractiveManager actions = QuickShop.getInstance().getShopManager().getInteractiveManager(); if(shop.playerAuthorize(p.getUniqueId(), BuiltInShopPermission.PURCHASE) || QuickShop.getInstance().perm().hasPermission(p, "quickshop.other.use")) { - final Info info = new SimpleInfo(shop.getLocation(), ShopAction.PURCHASE_SELL, null, null, shop, false); + final Info info = new SimpleInfo(shop.bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop, false); actions.put(p.getUniqueId(), info); if(!direct) { if(shop.isStackingShop()) { @@ -315,11 +317,11 @@ public static boolean buyFromShop(@NotNull final Player p, @Nullable final Shop final Inventory playerInventory = p.getInventory(); final String tradeAllWord = QuickShop.getInstance().getConfig().getString("shop.word-for-trade-all-items", "all"); final ShopManager.InteractiveManager actions = QuickShop.getInstance().getShopManager().getInteractiveManager(); - final double traderBalance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double traderBalance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int itemAmount = getPlayerCanBuy(shop, traderBalance, price, new BukkitInventoryWrapper(playerInventory)); if(shop.playerAuthorize(p.getUniqueId(), BuiltInShopPermission.PURCHASE) || QuickShop.getInstance().perm().hasPermission(p, "quickshop.other.use")) { - final Info info = new SimpleInfo(shop.getLocation(), ShopAction.PURCHASE_BUY, null, null, shop, false); + final Info info = new SimpleInfo(shop.bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop, false); actions.put(p.getUniqueId(), info); if(!direct) { if(shop.isStackingShop()) { @@ -368,7 +370,7 @@ private static int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNul final int invHaveItems = Util.countItems(new BukkitInventoryWrapper(p.getInventory()), shop); // Check if shop owner has enough money final double ownerBalance = eco - .balance(shop.getOwner(), shop.getLocation().getWorld().getName(), + .balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int ownerCanAfford; if(shop.getPrice() != 0) { @@ -401,9 +403,9 @@ private static int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNul // when typed 'all' but the shop owner doesn't have enough money to buy at least 1 // item (and shop isn't unlimited or pay-unlimited is true) QuickShop.getInstance().text().of(p, "the-owner-cant-afford-to-buy-from-you", - QuickShop.getInstance().getShopManager().format(shop.getPrice(), shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(shop.getPrice(), shop.bukkitLocation().getWorld(), shop.getCurrency()), - QuickShop.getInstance().getShopManager().format(ownerBalance, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(ownerBalance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return 0; } @@ -444,7 +446,7 @@ private static int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNu } // typed 'all', check if player has enough money than price * amount final double price = shop.getPrice(); - final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), + final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); amount = Math.min(amount, (int)Math.floor(balance / price)); if(amount < 1) { // typed 'all' but the auto set amount is 0 @@ -463,9 +465,9 @@ private static int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNu return 0; } QuickShop.getInstance().text().of(p, "you-cant-afford-to-buy", - QuickShop.getInstance().getShopManager().format(price, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(price, shop.bukkitLocation().getWorld(), shop.getCurrency()), - QuickShop.getInstance().getShopManager().format(balance, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(balance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); } return 0; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/Util.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/Util.java index c0857ac470..4ede5c4599 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/Util.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/Util.java @@ -1,5 +1,6 @@ package com.ghostchu.quickshop.util; +import com.destroystokyo.paper.ParticleBuilder; import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.event.Phase; import com.ghostchu.quickshop.api.event.management.ShopCreateEvent; @@ -17,16 +18,22 @@ import com.ghostchu.quickshop.shop.SimpleInfo; import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; import com.ghostchu.quickshop.util.logger.Log; +import dev.dejvokep.boostedyaml.route.Route; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; import lombok.Getter; import lombok.Setter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; +import org.bukkit.Color; import org.bukkit.DyeColor; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.OfflinePlayer; +import org.bukkit.Particle; +import org.bukkit.Registry; import org.bukkit.Sound; import org.bukkit.Tag; import org.bukkit.World; @@ -149,6 +156,168 @@ public static void playClickSound(@NotNull final Player player) { } } + public static void playSound(@NotNull final Player player, @NotNull final String config) { + + final boolean globalEnabled = plugin.getEffects().getBoolean("effect.sound.enabled"); + if(!globalEnabled) { + return; + } + + final float globalVolume = plugin.getEffects().getFloat("effect.sound.volume"); + final float globalPitch = plugin.getEffects().getFloat("effect.sound.pitch"); + + final Route route = Route.fromString(config); + + if(!plugin.getEffects().contains(route)) { + return; + } + + final Route parentEnabled = route.parent().add("enabled"); + if(plugin.getEffects().contains(parentEnabled) && !plugin.getEffects().getBoolean(parentEnabled)) { + return; + } + + final boolean enabled = plugin.getEffects().getBoolean(config + ".enabled", true); + if(!enabled) { + return; + } + + final float volume = plugin.getEffects().getFloat(config + ".volume", globalVolume); + final float pitch = plugin.getEffects().getFloat(config + ".pitch", globalPitch); + + //final Registry registryAccess = RegistryAccess.registryAccess().getRegistry(RegistryKey.SOUND_EVENT); + + player.playSound(player.getLocation(), Sound.valueOf(plugin.getEffects().getString(config + ".sound")), volume, pitch); + } + public static void playParticle(@NotNull final Player player, @NotNull final String config) { + + playParticle(player, player.getLocation(), config); + } + + public static void playParticle(@NotNull final Player player, final Location location, @NotNull final String config) { + + if(!plugin.getEffects().getBoolean("effect.particle.enabled")) { + return; + } + + final Route route = Route.fromString(config); + if(!plugin.getEffects().contains(route)) { + return; + } + + final Route parentEnabled = route.parent().add("enabled"); + if(plugin.getEffects().contains(parentEnabled) && !plugin.getEffects().getBoolean(parentEnabled)) { + + return; + } + + if(!plugin.getEffects().getBoolean(config + ".enabled", true)) { + + return; + } + + final String particleName = plugin.getEffects().getString(config + ".particle", ""); + if(particleName == null || particleName.isEmpty()) { + + return; + } + + final Particle particle; + try { + + particle = Particle.valueOf(particleName.toUpperCase()); + } catch (final Exception e) { + + plugin.logger().warn("Invalid particle: " + particleName); + return; + } + + final int count = plugin.getEffects().getInt(config + ".count", 1); + final double extra = plugin.getEffects().getDouble(config + ".extra", 0.0); + + final double offsetX = plugin.getEffects().getDouble(config + ".offset.x", 0.0); + final double offsetY = plugin.getEffects().getDouble(config + ".offset.y", 0.0); + final double offsetZ = plugin.getEffects().getDouble(config + ".offset.z", 0.0); + + final boolean selfOnly = plugin.getEffects().getBoolean("effect.particle.self-only", true); + final int receiverDistance = plugin.getEffects().getInt("effect.particle.receiver-distance", 24); + final boolean byDistance = plugin.getEffects().getBoolean("effect.particle.receiver-by-distance", true); + + final Location loc = location.clone().add(0, 1, 0); + + ParticleBuilder builder = new ParticleBuilder(particle) + .location(loc) + .count(count) + .extra(extra) + .offset(offsetX, offsetY, offsetZ); + + if(plugin.getEffects().contains(config + ".dust.color")) { + + final Color color = parseColor(plugin.getEffects().getString(config + ".dust.color")); + final float scale = (float) plugin.getEffects().getFloat(config + ".dust.scale", 1.0f); + + builder = builder.color(color, scale); + } + + if(plugin.getEffects().contains(config + ".dust-transition.from")) { + + final Color from = parseColor(plugin.getEffects().getString(config + ".dust-transition.from")); + final Color to = parseColor(plugin.getEffects().getString(config + ".dust-transition.to")); + final float scale = (float) plugin.getEffects().getFloat(config + ".dust-transition.scale", 1.0f); + + builder = builder.colorTransition(from, to, scale); + } + + if(plugin.getEffects().contains(config + ".trail")) { + + final Color color = parseColor(plugin.getEffects().getString(config + ".trail.color")); + final int duration = plugin.getEffects().getInt(config + ".trail.duration", 20); + + builder = builder.data(new Particle.Trail(loc, color, duration)); + } + + if(plugin.getEffects().contains(config + ".block.material")) { + + final Material mat = Material.matchMaterial(plugin.getEffects().getString(config + ".block.material")); + if(mat != null) { + + builder = builder.data(mat.createBlockData()); + } + } + + if(plugin.getEffects().contains(config + ".item.material")) { + + final Material mat = Material.matchMaterial(plugin.getEffects().getString(config + ".item.material")); + if(mat != null) { + + builder = builder.data(new ItemStack(mat)); + } + } + + if(selfOnly) { + + builder = builder.receivers(player); + } else { + + builder = builder.receivers(receiverDistance, byDistance); + } + + builder.spawn(); + } + + private static Color parseColor(String hex) { + if(hex == null) return Color.WHITE; + + hex = hex.replace("#", ""); + + try { + final int rgb = Integer.parseInt(hex, 16); + return Color.fromRGB(rgb); + } catch (final Exception e) { + return Color.WHITE; + } + } + public static boolean createShop(@NotNull final Player player, @Nullable final Block block, @NotNull final BlockFace blockFace, @NotNull final EquipmentSlot hand, @NotNull final ItemStack item) { Log.debug("==== Entering Shop Creation ===="); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java index 689785afb3..1870aa62fd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java @@ -113,7 +113,7 @@ private String getInternalFormat(final double amount, @Nullable final String cur @NotNull public String format(final double n, @NotNull final Shop shop) { - return format(n, disableVaultFormat, shop.getLocation().getWorld(), shop); + return format(n, disableVaultFormat, shop.bukkitLocation().getWorld(), shop); } @NotNull diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java index c1447fb33f..da0bed8533 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java @@ -26,6 +26,7 @@ import com.ghostchu.simplereloadlib.Reloadable; import dev.dejvokep.boostedyaml.block.implementation.Section; import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import org.bukkit.NamespacedKey; @@ -315,6 +316,7 @@ private void add(final Set> out, try { //System.out.println("ModernCustomMatcher.resolveTypeKeyFromDataComponentTypeKeys: " + name); final DataComponentType.Valued dataType = (DataComponentType.Valued)Registry.DATA_COMPONENT_TYPE.get(NamespacedKey.minecraft(name.toLowerCase(Locale.ROOT))); + return dataType; } catch(final ClassCastException ex) { //System.out.println("ModernCustomMatcher.resolveTypeKeyFromDataComponentTypeKeys: " + name + " is not a valued type!"); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java index c3bf1d42b3..99003d877d 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java @@ -1,61 +1,49 @@ package com.ghostchu.quickshop.util.metric; -import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.util.logger.Log; -import com.ghostchu.quickshop.util.metric.collect.BuiltInCollects; -import org.bstats.bukkit.Metrics; -import org.bstats.charts.CustomChart; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Method; -import java.util.Locale; +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.bstats.BStats; +import com.ghostchu.quickshop.util.metric.faststats.FastStats; + +/** + * MetricManager + * + * @author creatorfromhell + * @since 6.3.0.0 + */ public class MetricManager { - private final QuickShop plugin; - private final Metrics metrics; - - public MetricManager(final QuickShop plugin) { + private final BStats bStats; + private final FastStats fastStats; + private final StatCollector collector; - this.plugin = plugin; - this.metrics = new Metrics(plugin.getJavaPlugin(), 14281); - initCollects(); - } - - public void registerChart(final MetricDataType dataType, final String moduleName, final String reason, final CustomChart chart) { - - if(chart == null) { - return; // ignore - } - plugin.getPrivacyController().privacyReview(dataType, moduleName.replace(" ", "_").replace("-", "_").toUpperCase(Locale.ROOT), reason, ()->this.metrics.addCustomChart(chart), ()->Log.debug("Blocked chart register: failed privacy reviewing.")); - } + public MetricManager() { - public void initCollects() { + this.collector = new StatCollector(QuickShop.getInstance()); - registerMetricCollector(new BuiltInCollects(plugin)); + this.bStats = new BStats(collector); + this.fastStats = new FastStats(collector); } - public void registerMetricCollector(@NotNull final Object object) { - - for(final Method method : object.getClass().getDeclaredMethods()) { - final MetricCollectEntry collectEntry = method.getAnnotation(MetricCollectEntry.class); - if(collectEntry == null) { - continue; - } - if(method.getReturnType() != CustomChart.class) { - plugin.logger().warn("Failed loading MetricCollectEntry [{}]: Illegal test returns", method.getName()); - continue; - } - try { - final Object result = method.invoke(object, (Object[])null); - if(result != null) { - registerChart(collectEntry.dataType(), collectEntry.moduleName(), collectEntry.description(), - (CustomChart)result); - Log.debug("Registered metrics collector: " + collectEntry.moduleName()); - } - } catch(Throwable th) { - plugin.logger().warn("Failed to register metrics chart", th); - } - } + public void initPlatforms() { + bStats.register(); + fastStats.register(); } -} +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java new file mode 100644 index 0000000000..5ea2081939 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java @@ -0,0 +1,118 @@ +package com.ghostchu.quickshop.util.metric; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.logger.Log; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * MetricPlatform + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface MetricPlatform { + + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + String projectID(); + + /** + * Registers this MetricPlatform implementation for tracking or activation within the system. + * This method should be invoked to enable the platform's functionality and integrate it + * with the underlying metrics or analytics framework. + */ + void register(); + + /** + * Retrieves the validated class associated with this MetricPlatform. + * The validated class represents the type used for compliance or correctness + * validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + Class validatedClass(); + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform + * to the expectations of the underlying system or validation logic. + */ + void addMetric(final T metric); + + /** + * Registers a metric to be tracked or processed by the system, subject to a privacy review + * based on the provided data type, module name, and reason. + * + * @param dataType the data type of the metric, specifying its classification (e.g., STATISTIC, RESEARCH, DIAGNOSTIC) + * @param moduleName the name of the module associated with the metric, which will be reformatted internally + * for compatibility and privacy review purposes + * @param reason the reason or justification for collecting this metric, used for transparency and approval + * @param metric the metric of type {@code T} to be registered; if null, registration will be ignored + */ + default void registerMetric(final MetricDataType dataType, final String moduleName, final String reason, final T metric) { + + //Don't register if the metric is null + if(metric == null) { + return; + } + QuickShop.getInstance().getPrivacyController().privacyReview(dataType, moduleName.replace(" ", "_").replace("-", "_").toUpperCase(Locale.ROOT), reason, ()->addMetric(metric), ()->Log.debug("Blocked metric registration: failed privacy reviewing.")); + } + + /** + * Registers a metric collector by analyzing the provided object's methods for annotated + * metric collection entries. For each method annotated with {@link MetricCollectEntry}, the + * method's return value is used as the metric to register, provided it matches the expected + * validated class. Logs warnings for invalid configurations or failed registrations. + * + * @param object the target object to scan for methods annotated with {@link MetricCollectEntry}. + * Must not be {@code null}. + */ + default void registerMetricCollector(@NotNull final Object object) { + + for(final Method method : object.getClass().getDeclaredMethods()) { + final MetricCollectEntry collectEntry = method.getAnnotation(MetricCollectEntry.class); + if(collectEntry == null) { + continue; + } + if(!method.getReturnType().isAssignableFrom(validatedClass())) { + QuickShop.getInstance().logger().warn("Failed loading MetricCollectEntry [{}]: Illegal test returns", method.getName()); + continue; + } + try { + final Object result = method.invoke(object, (Object[])null); + if(result != null) { + registerMetric(collectEntry.dataType(), collectEntry.moduleName(), collectEntry.description(), validatedClass().cast(result)); + Log.debug("Registered metrics collector: " + collectEntry.moduleName()); + } + } catch(final Throwable th) { + QuickShop.getInstance().logger().warn("Failed to register metrics chart", th); + } + } + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java new file mode 100644 index 0000000000..eb2e8e6f72 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java @@ -0,0 +1,163 @@ +package com.ghostchu.quickshop.util.metric; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; + +/** + * StatCollector + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class StatCollector { + + private final QuickShop plugin; + + public StatCollector(final QuickShop plugin) { + this.plugin = plugin; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") + public Map researchAddonsCompats() { + final String myName = plugin.getJavaPlugin().getDescription().getName(); + final Map data = new HashMap<>(); + for(final Plugin discoverPlugin : Bukkit.getPluginManager().getPlugins()) { + final PluginDescriptionFile descriptionFile = discoverPlugin.getDescription(); + if(descriptionFile.getDepend().contains(myName) || descriptionFile.getSoftDepend().contains(myName)) { + data.put(descriptionFile.getName(), 1); + } + } + if(data.isEmpty()) { + data.put("None", 1); + } + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") + public boolean researchItemStackingShop() { + return plugin.isAllowStack(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") + public Map researchProtectionListenerBlacklist() { + final Map data = new HashMap<>(); + plugin.getConfig().getStringList("shop.protection-checking-listener-blacklist").forEach(s->data.put(s, 1)); + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") + public Map researchCommandAlias() { + final Map data = new HashMap<>(); + plugin.getConfig().getStringList("custom-commands").forEach(s->data.put(s, 1)); + + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") + public String statisticDatabaseType() { + return plugin.getDatabaseDriverType().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") + public String statisticEconomyType() { + return plugin.getEconomyManager().provider().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") + public String statisticItemMatcher() { + return plugin.getItemMatcher().getName(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public String statisticDisplayImpl() { + return AbstractDisplayItem.getNowUsing().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") + public int statisticAllShops() { + return plugin.getShopManager().getAllShops().size(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public int statisticAllTags() { + return plugin.tagManager().totalTags(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public String statisticBackgroundDebugLogger() { + return plugin.getConfig().getBoolean("debug.disable-debuglogger") ? "Disabled" : "Enabled"; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") + public String researchProtocolLibVersion() { + final Plugin protocolLib = Bukkit.getPluginManager().getPlugin("ProtocolLib"); + if(protocolLib == null) { + return "Not Installed"; + } + return protocolLib.getDescription().getVersion(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") + public String researchPacketEventsVersion() { + final Plugin packetevents = Bukkit.getPluginManager().getPlugin("packetevents"); + if(packetevents == null) { + return "Not Installed"; + } + return packetevents.getDescription().getVersion(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") + public Map> statisticServerSoftwareBuildVersion() { + final Map> map = new HashMap<>(); + final Map entry = new HashMap<>(); + entry.put(Bukkit.getServer().getVersion(), 1); + map.put(Bukkit.getServer().getName(), entry); + return map; + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") + public String statisticPublisher() { + return plugin.getBuildInfo().getGitInfo().getCommitUsername() + "@" + plugin.getFork(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") + public String researchGeyser() { + + final StringJoiner joiner = new StringJoiner("+"); + joiner.setEmptyValue("Not detected"); + final Plugin geyser = Bukkit.getPluginManager().getPlugin("Geyser-Spigot"); + if(geyser != null) { + joiner.add("Geyser-Spigot"); + } + final Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); + if(floodgate != null) { + joiner.add("Floodgate"); + } + return joiner.toString(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java new file mode 100644 index 0000000000..9932081bfe --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java @@ -0,0 +1,90 @@ +package com.ghostchu.quickshop.util.metric.bstats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.MetricPlatform; +import com.ghostchu.quickshop.util.metric.StatCollector; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.CustomChart; + +/** + * BStats + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class BStats implements MetricPlatform { + + private final Metrics metrics; + private final StatCollector collector; + + public BStats(final StatCollector collector) { + + this.collector = collector; + this.metrics = new Metrics(QuickShop.getInstance().getJavaPlugin(), Integer.parseInt(projectID())); + } + + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + @Override + public String projectID() { + + return "14281"; + } + + /** + * Registers this MetricPlatform implementation for tracking or activation within the system. This + * method should be invoked to enable the platform's functionality and integrate it with the + * underlying metrics or analytics framework. + */ + @Override + public void register() { + + registerMetricCollector(new BStatsCollector(collector)); + } + + /** + * Retrieves the validated class associated with this MetricPlatform. The validated class + * represents the type used for compliance or correctness validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + @Override + public Class validatedClass() { + + return CustomChart.class; + } + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform to + * the expectations of the underlying system or validation logic. + */ + @Override + public void addMetric(final CustomChart metric) { + + metrics.addCustomChart(metric); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java new file mode 100644 index 0000000000..da8fb1e094 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java @@ -0,0 +1,115 @@ +package com.ghostchu.quickshop.util.metric.bstats; + +import com.ghostchu.quickshop.util.metric.MetricCollectEntry; +import com.ghostchu.quickshop.util.metric.MetricDataType; +import com.ghostchu.quickshop.util.metric.StatCollector; +import org.bstats.charts.AdvancedPie; +import org.bstats.charts.CustomChart; +import org.bstats.charts.DrilldownPie; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; + +public class BStatsCollector {//Statistic + + private final StatCollector collector; + + public BStatsCollector(final StatCollector collector) { + this.collector = collector; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") + public CustomChart researchAddonsCompacts() { + + return new AdvancedPie("research_addons_or_compacts_discovered", collector::researchAddonsCompats); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") + public CustomChart researchItemStackingShop() { + + return new SimplePie("research_item_stacking_shop", ()->String.valueOf(collector.researchItemStackingShop())); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") + public CustomChart researchProtectionListenerBlacklist() { + + return new AdvancedPie("research_protection_checker_blacklist", collector::researchProtectionListenerBlacklist); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") + public CustomChart researchCommandAlias() { + + return new AdvancedPie("research_command_alias", collector::researchCommandAlias); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") + public CustomChart statisticDatabaseTypes() { + + return new SimplePie("statistic_database_types", collector::statisticDatabaseType); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") + public CustomChart statisticEconomyTypes() { + + return new SimplePie("statistic_economy_types", collector::statisticEconomyType); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") + public CustomChart statisticItemMatcher() { + + return new SimplePie("statistic_item_matcher", collector::statisticItemMatcher); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public CustomChart statisticDisplayImpl() { + + return new SimplePie("statistic_displayimpl", collector::statisticDisplayImpl); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") + public CustomChart statisticAllShops() { + + return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", collector::statisticAllShops); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public CustomChart statisticAllTags() { + + return new SingleLineChart("statistic_all_tags_hosting_across_all_servers", collector::statisticAllTags); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public CustomChart statisticBackgroundDebugLogger() { + + return new SimplePie("statistic_background_debug_logger", collector::statisticBackgroundDebugLogger); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") + public CustomChart researchProtocolLibVersion() { + + return new SimplePie("research_protocollib_version", collector::researchProtocolLibVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") + public CustomChart researchPacketEventsVersion() { + + return new SimplePie("research_packetevents_version", collector::researchPacketEventsVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") + public CustomChart statisticServerSoftwareBuildVersion() { + + return new DrilldownPie("statistic_server_software_build_version", collector::statisticServerSoftwareBuildVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") + public CustomChart statisticPublisher() { + + return new SimplePie("statistic_publisher", collector::statisticPublisher); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") + public CustomChart researchGeyser() { + + return new SimplePie("research_geyser", collector::researchGeyser); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java new file mode 100644 index 0000000000..a0cdfcffe9 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java @@ -0,0 +1,84 @@ +package com.ghostchu.quickshop.util.metric.faststats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.MetricPlatform; +import com.ghostchu.quickshop.util.metric.StatCollector; +import dev.faststats.bukkit.BukkitMetrics; +import dev.faststats.core.ErrorTracker; +import dev.faststats.core.data.Metric; + +/** + * FastStats + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class FastStats implements MetricPlatform {; + + private BukkitMetrics.Factory metrics = BukkitMetrics.factory().token(projectID()); + private final StatCollector collector; + + public FastStats(final StatCollector collector) { + + this.collector = collector; + } + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + @Override + public String projectID() { + + return "7883edc919d2a4fa1bec3d78e190d9a5"; + } + + @Override + public void register() { + + registerMetricCollector(new FastStatsCollector(collector)); + metrics.create(QuickShop.getInstance().getJavaPlugin()); + } + + /** + * Retrieves the validated class associated with this MetricPlatform. The validated class + * represents the type used for compliance or correctness validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + @Override + public Class validatedClass() { + + return Metric.class; + } + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform to + * the expectations of the underlying system or validation logic. + */ + @Override + public void addMetric(final Metric metric) { + metrics = metrics.addMetric(metric); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java similarity index 51% rename from quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java index 5933d830dd..d1d5a7e0b2 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java @@ -1,209 +1,140 @@ -package com.ghostchu.quickshop.util.metric.collect; +package com.ghostchu.quickshop.util.metric.faststats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ -import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; import com.ghostchu.quickshop.util.metric.MetricCollectEntry; import com.ghostchu.quickshop.util.metric.MetricDataType; +import com.ghostchu.quickshop.util.metric.StatCollector; import org.bstats.charts.AdvancedPie; import org.bstats.charts.CustomChart; import org.bstats.charts.DrilldownPie; import org.bstats.charts.SimplePie; import org.bstats.charts.SingleLineChart; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import java.util.HashMap; -import java.util.Map; -import java.util.StringJoiner; +/** + * FastStatsCollector + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class FastStatsCollector { -public class BuiltInCollects {//Statistic - private final QuickShop plugin; + private final StatCollector collector; - public BuiltInCollects(final QuickShop plugin) { - - this.plugin = plugin; + //TODO: Fully implement this class. + public FastStatsCollector(final StatCollector collector) { + this.collector = collector; } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") public CustomChart researchAddonsCompacts() { - return new AdvancedPie("research_addons_or_compacts_discovered", ()->{ - final String myName = plugin.getJavaPlugin().getDescription().getName(); - final Map data = new HashMap<>(); - for(final Plugin discoverPlugin : Bukkit.getPluginManager().getPlugins()) { - final PluginDescriptionFile descriptionFile = discoverPlugin.getDescription(); - if(descriptionFile.getDepend().contains(myName) || descriptionFile.getSoftDepend().contains(myName)) { - data.put(descriptionFile.getName(), 1); - } - } - if(data.isEmpty()) { - data.put("None", 1); - } - return data; - }); + return new AdvancedPie("research_addons_or_compacts_discovered", collector::researchAddonsCompats); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") public CustomChart researchItemStackingShop() { - return new SimplePie("research_item_stacking_shop", ()->String.valueOf(plugin.isAllowStack())); + return new SimplePie("research_item_stacking_shop", ()->String.valueOf(collector.researchItemStackingShop())); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") public CustomChart researchProtectionListenerBlacklist() { - return new AdvancedPie("research_protection_checker_blacklist", ()->{ - final Map data = new HashMap<>(); - plugin.getConfig().getStringList("shop.protection-checking-listener-blacklist").forEach(s->data.put(s, 1)); - return data; - }); + return new AdvancedPie("research_protection_checker_blacklist", collector::researchProtectionListenerBlacklist); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") public CustomChart researchCommandAlias() { - return new AdvancedPie("research_command_alias", ()->{ - final Map data = new HashMap<>(); - plugin.getConfig().getStringList("custom-commands").forEach(s->data.put(s, 1)); - return data; - }); + return new AdvancedPie("research_command_alias", collector::researchCommandAlias); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") public CustomChart statisticDatabaseTypes() { - return new SimplePie("statistic_database_types", ()->plugin.getDatabaseDriverType().name()); - } -// -// private String databaseProduct = null; -// @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Product", description = "We collect this so we can know the database vendor that users using, and provide dedicated driver if possible.") -// public CustomChart statisticDatabaseProject() { -// return new SimplePie("statistic_database_product", () -> { -// if(databaseProduct != null){ -// return databaseProduct; -// } -// CompletableFuture future = CompletableFuture.supplyAsync(()->{ -// try (Connection connection = plugin.getSqlManager().getConnection()) { -// DatabaseMetaData metaData = connection.getMetaData(); -// return metaData.getDatabaseProductName(); -// } catch (Throwable throwable) { -// Log.debug("Populate statistic database version failed: " + throwable.getClass().getName() + ": " + throwable.getMessage()); -// return "Error"; -// } -// }); -// databaseProduct = future.get(3, TimeUnit.SECONDS); -// return databaseProduct; -// }); -// } -// -// @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Version", description = "We collect this so we can know the database versions that users using, and provide/update dedicated driver or drop the support for too old database versions.") -// public CustomChart statisticDatabaseVersion() { -// return new SimplePie("statistic_database_version", () -> { -// try (Connection connection = plugin.getSqlManager().getConnection()) { -// DatabaseMetaData metaData = connection.getMetaData(); -// return metaData.getDatabaseProductName() + "@" + metaData.getDatabaseProductVersion(); -// } catch (Throwable throwable) { -// Log.debug("Populate statistic database version failed: " + throwable.getClass().getName() + ": " + throwable.getMessage()); -// return "Error"; -// } -// }); -// } + return new SimplePie("statistic_database_types", collector::statisticDatabaseType); + } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") public CustomChart statisticEconomyTypes() { - return new SimplePie("statistic_economy_types", ()->plugin.getEconomyManager().provider().name()); + return new SimplePie("statistic_economy_types", collector::statisticEconomyType); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") public CustomChart statisticItemMatcher() { - return new SimplePie("statistic_item_matcher", ()->plugin.getItemMatcher().getName()); + return new SimplePie("statistic_item_matcher", collector::statisticItemMatcher); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") public CustomChart statisticDisplayImpl() { - return new SimplePie("statistic_displayimpl", ()->AbstractDisplayItem.getNowUsing().name()); + return new SimplePie("statistic_displayimpl", collector::statisticDisplayImpl); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") public CustomChart statisticAllShops() { - return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", ()->plugin.getShopManager().getAllShops().size()); + return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", collector::statisticAllShops); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public CustomChart statisticAllTags() { + + return new SingleLineChart("statistic_all_tags_hosting_across_all_servers", collector::statisticAllTags); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") public CustomChart statisticBackgroundDebugLogger() { - return new SimplePie("statistic_background_debug_logger", ()->{ - if(plugin.getConfig().getBoolean("debug.disable-debuglogger")) { - return "Disabled"; - } else { - return "Enabled"; - } - }); + return new SimplePie("statistic_background_debug_logger", collector::statisticBackgroundDebugLogger); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") public CustomChart researchProtocolLibVersion() { - return new SimplePie("research_protocollib_version", ()->{ - final Plugin protocolLib = Bukkit.getPluginManager().getPlugin("ProtocolLib"); - if(protocolLib == null) { - return "Not Installed"; - } - return protocolLib.getDescription().getVersion(); - }); + return new SimplePie("research_protocollib_version", collector::researchProtocolLibVersion); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") public CustomChart researchPacketEventsVersion() { - return new SimplePie("research_packetevents_version", ()->{ - final Plugin packetevents = Bukkit.getPluginManager().getPlugin("packetevents"); - if(packetevents == null) { - - return "Not Installed"; - } - return packetevents.getDescription().getVersion(); - }); + return new SimplePie("research_packetevents_version", collector::researchPacketEventsVersion); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") public CustomChart statisticServerSoftwareBuildVersion() { - return new DrilldownPie("statistic_server_software_build_version", ()->{ - final Map> map = new HashMap<>(); - final Map entry = new HashMap<>(); - entry.put(Bukkit.getServer().getVersion(), 1); - map.put(Bukkit.getServer().getName(), entry); - return map; - }); + return new DrilldownPie("statistic_server_software_build_version", collector::statisticServerSoftwareBuildVersion); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") public CustomChart statisticPublisher() { - return new SimplePie("statistic_publisher", ()->plugin.getBuildInfo().getGitInfo().getCommitUsername() + "@" + plugin.getFork()); + return new SimplePie("statistic_publisher", collector::statisticPublisher); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") public CustomChart researchGeyser() { - return new SimplePie("research_geyser", ()->{ - final StringJoiner joiner = new StringJoiner("+"); - joiner.setEmptyValue("Not detected"); - final Plugin geyser = Bukkit.getPluginManager().getPlugin("Geyser-Spigot"); - if(geyser != null) { - joiner.add("Geyser-Spigot"); - } - final Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); - if(floodgate != null) { - joiner.add("Floodgate"); - } - return joiner.toString(); - }); - } -} + return new SimplePie("research_geyser", collector::researchGeyser); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/Pagination.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/Pagination.java new file mode 100644 index 0000000000..576eed9533 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/Pagination.java @@ -0,0 +1,119 @@ +package com.ghostchu.quickshop.util.pagination; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +/** + * Pagination + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public final class Pagination { + + private final PaginationOptions options; + private int page; + private int totalPages; + + public Pagination(final PaginationOptions options) { + this.options = options; + + this.totalPages = (options.entries().size() + options.maxPerPage() - 1) / options.maxPerPage(); + + if(options.currentPage() < 1) { + this.page = 1; + } else if(options.currentPage() > totalPages) { + this.page = totalPages; + } else { + this.page = options.currentPage(); + } + } + + public void print(final @Nullable CommandSender sender, final @Nullable Object[] headerArgs, final @Nullable Object[] footerArgs) { + + if(sender == null) return; + + if(headerArgs != null) { + + printHeader(sender, headerArgs); + } + + printEntries(sender); + + if(footerArgs != null) { + printFooter(sender, footerArgs); + } + } + + public void printHeader(final @Nullable CommandSender sender, @Nullable final Object... args) { + + QuickShop.getInstance().text().of(sender, options().headerLanguageKey(), args).send(); + } + + public void printFooter(final @Nullable CommandSender sender, @Nullable final Object... args) { + + QuickShop.getInstance().text().of(sender, options().footerLanguageKey(), args).send(); + } + + public void printEntries(final @Nullable CommandSender sender) { + + if(sender == null) return; + + final int start = (page - 1) * options.maxPerPage(); + final int end = Math.min(start + options.maxPerPage(), options.entries().size()); + for(int i = start; i < end; i++) { + + options.entryConsumer().accept(i + 1, options.entries().get(i)); + } + } + + public PaginationOptions options() { + + return options; + } + + public int page() { + + return page; + } + + public int totalPages() { + + return totalPages; + } + + public int previousPage() { + if(page <= 1) { + + return totalPages; + } + return page - 1; + } + + public int nextPage() { + if(page >= totalPages) { + + return 1; + } + return page + 1; + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/PaginationOptions.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/PaginationOptions.java new file mode 100644 index 0000000000..14b66c94f4 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/pagination/PaginationOptions.java @@ -0,0 +1,169 @@ +package com.ghostchu.quickshop.util.pagination; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.api.shop.trading.TradeOptions; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * PaginationOptions + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public final class PaginationOptions { + + private final List entries; + private final String headerLanguageKey; + private final String footerLanguageKey; + private final String command; + private final int maxPerPage; + private final int currentPage; + private final BiConsumer entryConsumer; + + private PaginationOptions(final Builder builder) { + + this.entries = builder.entries; + this.headerLanguageKey = builder.headerLanguageKey; + this.footerLanguageKey = builder.footerLanguageKey; + this.command = builder.command; + this.maxPerPage = builder.maxPerPage; + this.currentPage = builder.currentPage; + this.entryConsumer = builder.entryConsumer; + } + + public static Builder builder() { + + return new Builder(); + } + + public static Builder builder(final PaginationOptions copy) { + + return new Builder(copy); + } + + public List entries() { + + return entries; + } + + public String headerLanguageKey() { + + return headerLanguageKey; + } + + public String footerLanguageKey() { + + return footerLanguageKey; + } + + public String command() { + + return command; + } + + public int maxPerPage() { + + return maxPerPage; + } + + public int currentPage() { + + return currentPage; + } + + public BiConsumer entryConsumer() { + + return entryConsumer; + } + + public static final class Builder { + + private List entries; + private String headerLanguageKey; + private String footerLanguageKey; + private String command; + private int maxPerPage; + private int currentPage; + private BiConsumer entryConsumer; + + private Builder() { } + + public Builder(final PaginationOptions copy) { + + this.entries = copy.entries; + this.headerLanguageKey = copy.headerLanguageKey; + this.footerLanguageKey = copy.footerLanguageKey; + this.command = copy.command; + this.maxPerPage = copy.maxPerPage; + this.currentPage = copy.currentPage; + this.entryConsumer = copy.entryConsumer; + } + + public Builder setEntries(final List entries) { + + this.entries = entries; + return this; + } + + public Builder setHeaderLanguageKey(final String headerLanguageKey) { + + this.headerLanguageKey = headerLanguageKey; + return this; + } + + public Builder setFooterLanguageKey(final String footerLanguageKey) { + + this.footerLanguageKey = footerLanguageKey; + return this; + } + + public Builder setCommand(final String command) { + + this.command = command; + return this; + } + + public Builder setMaxPerPage(final int maxPerPage) { + + this.maxPerPage = maxPerPage; + return this; + } + + public Builder setCurrentPage(final int currentPage) { + + this.currentPage = currentPage; + return this; + } + + public Builder setEntryConsumer(final BiConsumer entryConsumer) { + + this.entryConsumer = entryConsumer; + return this; + } + + public PaginationOptions build() { + + return new PaginationOptions<>(this); + } + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java index c444d7b8d2..9ff3a6bf4f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java @@ -19,15 +19,15 @@ public ShopsInfoItem() { final QuickShop plugin = QuickShop.getInstance(); this.totalShops = String.valueOf(plugin.getShopManager().getAllShops().size()); plugin.getShopManager().getAllShops().stream() - .filter(shop->shop.getLocation().getWorld() != null) + .filter(shop->shop.bukkitLocation().getWorld() != null) .forEach(shop->{ - List worldShops = shopsMapping.get(shop.getLocation().getWorld().getName()); + List worldShops = shopsMapping.get(shop.bukkitLocation().getWorld().getName()); if(worldShops == null) { worldShops = new ArrayList<>(); } worldShops.add(shop); - shopsMapping.put(shop.getLocation().getWorld().getName(), worldShops); + shopsMapping.put(shop.bukkitLocation().getWorld().getName(), worldShops); }); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java index 5ddfcea839..f4734ef158 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java @@ -65,8 +65,8 @@ public void run() { if(shop.isDisableDisplay()) { continue; } - final Location location = shop.getLocation(); - final World world = shop.getLocation().getWorld(); //Cache this, because it will took some time. + final Location location = shop.bukkitLocation(); + final World world = shop.bukkitLocation().getWorld(); //Cache this, because it will took some time. final AbstractDisplayItem displayItem = ((ContainerShop)shop).getDisplayItem(); if(displayItem != null) { // Check the range has player? diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java index 18f3f4b389..05eb4ae8f7 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java @@ -50,7 +50,7 @@ public void run() { for(final Shop shop : plugin.getShopManager().getAllShops()) { if(!shop.isUnlimited() || !ignoreUnlimited) { final QUser shopOwner = shop.getOwner(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(!location.isWorldLoaded()) { //ignore unloaded world continue; @@ -121,7 +121,7 @@ public void stop() { */ public void removeShop(@NotNull final Shop shop) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); Util.regionThread(location, ()->plugin.getShopManager().deleteShop(shop)); MsgUtil.send(shop, shop.getOwner(), plugin.text().of("shop-removed-cause-ongoing-fee", LegacyComponentSerializer.legacySection().deserialize("World:" + Objects.requireNonNull(location.getWorld()).getName() diff --git a/quickshop-bukkit/src/main/resources/config.yml b/quickshop-bukkit/src/main/resources/config.yml index 0a523e5e30..40595136dc 100644 --- a/quickshop-bukkit/src/main/resources/config.yml +++ b/quickshop-bukkit/src/main/resources/config.yml @@ -81,12 +81,14 @@ shop-tax: # Disable taxes for unlimited shops. disable-for-unlimited-shop: false - # Determines who pays tax: - # player = The player interacting with the shop - # shop = The shop owner - # both = Both the player and the shop owner - # - # Permission checks are applied to whoever is being taxed. + + # Who to apply the tax to, be it the shop or the player interacting with the shop. + # The person that the tax is applied to will have the permission check ran on them to see if they + # should be taxed be it the player or the shop owner or both. + # player = Apply tax to the player who is interacting with the shop. + # shop = Apply tax to the shop owner. + # payee = Apply tax to the person receiving the money. + # both = Apply tax to both the player and the shop owner. apply-to: player # ----------------------------- @@ -150,12 +152,27 @@ shop-tax: # > 5,000,000 → 12% -1: 0.12 +# Configurations relating to the shop tags functionality +shop-tag: + + # Should the shop tags functionality be enabled? + enabled: true + + # Should the avoid system tag be enabled? + avoid-enabled: true + + # Should the favorite system tag be enabled? + favorite-enabled: true + + # Should the watch system tag be enabled? + watch-enabled: true + # When enabled, switching a shop to "unlimited" will migrate its owner # to a specified account. # # Note: # - This does not affect existing unlimited shops automatically. -# - You must toggle the shop to unlimited again to apply the change. +# - You must toggle the shop to unlimited again to apply the change.>>>>>>> hikari unlimited-shop-owner-change: false # Target account used by unlimited-shop-owner-change. diff --git a/quickshop-bukkit/src/main/resources/effects.yml b/quickshop-bukkit/src/main/resources/effects.yml new file mode 100644 index 0000000000..1915e9338f --- /dev/null +++ b/quickshop-bukkit/src/main/resources/effects.yml @@ -0,0 +1,466 @@ +# Internal configuration version. +# Do not change this unless QuickShop support instructs you to. +config-version: 1 + +# Special in-game effect +effect: + + sound: + + # Guide: + # - enabled: Enables all sounds in this section. + # - volume: Default volume for all sounds. + # - pitch: Default pitch for all sounds. + # - Each subsection (command, trade, etc.) can be enabled/disabled individually. + # - Each sound entry supports: + # sound: The Bukkit sound name. + # volume: Overrides default volume (optional). + # pitch: Overrides default pitch (optional). + + # Enable QuickShop sound effects. + enabled: true + + # Default volume for all sounds. + volume: 1.0 + + # Default pitch for all sounds. + pitch: 1.0 + + # Play a sound when the player uses tab completion for QuickShop commands. + ontabcomplete: true + + # Play a sound when the player executes a QuickShop command. + oncommand: true + + # Play a sound when the player clicks a shop sign, shop block, or container. + onclick: true + + # Interaction sounds + interaction: + + # Enable sounds for shop interaction and GUI usage. + enabled: true + + # Sound played when clicking a shop. + click: + enabled: true + sound: UI_BUTTON_CLICK + volume: 0.7 + pitch: 1.2 + + # Sound played when opening a GUI. + open: + enabled: true + sound: BLOCK_CHEST_OPEN + + # Sound played when closing a GUI. + close: + enabled: true + sound: BLOCK_CHEST_CLOSE + + # Sound played when changing pages. + page: + enabled: true + sound: ITEM_BOOK_PAGE_TURN + pitch: 1.1 + + # Sound played when modifying filters or sorting. + modify: + enabled: true + sound: BLOCK_LEVER_CLICK + pitch: 0.9 + + # Trade sounds + trade: + # Enable sounds for trading actions. + enabled: true + + # Successful buy. + buy: + enabled: true + sound: ENTITY_EXPERIENCE_ORB_PICKUP + pitch: 1.1 + + # Successful sell. + sell: + enabled: true + sound: ENTITY_VILLAGER_TRADE + + # Bulk trade success. + bulk: + enabled: true + sound: ENTITY_PLAYER_LEVELUP + pitch: 1.2 + + # Trade failed. + fail: + enabled: true + sound: ENTITY_VILLAGER_NO + pitch: 0.6 + + # Not enough money. + no-money: + enabled: true + sound: ENTITY_VILLAGER_NO + pitch: 0.5 + + # Inventory full. + inventory-full: + enabled: true + sound: BLOCK_NOTE_BLOCK_BASS + pitch: 0.5 + + # Out of stock. + out-of-stock: + enabled: true + sound: ENTITY_ITEM_BREAK + pitch: 0.9 + + # Shop management sounds + shop: + # Enable sounds for shop management. + enabled: true + + # Shop creation. + create: + enabled: true + sound: BLOCK_WOOD_PLACE + + # Shop removal. + remove: + enabled: true + sound: ITEM_SHIELD_BREAK + pitch: 0.8 + + # Price change. + price-change: + enabled: true + sound: BLOCK_NOTE_BLOCK_CHIME + pitch: 1.3 + + # Mode change. + mode-change: + enabled: true + sound: BLOCK_NOTE_BLOCK_BELL + pitch: 1.2 + + # Shop freeze/unfreeze toggle + freeze-toggle: + enabled: true + sound: BLOCK_AMETHYST_BLOCK_RESONATE + volume: 1.0 + pitch: 0.9 + + # Display change + toggle-display: + enabled: true + sound: BLOCK_AMETHYST_BLOCK_CHIME + volume: 1.0 + pitch: 1.2 + + # Open shop inventory. + inventory: + enabled: true + sound: BLOCK_CHEST_OPEN + + # Open history. + history: + enabled: true + sound: ITEM_BOOK_PAGE_TURN + pitch: 1.1 + + # Command-related sounds + commands: + # Enable command sounds + enabled: true + + # Command execution + execute: + enabled: true + sound: UI_BUTTON_CLICK + volume: 0.8 + + # Tab complete feedback + tab-complete: + enabled: true + sound: UI_BUTTON_CLICK + volume: 0.5 + pitch: 1.5 + + # Command success + success: + enabled: true + sound: ENTITY_EXPERIENCE_ORB_PICKUP + pitch: 1.3 + + # Command failure + failure: + enabled: true + sound: BLOCK_NOTE_BLOCK_BASS + pitch: 0.6 + + + # Particle related + particle: + # Guide: + # - enabled: Enables all particles in this section. + # - self-only: Show particles only to the player performing the action. + # - receiver-distance: Range for nearby players if self-only is false. + # - receiver-by-distance: Use spherical distance instead of cubic. + # - Each particle entry supports: + # particle: Bukkit particle name. + # count: Number of particles. + # offset: Spread (x, y, z). + # extra: Speed or additional data depending on particle. + # - Some particles support extra sections: + # dust: color + scale + # dust-transition: from/to colors + scale + # block: material + # item: material + + # Enable QuickShop particle effects. + enabled: true + + # Show particles only to the player performing the action. + self-only: true + + # Distance for sending particles to nearby players. + receiver-distance: 24 + + # Use spherical distance checks. + receiver-by-distance: true + + # Interaction particles + interaction: + # Enable particles for interaction and GUI usage. + enabled: true + + # Clicking a shop. + click: + particle: WAX_ON + count: 4 + offset: + x: 0.25 + y: 0.6 + z: 0.25 + + # Opening GUI. + open: + particle: ENCHANT + count: 8 + extra: 0.2 + offset: + x: 0.35 + y: 0.8 + z: 0.35 + + # Closing GUI. + close: + particle: POOF + count: 6 + extra: 0.02 + offset: + x: 0.25 + y: 0.8 + z: 0.25 + + # Changing pages. + page: + particle: DUST_COLOR_TRANSITION + count: 6 + offset: + x: 0.35 + y: 0.8 + z: 0.35 + dust-transition: + from: '#7EC8FF' + to: '#FFFFFF' + scale: 1.0 + + # Changing filters or sorting. + modify: + particle: COMPOSTER + count: 5 + offset: + x: 0.3 + y: 0.75 + z: 0.3 + + # Trade particles + trade: + # Enable particles for trading. + enabled: true + + # Successful buy. + buy: + particle: DUST + count: 10 + offset: + x: 0.4 + y: 0.9 + z: 0.4 + dust: + color: '#7CFF7C' + scale: 1.2 + + # Successful sell. + sell: + particle: DUST + count: 10 + offset: + x: 0.4 + y: 0.9 + z: 0.4 + dust: + color: '#FFD66B' + scale: 1.2 + + # Bulk trade. + bulk: + particle: FIREWORK + count: 14 + extra: 0.15 + offset: + x: 0.5 + y: 1.0 + z: 0.5 + + # Trade failed. + fail: + particle: SMOKE + count: 6 + extra: 0.03 + offset: + x: 0.25 + y: 0.8 + z: 0.25 + + # Partial trade. + partial: + particle: CLOUD + count: 7 + extra: 0.01 + offset: + x: 0.35 + y: 0.8 + z: 0.35 + + # Not enough money. + no-money: + particle: ANGRY_VILLAGER + count: 1 + offset: + x: 0.0 + y: 1.0 + z: 0.0 + + # Inventory full. + inventory-full: + particle: ITEM + count: 4 + offset: + x: 0.2 + y: 1.0 + z: 0.2 + item: + material: CHEST + + # Out of stock. + out-of-stock: + particle: BLOCK_CRUMBLE + count: 5 + offset: + x: 0.25 + y: 0.7 + z: 0.25 + block: + material: BARRIER + + # Shop particles + shop: + # Enable particles for shop management. + enabled: true + + # Shop creation. + create: + particle: BLOCK + count: 8 + offset: + x: 0.3 + y: 0.6 + z: 0.3 + block: + material: OAK_PLANKS + + # Shop removal. + remove: + particle: BLOCK_CRUMBLE + count: 10 + offset: + x: 0.35 + y: 0.8 + z: 0.35 + block: + material: OAK_SIGN + + # Price change. + price-change: + particle: DUST_COLOR_TRANSITION + count: 8 + offset: + x: 0.3 + y: 0.9 + z: 0.3 + dust-transition: + from: '#FFD54A' + to: '#7CFF7C' + scale: 1.0 + + # Mode change. + mode-change: + particle: TRAIL + count: 4 + offset: + x: 0.3 + y: 0.8 + z: 0.3 + trail: + color: '#7EC8FF' + duration: 20 + + # Shop freeze/unfreeze toggle + freeze-toggle: + particle: SNOWFLAKE + count: 20 + offset: + x: 0.4 + y: 0.6 + z: 0.4 + + # Display toggled + toggle-display: + particle: ENCHANT + count: 15 + offset: + x: 0.3 + y: 0.5 + z: 0.3 + + # History view. + history: + particle: ENCHANT + count: 8 + extra: 0.15 + offset: + x: 0.35 + y: 0.8 + z: 0.35 + + # Staff management. + staff: + particle: WITCH + count: 6 + offset: + x: 0.25 + y: 0.9 + z: 0.25 \ No newline at end of file diff --git a/quickshop-bukkit/src/main/resources/gui.yml b/quickshop-bukkit/src/main/resources/gui.yml index 9e67bfd5b7..a477ccd65e 100644 --- a/quickshop-bukkit/src/main/resources/gui.yml +++ b/quickshop-bukkit/src/main/resources/gui.yml @@ -20,7 +20,7 @@ # Common tags: , , , , , , # If you wish to use a value from the language file for display names or lore, start your value with "lang:" -version: 3 +version: 5 # =================== # GLOBAL STYLE SETTINGS @@ -115,7 +115,22 @@ keeper: # Shop item preview - top center shop-item: + lore: + - "lang:gui.keeper.freeze-toggle.lore" slot: 4 + + display-toggle: + + active: + material: GLOW_ITEM_FRAME + custom-model-data: 0 + inactive: + material: ITEM_FRAME + custom-model-data: 0 + name: "lang:gui.keeper.display-toggle.display" + lore: + - "lang:gui.keeper.display-toggle.lore" + slot: 18 # Change price button - GOLD_NUGGET for "price" # {0} = current price @@ -126,8 +141,21 @@ keeper: - "lang:gui.keeper.change-price.lore" custom-model-data: 0 slot: 19 - - # Mode toggle (buy/sell/frozen) - concrete for clean look + + freeze-toggle: + + freeze: + material: LIGHT_BLUE_CONCRETE + custom-model-data: 0 + unfreeze: + material: RED_CONCRETE + custom-model-data: 0 + name: "lang:gui.keeper.freeze-toggle.display" + lore: + - "lang:gui.keeper.freeze-toggle.lore" + slot: 20 + + # Mode toggle (buy/sell) - concrete for clean look # {0} = current mode, {1} = next mode mode-toggle: selling: @@ -136,9 +164,6 @@ keeper: buying: material: ORANGE_CONCRETE custom-model-data: 0 - frozen: - material: LIGHT_BLUE_CONCRETE - custom-model-data: 0 name: "lang:gui.keeper.mode-toggle.display" lore: - "lang:gui.keeper.mode-toggle.lore" diff --git a/quickshop-bukkit/src/main/resources/lang/messages.yml b/quickshop-bukkit/src/main/resources/lang/messages.yml index b8186c22f9..f534d2908d 100644 --- a/quickshop-bukkit/src/main/resources/lang/messages.yml +++ b/quickshop-bukkit/src/main/resources/lang/messages.yml @@ -29,8 +29,19 @@ tabcomplete: price: '[price]' name: '[name]' range: '[range]' + list: 'list' + page: '[page #]' + shop-id: '[shop id]' currency: '[currency name]' percentage: '[percentage%]' + add: 'add' + remove: 'remove' + clear: 'clear' + clearall: 'clearall' + shops: 'shops' + tagged: 'tagged' + purge: 'purge' + tag: '[tag]' taxaccount-unset: This shop's tax account is now following the global server setting. blacklisted-item: This item cannot be sold as it is on the blacklist @@ -133,6 +144,66 @@ shop-staff-empty: This shop have no staff members. shops-recovering: Recovering shops from backup... virtual-player-component-hover: 'This is a virtual player.\nRefers to a system account of this name with the same name.\nUUID: {0}\nUsername: {1}\nDisplay as: {2}' real-player-component-hover: 'This is a real exists player.\nUUID: {0}\nUsername: {1}\nDisplay as: {2}\nIf you wish to use a virtual system account with the same name, add "[]" to the both side of the username: [{1}].' +pagination: + #header: ───────────────────────────────────{0} - Page {1}/{2}─────────────────────────────────── + #header: "[ {0} ] Page {1}/{2}" + #footer: "Previous page\">[<] {1}/{2} Next page\">[>]" + #header: "{0} (Page {1}/{2}):" + header: "{0}:" + footer: "[Previous page\"><] {1}/{2} [Next page\">>]" +tags: + commands: + tag: "Manage your shop tags." + favorite: "Toggle favorite on looked-at shop." + watch: "Toggle watch on looked-at shop." + avoid: "Toggle avoid on looked-at shop." + general: + database-error: "An error occurred while executing a tag operation." + list-entry: "{0}. {1} [View all shops with this tag\">View Shops] [Remove this tag from this shop\">Remove]" + invalid: "Invalid tag. Only letters, dash (-), and underscore (_) are allowed. Max length: {0}" + avoid: + added: "Added this shop to your avoid list." + removed: "Removed this shop from your avoid list." + unable: "Unable to toggle avoid on this shop." + none: "You have no avoided shops." + label: "Avoid" + favorite: + added: "Added this shop to your favorite list." + removed: "Removed this shop from your favorite list." + unable: "Unable to toggle favorite on this shop." + none: "You have no favorite shops." + label: "Favorite" + watch: + added: "You are now watching this shop." + removed: "You are no longer watching this shop." + unable: "Unable to toggle watch on this shop." + none: "You are not watching any shops." + label: "Watching" + tag: + title-avoided-shops: "Avoided shops (Total {0})" + title-favorited-shops: "Favorited shops (Total {0})" + title-watched-shops: "Watched shops (Total {0})" + list-player-shops-title: "Your tagged shops ({0} shops, {1} tags)" + list-player-shop-entry: "Shop #{1} - [View all tags on this shop\">{2} tags]" + list-player-shop-title: "Shop tags (Total {0})" + list-tag-entry: "{0}. {1} [View all shops with this tag\">View Shops] [Remove this tag from this shop\">Remove]" + list-tag-title: "Tag usage ({0} shops)" + list-tag-shop-entry: "{0}. Shop #{1} - {2} - {3} {4} [Remove this tag from this shop\">Remove Tag]" + list-tag-shop-entry-tp: "[Teleport to this shop\">Teleport]" + added: "Added tag {0} to this shop." + duplicate: "This shop already has tag {0}." + failed-add: "An error occurred while adding tag {0} to this shop." + failed-remove: "An error occurred while removing tag {0} from this shop." + removed: "Removed tag {0} from this shop." + does-not-exist: "This shop does not have tag {0}." + cleared: "Cleared all tags from this shop." + cleared-all: "Cleared all tags from all shops." + cleared-tag: "Cleared tag {0} from all your tagged shops." + no-tags-all: "There are no tags on any shop." + no-tags: "This shop has no tags." + no-tagged-shops-player: "You have no tags on this shop." + no-tagged-shops: "You have no tagged shops." + no-tagged-shops-tag: "You have no shops with tag {0}." menu: sell-tax: You paid {0} in taxes. owner: 'Owner: {0}' @@ -165,9 +236,9 @@ currency-not-exists: Cannot find the currency that you want to set. Maybe t no-nearby-shop: No nearby shops matching {0}. translation-author: 'Ghost_chu, Andre_601' integrations-check-failed-trade: Integration {0} denied the Shop trading -shop-transaction-failed: Sorry, but an internal error occurred while processing +shop-transaction-failed: "Sorry, but an internal error occurred while processing your purchase. The purchase has been cancelled and any other operations have been - rolled back. Please contact the server administrators if this error persists. + rolled back. Reason: {0}" success-change-owner-to-server: Successfully set the shop owner to Server. shop-name-not-found: The shop named {0} does not exist. shop-name-too-long: This shop name is too long (max length {0}), please pick @@ -235,6 +306,14 @@ shop-type: selling: SELLING buying: BUYING frozen: FROZEN +shop-state: + frozen: FROZEN + unfrozen: UNFROZEN + freeze: Freeze + unfreeze: Unfreeze +shop-display: + enabled: Enabled + disabled: Disabled language: qa-issues: 'Quality assurance issues: {0}%' code: 'Code: {0}' @@ -879,18 +958,6 @@ benefit-added: Player {0} has been added into shop benefits! benefit-updated: Player {0}'s benefits has been updated! benefit-query: This shop have {0} players in benefits list! benefit-query-list: - Player {0}, Benefit {1}% -tag-added: Successfully added #{0} to this shop! -tag-add-duplicate: The tag #{0} already exists at this shop! -tag-removed: Successfully removed #{0} from this shop! -tag-remove-not-exists: The tag #{0} not exists at this shop! -tag-cleared: Successfully cleared all tags from this shop! -tag-shops-cleared: Successfully cleared #{0} from all your tagged - shops! -tag-query: 'This shop have {0} tags:' -tag-query-listing: - #{0} -tag-query-no-tag: This shop have no tags. -tag-query-shops: 'This tag contains {0} shops:' -tag-query-shops-listing: - {0} batch-operations-based-on-tags-no-failure: Successfully batch processed {0} shops. batch-operations-based-on-tags-have-failure: Total {0} shops in batch processing @@ -1816,6 +1883,16 @@ gui: - "Current price: {0}" - "" - "Click to change price" + display-toggle: + display: "Toggle Preview" + lore: + - "Toggle item preview in shops" + - "Status: {0}. Click to toggle." + freeze-toggle: + display: "Toggle Freeze" + lore: + - "Currently {0}" + - "Click to {1}" mode-toggle: display: "Change Mode" lore: diff --git a/quickshop-bukkit/src/main/resources/plugin.yml b/quickshop-bukkit/src/main/resources/plugin.yml index 6b83a76350..19874e59c1 100644 --- a/quickshop-bukkit/src/main/resources/plugin.yml +++ b/quickshop-bukkit/src/main/resources/plugin.yml @@ -87,7 +87,11 @@ permissions: description: The permission that provide all player need have permissions default: op children: - quickshop.use: true + quickshop.avoid: true + quickshop.avoid.list: true + quickshop.benefit: true + quickshop.browse: true + quickshop.browse.teleport: true quickshop.create.sell: true quickshop.create.buy: true quickshop.create.stacks: true @@ -96,30 +100,42 @@ permissions: quickshop.create.changeprice: true quickshop.create.double: true quickshop.create.cmd: true - quickshop.transferall: true - quickshop.transferownership: true - quickshop.find: true - quickshop.fetchmessage: true - quickshop.staff: true - quickshop.staffall: true - quickshop.preview: true quickshop.currency: true - quickshop.toggledisplay: true + quickshop.favorite: true + quickshop.favorite.list: true + quickshop.fetchmessage: true + quickshop.find: true + quickshop.history: true + quickshop.history.accessible: true + quickshop.history.owned: true quickshop.inventory: true - quickshop.shopnaming: true quickshop.permission: true - quickshop.benefit: true - quickshop.browse: true - quickshop.browse.teleport: true + quickshop.preview: true + quickshop.shopnaming: true quickshop.sign: true - quickshop.history: true - quickshop.history.owned: true - quickshop.history.accessible: true + quickshop.staff: true + quickshop.staffall: true quickshop.suggestprice: true + quickshop.tag: true + quickshop.tag.add: true + quickshop.tag.delete: true + quickshop.tag.list: true + quickshop.tag.purge: true + quickshop.tag.shops: true + quickshop.tag.tagged: true + quickshop.tag.tagged.teleport: true + quickshop.toggledisplay: true + quickshop.transferall: true + quickshop.transferownership: true + quickshop.use: true + quickshop.watch: true + quickshop.watch.list: true quickshop.moderator: description: The permission that provide all moderator need have permissions default: op children: + quickshop.tag.clear: true + quickshop.tag.clearall: true quickshop.setowner: true quickshop.other.destroy: true quickshop.other.open: true diff --git a/quickshop-common/pom.xml b/quickshop-common/pom.xml index 34f122ef30..04144af8bb 100644 --- a/quickshop-common/pom.xml +++ b/quickshop-common/pom.xml @@ -5,7 +5,7 @@ quickshop-hikari com.ghostchu - 6.3.0.0-SNAPSHOT-5 + 6.3.0.0-SNAPSHOT-6