diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index b165b5a4da..70a4d20d35 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -42,13 +42,13 @@ All comparisons are configured under matcher.components. Keys can be: #### Added * Modrinth update source support - * Uses Modrinth API for version checks - * Detects latest & latest stable release via SemVer + * Uses Modrinth API for version checks + * Detects latest & latest stable release via SemVer * New UpdateProvider interface for extensible update sources * New centralized UpdateManager - * 1-hour metadata cache - * Unified version comparison - * Paste output now shows the active provider + * 1-hour metadata cache + * Unified version comparison + * Paste output now shows the active provider #### Changed @@ -67,34 +67,30 @@ updater-source: "modrinth" This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) without core rewrites. -### New: Shop Tagging System +## New: Shop Tagging System A new player-driven tagging system has been introduced, allowing players to organize and track shops using custom tags. -**Highlights** +### Key Points - 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 + - @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. +### 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 shops - 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 ## Minor Changes - Replaced startup parameter "playerDBFindUUIDTask" with config option "uuid-lookup.allow-playerdb-uuid" @@ -136,6 +132,10 @@ A new player-driven tagging system has been introduced, allowing players to orga - 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. +- The per-player-shop-sign option no longer requires ProtocolLib. (Warrior) +- - Added PriceLimiter methods to allow external plugins and addons to get Results for price limits without running any logical validations. +- Refactored PriceLimiterCheckResult into the shop.limit package +- Implemented a TradeService interface to handle the logical side of shop trading, and allow easier connection between external plugins and addons. ## Deprecation Removals - Removed ShopType enum and methods @@ -148,4 +148,5 @@ A new player-driven tagging system has been introduced, allowing players to orga - 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) -- Remove blocking in AbstractShopManager#registerShop to fix deadlock on Folia. (Warrior) \ No newline at end of file +- Remove blocking in AbstractShopManager#registerShop to fix deadlock on Folia. (Warrior) +- Fix AbstractShopManager#getShops returning null while marked as not-null (Warrior) \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/QuickShopAPI.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/QuickShopAPI.java index 0d2eeea8a8..321d18a2aa 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/QuickShopAPI.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/QuickShopAPI.java @@ -12,6 +12,7 @@ import com.ghostchu.quickshop.api.shop.ShopControlPanelManager; import com.ghostchu.quickshop.api.shop.ShopItemBlackList; import com.ghostchu.quickshop.api.shop.ShopManager; +import com.ghostchu.quickshop.api.shop.ShopPermissionManager; import com.ghostchu.quickshop.api.shop.interaction.InteractionManager; import com.ghostchu.quickshop.api.shop.tag.TagManager; import com.vdurmont.semver4j.Semver; diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ModernShop.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ModernShop.java new file mode 100644 index 0000000000..e6113f6333 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ModernShop.java @@ -0,0 +1,145 @@ +package com.ghostchu.quickshop.api.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.api.database.bean.DataRecord; +import com.ghostchu.quickshop.api.shop.builder.ShopBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopInteraction; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import com.ghostchu.quickshop.api.shop.components.ShopLifecycle; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; +import com.ghostchu.quickshop.api.shop.components.ShopTrading; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.UUID; +import java.util.function.Consumer; + +/** + * ShopModern + * T = price object, such as BigDecimal or Double + * S = Location object such as Bukkit's Location object + * U = Preview object such as Bukkit's Inventory object + * V = Inventory object such as InventoryWrapper + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ModernShop extends Locatable { + + /** + * WARNING: This UUID will changed after plugin reload, shop reload or server restart DO NOT USE + * IT TO STORE DATA! + * + * @return Random UUID + */ + @NotNull + UUID getRuntimeRandomUniqueId(); + + ShopItem item(); + + ShopInteraction interaction(); + + ShopLifecycle lifecycle(); + + ShopMeta meta(); + + ShopPermission permission(); + + ShopPrice price(); + + ShopTrading trading(); + + /** + * Converts the current shop instance into a {@link ShopSignStorage}. + * + * This method provides a {@link ShopSignStorage} object that acts as a + * container for storing the shop's sign location data, such as world, x, y, and z coordinates. + * + * @return a {@link ShopSignStorage} instance that contains the location data of the shop's sign. + */ + ShopSignStorage asShopSignStorage(); + + /** + * Converts the current shop instance into a {@link DataRecord}. + * + * This method provides a {@link DataRecord} representation of the shop instance, + * encapsulating all relevant shop-related data such as item details, pricing, + * permissions, owner information, and additional metadata. + * + * @return a {@link DataRecord} instance representing the shop's data. + */ + @NotNull + DataRecord asDataRecord(); + + /** + * Getting ShopInfoStorage that you can use for storage the shop data + * + * @return ShopInfoStorage + */ + ShopInfoStorage asInfoStorage(); + + /** + * Gets the symbol link that created by InventoryWrapperManager + * + * @return InventoryWrapper + */ + @NotNull + String asSymbolLink(); + + /** + * Compares the current {@code ModernShop} instance with another provided instance and determines the set of + * differences between them. These differences are represented as a set of {@code ShopChangeType} values, + * where each type corresponds to a category of change (e.g., item, price, owner, etc.). + * + * @param compare The {@code ModernShop} instance to compare against. If {@code null}, the method assumes + * comparison with a non-existent or empty shop. + * @return An {@code EnumSet} of {@code ShopChangeType} values that represent the changes detected between + * the current shop instance and the provided shop. If no changes are detected, an empty set is returned. + */ + EnumSet diff(final @Nullable ModernShop compare); + + /** + * Applies a series of changes to the current shop instance using a {@link ShopBuilder}. + * The specified consumer allows modification of the {@code ShopBuilder}, + * after which the shop is rebuilt with the applied changes. + * + * NOTE: This does not apply the changes to the Shop cache or database, you'll need to utilize the + * {@link ShopService} to do that. + * + * @param changes a {@code Consumer} that defines the modifications to the {@link ShopBuilder}. + * @return a {@link ModernShop} instance with the applied modifications. + */ + default ModernShop withChanges(final Consumer> changes) { + + final ShopBuilder builder = builder(); + changes.accept(builder); + return builder.build(); + } + + /** + * Converts the current shop instance into a {@link ShopBuilder} for applying modifications or rebuilding. + * + * @return a {@link ShopBuilder} instance initialized with the current shop's state, enabling further modifications. + */ + ShopBuilder builder(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiter.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiter.java deleted file mode 100644 index 44ca11e3f6..0000000000 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.ghostchu.quickshop.api.shop; - -import com.ghostchu.quickshop.api.obj.QUser; -import org.bukkit.command.CommandSender; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Utility used for shop price validating - */ -public interface PriceLimiter { - - /** - * Check the price restriction rules - * - * @param sender the sender - * @param stack the item to check - * @param currency the currency - * @param price the price - * - * @return the result - */ - @NotNull - PriceLimiterCheckResult check(@NotNull CommandSender sender, @NotNull ItemStack stack, @Nullable String currency, double price); - - /** - * Check the price restriction rules - * - * @param user the user - * @param stack the item to check - * @param currency the currency - * @param price the price - * - * @return the result - */ - @NotNull - PriceLimiterCheckResult check(@NotNull QUser user, @NotNull ItemStack stack, @Nullable String currency, double price); -} diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Shop.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Shop.java index 975abbf83c..434eedb0c3 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Shop.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Shop.java @@ -1,35 +1,19 @@ package com.ghostchu.quickshop.api.shop; import com.ghostchu.quickshop.api.QuickShopAPI; -import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; -import com.ghostchu.quickshop.api.inventory.InventoryWrapper; -import com.ghostchu.quickshop.api.inventory.InventoryWrapperManager; -import com.ghostchu.quickshop.api.localization.text.ProxiedLocale; -import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.inventory.ShopInventory; -import com.ghostchu.quickshop.api.shop.meta.ShopDisplay; -import com.ghostchu.quickshop.api.shop.meta.ShopMeta; -import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; -import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; -import com.ghostchu.quickshop.api.shop.permission.ShopPermission; -import com.ghostchu.quickshop.api.shop.state.ShopState; -import com.ghostchu.quickshop.api.shop.trading.ShopTrading; -import net.kyori.adventure.text.Component; -import org.bukkit.Location; +import com.ghostchu.quickshop.api.shop.components.ShopDisplay; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.api.shop.components.ShopTrading; import org.bukkit.NamespacedKey; import org.bukkit.block.Block; -import org.bukkit.block.Sign; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Map; -import java.util.UUID; import java.util.concurrent.CompletableFuture; /** @@ -94,11 +78,6 @@ public interface Shop extends Locatable, ShopInventory, ShopMeta, Sh */ boolean isValid(); - /** - * Execute codes when player click the shop will did things - */ - void onClick(@NotNull Player clicker); - @Deprecated() @ApiStatus.Internal diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopBuilderFactory.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopBuilderFactory.java new file mode 100644 index 0000000000..bba4f9975f --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopBuilderFactory.java @@ -0,0 +1,76 @@ +package com.ghostchu.quickshop.api.shop; + +import com.ghostchu.quickshop.api.shop.builder.BuilderProvider; +import com.ghostchu.quickshop.api.shop.builder.ShopBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopItemBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopMetaBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopPermissionBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopPriceBuilder; + +public class ShopBuilderFactory { + + protected BuilderProvider> shopBuilder; + protected BuilderProvider shopItemBuilder; + protected BuilderProvider shopMetaBuilderBuilder; + protected BuilderProvider shopPermissionBuilderBuilder; + protected BuilderProvider> shopPriceBuilder; + + public ShopBuilderFactory(final BuilderProvider> shopBuilder, + final BuilderProvider shopItemBuilder, + final BuilderProvider shopMetaBuilderBuilder, + final BuilderProvider shopPermissionBuilderBuilder, + final BuilderProvider> shopPriceBuilder) { + + this.shopBuilder = shopBuilder; + this.shopItemBuilder = shopItemBuilder; + this.shopMetaBuilderBuilder = shopMetaBuilderBuilder; + this.shopPermissionBuilderBuilder = shopPermissionBuilderBuilder; + this.shopPriceBuilder = shopPriceBuilder; + } + + public ShopBuilder shopBuilder() { + return shopBuilder.builder(); + } + + public ShopItemBuilder shopItemBuilder() { + return shopItemBuilder.builder(); + } + + public ShopMetaBuilder shopMetaBuilder() { + return shopMetaBuilderBuilder.builder(); + } + + public ShopPermissionBuilder shopPermissionBuilder() { + return shopPermissionBuilderBuilder.builder(); + } + + public ShopPriceBuilder shopPriceBuilder() { + return shopPriceBuilder.builder(); + } + + public void setShopBuilder(final BuilderProvider> shopBuilder) { + + this.shopBuilder = shopBuilder; + } + + //setShopItemBuilder(SimpleShopItemBuilder::new) + public void setShopItemBuilder(final BuilderProvider shopItemBuilder) { + + this.shopItemBuilder = shopItemBuilder; + } + + public void setShopMetaBuilderBuilder(final BuilderProvider shopMetaBuilderBuilder) { + + this.shopMetaBuilderBuilder = shopMetaBuilderBuilder; + } + + public void setShopPermissionBuilderBuilder(final BuilderProvider shopPermissionBuilderBuilder) { + + this.shopPermissionBuilderBuilder = shopPermissionBuilderBuilder; + } + + public void setShopPriceBuilder(final BuilderProvider> shopPriceBuilder) { + + this.shopPriceBuilder = shopPriceBuilder; + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopInfoStorage.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopInfoStorage.java index 44c617de20..9c937e9480 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopInfoStorage.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopInfoStorage.java @@ -3,6 +3,7 @@ import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.serialize.BlockPos; import lombok.Data; +import org.jetbrains.annotations.NotNull; import java.util.Map; import java.util.UUID; @@ -28,7 +29,11 @@ public class ShopInfoStorage { private final String symbolLink; private final Map permission; - public ShopInfoStorage(final String world, final BlockPos position, final QUser owner, final double price, final String item, final int unlimited, final int shopType, final String extra, final String currency, final boolean disableDisplay, final QUser taxAccount, final String inventoryWrapperName, final String symbolLink, final Map permission) { + public ShopInfoStorage(final String world, final BlockPos position, final QUser owner, + final double price, final String item, final int unlimited, + final int shopType, final String extra, final String currency, + final boolean disableDisplay, final QUser taxAccount, final String inventoryWrapperName, + final String symbolLink, final Map permission) { this.world = world; this.position = position; @@ -49,4 +54,15 @@ public ShopInfoStorage(final String world, final BlockPos position, final QUser this.symbolLink = symbolLink; this.permission = permission; } + + public static ShopInfoStorage fromShop(@NotNull final ModernShop shop) { + + return new ShopInfoStorage(shop.bukkitLocation().getWorld().getName(), + new BlockPos(shop.bukkitLocation()), shop.meta().getOwner(), this.price, + QuickShop.getInstance().platform().encodeStack(this.originalItem), + isUnlimited()? 1 : 0, shopType().id(), + saveExtraToYaml(), this.currency, this.disableDisplay, + this.taxAccount, inventoryWrapperProvider, + saveToSymbolLink(), this.playerGroup); + } } diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopManager.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopManager.java index 9b8128cf27..a577d0c528 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopManager.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopManager.java @@ -4,6 +4,7 @@ import com.ghostchu.quickshop.api.inventory.InventoryWrapper; import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; import com.ghostchu.quickshop.api.shop.state.ShopState; import com.ghostchu.quickshop.api.shop.tax.TaxManager; import com.ghostchu.quickshop.api.shop.trading.TradeService; @@ -48,6 +49,14 @@ public interface ShopManager { */ TaxManager taxManager(); + /** + * Provides an instance of {@link ShopService}. + * + * @return a non-null instance of ShopService + */ + @NotNull + ShopService shopService(); + /** * Retrieves the TradeService associated with the EconomyManager. * @@ -57,6 +66,14 @@ public interface ShopManager { @NotNull TradeService tradeService(); + /** + * Getting the Shop Price Limiter + * + * @return The shop price limiter + */ + @NotNull + PriceLimiter getPriceLimiter(); + /** * Sets the shop layout provider to customize the layout of the shop. * @@ -279,7 +296,9 @@ boolean actionSelling( *

Make sure you have caching this, because this need a while to get all shops * * @return All shop in the database + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull List getAllShops(); @@ -287,7 +306,9 @@ boolean actionSelling( * Get all loaded shops. * * @return All loaded shops. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Set getLoadedShops(); @@ -299,7 +320,9 @@ boolean actionSelling( * @param playerUUID The player's uuid. * * @return The list have this player's all shops. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull List getAllShops(@NotNull QUser playerUUID); @@ -311,23 +334,19 @@ boolean actionSelling( * @param playerUUID The player's uuid. * * @return The list have this player's all shops. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull List getAllShops(@NotNull UUID playerUUID); - /** - * Getting the Shop Price Limiter - * - * @return The shop price limiter - */ - @NotNull - PriceLimiter getPriceLimiter(); - /** * Gets a shop by shop Id * * @return The shop object + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShop(long shopId); @@ -337,7 +356,9 @@ boolean actionSelling( * @param loc The location to get the shop from * * @return The shop at that location + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShop(@NotNull Location loc); @@ -348,7 +369,9 @@ boolean actionSelling( * @param loc The location to get the shop from * * @return The shop at that location but via cache + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShopViaCache(@NotNull Location loc); @@ -359,14 +382,30 @@ boolean actionSelling( * @param skipShopableChecking whether to check is shopable * * @return The shop at that location + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShop(@NotNull Location loc, boolean skipShopableChecking); - + /** + * + * @param runtimeRandomUniqueId + * @return + * @Deprecated Replaced with {@link ShopService} for shop operations. + */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShopFromRuntimeRandomUniqueId(@NotNull UUID runtimeRandomUniqueId); + /** + * + * @param runtimeRandomUniqueId + * @param includeInvalid + * @return + * @Deprecated Replaced with {@link ShopService} for shop operations. + */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShopFromRuntimeRandomUniqueId(@NotNull UUID runtimeRandomUniqueId, boolean includeInvalid); @@ -376,7 +415,9 @@ boolean actionSelling( * @param loc The location to get the shop from * * @return The shop at that location + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShopIncludeAttached(@Nullable Location loc); @@ -387,7 +428,9 @@ boolean actionSelling( * @param loc The location to get the shop from * * @return The shop at that location but via cache + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @Nullable Shop getShopIncludeAttachedViaCache(@Nullable Location loc); @@ -397,7 +440,9 @@ boolean actionSelling( * through a 3D map. * * @return a new shop iterator object. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Iterator getShopIterator(); @@ -405,7 +450,9 @@ boolean actionSelling( * Returns a map of World - Chunk - Shop * * @return a map of World - Chunk - Shop + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Map>> getShops(); @@ -415,7 +462,9 @@ boolean actionSelling( * @param c The chunk to search. Referencing doesn't matter, only coordinates and world are used. * * @return Shops + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Map getShops(@NotNull Chunk c); @@ -427,7 +476,9 @@ boolean actionSelling( * @param chunkZ The chunk z coordinate * * @return The shop at the world and specific chunk. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Map getShops(@NotNull String world, int chunkX, int chunkZ); @@ -437,7 +488,9 @@ boolean actionSelling( * @param shopChunk The shop chunk * * @return The shop at the world and specific chunk. + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Map getShops(@NotNull ShopChunk shopChunk); @@ -447,7 +500,9 @@ boolean actionSelling( * @param world The name of the world (case sensitive) to get the list of shops from * * @return a map of Chunk - Shop + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull Map> getShops(@NotNull String world); @@ -457,7 +512,9 @@ boolean actionSelling( * @param world The world you want get the shops. * * @return The list have this world all shops + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull List getShopsInWorld(@NotNull World world); @@ -467,7 +524,9 @@ boolean actionSelling( * @param worldName The world you want get the shops. * * @return The list have this world all shops + * @Deprecated Replaced with {@link ShopService} for shop operations. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) @NotNull List getShopsInWorld(@NotNull String worldName); @@ -505,6 +564,7 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * @param world The world the shop is in * @param shop The shop to load */ + @Deprecated(since = "6.3.0.0", forRemoval = true) void loadShop(@NotNull Shop shop); /** @@ -514,6 +574,7 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * @param world The world the shop is in * @param shop The shop to load */ + @Deprecated(since = "6.3.0.0", forRemoval = true) void unloadShop(@NotNull Shop shop); /** @@ -526,11 +587,13 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * QuickShop will try avoid any main-thread opreations to avoid * load-unload-load loop */ + @Deprecated(since = "6.3.0.0", forRemoval = true) void unloadShop(@NotNull Shop shop, boolean chunkUnloading); /** * Change the owner to unlimited shop owner. It defined in configuration. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) void migrateOwnerToUnlimitedShopOwner(Shop shop); /** @@ -540,6 +603,7 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * * @return True if the shop was register successfully. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) CompletableFuture registerShop(@NotNull Shop shop, boolean persist); /** @@ -549,6 +613,7 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * * @return True if the shop was unregister successfully. */ + @Deprecated(since = "6.3.0.0", forRemoval = true) CompletableFuture unregisterShop(@NotNull Shop shop, boolean persist); /** @@ -589,6 +654,7 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { * * @return If the shop is not valided for the player */ + @Deprecated(since = "6.3.0.0", forRemoval = true) boolean shopIsNotValid(@NotNull QUser uuid, @NotNull Info info, @NotNull Shop shop); /** @@ -600,25 +666,17 @@ default double getTax(@NotNull Shop shop, @NotNull QUser p) { ShopManager.InteractiveManager getInteractiveManager(); @NotNull + @Deprecated(since = "6.3.0.0", forRemoval = true) BlockState makeShopSign(@NotNull Block container, @NotNull Block signBlock, @Nullable Material signMaterial); @NotNull CompletableFuture<@NotNull List> queryTaggedShops(@NotNull UUID tagger, @NotNull String tag); - CompletableFuture<@Nullable Integer> clearShopTags(@NotNull UUID tagger, @NotNull Shop shop); - - CompletableFuture<@Nullable Integer> clearTagFromShops(@NotNull UUID tagger, @NotNull String tag); - - CompletableFuture<@Nullable Integer> removeTag(@NotNull UUID tagger, @NotNull Shop shop, @NotNull String tag); - - CompletableFuture<@Nullable Integer> tagShop(@NotNull UUID tagger, @NotNull Shop shop, @NotNull String tag); - - @NotNull - List listTags(@NotNull UUID tagger); - + @Deprecated(since = "6.2.0.11", forRemoval = true) void deleteShop(@NotNull Shop shop); @NotNull + @Deprecated(since = "6.3.0.0", forRemoval = true) CompletableFuture<@NotNull ShopInventoryCountCache> queryShopInventoryCacheInDatabase(@NotNull Shop shop); /** diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopService.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopService.java new file mode 100644 index 0000000000..7d1d2b208c --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopService.java @@ -0,0 +1,352 @@ +package com.ghostchu.quickshop.api.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.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; +import com.ghostchu.quickshop.api.shop.service.ShopActionResult; +import com.ghostchu.quickshop.api.shop.service.request.ShopCreateRequest; +import com.ghostchu.quickshop.api.shop.service.request.ShopDeleteRequest; +import com.ghostchu.quickshop.api.shop.service.request.ShopUpdateRequest; +import com.ghostchu.quickshop.api.shop.service.result.ShopCreateResult; +import com.ghostchu.quickshop.api.shop.service.result.ShopDeleteResult; +import com.ghostchu.quickshop.api.shop.service.result.ShopUpdateResult; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * ShopService + * + * T = Shop object + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopService> { + + /** + * Retrieves the ShopBuilderFactory instance associated with the ShopService implementation. + * + * @return ShopBuilderFactory instance responsible for providing builder objects for shop-related components, + * such as ShopItemBuilder, ShopMetaBuilder, ShopPermissionBuilder, and ShopPriceBuilder. + */ + ShopBuilderFactory shopBuilderFactory(); + + /** + * Creates a new shop based on the provided creation request. + * + * @param request The request object containing all necessary information for shop creation, + * including the actor initiating the operation and the shop details. + * @return A result object encapsulating details about the shop creation operation, + * including the success status, the created shop instance (if successful), + * and potential failure reasons if the operation failed. + */ + @NotNull + ShopActionResult createShop(@NotNull final ShopCreateRequest request); + + /** + * Updates an existing shop based on the provided update request. + * + * @param request The request object containing the necessary details to update a shop, + * including the actor performing the operation and the shop to be updated. + * @return A result object containing details about the update operation, including + * the success status, any changes made, and potential failure reasons if the + * operation was unsuccessful. + */ + @NotNull + ShopActionResult updateShop(@NotNull final ShopUpdateRequest request); + + /** + * Deletes a shop based on the provided delete request. + * + * @param request The request object containing the necessary details + * to delete a shop, such as the actor performing the operation + * and the target shop ID. + * @return A result object containing information about the outcome of + * the delete operation, including success status and possible + * failure reasons. + */ + @NotNull + ShopActionResult deleteShop(@NotNull final ShopDeleteRequest request); + + /** + * Returns all shops in the whole database, include unloaded. + * + *

Make sure you have caching this, because this need a while to get all shops + * + * @return All shop in the database + */ + @NotNull + List getAllShops(); + + /** + * Get all loaded shops. + * + * @return All loaded shops. + */ + @NotNull + Set getLoadedShops(); + + /** + * Get a players all shops. + * + *

Make sure you have caching this, because this need a while to get player's all shops + * + * @param playerUUID The player's uuid. + * + * @return The list have this player's all shops. + */ + @NotNull + List getAllShops(@NotNull QUser playerUUID); + + /** + * Get a players all shops. + * + *

Make sure you have caching this, because this need a while to get player's all shops + * + * @param playerUUID The player's uuid. + * + * @return The list have this player's all shops. + */ + @NotNull + List getAllShops(@NotNull UUID playerUUID); + + /** + * Gets a shop by shop Id + * + * @return The shop object + */ + @Nullable + T getShop(long shopId); + + /** + * Gets a shop in a specific location ATTENTION: This not include attached shops (double-chest) + * + * @param loc The location to get the shop from + * + * @return The shop at that location + */ + @Nullable + T getShop(@NotNull Location loc); + + /** + * Gets a shop in a specific location but via cache ATTENTION: This not include attached shops + * (double-chest) + * + * @param loc The location to get the shop from + * + * @return The shop at that location but via cache + */ + @Nullable + T getShopViaCache(@NotNull Location loc); + + /** + * Gets a shop in a specific location ATTENTION: This not include attached shops (double-chest) + * + * @param loc The location to get the shop from + * @param skipShopableChecking whether to check is shopable + * + * @return The shop at that location + */ + @Nullable + T getShop(@NotNull Location loc, boolean skipShopableChecking); + + + @Nullable + T getShopFromRuntimeRandomUniqueId(@NotNull UUID runtimeRandomUniqueId); + + @Nullable + T getShopFromRuntimeRandomUniqueId(@NotNull UUID runtimeRandomUniqueId, boolean includeInvalid); + + /** + * Gets a shop in a specific location Include the attached shop, e.g DoubleChest shop. + * + * @param loc The location to get the shop from + * + * @return The shop at that location + */ + @Nullable + T getShopIncludeAttached(@Nullable Location loc); + + /** + * Gets a shop in a specific location Include the attached shop, e.g DoubleChest shop. but via + * cache + * + * @param loc The location to get the shop from + * + * @return The shop at that location but via cache + */ + @Nullable + T getShopIncludeAttachedViaCache(@Nullable Location loc); + + + /** + * Returns a new shop iterator object, allowing iteration over shops easily, instead of sorting + * through a 3D map. + * + * @return a new shop iterator object. + */ + @NotNull + Iterator getShopIterator(); + + /** + * Returns a map of World - Chunk - Shop + * + * @return a map of World - Chunk - Shop + */ + @NotNull + Map>> getShops(); + + /** + * Returns a map of Shops + * + * @param c The chunk to search. Referencing doesn't matter, only coordinates and world are used. + * + * @return Shops + */ + @NotNull + Map getShops(@NotNull Chunk c); + + /** + * Gets the shop at the world and specific chunk. + * + * @param world The world to get the shop from + * @param chunkX The chunk x coordinate + * @param chunkZ The chunk z coordinate + * + * @return The shop at the world and specific chunk. + */ + @NotNull + Map getShops(@NotNull String world, int chunkX, int chunkZ); + + /** + * Gets the shop at the world and specific chunk. + * + * @param shopChunk The shop chunk + * + * @return The shop at the world and specific chunk. + */ + @NotNull + Map getShops(@NotNull ShopChunk shopChunk); + + /** + * Returns a map of Chunk - Shop + * + * @param world The name of the world (case sensitive) to get the list of shops from + * + * @return a map of Chunk - Shop + */ + @NotNull + Map> getShops(@NotNull String world); + + /** + * Get the all shops in the world. + * + * @param world The world you want get the shops. + * + * @return The list have this world all shops + */ + @NotNull + List getShopsInWorld(@NotNull World world); + + /** + * Get the all shops in the world. + * + * @param worldName The world you want get the shops. + * + * @return The list have this world all shops + */ + @NotNull + List getShopsInWorld(@NotNull String worldName); + + //Shop database operations. + /** + * Queries the database to retrieve the inventory cache for the specified shop. + * + * @param shop The shop instance for which the inventory cache is being queried. Must not be null. + * @return A CompletableFuture that completes with the ShopInventoryCountCache containing + * inventory information for the given shop. The result is guaranteed to be non-null. + */ + @NotNull + CompletableFuture<@NotNull ShopInventoryCountCache> queryShopInventoryCacheInDatabase(@NotNull T shop); + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + */ + void loadShop(@NotNull T shop); + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + */ + void unloadShop(@NotNull T shop); + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + * @param chunkUnloading If unloadShop called caused by chunk unloading, when this is true, + * QuickShop will try avoid any main-thread opreations to avoid + * load-unload-load loop + */ + void unloadShop(@NotNull T shop, boolean chunkUnloading); + + @Deprecated() + ShopActionResult handleLoading(); + + @Deprecated() + ShopActionResult handleUnloading(boolean dontTouchWorld); + + /** + * Registers a shop with the system and optionally persists the shop information. + * + * @param shop the shop object to be registered; must not be null + * @param persist a flag indicating whether the shop should be persisted to storage + * @return a CompletableFuture representing the asynchronous operation of registering the shop + */ + CompletableFuture registerShop(@NotNull T shop, boolean persist); + + /** + * Unregisters the specified shop from the system. If persistence is enabled, + * the removal will be reflected in the underlying storage to ensure the shop + * is no longer persisted. + * + * @param shop the shop instance to be unregistered; must not be null + * @param persist indicates whether the unregister operation should be + * persisted in the storage + * @return a CompletableFuture representing the asynchronous operation of + * unregistering the shop + */ + CompletableFuture unregisterShop(@NotNull T shop, boolean persist); +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopSignStorage.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopSignStorage.java similarity index 94% rename from quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopSignStorage.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopSignStorage.java index 5c9e7f6a59..d49bb73861 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopSignStorage.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopSignStorage.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.shop; +package com.ghostchu.quickshop.api.shop; import lombok.Builder; import lombok.Data; diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopWorldAdapter.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopWorldAdapter.java new file mode 100644 index 0000000000..0092add312 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopWorldAdapter.java @@ -0,0 +1,175 @@ +package com.ghostchu.quickshop.api.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.api.localization.text.ProxiedLocale; +import com.ghostchu.quickshop.api.obj.QUser; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Defines an adapter for interacting with the world context of a shop within the system. + * This interface provides methods for handling shop signs, validating shops, updating display + * locations, and managing localized sign text among other operations. + * NOTE: Any methods related to actions that need to be performed in the world itself, any methods in here + * need to happen on the main thread + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopWorldAdapter { + + /** + * Checks whether the specified shop instance is valid. + * This method determines if the given shop meets the necessary criteria + * for being considered a valid shop within the system. + * + * @param shop The shop instance to validate, represented by a {@code ModernShop}. + * @return {@code true} if the shop is valid, {@code false} otherwise. + * @since 6.3.0.0 + */ + boolean isValidShop(@NotNull final ModernShop shop); + + /** + * Check if shop is not valided for specific player + * + * @param uuid The uuid of the player + * @param info The info of the shop + * @param shop The shop + * + * @return If the shop is not valided for the player + */ + boolean shopIsNotValid(@NotNull QUser uuid, @NotNull Info info, @NotNull ModernShop shop); + + /** + * Determines whether the specified block is attached to the given shop instance. + * + * This method checks if the provided block is associated with the given shop + * based on certain conditions, which could involve physical proximity, structural + * linkage, or other criteria defined within the system. + * + * @param shop The shop instance to check against, represented by a {@code ModernShop}. + * Must not be null. + * @param b The block to verify as being attached. Must not be null. + * @return {@code true} if the block is attached to the specified shop, {@code false} otherwise. + * @since 6.3.0.0 + */ + boolean isAttached(@NotNull final ModernShop shop, @NotNull final Block b); + + /** + * Ensures that the display location for the specified shop is correctly handled. + * This method may involve verifying the shop's display location, teleporting entities, + * or respawning display objects as required to maintain consistency. + * + * @param shop The shop instance whose display location is to be checked and updated, + * represented by a {@code ModernShop}. + * @since 6.3.0.0 + */ + void checkDisplay(@NotNull final ModernShop shop); + + /** + * Claim a sign as shop sign (modern method) + * + * @param shop The shop instance that the sign belongs to, represented by a {@code ModernShop}. + * @param sign The shop sign + * @since 6.3.0.0 + */ + void claimShopSign(final @NotNull ModernShop shop, @NotNull Sign sign); + + @NotNull + BlockState makeShopSign(@NotNull Block container, @NotNull Block signBlock, @Nullable Material signMaterial); + + /** + * Retrieves the localized text to be displayed on a shop's sign. + * This method provides the sign text in the form of a list of {@link Component}, + * customized according to the specified shop instance and locale. + * + * @param shop The shop instance for which the sign text is to be retrieved, + * represented by a {@code ModernShop}. + * @param locale The locale to be used for generating the sign text, + * represented by a {@code ProxiedLocale}. + * @return A list of {@link Component} objects representing the text of the shop's sign, + * with each list entry corresponding to a line of text. + * @since 6.3.0.0 + */ + default List getSignText(@NotNull final ModernShop shop, + @NotNull final ProxiedLocale locale) { + //backward support + throw new UnsupportedOperationException(); + } + + /** + * Get shop signs, may have multi signs + * + * @return Signs for the shop + * @since 6.3.0.0 + */ + @NotNull + List getSigns(@NotNull final ModernShop shop); + + /** + * Checks if a Sign is a ShopSign + * + * @param shop The shop instance that the sign belongs to, represented by a {@code ModernShop}. + * @param sign Target {@link Sign} + * + * @return Is shop info sign + * @since 6.3.0.0 + */ + boolean isShopSign(@NotNull final ModernShop shop, @NotNull Sign sign); + + /** + * Generate new sign texts on shop's sign. + * @since 6.3.0.0 + */ + void setSignText(@NotNull final ModernShop shop); + + /** + * Sets the text displayed on a shop's sign. + * This method allows for updating the sign text of the specified shop + * using a list of {@link Component} objects where each entry represents a line of text. + * + * @param shop The shop instance whose sign text is to be updated, represented by a + * {@code ModernShop}. + * @param paramArrayOfString A list of {@link Component} objects representing the new sign text, + * with each list entry corresponding to a line of text. + * @since 6.3.0.0 + */ + void setSignText(@NotNull final ModernShop shop, @NotNull List paramArrayOfString); + + /** + * Updates the text displayed on a shop's sign with content localized to the given locale. + * This method dynamically adjusts the shop sign's text based on the specified shop instance + * and target locale for display purposes. + * + * @param shop The shop instance whose sign text is to be updated, represented by a + * {@code ModernShop}. + * @param locale The locale used to localize the text displayed on the shop's sign, + * represented by a {@code ProxiedLocale}. + * @since 6.3.0.0 + */ + void setSignText(@NotNull final ModernShop shop, @NotNull ProxiedLocale locale); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/BuilderProvider.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/BuilderProvider.java new file mode 100644 index 0000000000..9e4f29e877 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/BuilderProvider.java @@ -0,0 +1,31 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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 . + */ + + +/** + * BuilderProvider + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface BuilderProvider { + + T builder(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopBuilder.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopBuilder.java new file mode 100644 index 0000000000..815224f417 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopBuilder.java @@ -0,0 +1,197 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopInteraction; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import com.ghostchu.quickshop.api.shop.components.ShopLifecycle; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; + +import java.util.UUID; + +/** + * ShopBuilder + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopBuilder { + + /** + * Retrieves the location associated with the shop being built. + * + * @return the location of the shop, represented as an instance of type {@code T}. + */ + T location(); + + /** + * Sets the location for the shop being built. + * + * @param location the location to assign to the shop, represented by an instance of type {@code T}; + * must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder location(T location); + + /** + * Retrieves the symbolic link associated with the shop being built. + * The symbolic link can be used as an identifier or a reference to the shop in various contexts. + * + * @return a {@code String} representing the symbolic link for the shop. + */ + String symbolLink(); + + /** + * Associates a symbolic identifier with the shop being built. This symbolic identifier + * can be used for various purposes, such as referencing the shop in external systems + * or providing human-readable identifiers for debugging. + * + * @param symbolLink the symbolic identifier to associate with the shop; must not be null + * or empty + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder symbolLink(String symbolLink); + + /** + * Generates and retrieves a runtime-unique {@code UUID}. This identifier is intended + * to uniquely distinguish a specific shop instance during its lifecycle. + * + * @return a {@code UUID} representing a randomly generated runtime-unique identifier. + */ + UUID getRuntimeRandomUniqueId(); + + /** + * Sets the runtime UUID for the shop being built. The runtime UUID is used + * to uniquely identify the shop during its lifecycle. + * + * @param runTimeUUID the {@code UUID} to associate with the shop runtime; + * must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder runTimeUUID(UUID runTimeUUID); + + /** + * Retrieves the {@code ShopItem} instance currently associated with the {@code ShopBuilder}. + * + * @return the current {@code ShopItem} instance, representing the item configuration for the shop. + */ + ShopItem item(); + + /** + * Adds a {@code ShopItem} to the shop being built. + * + * @param item the {@code ShopItem} instance to add; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder item(ShopItem item); + + /** + * Retrieves the {@code ShopInteraction} instance associated with the shop being built. + * + * @return the {@code ShopInteraction} instance defining the interaction logic + * for the shop, including player interactions and preview handling. + */ + ShopInteraction interaction(); + + /** + * Sets the interaction configuration for the shop being built. + * + * @param interaction the {@code ShopInteraction} instance representing the interaction logic + * to be applied to the shop; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder interaction(ShopInteraction interaction); + + /** + * Retrieves the {@code ShopLifecycle} instance associated with the shop being built. + * + * @return the {@code ShopLifecycle} representing the lifecycle management of the shop. + */ + ShopLifecycle lifecycle(); + + /** + * Sets the lifecycle configuration for the shop being built. The lifecycle is used + * to manage the state and behavior of the shop throughout its existence, such as + * handling loading, saving, and dirty state tracking. + * + * @param lifecycle the {@code ShopLifecycle} instance that defines the lifecycle + * management for the shop; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder lifecycle(ShopLifecycle lifecycle); + + /** + * Retrieves the metadata associated with the shop being built. + * + * @return the {@code ShopMeta} instance that contains metadata information for the shop. + */ + ShopMeta meta(); + + /** + * Sets the metadata for the shop being built. + * + * @param meta the {@code ShopMeta} instance that contains metadata + * information for the shop; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder meta(ShopMeta meta); + + /** + * Retrieves the {@code ShopPermission} associated with the shop being built. + * + * @return the {@code ShopPermission} instance representing the permissions configuration of the shop. + */ + ShopPermission permission(); + + /** + * Sets the permission configuration for the shop being built. + * + * @param permission the {@code ShopPermission} instance representing the permissions + * to apply to the shop; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder permission(ShopPermission permission); + + /** + * Retrieves the {@code ShopPrice} instance associated with the shop being built. + * + * @return the {@code ShopPrice} of type {@code T}, representing the price configuration of the shop. + */ + ShopPrice price(); + + /** + * Sets the price for the shop using a {@link ShopPrice} instance. + * + * @param price the {@code ShopPrice} instance representing the price to assign to the shop; must not be null + * @return the current {@code ShopBuilder} instance for method chaining + */ + ShopBuilder price(ShopPrice price); + + /** + * Builds and returns a completed instance of {@link ModernShop}, using the + * properties and configurations set in this {@code ShopBuilder}. + * + * @return a fully constructed {@link ModernShop} instance with the applied configurations. + */ + ModernShop build(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopItemBuilder.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopItemBuilder.java new file mode 100644 index 0000000000..49096c5c8c --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopItemBuilder.java @@ -0,0 +1,72 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import org.bukkit.inventory.ItemStack; + +/** + * ShopItemBuilder + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopItemBuilder { + + /** + * Retrieves the {@code ItemStack} associated with the shop item being built. + * + * @return the {@code ItemStack} instance representing the item configuration for the shop. + */ + ItemStack item(); + + /** + * Sets the item to be associated with the shop. + * + * @param item the {@code ItemStack} representing the item to set; must not be null + * @return the current {@code ShopItemBuilder} instance for method chaining + */ + ShopItemBuilder item(ItemStack item); + + /** + * Checks whether the display of the shop item is disabled. + * + * @return true if the display is disabled, false otherwise. + */ + boolean isDisableDisplay(); + + /** + * Sets whether the display of the shop item should be disabled. + * + * @param disabled a boolean value indicating whether to disable the display; + * {@code true} to disable the display, {@code false} to enable it + * @return the current {@code ShopItemBuilder} instance for method chaining + */ + ShopItemBuilder disableDisplay(boolean disabled); + /** + * Builds and returns a {@code ShopItem} instance based on the specified shop. + * + * @param shop the {@code ModernShop} instance used to construct the {@code ShopItem}; + * must not be null + * @return a newly constructed {@code ShopItem} instance + */ + ShopItem build(ModernShop shop); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopMetaBuilder.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopMetaBuilder.java new file mode 100644 index 0000000000..2a9938123b --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopMetaBuilder.java @@ -0,0 +1,241 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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.economy.benefit.BenefitOverflowException; +import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; +import com.ghostchu.quickshop.api.economy.benefit.BenefitsAlreadyException; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.IShopType; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.function.Consumer; + +/** + * ShopMetaBuilder + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopMetaBuilder { + + /** + * Retrieves the unique identifier of the shop. + * This identifier is used to distinguish the shop within the system. + * + * @return the unique identifier of the shop as a long value. + */ + long shopId(); + + /** + * Sets the unique identifier for the shop. + * This identifier is used to distinguish the shop within the system. + * + * @param shopId the unique identifier for the shop. + * It should be a valid long value. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder shopId(final long shopId); + + /** + * Retrieves the name of the shop. + * The shop name represents the identity or title of the shop and can be null if no name is set. + * + * @return the name of the shop, or null if no name is set. + */ + @Nullable + String shopName(); + + /** + * Sets the name of the shop. + * The shop name represents the identity or title of the shop. + * + * @param shopName the name of the shop to set. This can be {@code null} if no name is provided. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder shopName(@Nullable final String shopName); + + /** + * Retrieves the current {@code ShopState} associated with the shop. + * The {@code ShopState} defines the behavior, accessibility, and + * overall status of the shop (e.g. active, frozen, or disabled). + * + * @return the {@code ShopState} instance representing the shop's current state. + */ + ShopState shopState(); + + /** + * Sets the state of the shop using the specified {@code ShopState} instance. + * The shop state determines the behavior, accessibility, and status of the shop. + * + * @param shopState the {@code ShopState} instance representing the state of the shop. + * Must not be null. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder shopState(@NotNull final ShopState shopState); + + /** + * Retrieves the type of shop being built or represented. + * + * @return an {@code IShopType} instance that defines the specific type of shop, + * such as a buying shop, selling shop, or other defined shop type. + */ + IShopType shopType(); + + /** + * Sets the shop type for the shop being built. The shop type determines the nature + * of the shop, such as whether it is a buying shop, selling shop, or other specific + * type defined by an {@code IShopType} instance. + * + * @param shopType the {@code IShopType} instance representing the type of shop. + * Must not be null. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder shopType(@NotNull final IShopType shopType); + + /** + * Retrieves the owner of the shop. The owner is typically the user + * responsible for managing or associated with the shop. + * + * @return the {@code QUser} instance representing the owner of the shop. + */ + QUser owner(); + + /** + * Sets the owner of the shop. The owner is typically the user associated with + * creating or managing the shop. + * + * @param owner the {@code QUser} instance representing the owner of the shop. + * Must not be null. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder owner(@NotNull final QUser owner); + + /** + * Retrieves the tax account associated with the shop. The tax account is typically + * a user or entity responsible for handling tax-related operations for the shop. + * + * @return the {@code QUser} instance representing the tax account, or {@code null} + * if no tax account is set. + */ + @Nullable + QUser taxAccount(); + + /** + * Sets the tax account for the shop. The tax account is typically associated with + * a user or entity used for managing shop-related tax operations. + * + * @param taxAccount the {@code QUser} instance to assign as the tax account for the shop. + * This parameter can be {@code null} if no tax account is to be set. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder taxAccount(@Nullable final QUser taxAccount); + + /** + * Determines if the shop is unlimited. An unlimited shop typically has no stock constraints + * and can be used as an indicator for special shop configurations. + * + * @return true if the shop has no stock limitations (unlimited), false otherwise. + */ + boolean isUnlimited(); + + /** + * Sets whether the shop is unlimited. An "unlimited" shop typically has no stock constraints. + * + * @param unlimited a boolean where true indicates the shop is unlimited, + * and false indicates the shop has normal stock limits. + * @return the current instance of ShopMetaBuilder for method chaining. + */ + ShopMetaBuilder isUnlimited(final boolean unlimited); + + /** + * Retrieves the BenefitProvider associated with the shop. + * + * @return The BenefitProvider instance that manages benefits for the shop. + */ + BenefitProvider benefit(); + + /** + * Configures a benefit for the given user with a specified percentage and a result consumer. + * + * @param user the {@code QUser} instance representing the user to whom the benefit will be applied. + * Must not be null. + * @param percent the percentage of the benefit to be applied. Can be null, but should be a valid + * {@code BigDecimal} value if provided. + * @param result a {@code Consumer} to process the outcome of the operation. + * Must not be null. + * @return the {@code ShopMetaBuilder} instance after applying the benefit. + */ + ShopMetaBuilder withBenefit(final @NotNull QUser user, final BigDecimal percent, @NotNull final Consumer result); + + /** + * Reduces the benefit associated with the specified user. + * + * @param user The {@code QUser} instance representing the user whose benefit should be reduced. + * Must not be null. + * @return A {@code ShopMetaBuilder} instance after the benefit has been reduced for the specified user. + */ + ShopMetaBuilder lessBenefit(final @NotNull QUser user); + + /** + * Sets the benefit provider for the shop. + * + * @param benefit The BenefitProvider to associate with the shop. Must not be null. + * @return The current instance of ShopMetaBuilder for method chaining. + */ + ShopMetaBuilder benefit(@NotNull final BenefitProvider benefit); + + /** + * Sets additional configuration for the shop using the provided {@code YamlConfiguration} instance. + * This method allows for extending the shop's metadata with extra custom-defined settings. + * + * @param extra the {@code YamlConfiguration} instance containing additional shop configuration. + * Must not be null. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder extra(final @NotNull YamlConfiguration extra); + + /** + * Configures an inventory wrapper provider for the shop. + * The inventory wrapper provider manages inventory-related operations + * within the shop system and must not be null. + * + * @param inventoryWrapperProvider the name or identifier of the + * inventory wrapper provider to associate with the shop. + * Must be a non-null string. + * @return the current instance of {@code ShopMetaBuilder} for method chaining. + */ + ShopMetaBuilder inventoryWrapperProvider(@NotNull final String inventoryWrapperProvider); + + /** + * Builds and returns a {@code ShopMeta} instance using the specified {@code ModernShop}. + * + * @param shop the {@code ModernShop} instance used to build a {@code ShopMeta}. + * Must be a valid {@code ModernShop} object. + * @return a {@code ShopMeta} instance representing the metadata of the provided {@code ModernShop}. + */ + ShopMeta build(ModernShop shop); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPermissionBuilder.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPermissionBuilder.java new file mode 100644 index 0000000000..c0a797bc2b --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPermissionBuilder.java @@ -0,0 +1,77 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; + +/** + * ShopPermissionBuilder + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopPermissionBuilder { + + /** + * Retrieves a map of permissions associated with a shop. + * The map uses {@code UUID} as the key to uniquely identify the entity or user, + * and {@code String} as the value to represent the associated permission level or configuration. + * + * @return a non-null map where keys are {@code UUID} values representing unique identifiers, + * and values are {@code String} values representing associated permissions. + */ + @NotNull + Map permissions(); + + /** + * Sets the permissions for the shop being built. + * + * @param permissions a map where each key is a {@code UUID} representing a unique identifier, + * and each value is a {@code String} representing the associated permission; + * must not be null + * @return the current {@code ShopPermissionBuilder} instance for method chaining + */ + ShopPermissionBuilder permissions(@NotNull Map permissions); + + /** + * Associates a specific permission group with the entity identified by the provided unique identifier (UUID). + * This method adds or updates a permission mapping within the builder's internal configuration. + * + * @param uuid the unique identifier (UUID) of the entity to associate the permission with; must not be null + * @param group the permission group name to assign to the specified entity; must not be null + * @return the current {@code ShopPermissionBuilder} instance for method chaining + */ + ShopPermissionBuilder permission(@NotNull UUID uuid, @NotNull String group); + + /** + * Builds and returns a {@link ShopPermission} instance for the specified shop. + * + * @param shop the {@link ModernShop} instance for which the permissions will be built; + * must not be null and defines the context for the resulting {@link ShopPermission}. + * @return a {@link ShopPermission} instance that represents the permissions associated + * with the specified {@link ModernShop}. + */ + ShopPermission build(ModernShop shop); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPriceBuilder.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPriceBuilder.java new file mode 100644 index 0000000000..6f32933d9f --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/builder/ShopPriceBuilder.java @@ -0,0 +1,77 @@ +package com.ghostchu.quickshop.api.shop.builder; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * ShopPriceBuilder + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopPriceBuilder { + + /** + * Retrieves the price configuration associated with the shop being built. + * + * @return the price configuration of type {@code T}. + */ + T price(); + + /** + * Assigns a price to the shop being built. + * + * @param price the price to assign to the shop; must not be null + * @return the current {@code ShopPriceBuilder} instance for method chaining + */ + ShopPriceBuilder price(@NotNull final T price); + + /** + * Retrieves the currency identifier associated with the shop price being built. + * The currency identifier represents the type of currency, such as USD, EUR, etc. + * + * @return the currency identifier as a String, or {@code null} if no specific currency is set. + */ + @Nullable + String currency(); + + /** + * Sets the currency for the shop price being built. + * + * @param currency the currency identifier to assign to the shop price; + * can be null to indicate no specific currency is set. + * @return the current {@code ShopPriceBuilder} instance for method chaining. + */ + ShopPriceBuilder currency(@Nullable final String currency); + + /** + * Builds and returns a new {@link ShopPrice} instance associated with the specified shop. + * + * @param shop the {@link ModernShop} instance for which the {@link ShopPrice} is being constructed; + * must not be null and should be a valid representation of a shop. + * @return a new {@link ShopPrice} instance containing the price configuration and currency + * settings as defined in the builder for the provided shop. + */ + ShopPrice build(ModernShop shop); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopInteraction.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopInteraction.java new file mode 100644 index 0000000000..417d39279d --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopInteraction.java @@ -0,0 +1,50 @@ +package com.ghostchu.quickshop.api.shop.components; + +/* + * 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.inventory.InventoryWrapper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * ShopInteraction + * T = player object + * U = preview object + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopInteraction { + + /** + * Handles the action triggered when a player interacts with an element in the shop. + * + * @param player the player object interacting with the shop + */ + void onClick(@NotNull final T player); + + U preview(); + + /** + * Opens a preview for the specified player. This is typically used to display + * shop items to the player in a preview interface. + * + * @param player the player object for whom the preview will be displayed; must not be null + */ + void openPreview(@NotNull final T player); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopItem.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopItem.java new file mode 100644 index 0000000000..6badf6b61f --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopItem.java @@ -0,0 +1,170 @@ +package com.ghostchu.quickshop.api.shop.components; + +/* + * 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.localization.text.ProxiedLocale; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopService; +import com.ghostchu.quickshop.api.shop.builder.ShopBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopItemBuilder; +import com.ghostchu.quickshop.api.shop.display.DisplayItem; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import net.kyori.adventure.text.Component; +import org.bukkit.block.Sign; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.List; +import java.util.function.Consumer; + +/** + * ShopDisplay + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopItem { + + /** + * Get shop item's ItemStack + * + * @return The shop's ItemStack + */ + @NotNull + ItemStack getItem(); + + /** + * Set shop item's ItemStack + * + * @param item ItemStack to set + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. + */ + @Deprecated(forRemoval = true, since = "6.3.0.0") + void setItem(@NotNull ItemStack item); + + /** + * Encodes and retrieves information related to the shop's item. + * + * @return a string representation of the encoded item data + */ + String encodedItem(); + + /** + * Checks whether the provided {@code ItemStack} matches the current shop item. + * + * @param item The {@code ItemStack} to compare against the shop's item. + * The parameter can be {@code null}. + * @return {@code true} if the provided {@code ItemStack} matches the shop's item, + * {@code false} otherwise. + */ + boolean matches(@Nullable final ItemStack item); + + /** + * Gets shop status is stacking shop + * + * @return The shop stacking status + */ + boolean isStackingShop(); + + /** + * Getting the item stacking amount of the shop. + * + * @return The item stacking amount of the shop. + */ + int getShopStackingAmount(); + + /** + * Getting if this shop has been disabled the display + * + * @return Does display has been disabled + */ + boolean isDisableDisplay(); + + /** + * Set the display disable state + * + * @param disabled Has been disabled + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. + */ + @Deprecated(forRemoval = true, since = "6.3.0.0") + void setDisableDisplay(boolean disabled); + + /** + * Get the display item + * + * @return The display item + */ + DisplayItem getDisplayItem(); + + /** + * Determines whether a custom item name should be used. + * + * @return true if a custom item name is enabled, false otherwise + */ + boolean useCustomItemName(); + + /** + * Customizes and returns a Component representing an item name. + * + * @return a Component representing the customized item name + */ + Component customItemName(); + + /** + * Computes and returns the differences between the current shop item and another specified shop item. + * The differences are identified and represented as a set of {@code ShopChangeType} enumerations. + * + * @param compare The shop item to compare against. The provided {@code ShopItem} may be null. + * If null, this method will evaluate differences assuming the absence of a comparison item. + * @return An {@code EnumSet} of {@code ShopChangeType} representing the differences between + * the two shop items. Returns an empty set if there are no differences. + */ + EnumSet diff(final @Nullable ShopItem compare); + + /** + * Applies a series of changes to a {@link ShopItem} using the provided {@link Consumer}, + * and rebuilds the item with the applied modifications. + * + * NOTE: This does not apply the changes to the Shop Object, Shop cache or database, + * you'll need to utilize the {@link ShopService} to do that. + * + * @param shop the {@link ModernShop} instance to associate with the modified {@link ShopItem}; + * must not be null + * @param changes a {@link Consumer} that defines the modifications to apply using a {@link ShopItemBuilder}; + * must not be null + * @return a newly constructed {@link ShopItem} instance with the applied changes + */ + default ShopItem withChanges(@NotNull final ModernShop shop, @NotNull final Consumer changes) { + + final ShopItemBuilder builder = builder(); + changes.accept(builder); + return builder.build(shop); + } + + /** + * Creates and returns a {@link ShopItemBuilder} instance to customize and build a {@link ShopItem}. + * + * @return a {@link ShopItemBuilder} to configure and construct a new {@link ShopItem}. + */ + ShopItemBuilder builder(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopLifecycle.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopLifecycle.java new file mode 100644 index 0000000000..31c6db5503 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopLifecycle.java @@ -0,0 +1,91 @@ +package com.ghostchu.quickshop.api.shop.components; + +/* + * 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 org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +/** + * ShopLifecycle + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopLifecycle { + + /** + * Gets if shop is dirty (so shop will be save) + * + * @return Is dirty + */ + boolean isDirty(); + + /** + * Sets dirty status + * + * @param isDirty Shop is dirty + */ + void setDirty(boolean isDirty); + + /** + * Sets shop is dirty + */ + void markDirty(); + + /** + * Get this container shop is loaded or unloaded. + * + * @return Loaded + */ + boolean isLoaded(); + + /** + * Checks whether the shop is marked as deleted. + * + * @return {@code true} if the shop is deleted, {@code false} otherwise + */ + boolean isDeleted(); + + @Deprecated() + @ApiStatus.Internal + void handleLoading(); + + @Deprecated() + @ApiStatus.Internal + void handleUnloading(boolean dontTouchWorld); + + /** + * Update shop data to database + */ + @NotNull + CompletableFuture update(); + + /** + * Updates the shop data to the database synchronously. This method blocks the thread + * until the update operation completes. + * IMPORTANT: Use this method only if you are certain of its implications, + * as it may cause performance issues or deadlocks when used incorrectly in asynchronous + * or multi-threaded environments. + * + * @throws RuntimeException if an error occurs during the update process + */ + void updateSync() throws RuntimeException; +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopMeta.java similarity index 50% rename from quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopMeta.java index 8dc506e776..29f16b7b65 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopMeta.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.api.shop.meta; +package com.ghostchu.quickshop.api.shop.components; /* * QuickShop-Hikari @@ -19,17 +19,27 @@ */ import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; +import com.ghostchu.quickshop.api.inventory.InventoryWrapper; import com.ghostchu.quickshop.api.localization.text.ProxiedLocale; import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.IShopType; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopService; +import com.ghostchu.quickshop.api.shop.builder.ShopItemBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopMetaBuilder; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; import com.ghostchu.quickshop.api.shop.state.ShopState; import net.kyori.adventure.text.Component; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.EnumSet; import java.util.UUID; +import java.util.function.Consumer; /** * ShopIdentity @@ -37,16 +47,7 @@ * @author creatorfromhell * @since 6.3.0.0 */ -public interface ShopMeta extends ShopPrice { - - /** - * WARNING: This UUID will changed after plugin reload, shop reload or server restart DO NOT USE - * IT TO STORE DATA! - * - * @return Random UUID - */ - @NotNull - UUID getRuntimeRandomUniqueId(); +public interface ShopMeta { /** * Gets the Shop ID to identify the shop. @@ -75,56 +76,11 @@ public interface ShopMeta extends ShopPrice { * Sets shop name * * @param shopName shop name, null to remove currently name - */ - void setShopName(@Nullable String shopName); - - /** - * Get shop item's ItemStack - * - * @return The shop's ItemStack - */ - @NotNull - ItemStack getItem(); - - /** - * Set shop item's ItemStack - * - * @param item ItemStack to set - */ - void setItem(@NotNull ItemStack item); - - /** - * Gets the currency that shop use - * - * @return The currency name - */ - @Nullable - String getCurrency(); - - /** - * Sets the currency that shop use * - * @param currency The currency name; null to use default currency - */ - void setCurrency(@Nullable String currency); - - /** - * Get shop's price - * - * @return Price - * @deprecated Use {@link ShopPrice#price()} instead + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ @Deprecated(forRemoval = true, since = "6.3.0.0") - double getPrice(); - - /** - * Set shop's new price - * - * @param paramDouble New price - * @deprecated Use {@link ShopPrice#price(Object)} instead. - */ - @Deprecated(forRemoval = true, since = "6.3.0.0") - void setPrice(double paramDouble); + void setShopName(@Nullable String shopName); /** * Retrieves the current state of the shop. @@ -137,7 +93,10 @@ public interface ShopMeta extends ShopPrice { * Updates the current state of the shop based on the provided {@code ShopState}. * * @param state the new state to set for the shop; must not be null + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void shopState(@NotNull ShopState state); /** @@ -145,7 +104,10 @@ public interface ShopMeta extends ShopPrice { * * @param shopStateIdentifier a non-null string representing the unique identifier * for the shop state to be updated or processed. + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void shopState(@NotNull String shopStateIdentifier); /** @@ -159,14 +121,20 @@ public interface ShopMeta extends ShopPrice { * Sets the type of shop using the provided shop type parameter. * * @param newShopType the shop type to set, must not be null + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void shopType(@NotNull IShopType newShopType); /** * Specifies the type of shop based on the given identifier. * * @param shopTypeIdentifier the identifier representing the type of shop. Must not be null. + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void shopType(@NotNull String shopTypeIdentifier); /** @@ -211,9 +179,12 @@ public interface ShopMeta extends ShopPrice { /** * Set new owner to the shop's owner * - * @param qUser New owner user + * @param owner New owner user + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ - void setOwner(@NotNull QUser qUser); + @Deprecated(forRemoval = true, since = "6.3.0.0") + void setOwner(@NotNull QUser owner); /** * Getting the shop tax account for using, it can be specific uuid or general tax account @@ -227,7 +198,10 @@ public interface ShopMeta extends ShopPrice { * Sets shop taxAccount * * @param taxAccount tax account, null to use general tax account + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void setTaxAccount(@Nullable QUser taxAccount); /** @@ -239,13 +213,6 @@ public interface ShopMeta extends ShopPrice { @Nullable QUser getTaxAccountActual(); - /** - * Gets shop status is stacking shop - * - * @return The shop stacking status - */ - boolean isStackingShop(); - /** * Get shop is or not in Unlimited Mode (Admin Shop) * @@ -254,11 +221,16 @@ public interface ShopMeta extends ShopPrice { boolean isUnlimited(); /** - * Set shop is or not Unlimited Mode (Admin Shop) + * Sets the shop's mode to unlimited or not. + * This determines whether the shop operates in "Admin Shop" mode, allowing + * unlimited transactions without inventory limitations. + * + * @param unlimited true to enable unlimited mode, false to disable it * - * @param paramBoolean status + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ - void setUnlimited(boolean paramBoolean); + @Deprecated(forRemoval = true, since = "6.3.0.0") + void setUnlimited(boolean unlimited); /** * Gets the benefit in this shop @@ -268,6 +240,88 @@ public interface ShopMeta extends ShopPrice { /** * Sets the benefit in this shop + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void setShopBenefit(@NotNull BenefitProvider benefit); + + /** + * Getting ConfigurationSection (extra data) instance of your plugin namespace) + * + * @param plugin The plugin and plugin name will used for namespace + * + * @return ExtraSection, save it through Shop#setExtra. If you don't save it, it may randomly lose + * or save + */ + @NotNull + ConfigurationSection getExtra(@NotNull Plugin plugin); + + /** + * Sets additional data for the shop associated with the specified plugin. + * + * @param plugin The plugin instance for which the extra data is being set. Must not be null. + * @param data The configuration section containing the extra data to be associated with the shop. + * Can be null if the extra data needs to be cleared. + */ + void setExtra(@NotNull Plugin plugin, @Nullable ConfigurationSection data); + + /** + * Returns the name of the inventory wrapper provider used in the application. + * + * @return A non-null string representing the inventory wrapper provider's name. + */ + @NotNull String getInventoryWrapperProvider(); + + /** + * Retrieves the current inventory encapsulated within an InventoryWrapper object. + * If no inventory is available, this method may return null. + * + * @return the InventoryWrapper containing inventory data, or null if no inventory is present. + */ + @Nullable InventoryWrapper getInventory(); + + /** + * Save the plugin extra data to Json format + * + * @return The json string + */ + @NotNull + String saveExtraToYaml(); + + /** + * Compares the current {@code ShopMeta} instance with another {@code ShopMeta} instance + * and determines the set of differences between them. + * + * @param compare the {@code ShopMeta} instance to compare with the current instance. + * If null, no comparison is performed, and an empty set is returned. + * @return an {@code EnumSet} of {@code ShopChangeType} representing the detected differences + * between the two {@code ShopMeta} instances. The set is empty if there are no differences. + */ + EnumSet diff(final @Nullable ShopMeta compare); + + /** + * Applies the specified changes to a {@link ShopMetaBuilder}, builds a new {@link ShopMeta} instance, + * and associates it with the provided {@link ModernShop}. + * + * NOTE: This does not apply the changes to the Shop Object, Shop cache or database, + * you'll need to utilize the {@link ShopService} to do that. + * + * @param shop the {@link ModernShop} instance associated with the {@link ShopMeta}; must not be null + * @param changes a {@link Consumer} that accepts a {@link ShopMetaBuilder} to apply changes; must not be null + * @return a new {@link ShopMeta} instance with the applied changes + */ + default ShopMeta withChanges(@NotNull final ModernShop shop, @NotNull final Consumer changes) { + + final ShopMetaBuilder builder = builder(); + changes.accept(builder); + return builder.build(shop); + } + + /** + * Creates and returns a {@link ShopMetaBuilder} instance to customize and build a {@link ShopMeta}. + * + * @return a {@link ShopMetaBuilder} to configure and construct a new {@link ShopMeta}. + */ + ShopMetaBuilder builder(); } \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/permission/ShopPermission.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPermission.java similarity index 53% rename from quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/permission/ShopPermission.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPermission.java index a80263cf1d..e6e00e27bc 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/permission/ShopPermission.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPermission.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.api.shop.permission; +package com.ghostchu.quickshop.api.shop.components; /* * QuickShop-Hikari @@ -18,13 +18,22 @@ * along with this program. If not, see . */ +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopService; +import com.ghostchu.quickshop.api.shop.builder.ShopMetaBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopPermissionBuilder; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Consumer; /** * ShopPermission @@ -106,7 +115,10 @@ public interface ShopPermission { * * @param player player * @param group namespaced group name + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void setPlayerGroup(@NotNull UUID player, @Nullable String group); /** @@ -114,6 +126,46 @@ public interface ShopPermission { * * @param player player * @param group group + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") void setPlayerGroup(@NotNull UUID player, @Nullable BuiltInShopPermissionGroup group); + + /** + * Computes the differences between the current shop permissions and the specified shop's permissions. + * The differences are represented as a set of {@code ShopChangeType} enumerations. + * + * @param compare The shop permissions to compare against. The provided {@code ShopPermission} may be null. + * If null, this method will evaluate differences assuming the absence of a comparison item. + * @return An {@code EnumSet} of {@code ShopChangeType} representing the differences between + * the two shop permissions. Returns an empty set if there are no differences. + */ + EnumSet diff(final @Nullable ShopPermission compare); + + /** + * Creates a new {@link ShopPermission} instance with modifications applied to its configuration. + * This method initializes a {@link ShopPermissionBuilder}, applies the given changes using the provided consumer, + * and then builds the resulting {@link ShopPermission}. + * + * NOTE: This does not apply the changes to the Shop Object, Shop cache or database, + * you'll need to utilize the {@link ShopService} to do that. + * + * @param shop the {@link ModernShop} instance for which the {@link ShopPermission} will be constructed; must not be null. + * @param changes a {@link Consumer} that defines the modifications to be applied to the {@link ShopPermissionBuilder}; must not be null. + * @return a new {@link ShopPermission} instance with the specified changes applied. + */ + default ShopPermission withChanges(@NotNull final ModernShop shop, @NotNull final Consumer changes) { + + final ShopPermissionBuilder builder = builder(); + changes.accept(builder); + return builder.build(shop); + } + + /** + * Creates and returns a {@link ShopPermissionBuilder} instance to customize and build a {@link ShopPermission}. + * + * @return a {@link ShopPermissionBuilder} to configure and construct a new {@link ShopPermission}. + */ + ShopPermissionBuilder builder(); } \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopPrice.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPrice.java similarity index 53% rename from quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopPrice.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPrice.java index 5cfdda5e70..7ef833f80b 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopPrice.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopPrice.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.api.shop.meta; +package com.ghostchu.quickshop.api.shop.components; /* * QuickShop-Hikari @@ -18,11 +18,17 @@ * along with this program. If not, see . */ +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopService; +import com.ghostchu.quickshop.api.shop.builder.ShopPermissionBuilder; +import com.ghostchu.quickshop.api.shop.builder.ShopPriceBuilder; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.math.BigDecimal; import java.util.Comparator; +import java.util.EnumSet; +import java.util.function.Consumer; /** * ShopPrice @@ -30,21 +36,49 @@ * @author creatorfromhell * @since 6.3.0.0 */ -public interface ShopPrice { +public interface ShopPrice { /** * Retrieves the price of the shop. * * @return the price of the shop as an instance of type U, where U represents a generic type. */ - U price(); + T 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 + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. + */ + @Deprecated(forRemoval = true, since = "6.3.0.0") + void price(T price); + + /** + * Gets the currency that shop use + * + * @return The currency name */ - void price(U price); + @Nullable + String getCurrency(); + + /** + * Sets the currency that shop use + * + * @param currency The currency name; null to use default currency + * + * @deprecated All setting operations should go through the {@link ShopService} going forward. + */ + @Deprecated(forRemoval = true, since = "6.3.0.0") + void setCurrency(@Nullable String currency); + + /** + * Check if this shop is free shop + * + * @return Free Shop + */ + boolean isFreeShop(); /** * Formats a string representation based on the provided world and optional currency. @@ -72,7 +106,7 @@ public interface ShopPrice { * * @return a {@link Comparator} for comparing values of type U */ - Comparator priceComparator(); + Comparator priceComparator(); /** * Compares the price of the current shop with the price of another shop. @@ -82,7 +116,7 @@ public interface ShopPrice { * @return a negative integer, zero, or a positive integer as the price of this shop * is less than, equal to, or greater than the price of the other shop */ - default int comparePrice(final U other, final boolean reversed) { + default int comparePrice(final T other, final boolean reversed) { if(reversed) { return priceComparator().reversed().compare(this.price(), other); @@ -105,4 +139,42 @@ default int comparePrice(final U other, final boolean reversed) { * @return true if the shop can afford the specified number of items, false otherwise */ boolean canAfford(final int itemAmount); + + /** + * Computes and returns the differences between the current shop and another specified shop based on their properties. + * The differences are represented as a set of {@code ShopChangeType} enumerations. + * + * @param compare The {@code ShopPrice} object to compare against. Can be null, which will indicate + * a comparison against the absence of a shop. + * @return An {@code EnumSet} of {@code ShopChangeType} representing the differences between the + * current shop and the provided shop. Returns an empty set if no differences are found. + */ + EnumSet diff(final @Nullable ShopPrice compare); + + /** + * Applies modifications to the current {@code ShopPrice} instance using a {@link ShopPriceBuilder}. + * The specified consumer is used to define the changes, and the resulting {@link ShopPrice} + * is constructed and returned after applying the modifications. + * + * NOTE: This does not apply the changes to the Shop Object, Shop cache or database, + * you'll need to utilize the {@link ShopService} to do that. + * + * @param shop the {@link ModernShop} instance associated with the {@code ShopPrice}; must not be null. + * @param changes a {@link Consumer} that defines the modifications to be applied to the {@link ShopPriceBuilder}; + * must not be null. + * @return a new {@link ShopPrice} instance with the applied changes. + */ + default ShopPrice withChanges(@NotNull final ModernShop shop, @NotNull final Consumer> changes) { + + final ShopPriceBuilder builder = builder(); + changes.accept(builder); + return builder.build(shop); + } + + /** + * Creates and returns a {@link ShopPriceBuilder} instance to customize and build a {@link ShopPrice}. + * + * @return a {@link ShopPriceBuilder} to configure and construct a new {@link ShopPrice}. + */ + ShopPriceBuilder builder(); } \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/ShopTrading.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopTrading.java similarity index 69% rename from quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/ShopTrading.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopTrading.java index 864a9837db..52b2fc987c 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/ShopTrading.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/components/ShopTrading.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.api.shop.trading; +package com.ghostchu.quickshop.api.shop.components; /* * QuickShop-Hikari @@ -20,6 +20,8 @@ import com.ghostchu.quickshop.api.inventory.InventoryWrapper; import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.trading.TradeResult; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import org.bukkit.Location; import org.jetbrains.annotations.NotNull; @@ -29,47 +31,50 @@ * @author creatorfromhell * @since 6.3.0.0 */ +@Deprecated(forRemoval = true, since = "6.3.0.0") public interface ShopTrading { /** * Get shop is or not in buying mode * * @return yes or no + * @deprecated Use the #shopType() method instead. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") boolean isBuying(); /** * Get shop is frozen or not * * @return yes or no + * @deprecated Use the #shopState() method instead. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") boolean isFrozen(); /** * Get shop is or not in selling mode * * @return yes or no + * @deprecated Use the #shopType() method instead. */ + @Deprecated(forRemoval = true, since = "6.3.0.0") boolean isSelling(); - /** - * Check if this shop is free shop - * - * @return Free Shop - */ - boolean isFreeShop(); - /** * Execute buy action for player with x items. * * @param buyer The player buying * @param buyerInventory The buyer inventory ( may not a player inventory ) * @param loc2Drop The location to drops items if player inventory are full - * @param paramInt How many buyed? + * @param amount The amount to buy * * @throws Exception Possible exception thrown if anything wrong. + * @deprecated Use the {@link TradeService} instead. */ - TradeResult buy(@NotNull QUser buyer, @NotNull InventoryWrapper buyerInventory, @NotNull Location loc2Drop, int paramInt); + @Deprecated(forRemoval = true, since = "6.3.0.0") + TradeResult buy(@NotNull QUser buyer, @NotNull InventoryWrapper buyerInventory, + @NotNull Location loc2Drop, int amount); /** * Execute sell action for player with x items. @@ -78,9 +83,12 @@ public interface ShopTrading { * @param sellerInventory Seller's inventory ( may not a player inventory ) * @param loc2Drop The location to be drop if buyer inventory full ( if player enter a * number that < 0, it will turn to buying item) - * @param paramInt How many sold? + * @param amount The amount to sell * * @throws Exception Possible exception thrown if anything wrong. + * @deprecated Use the {@link TradeService} instead. */ - TradeResult sell(@NotNull QUser seller, @NotNull InventoryWrapper sellerInventory, @NotNull Location loc2Drop, int paramInt); + @Deprecated(forRemoval = true, since = "6.3.0.0") + TradeResult sell(@NotNull QUser seller, @NotNull InventoryWrapper sellerInventory, + @NotNull Location loc2Drop, int amount); } \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/display/DisplayItem.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/display/DisplayItem.java new file mode 100644 index 0000000000..0ff9979db1 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/display/DisplayItem.java @@ -0,0 +1,195 @@ +package com.ghostchu.quickshop.api.shop.display; + +/* + * 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.ModernShop; +import com.ghostchu.simplereloadlib.Reloadable; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * DisplayItem + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface DisplayItem { + + /** + * Gets the display location for an item. If it is a double shop and it is not the left shop, it + * will average the locations of the two chests comprising it to be perfectly in the middle. If it + * is the left shop, it will return null since the left shop does not spawn an item. Otherwise, it + * will give you the middle of the single chest. + * + * @return The Location that the item *should* be displaying at. + */ + @Nullable Location getDisplayLocation(); + + /** + * Checks whether the display item has been moved from its intended location. + * + * @return true if the display item has been moved, false otherwise + */ + boolean checkDisplayIsMoved(); + + /** + * Checks whether the display item needs to regenerate its state or visual representation. + * + * @return true if the display item requires regeneration, false otherwise + */ + boolean checkDisplayNeedRegen(); + + /** + * Checks whether the given entity is associated with a shop. + * + * @param entity the entity to check + * @return true if the entity is part of a shop, false otherwise + */ + boolean checkIsShopEntity(Entity entity); + + /** + * Retrieves the display entity associated with this shop display. + * + * @return The entity that represents the visual display of the shop, or null if no display entity is present. + */ + Entity getDisplay(); + + /** + * Restores the display entity to its intended location if it has been moved. + * + * This method is responsible for repositioning the shop's display entity + * back to its designated location if it has been detected as being moved + * from its intended position. The method ensures that the visual representation + * of the shop remains consistent and properly aligned with the shop's mechanics. + * + * It may internally utilize location data, such as the result of {@code getDisplayLocation()}, + * for determining the correct position and validating the current position. + */ + void fixDisplayMoved(); + + /** + * Ensures the display entity's state or visual representation is regenerated when needed. + * + * This method is responsible for handling cases where the display entity associated with a shop + * needs to be recreated or visually refreshed. Regeneration may involve updating the display's + * appearance, state, or position to ensure it aligns with the current shop settings and mechanics. + * + * It is typically invoked when the display is flagged for regeneration, such as after a configuration + * change or when a visual inconsistency is detected. + */ + void fixDisplayNeedRegen(); + + /** + * Checks whether the display item is currently spawned in the world. + * + * @return true if the display item is currently spawned, false otherwise + */ + boolean isSpawned(); + + /** + * Determines whether the display item is relevant or applicable to the specified player. + * + * @param player the player to check applicability for, cannot be null + * @return true if the display item is applicable to the given player, false otherwise + */ + boolean isApplicableForPlayer(Player player); + + /** + * Checks whether the display item is flagged for removal from the world. + * + * This method determines if the associated display item is in a state + * where it is marked for removal but has not yet been removed. + * + * @return true if the display item is pending removal, false otherwise + */ + boolean isPendingRemoval(); + + /** + * Marks the display item for removal from the world. + * + * This method is responsible for initiating the removal process of the associated display item. + * When invoked, the display item transitions into a state where it is marked for removal. + * The actual removal process may be deferred until certain conditions are met or when the + * system is ready to complete the removal. + * + * Implementations may use this method to safely handle removal operations while maintaining + * consistency in the system's state. + */ + void pendingRemoval(); + + /** + * Removes the display item from the system, with an option to preserve its state in the world. + * + * @param dontTouchWorld if true, the display item's state in the world will not be altered; if false, + * the display item will also be removed from the world. + */ + void remove(boolean dontTouchWorld); + + /** + * Respawns the display item associated with the shop in its correct state and location. + * + * This method is responsible for recreating and reinitializing the display entity + * in situations where it may have been removed, despawned, or otherwise + * misplaced. It ensures that the display item is properly represented within + * the world and aligned with the shop's mechanics. + * + * Implementations may internally manage visual configuration, spawn location, + * and state synchronization. This ensures that the display item maintains + * consistency with the intended state of the shop at all times. + */ + void respawn(); + + /** + * Ensures the given entity is safeguarded, typically for maintaining correct state, + * alignment, or protection within the system. This method may involve verifying + * the entity's position, state, or association, ensuring it adheres to the intended mechanics. + * + * @param entity the entity to be safeguarded, must not be null + */ + void safeGuard(@NotNull Entity entity); + + /** + * Spawns the display item associated with the shop into the world. + * + * This method is responsible for initializing and placing the display item + * at its correct location in the world. It ensures that the item is visible + * and interacts properly with the shop's mechanics. The display item may be + * represented as a physical entity, virtual item, or any other visual representation + * supported by the shop system. + * + * Implementations may handle visual positioning, state initialization, and + * alignment with the intended shop configuration. The method does not handle + * despawning or removal of the display item; those actions are managed by separate + * methods. + */ + void spawn(); + + /** + * Retrieves the shop associated with this display item. + * The shop provides access to its various components, such as item, interaction, lifecycle, + * metadata, permissions, pricing, and trading mechanisms. + * + * @return The {@link ModernShop} associated with this display item, parameterized with types + * for price, location, and player. + */ + ModernShop shop(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiter.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiter.java new file mode 100644 index 0000000000..bef413f5e5 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiter.java @@ -0,0 +1,105 @@ +package com.ghostchu.quickshop.api.shop.limit; + +import com.ghostchu.quickshop.api.obj.QUser; +import org.bukkit.command.CommandSender; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +/** + * Utility used for shop price validating + */ +public interface PriceLimiter

{ + + /** + * Finds the set of rule sets that are applicable to the provided ItemStack. + * + * @param stack the ItemStack to evaluate; must not be null + * @return a set of RuleSet objects applicable to the provided ItemStack + */ + Set> findApplicableRules(@NotNull ItemStack stack); + + /** + * Determines the set of rule sets applicable to the specified command sender + * and item stack. The method evaluates the provided CommandSender and ItemStack + * against the conditions defined within the rule sets and only includes those + * rule sets that are relevant. + * + * @param sender the command sender to be evaluated; must not be null + * @param stack the item stack to be evaluated; must not be null + * @return a set of RuleSet objects that are applicable based on the provided + * CommandSender and ItemStack + */ + Set> findApplicableRules(@NotNull CommandSender sender, @NotNull ItemStack stack); + + /** + * Determines the set of rule sets that are applicable to the provided `ItemStack` for a given `QUser`. + * + * @param user the QUser to evaluate; must not be null + * @param stack the ItemStack to evaluate; must not be null + * @return a set of RuleSet objects that are applicable to the given QUser and ItemStack + */ + Set> findApplicableRules(@NotNull QUser user, @NotNull ItemStack stack); + + /** + * Determines the set of rule sets that are applicable to the provided ItemStack and currency. + * + * @param stack the ItemStack to evaluate; must not be null + * @param currency the currency to evaluate; must not be null + * @return a set of RuleSet objects that are applicable to the given ItemStack and currency + */ + Set> findApplicableRules(@NotNull ItemStack stack, @NotNull String currency); + + /** + * Determines the set of rule sets that are applicable based on the provided + * command sender, item stack, and currency. This method evaluates the given + * parameters against the conditions defined in the rule sets and returns the + * relevant ones. + * + * @param sender the command sender to evaluate; must not be null + * @param stack the item stack to evaluate; must not be null + * @param currency the currency to evaluate; must not be null + * @return a set of RuleSet objects applicable to the given CommandSender, ItemStack, and currency + */ + Set> findApplicableRules(@NotNull CommandSender sender, @NotNull ItemStack stack, @NotNull String currency); + + /** + * Determines the set of rule sets that are applicable to the provided user, item stack, and currency. + * The method evaluates the given QUser, ItemStack, and currency string against the conditions defined + * in the rule sets, returning only those rule sets that are relevant. + * + * @param user the QUser to evaluate; must not be null + * @param stack the ItemStack to evaluate; must not be null + * @param currency the currency to evaluate; must not be null + * @return a set of RuleSet objects that are applicable to the given QUser, ItemStack, and currency + */ + Set> findApplicableRules(@NotNull QUser user, @NotNull ItemStack stack, @NotNull String currency); + + /** + * Check the price restriction rules + * + * @param sender the sender + * @param stack the item to check + * @param currency the currency + * @param price the price + * + * @return the result + */ + @NotNull + PriceLimiterCheckResult check(@NotNull CommandSender sender, @NotNull ItemStack stack, @Nullable String currency, double price); + + /** + * Check the price restriction rules + * + * @param user the user + * @param stack the item to check + * @param currency the currency + * @param price the price + * + * @return the result + */ + @NotNull + PriceLimiterCheckResult check(@NotNull QUser user, @NotNull ItemStack stack, @Nullable String currency, double price); +} diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiterCheckResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiterCheckResult.java similarity index 81% rename from quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiterCheckResult.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiterCheckResult.java index bfcc97c648..36b7ec99b9 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/PriceLimiterCheckResult.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/PriceLimiterCheckResult.java @@ -1,5 +1,6 @@ -package com.ghostchu.quickshop.api.shop; +package com.ghostchu.quickshop.api.shop.limit; +import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; import org.jetbrains.annotations.NotNull; /** diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/RuleSet.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/RuleSet.java new file mode 100644 index 0000000000..d38b2112f8 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/limit/RuleSet.java @@ -0,0 +1,229 @@ +package com.ghostchu.quickshop.api.shop.limit; + +import com.ghostchu.quickshop.api.obj.QUser; +import org.bukkit.command.CommandSender; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; + + +public interface RuleSet

{ + + /** + * Retrieves a list of functions that evaluate a given ItemStack against a boolean condition. + * + * @return a list of functions where each function accepts an ItemStack and returns a boolean. + */ + List> items(); + + /** + * Retrieves a list of patterns representing currencies. + * + * @return a list of Pattern objects defining currency-related parameters. + */ + List currencies(); + + /** + * Retrieves the permission string required to bypass the associated rule set. + * + * @return a string representing the permission required to bypass the rule set. + */ + String bypassPermission(); + + /** + * Retrieves the minimum price defined in the rule set. + * + * @return the minimum price as an object of type P. + */ + P minPrice(); + + /** + * Retrieves the maximum price defined in the rule set. + * + * @return the maximum price as an object of type P. + */ + P maxPrice(); + + /** + * Determines whether a minimum price is set for this rule. + * + * @return true if a minimum price is defined and is greater than 0, false otherwise. + */ + boolean hasMinPrice(); + + /** + * Determines whether a maximum price is set for this rule. + * + * @return true if a maximum price is defined and is non-negative, false otherwise. + */ + boolean hasMaxPrice(); + + /** + * Determines whether the specified price is allowed according to the rule set. + * + * @param price the price to evaluate against the rule set + * @return true if the price satisfies the conditions of the rule set, false otherwise + */ + boolean isAllowed(P price); + + /** + * Checks if the provided CommandSender can bypass restrictions. + * + * @param sender the CommandSender to check + * @return true if the sender has the required bypass permission, otherwise false. + */ + boolean canBypass(@NotNull final CommandSender sender); + + /** + * Checks if the provided QUser can bypass restrictions. + * + * @param user the QUser to check + * @return true if the user can bypass the restrictions, false otherwise. + */ + boolean canBypass(@NotNull final QUser user); + + /** + * Checks if the provided currency matches any of the allowed currency patterns + * defined in this rule. If the currency is null, it is considered applicable. + * + * @param currency the currency to check. Can be null, indicating no specific currency to validate. + * + * @return true if the currency matches any defined pattern or is null; false otherwise + */ + default boolean isApplicableCurrency(@Nullable final String currency) { + + if(currency != null) { + return this.currencies().stream().anyMatch(pattern->pattern.matcher(currency).matches()); + } + return true; + } + + /** + * Tallies the total amount of `ItemStack` objects in the provided iterable + * that meet the conditions specified by the `isApplicable` method. + * + * @param stacks an iterable collection of `ItemStack` objects to evaluate; must not be null + * @return the cumulative total of amounts for all applicable `ItemStack` objects + */ + default int tallyApplicableItems(@NotNull final Iterable stacks) { + + int tally = 0; + for(final ItemStack is : stacks) { + + if(isApplicable(is)) { + + tally += is.getAmount(); + } + } + return tally; + } + + /** + * Determines if the provided CommandSender, ItemStack, and optional currency combination + * satisfies the conditions of the rule set. + * If the sender can bypass restrictions or the provided currency does not match the + * applicable patterns, the method returns false. Otherwise, it evaluates the item + * against rule-specific conditions. + * + * @param sender the CommandSender to evaluate; must not be null + * @param item the ItemStack to evaluate; must not be null + * @param currency the currency to evaluate; can be null, indicating no specific currency to validate + * @return true if the sender, item, and currency combination satisfies the rule set conditions; false otherwise + */ + default boolean isApplicable(@NotNull final CommandSender sender, @NotNull final ItemStack item, @Nullable final String currency) { + + if(canBypass(sender) || !isApplicableCurrency(currency)) { + return false; + } + return isApplicable(item); + } + + /** + * Determines if the given user, item, and currency combination is applicable according to the rule set. + * The method first checks if the user can bypass restrictions or if the provided currency is not applicable. + * If either condition is true, the method returns false. Otherwise, it evaluates the item against the rule set. + * + * @param user the QUser to evaluate; must not be null + * @param item the ItemStack to evaluate; must not be null + * @param currency the currency to evaluate; can be null, indicating no specific currency to validate + * @return true if the user, item, and currency combination satisfies the rule set conditions; false otherwise + */ + default boolean isApplicable(@NotNull final QUser user, @NotNull final ItemStack item, @Nullable final String currency) { + + if(canBypass(user) || !isApplicableCurrency(currency)) { + return false; + } + return isApplicable(item); + } + + /** + * Determines whether the provided {@link CommandSender} and {@link ItemStack} + * satisfy the conditions of the rule set. If the sender can bypass restrictions, + * the method immediately returns false. Otherwise, it evaluates the {@link ItemStack} + * against a list of predefined rule-specific conditions. + * + * @param sender the {@link CommandSender} to evaluate; must not be null + * @param stack the {@link ItemStack} to evaluate; must not be null + * @return true if the {@link ItemStack} satisfies the rule set conditions and + * the {@link CommandSender} cannot bypass restrictions; false otherwise + */ + default boolean isApplicable(@NotNull final CommandSender sender, @NotNull final ItemStack stack) { + + if(canBypass(sender)) { + return false; + } + + return items().stream().anyMatch(fun->fun.apply(stack)); + } + + /** + * Determines if the specified QUser and ItemStack combination satisfies the conditions of the rule set. + * If the user can bypass the restrictions, the method immediately returns false. Otherwise, it evaluates + * the ItemStack against a list of predefined rule-specific conditions. + * + * @param user the QUser to evaluate; must not be null + * @param stack the ItemStack to evaluate; must not be null + * @return true if the ItemStack satisfies the conditions of the rule set and the user cannot bypass restrictions, + * false otherwise + */ + default boolean isApplicable(@NotNull final QUser user, @NotNull final ItemStack stack) { + + if(canBypass(user)) { + return false; + } + + return items().stream().anyMatch(fun->fun.apply(stack)); + } + + /** + * Determines if the given ItemStack meets the conditions of the rule set. + * + * @param stack the ItemStack to evaluate; must not be null + * @return true if the ItemStack is applicable according to the rule set, false otherwise + */ + default boolean isApplicable(@NotNull final ItemStack stack) { + + return items().stream().anyMatch(fun->fun.apply(stack)); + } + + /** + * Determines whether the given ItemStack and currency meet the conditions of the rule set. + * First, it verifies if the provided currency is applicable. If not, the method returns false. + * Then, it evaluates the ItemStack against predefined rule-specific conditions. + * + * @param stack the ItemStack to evaluate; must not be null + * @param currency the currency to evaluate; must not be null + * @return true if the ItemStack and currency satisfy the conditions of the rule set, false otherwise + */ + default boolean isApplicable(@NotNull final ItemStack stack, @NotNull final String currency) { + + if(!isApplicableCurrency(currency)) { + return false; + } + return items().stream().anyMatch(fun->fun.apply(stack)); + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopDisplay.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopDisplay.java deleted file mode 100644 index 95c9d0d2be..0000000000 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopDisplay.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.ghostchu.quickshop.api.shop.meta; - -/* - * 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.localization.text.ProxiedLocale; -import net.kyori.adventure.text.Component; -import org.bukkit.block.Sign; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -/** - * ShopDisplay - * - * @author creatorfromhell - * @since 6.3.0.0 - */ -public interface ShopDisplay { - - /** - * Check the display location, and teleport, respawn if needs. - */ - void checkDisplay(); - - /** - * Claim a sign as shop sign (modern method) - * - * @param sign The shop sign - */ - void claimShopSign(@NotNull Sign sign); - - /** - * Get sign texts on shop's sign. - * - * @param locale The locale to be created for - * - * @return String arrays represents sign texts: Index | Content Line 0: Header Line 1: Shop Type - * Line 2: Shop Item Name Line 3: Price - */ - default List getSignText(@NotNull final ProxiedLocale locale) { - //backward support - throw new UnsupportedOperationException(); - } - - /** - * Get shop signs, may have multi signs - * - * @return Signs for the shop - */ - @NotNull - List getSigns(); - - /** - * Getting if this shop has been disabled the display - * - * @return Does display has been disabled - */ - boolean isDisableDisplay(); - - /** - * Set the display disable state - * - * @param disabled Has been disabled - */ - void setDisableDisplay(boolean disabled); - - /** - * Determines whether a custom item name should be used. - * - * @return true if a custom item name is enabled, false otherwise - */ - boolean useCustomItemName(); - - /** - * Customizes and returns a Component representing an item name. - * - * @return a Component representing the customized item name - */ - Component customItemName(); - - /** - * Checks if a Sign is a ShopSign - * - * @param sign Target {@link Sign} - * - * @return Is shop info sign - */ - boolean isShopSign(@NotNull Sign sign); - - /** - * Generate new sign texts on shop's sign. - */ - void setSignText(); - - /** - * Set texts on shop's sign - * - * @param paramArrayOfString The texts you want set - */ - void setSignText(@NotNull List paramArrayOfString); - - void setSignText(@NotNull ProxiedLocale locale); -} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionFailureReason.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionFailureReason.java new file mode 100644 index 0000000000..108dfeb7f7 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionFailureReason.java @@ -0,0 +1,34 @@ +package com.ghostchu.quickshop.api.shop.service; + +/* + * 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 . + */ + +/** + * ShopActionFailureReason + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public enum ShopActionFailureReason { + + SHOP_NOT_FOUND, + SHOP_ALREADY_EXISTS, + NO_PERMISSIONS, + ECONOMY_ACTION_FAILED, + UNKNOWN +} diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionResult.java new file mode 100644 index 0000000000..b0dfd600a6 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopActionResult.java @@ -0,0 +1,32 @@ +package com.ghostchu.quickshop.api.shop.service; + +/* + * 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 org.jetbrains.annotations.Nullable; + +/** + * ShopActionResult + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record ShopActionResult(T value, + boolean success, + @Nullable ShopActionFailureReason failureReason) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopRequest.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopRequest.java new file mode 100644 index 0000000000..3328a95386 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopRequest.java @@ -0,0 +1,35 @@ +package com.ghostchu.quickshop.api.shop.service; + +/* + * 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 . + */ + +/** + * ShopRequest + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopRequest { + + /** + * Retrieves the unique identifier associated with a shop. + * + * @return the ID of the shop as a long value + */ + long shopId(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopUpdateOptions.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopUpdateOptions.java new file mode 100644 index 0000000000..5e2fb040d1 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/ShopUpdateOptions.java @@ -0,0 +1,32 @@ +package com.ghostchu.quickshop.api.shop.service; + +/* + * 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 . + */ + +/** + * ShopUpdateOptions + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record ShopUpdateOptions(boolean checkPermissions, + boolean performEconomyActions, + boolean updateDisplay, + boolean updateSign, + boolean updateDB) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopCreateRequest.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopCreateRequest.java new file mode 100644 index 0000000000..c84cb66f14 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopCreateRequest.java @@ -0,0 +1,106 @@ +package com.ghostchu.quickshop.api.shop.service.request; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.service.ShopRequest; +import com.ghostchu.quickshop.api.shop.service.ShopUpdateOptions; +import org.bukkit.command.CommandSender; + +/** + * ShopCreateRequest + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopCreateRequest implements ShopRequest { + + protected final ShopUpdateOptions options; + protected final CommandSender actor; + protected final ModernShop shop; + + public ShopCreateRequest(final ShopUpdateOptions options, + final CommandSender actor, + final ModernShop shop) { + + this.options = options; + this.actor = actor; + this.shop = shop; + } + + public ShopCreateRequest(final Builder builder) { + this.options = builder.options; + this.actor = builder.actor; + this.shop = builder.shop; + } + + public static Builder builder() { + + return new Builder(); + } + + public ShopUpdateOptions options() { + + return options; + } + + public CommandSender actor() { + + return actor; + } + + public ModernShop shop() { + + return shop; + } + + @Override + public long shopId() { + + return shop.meta().getShopId(); + } + + public static final class Builder { + + private ShopUpdateOptions options; + private CommandSender actor; + private ModernShop shop; + + private Builder() { + } + public Builder options(final ShopUpdateOptions options) { + this.options = options; + return this; + } + + public Builder actor(final CommandSender actor) { + this.actor = actor; + return this; + } + + public Builder shop(final ModernShop shop) { + this.shop = shop; + return this; + } + + public ShopCreateRequest build() { + return new ShopCreateRequest(this); + } + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopDeleteRequest.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopDeleteRequest.java new file mode 100644 index 0000000000..77ab349c6a --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopDeleteRequest.java @@ -0,0 +1,100 @@ +package com.ghostchu.quickshop.api.shop.service.request; + +/* + * 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.service.ShopRequest; +import com.ghostchu.quickshop.api.shop.service.ShopUpdateOptions; +import org.bukkit.command.CommandSender; + +/** + * ShopDeleteRequest + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopDeleteRequest implements ShopRequest { + + protected final ShopUpdateOptions options; + protected final CommandSender actor; + protected final long shopId; + + public ShopDeleteRequest(final ShopUpdateOptions options, + final CommandSender actor, + final long shopId) { + + this.options = options; + this.actor = actor; + this.shopId = shopId; + } + + public ShopDeleteRequest(final Builder builder) { + this.options = builder.options; + this.actor = builder.actor; + this.shopId = builder.shopId; + } + + public static Builder builder() { + + return new Builder(); + } + + public ShopUpdateOptions options() { + + return options; + } + + public CommandSender actor() { + + return actor; + } + + @Override + public long shopId() { + + return shopId; + } + + public static final class Builder { + + private ShopUpdateOptions options; + private CommandSender actor; + private long shopId; + + private Builder() { + } + public Builder options(final ShopUpdateOptions options) { + this.options = options; + return this; + } + + public Builder actor(final CommandSender actor) { + this.actor = actor; + return this; + } + + public Builder shopId(final long shopId) { + this.shopId = shopId; + return this; + } + + public ShopDeleteRequest build() { + return new ShopDeleteRequest(this); + } + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopUpdateRequest.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopUpdateRequest.java new file mode 100644 index 0000000000..6176e44b78 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/request/ShopUpdateRequest.java @@ -0,0 +1,106 @@ +package com.ghostchu.quickshop.api.shop.service.request; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.service.ShopRequest; +import com.ghostchu.quickshop.api.shop.service.ShopUpdateOptions; +import org.bukkit.command.CommandSender; + +/** + * ShopUpdateRequest + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopUpdateRequest implements ShopRequest { + + protected final ShopUpdateOptions options; + protected final CommandSender actor; + protected final ModernShop shop; + + public ShopUpdateRequest(final ShopUpdateOptions options, + final CommandSender actor, + final ModernShop shop) { + + this.options = options; + this.actor = actor; + this.shop = shop; + } + + public ShopUpdateRequest(final Builder builder) { + this.options = builder.options; + this.actor = builder.actor; + this.shop = builder.shop; + } + + public static Builder builder() { + + return new Builder(); + } + + public ShopUpdateOptions options() { + + return options; + } + + public CommandSender actor() { + + return actor; + } + + public ModernShop shop() { + + return shop; + } + + @Override + public long shopId() { + + return shop.meta().getShopId(); + } + + public static final class Builder { + + private ShopUpdateOptions options; + private CommandSender actor; + private ModernShop shop; + + private Builder() { + } + public Builder options(final ShopUpdateOptions options) { + this.options = options; + return this; + } + + public Builder actor(final CommandSender actor) { + this.actor = actor; + return this; + } + + public Builder shop(final ModernShop shop) { + this.shop = shop; + return this; + } + + public ShopUpdateRequest build() { + return new ShopUpdateRequest(this); + } + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopChangeType.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopChangeType.java new file mode 100644 index 0000000000..ebf8f83b12 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopChangeType.java @@ -0,0 +1,47 @@ +package com.ghostchu.quickshop.api.shop.service.result; + +/* + * 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 . + */ + +/** + * ShopChangeType + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public enum ShopChangeType { + + ITEM, + DISPLAY_TOGGLE, + AMOUNT, + OWNER, + NAME, + ADMIN_STATUS, + TYPE, + STATE, + TAX_ACCOUNT, + BENEFITS, + PRICE, + CURRENCY, + PERMISSION, + EXTRA, + INVENTORY_WRAPPER, + LOCATION, + SYMBOL_LINK, + RUNTIME_ID +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopCreateResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopCreateResult.java new file mode 100644 index 0000000000..234eb412f1 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopCreateResult.java @@ -0,0 +1,31 @@ +package com.ghostchu.quickshop.api.shop.service.result; + +/* + * 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.ModernShop; +import org.jetbrains.annotations.Nullable; + +/** + * ShopCreateResult + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record ShopCreateResult(@Nullable ModernShop shop) { +} diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopDeleteResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopDeleteResult.java new file mode 100644 index 0000000000..8c610b523f --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopDeleteResult.java @@ -0,0 +1,31 @@ +package com.ghostchu.quickshop.api.shop.service.result; + +/* + * 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.ModernShop; +import org.jetbrains.annotations.Nullable; + +/** + * ShopDeleteResult + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record ShopDeleteResult(@Nullable ModernShop shop) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopUpdateResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopUpdateResult.java new file mode 100644 index 0000000000..5a20931f13 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/service/result/ShopUpdateResult.java @@ -0,0 +1,35 @@ +package com.ghostchu.quickshop.api.shop.service.result; + +/* + * 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.ModernShop; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; + +/** + * ShopChanges + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record ShopUpdateResult(EnumSet changes, + @Nullable ModernShop oldShop, + ModernShop newShop) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java index 4470c30d6e..04d6f6729a 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java @@ -42,6 +42,5 @@ public enum TradeFailureReason { ECONOMY_TRANSACTION_FAILED, INVENTORY_TRANSACTION_FAILED, SHOP_TRANSACTION_FAILED, - EVENT_CANCELLED, INTERNAL_ERROR } \ No newline at end of file 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 31398cae56..2312471f77 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 @@ -5,8 +5,8 @@ import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.event.Phase; import com.ghostchu.quickshop.api.event.settings.type.ShopCurrencyEvent; -import com.ghostchu.quickshop.api.shop.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Item.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Item.java index 5d11fc2814..97306e29b4 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Item.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Item.java @@ -3,8 +3,8 @@ 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.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Size.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Size.java index b7391f84a8..18bd7bd5da 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Size.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Size.java @@ -3,8 +3,8 @@ 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.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; 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 e1df63e0a8..0afdfa499a 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 @@ -23,8 +23,6 @@ import com.google.common.collect.MapMaker; import com.google.common.collect.Maps; import com.google.common.collect.Sets; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.Getter; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -43,7 +41,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -219,44 +216,6 @@ 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().removeAllShopTagsBy(tagger, shop.getShopId()); - } - - @Override - public CompletableFuture<@Nullable Integer> clearTagFromShops(@NotNull final UUID tagger, @NotNull String tag) { - - tag = tag.trim().toLowerCase(Locale.ROOT); - tag = tag.replace(" ", "_"); - return plugin.getDatabaseHelper().removeTagFromShops(tagger, tag); - } - - @Override - public CompletableFuture<@Nullable Integer> removeTag(@NotNull final UUID tagger, @NotNull final Shop shop, @NotNull String tag) { - - tag = tag.trim().toLowerCase(Locale.ROOT); - tag = tag.replace(" ", "_"); - return plugin.getDatabaseHelper().removeShopTag(tagger, shop.getShopId(), tag); - } - - @Override - public CompletableFuture<@Nullable Integer> tagShop(@NotNull final UUID tagger, @NotNull final Shop shop, @NotNull String tag) { - - tag = tag.trim().toLowerCase(Locale.ROOT); - tag = tag.replace(" ", "_"); - return plugin.getDatabaseHelper().tagShop(tagger, shop.getShopId(), tag); - } - - @Override - @NotNull - public List listTags(@NotNull final UUID tagger) { - - Util.ensureThread(true); - return plugin.getDatabaseHelper().listTags(tagger); - } - @Override public CompletableFuture unregisterShop(@NotNull final Shop shop, final boolean persist) { 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 dc4b0c56ec..29f19cbe7a 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 @@ -99,61 +99,8 @@ */ @EqualsAndHashCode 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"); - private static final String LEGACY_SHOP_SIGN_RECOGNIZE_PATTERN = "§d§o "; - @NotNull - private final Location location; @EqualsAndHashCode.Exclude private final QuickShop plugin; - @EqualsAndHashCode.Exclude - private final UUID runtimeRandomUniqueId = UUID.randomUUID(); - @NotNull - private final Map playerGroup; - @EqualsAndHashCode.Exclude - private final boolean isDeleted = false; - private YamlConfiguration extra; - private long shopId; - private QUser owner; - private double price; - private IShopType shopType; - private ShopState shopState; - private boolean unlimited; - @NotNull - private ItemStack item; - @NotNull - private ItemStack originalItem; - @Nullable - @EqualsAndHashCode.Exclude - private AbstractDisplayItem displayItem = null; - @EqualsAndHashCode.Exclude - private volatile boolean isLoaded = false; - @EqualsAndHashCode.Exclude - private volatile boolean createBackup = false; - @EqualsAndHashCode.Exclude - private InventoryPreview inventoryPreview = null; - @EqualsAndHashCode.Exclude - private boolean dirty; - @EqualsAndHashCode.Exclude - private boolean updating = false; - @Nullable - private String currency; - private boolean disableDisplay; - private QUser taxAccount; - @NotNull - private String inventoryWrapperProvider; - @NotNull - private String symbolLink; - @Nullable - private String shopName; - - @NotNull - private BenefitProvider benefit; - - //updating objects - private final AtomicBoolean updatingAtomic = new AtomicBoolean(false); - private volatile CompletableFuture inFlightUpdate; /** @@ -282,23 +229,6 @@ public void add(@NotNull ItemStack item, final int amount) { this.setSignText(); } - /** - * Buys amount of item from Player p. Does NOT check our inventory, or balances - * - * @param buyer The player to buy from - * @param buyerInventory The buyer's inventory - * @param loc2Drop The location to drop items if inventory are full - * @param amount The amount to buy - */ - @Override - public TradeResult buy(@NotNull final QUser buyer, - @NotNull final InventoryWrapper buyerInventory, - @NotNull final Location loc2Drop, - final int amount) { - - return plugin.getShopManager().tradeService().executeSellToShop(this, buyer, buyerInventory, loc2Drop, amount); - } - @Override public void checkDisplay() { @@ -368,75 +298,6 @@ public void checkDisplay() { } } - @Override - public void claimShopSign(@NotNull final Sign sign) { - - if(!sign.getPersistentDataContainer().has(Shop.SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE)) { - sign.getPersistentDataContainer().set(Shop.SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE, saveToShopSignStorage()); - sign.update(); - } - } - - /** - * Gets the currency that shop use - * - * @return The currency name - */ - @Override - public @Nullable String getCurrency() { - - final ShopCurrencyEvent event = ShopCurrencyEvent.RETRIEVE(this, this.currency); - event.callEvent(); - - return event.updated(); - } - - /** - * Sets the currency that shop use - * - * @param currency The currency name; null to use default currency - */ - @Override - public void setCurrency(@Nullable final String currency) { - - if(Objects.equals(this.currency, currency)) { - return; - } - this.currency = currency; - setDirty(); - } - - /** - * @return The durability of the item - */ - @Override - public short getDurability() { - - return (short)((Damageable)this.item.getItemMeta()).getDamage(); - } - - /** - * Gets the plugin's k-v map to storage the data. It is spilt by plugin name, different name have - * different map, the data won't conflict. But if you plugin name is too common, add a prefix will - * be a good idea. - * - * @param plugin Plugin instance - * - * @return The data table - */ - @Override - public @NotNull ConfigurationSection getExtra(@NotNull final Plugin plugin) { - - if(this.extra == null) { - this.extra = new YamlConfiguration(); - } - ConfigurationSection section = extra.getConfigurationSection(plugin.getName()); - if(section == null) { - section = extra.createSection(plugin.getName()); - } - return section; - } - /** * @return The chest this shop is based on. */ @@ -466,309 +327,6 @@ public short getDurability() { return null; } - @Override - public @NotNull String getInventoryWrapperProvider() { - - return inventoryWrapperProvider; - } - - /** - * @return Returns a dummy itemstack of the item this shop is selling. - */ - @Override - public @NotNull ItemStack getItem() { - - final ShopItemEvent event = new ShopItemEvent(Phase.RETRIEVE, this, this.item.clone()); - - return event.updated(); - } - - @Override - public void setItem(@NotNull final ItemStack item) { - - Util.ensureThread(false); - - //Create our shop event with Pre Phase and call - ShopItemEvent event = new ShopItemEvent(Phase.PRE, this, this.item, item); - event.callEvent(); - - //Call our Main Phase - event = event.clone(Phase.MAIN); - if(event.callCancellableEvent()) { - - Log.debug("A plugin cancelled the item change event."); - return; - } - - - this.item = event.updated(); - this.originalItem = item; - - //call our Post Phase - event.clone(Phase.POST).callEvent(); - - if(this.displayItem != null) { - - this.displayItem.remove(false); - } - this.displayItem = null; - checkDisplay(); - setSignText(); - setDirty(); - } - - /** - * @return The location of the shops chest - */ - @Override - public @NotNull Location getLocation() { - - 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. - */ - @Override - public @NotNull QUser getOwner() { - - return this.owner; - } - - /** - * Changes the owner of this shop to the given player. - * - * @param owner the new owner - */ - @Override - public void setOwner(@NotNull final QUser owner) { - - Util.ensureThread(false); - if(this.owner.equals(owner)) { - return; - } - this.owner = owner; - setDirty(); - setSignText(plugin.getTextManager().findRelativeLanguages(owner, false)); - } - - /** - * Gets registered to this shop's permission audiences. - * - * @return registered audiences - */ - @Override - public @NotNull Map getPermissionAudiences() { - - final Map clonedPlayerGroup = new HashMap<>(playerGroup); - final Optional uuid = getOwner().getUniqueIdOptional(); - if(uuid.isPresent()) { - clonedPlayerGroup.put(getOwner().getUniqueId(), BuiltInShopPermissionGroup.ADMINISTRATOR.getNamespacedNode()); - } - return clonedPlayerGroup; - } - - /** - * Gets the player's group in this shop - * - * @param player the player - * - * @return the group - */ - @Override - public @NotNull String getPlayerGroup(@NotNull final UUID player) { - - if(player.equals(getOwner().getUniqueId())) { - return BuiltInShopPermissionGroup.ADMINISTRATOR.getNamespacedNode(); - } - - final String group = getPermissionAudiences().getOrDefault(player, BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode()); - if(plugin.getShopPermissionManager().hasGroup(group)) { - - final ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.RETRIEVE, this, player, group); - event.callEvent(); - - return event.updated(); - } - return BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode(); - } - - /** - * @return The price per item this shop is selling - */ - @SuppressWarnings("removal") - @Override - public double getPrice() { - - return this.price; - } - - /** - * Sets the price of the shop. - * - * @param price The new price of the shop. - */ - @SuppressWarnings("removal") - @Override - public void setPrice(final double price) { - - Util.ensureThread(false); - this.price = price; - setDirty(); - 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. * @@ -842,768 +400,32 @@ public int getRemainingStock() { return future.join(); } - /** - * Retrieves the runtime-generated random unique identifier for the current instance. DO NOT USE FOR - * DATA STORAGE. - * - * @return a non-null {@link UUID} representing a unique identifier that was generated at runtime. - */ - @Override - public @NotNull UUID getRuntimeRandomUniqueId() { - - return this.runtimeRandomUniqueId; - } - - @Override - public long getShopId() { - - return this.shopId; - } - @Override - public void setShopId(final long newId) { + public boolean inventoryAvailable() { - if(this.shopId != -1) { - throw new IllegalStateException("Cannot set shop id once it fully created."); + if(isUnlimited()) { + return true; + } + if(isSelling()) { + return getRemainingStock() > 0; + } + if(isBuying()) { + return getRemainingSpace() > 0; } - this.shopId = newId; - setDirty(); + if(isFrozen()) { + return false; + } + return true; } /** - * Gets this shop name that set by player + * Removes an item from the shop. * - * @return Shop name, or null if not set + * @param item The itemstack. The amount does not matter, just everything else + * @param amount The amount to remove from the shop. */ @Override - public @Nullable String getShopName() { - - return this.shopName; - } - - /** - * Sets shop name - * - * @param shopName shop name, null to remove currently name - */ - @Override - public void setShopName(@Nullable final String shopName) { - - if(com.ghostchu.quickshop.common.util.CommonUtil.strEquals(this.shopName, shopName)) { - return; - } - this.shopName = shopName; - setDirty(); - } - - /** - * Getting the item stacking amount of the shop. - * - * @return The item stacking amount of the shop. - */ - @Override - public int getShopStackingAmount() { - - if(isStackingShop()) { - return item.getAmount(); - } - return 1; - } - - /** - * 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 void shopState(@NotNull final String shopStateIdentifier) { - - shopState(QuickShop.getInstance().getShopManager().shopStateOrDefault(shopStateIdentifier)); - } - - /** - * Retrieves the type of shop associated with this entity. - * - * @return an instance of IShopType representing the shop type - */ - @Override - public IShopType shopType() { - - final ShopTypeEnhancedEvent event = new ShopTypeEnhancedEvent(Phase.RETRIEVE, this, this.shopType); - event.callEvent(); - - return event.updated(); - } - - /** - * Sets the type of shop using the provided shop type parameter. - * - * @param newShopType the shop type to set, must not be null - */ - @Override - public void shopType(@NotNull final IShopType newShopType) { - - Util.ensureThread(false); - - if(this.shopType.identifier().equalsIgnoreCase(newShopType.identifier())) { - - return; - } - - ShopTypeEnhancedEvent event = new ShopTypeEnhancedEvent(Phase.PRE, this, this.shopType, newShopType); - event.callEvent(); - - event = event.clone(Phase.MAIN); - - if(event.callCancellableEvent()) { - - Log.debug("Some addon cancelled shop type changes, target shop: " + this); - return; - } - - this.shopType = event.updated(); - - event = event.clone(Phase.POST); - event.callEvent(); - - this.setSignText(); - setDirty(); - } - - /** - * Specifies the type of shop based on the given identifier. - * - * @param shopTypeIdentifier the identifier representing the type of shop. Must not be null. - */ - @Override - public void shopType(@NotNull final String shopTypeIdentifier) { - - shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(shopTypeIdentifier)); - } - - @Override - public List getSignText(@NotNull final ProxiedLocale locale) { - - Util.ensureThread(false); - - final LinkedList lines = plugin.getShopManager().shopLayoutProvider().render(this, locale); - - final ShopSignLinesEvent event = new ShopSignLinesEvent(Phase.RETRIEVE, this, lines); - event.callEvent(); - - return event.updated(); - } - - /** - * Returns a list of signs that are attached to this shop (QuickShop and blank signs only) - * - * @return a list of signs that are attached to this shop (QuickShop and blank signs only) - */ - @Override - public @NotNull List getSigns() { - - Util.ensureThread(false); - final List signs = new ArrayList<>(4); - if(this.bukkitLocation().getWorld() == null) { - return Collections.emptyList(); - } - final Block[] blocks = new Block[4]; - blocks[0] = location.getBlock().getRelative(BlockFace.EAST); - blocks[1] = location.getBlock().getRelative(BlockFace.NORTH); - blocks[2] = location.getBlock().getRelative(BlockFace.SOUTH); - blocks[3] = location.getBlock().getRelative(BlockFace.WEST); - for(final Block b : blocks) { - if(b == null) { - continue; - } - final BlockState state = b.getState(false); - if(!(state instanceof final Sign sign)) { - continue; - } - if(!location.getBlock().equals(Util.getAttached(b))) { - continue; - } - if(isShopSign(sign)) { - claimShopSign(sign); - signs.add(sign); - } - } - - return signs; - } - - - @Override - @Nullable - public QUser getTaxAccount() { - - QUser uuid = null; - if(taxAccount != null) { - uuid = taxAccount; - } else { - if(((SimpleShopManager)plugin.getShopManager()).getCacheTaxAccount() != null) { - uuid = ((SimpleShopManager)plugin.getShopManager()).getCacheTaxAccount(); - } - } - final ShopTaxAccountEvent event = new ShopTaxAccountEvent(Phase.RETRIEVE, this, uuid); - event.callEvent(); - - return event.updated(); - - } - - @Override - public void setTaxAccount(@Nullable final QUser taxAccount) { - - if(Objects.equals(taxAccount, this.taxAccount)) { - return; - } - - ShopTaxAccountEvent event = new ShopTaxAccountEvent(Phase.PRE, this, this.taxAccount, taxAccount); - event.callEvent(); - - event = event.clone(Phase.MAIN); - if(event.callCancellableEvent()) { - - return; - } - - - this.taxAccount = event.updated(); - - event = event.clone(Phase.POST); - event.callEvent(); - - setDirty(); - } - - @Override - @Nullable - public QUser getTaxAccountActual() { - - return taxAccount; - } - - @Override - public boolean inventoryAvailable() { - - if(isUnlimited()) { - return true; - } - if(isSelling()) { - return getRemainingStock() > 0; - } - if(isBuying()) { - return getRemainingSpace() > 0; - } - if(isFrozen()) { - return false; - } - return true; - } - - @Override - public boolean isAttached(@NotNull final Block b) { - - Util.ensureThread(false); - return this.bukkitLocation().getBlock().equals(Util.getAttached(b)); - } - - @Override - public boolean isBuying() { - - return this.shopType.isBuying(); - } - - @Override - public boolean isFrozen() { - - return this.shopType.isTradingBlocked() || !this.shopState.isTradingAllowed(); - } - - private boolean isDeleted() { - - return this.isDeleted; - } - - @Override - public boolean isDirty() { - - return this.dirty; - } - - @Override - public void setDirty(final boolean isDirty) { - - this.dirty = isDirty; - } - - @Override - public boolean isDisableDisplay() { - - final ShopDisplayEvent event = ShopDisplayEvent.RETRIEVE(this, this.disableDisplay); - event.callEvent(); - - return event.updated(); - } - - @Override - public void setDisableDisplay(final boolean disabled) { - - if(this.disableDisplay == disabled) { - return; - } - this.disableDisplay = disabled; - setDirty(); - checkDisplay(); - } - - /** - * Determines whether a custom item name should be used. - * - * @return true if a custom item name is enabled, false otherwise - */ - @Override - public boolean useCustomItemName() { - - if(!plugin.getConfig().getBoolean("shop.force-use-item-original-name")) { - return false; - } - - final ItemMeta itemMeta = this.item.getItemMeta(); - if(itemMeta == null) { - return false; - } - - try { - if(itemMeta.hasItemName()) { - return true; - } - } catch(final NoSuchMethodError ignore) { - //old version - } - - try { - if(itemMeta.hasCustomName()) { - return true; - } - } catch(final NoSuchMethodError ignore) { - //old version - } - - return itemMeta.hasDisplayName(); - } - - /** - * Customizes and returns a Component representing an item name. - * - * @return a Component representing the customized item name - */ - @Override - public Component customItemName() { - - return Util.getItemStackName(getItem()); - } - - /** - * Check if this shop is free shop - * - * @return Free Shop - */ - @Override - public boolean isFreeShop() { - - return this.price == 0.0d; - } - - @Override - public boolean isLoaded() { - - return this.isLoaded; - } - - @Override - public boolean isSelling() { - - return !this.shopType.isBuying(); - } - - /** - * Checks if a Sign is a ShopSign - * - * @param sign Target {@link Sign} - * - * @return Is shop info sign - */ - @Override - public boolean isShopSign(@NotNull final Sign sign) { - // Check for new shop sign - final Component[] lines = new Component[sign.getLines().length]; - for(int i = 0; i < sign.getLines().length; i++) { - lines[i] = plugin.platform().getLine(sign, i); - } - // Can be claim - - boolean empty = true; - for(final Component line : lines) { - if(!Util.isEmptyComponent(line)) { - empty = false; - break; - } - } - - if(empty) { - return true; - } - - // Check for exists shop sign (modern) - ShopSignStorage shopSignStorage = sign.getPersistentDataContainer().get(SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE); - if(shopSignStorage == null) { - // Try to read Reremake sign namespaced key - shopSignStorage = sign.getPersistentDataContainer().get(LEGACY_SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE); - } - if(shopSignStorage == null) { - // Try more hard to read Reremake sign namespaced key - if(sign.getLine(1).startsWith(LEGACY_SHOP_SIGN_RECOGNIZE_PATTERN)) { - return true; - } - } - if(shopSignStorage != null) { - return shopSignStorage.equals(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); - } - return false; - } - - /** - * Gets shop status is stacking shop - * - * @return The shop stacking status - */ - @Override - public boolean isStackingShop() { - - return plugin.isAllowStack() && this.item.getAmount() > 1; - } - - @Override - public boolean isUnlimited() { - - return this.unlimited; - } - - @Override - public void setUnlimited(final boolean unlimited) { - - if(this.unlimited == unlimited) { - return; - } - Util.ensureThread(false); - this.unlimited = unlimited; - setDirty(); - this.setSignText(); - } - - /** - * Check shop is or not still Valid. - * - * @return isValid - */ - @Override - public boolean isValid() { - - Util.ensureThread(false); - if(this.isDeleted) { - return false; - } - return Util.canBeShop(this.bukkitLocation().getBlock()); - } - - /** - * Returns true if the ItemStack matches what this shop is selling/buying - * - * @param item The ItemStack - * - * @return True if the ItemStack is the same (Excludes amounts) - */ - @Override - public boolean matches(@Nullable final ItemStack item) { - - if(item == null) { - return false; - } - final ItemStack givenItem = item.clone(); - givenItem.setAmount(1); - final ItemStack shopItem = this.item.clone(); - shopItem.setAmount(1); - return plugin.getItemMatcher().matches(shopItem, givenItem); - } - - @Override - public void onClick(@NotNull final Player clicker) { - - Util.ensureThread(false); - ShopClickEvent event = new ShopClickEvent(this, QUserImpl.createFullFilled(clicker)); - event.callEvent(); - - event = event.clone(Phase.MAIN); - if(event.callCancellableEvent()) { - - Log.debug("Ignore shop click, because some plugin cancelled it."); - return; - } - setSignText(plugin.getTextManager().findRelativeLanguages(clicker)); - - event = event.clone(Phase.POST); - event.callEvent(); - } - - /** - * Load ContainerShop. - */ - @Override - public void handleLoading() { - - Util.ensureThread(false); - if(this.isLoaded) { - Log.debug("Dupe load request, canceled."); - return; - } - try(final PerfMonitor ignored = new PerfMonitor("Shop Inventory Locate", Duration.of(1, ChronoUnit.SECONDS))) { - if(getInventory() == null) { - plugin.logger().warn("Failed to load shop: {}: {}: {}", symbolLink, this.getClass().getName(), "Inventory is null"); - if(plugin.getConfig().getBoolean("debug.delete-corrupt-shops")) { - plugin.logger().warn("Deleting corrupt shop..."); - Util.regionThread(location, () -> plugin.getShopManager().deleteShop(this)); - } else { - plugin.logger().warn("Unloading shops from memory, set `debug.delete-corrupt-shops` to true to delete corrupted shops."); - plugin.getShopManager().unloadShop(this); - } - return; - } - } - if(Util.fireCancellableEvent(new ShopLoadEvent(this))) { - return; - } - this.isLoaded = true; - //disable schedule check due to performance issue - //plugin.getShopContainerWatcher().scheduleCheck(this); - try(final PerfMonitor ignored = new PerfMonitor("Shop Display Check", Duration.of(1, ChronoUnit.SECONDS))) { - checkDisplay(); - } - if(plugin.getConfig().getBoolean("shop.update-sign-on-load", false)) { - Log.debug("Scheduled sign update for shop " + this + " because updateShopSignOnLoad has been enabled."); - plugin.getSignUpdateWatcher().scheduleSignUpdate(this); - } - } - - /** - * Unload ContainerShop. - */ - @Override - public void handleUnloading(final boolean dontTouchWorld) { - - Util.ensureThread(false); - if(!this.isLoaded) { - Log.debug("Dupe unload request, canceled."); - return; - } - if(inventoryPreview != null) { - inventoryPreview.close(); - } - if(this.displayItem != null) { - this.displayItem.remove(dontTouchWorld); - } - this.isLoaded = false; - plugin.getShopManager().getLoadedShops().remove(this); - new ShopUnloadEvent(Phase.POST, this).callEvent(); - } - - @Override - public void openPreview(@NotNull final Player player) { - - if(inventoryPreview == null) { - inventoryPreview = new InventoryPreview(plugin, getItem().clone(), player.getLocale()); - } - inventoryPreview.show(player); - - } - - @Override - public @NotNull Component ownerName(final boolean forceUsername, @NotNull final ProxiedLocale locale) { - - Component name; - if(!forceUsername && isUnlimited()) { - name = plugin.text().of("admin-shop").forLocale(locale.getLocale()); - } else { - final String playerName = this.getOwner().getUsername(); - if(playerName == null) { - name = plugin.text().of("unknown-owner").forLocale(locale.getLocale()); - } else { - name = Component.text(playerName); - } - } - if(getOwner().isRealPlayer()) { - name = name.hoverEvent( - plugin.text().of("real-player-component-hover", getOwner().getUniqueId(), getOwner().getUsername(), getOwner().getDisplay()).forLocale(locale.getLocale()) - ); - } else { - name = name.hoverEvent( - plugin.text().of("virtual-player-component-hover", getOwner().getUniqueId(), getOwner().getUsername(), getOwner().getDisplay()).forLocale(locale.getLocale()) - ); - - } - - final ShopOwnerNameEvent event = new ShopOwnerNameEvent(Phase.RETRIEVE, this, name); - event.callEvent(); - - name = event.updated(); - return name; - } - - @Override - public @NotNull Component ownerName(@NotNull final ProxiedLocale locale) { - - return ownerName(false, locale); - } - - @Override - public @NotNull Component ownerName() { - - return ownerName(false, MsgUtil.getDefaultGameLanguageLocale()); - } - - /** - * Check if player has permission to authorize the specified permission node. - * - * @param player the player - * @param namespace the plugin instance for the permission node (namespace) - * @param permission the permission node - * - * @return true if player has permission, false otherwise - */ - @Override - public boolean playerAuthorize(@NotNull final UUID player, @NotNull final Plugin namespace, @NotNull final String permission) { - - if(player.equals(getOwner().getUniqueId())) { - Log.permission("Check permission " + namespace.getName().toLowerCase(Locale.ROOT) + "." + permission + " for " + player + " -> " + "true"); - return true; - } - - final String group = getPlayerGroup(player); - final boolean hasPermission = plugin.getShopPermissionManager().hasPermission(group, namespace, permission); - - final ShopPermissionCheckEvent event = new ShopPermissionCheckEvent(Phase.MAIN, this, player, namespace.getName(), permission, hasPermission); - event.callEvent(); - - Log.permission("Check permission " + namespace.getName().toLowerCase(Locale.ROOT) + "." + permission + ": " + player + " -> " + event.hasPermission()); - - return event.hasPermission(); - } - - /** - * Check if player has permission to authorize the specified permission node. - * - * @param player the player - * @param permission the permission node - * - * @return true if player has permission, false otherwise - */ - @Override - public boolean playerAuthorize(@NotNull final UUID player, @NotNull final BuiltInShopPermission permission) { - - return playerAuthorize(player, plugin.getJavaPlugin(), permission.getRawNode()); - } - - @Override - public List playersCanAuthorize(@NotNull final BuiltInShopPermission permission) { - - return playersCanAuthorize(plugin.getJavaPlugin(), permission.getRawNode()); - } - - @Override - public List playersCanAuthorize(@NotNull final BuiltInShopPermissionGroup permissionGroup) { - - return getPermissionAudiences().entrySet().stream().filter(entry->entry.getValue().equals(permissionGroup.getNamespacedNode())).map(Map.Entry::getKey).toList(); - } - - @Override - public List playersCanAuthorize(@NotNull final Plugin namespace, @NotNull final String permission) { - - final List result = new ArrayList<>(); - for(final Map.Entry uuidStringEntry : this.getPermissionAudiences().entrySet()) { - - final String group = uuidStringEntry.getValue(); - final boolean hasPermission = plugin.getShopPermissionManager().hasPermission(group, namespace, permission); - - final ShopPermissionCheckEvent event = new ShopPermissionCheckEvent(Phase.MAIN, this, uuidStringEntry.getKey(), namespace.getName(), permission, hasPermission); - event.callEvent(); - - if(event.hasPermission()) { - result.add(uuidStringEntry.getKey()); - } - } - Log.permission("Check permission " + namespace.getName().toLowerCase(Locale.ROOT) + "." + permission + ": " + CommonUtil.list2String(result.stream().map(UUID::toString).toList())); - return result; - } - - /** - * Removes an item from the shop. - * - * @param item The itemstack. The amount does not matter, just everything else - * @param amount The amount to remove from the shop. - */ - @Override - public void remove(@NotNull ItemStack item, final int amount) { + public void remove(@NotNull ItemStack item, final int amount) { Util.ensureThread(false); if(this.unlimited) { @@ -1626,340 +448,6 @@ public void remove(@NotNull ItemStack item, final int amount) { this.setSignText(); } - @Override - public @NotNull String saveExtraToYaml() { - - return extra == null? "" : extra.saveToString(); - } - - @Override - public ShopInfoStorage saveToInfoStorage() { - - 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, - this.taxAccount, inventoryWrapperProvider, - saveToSymbolLink(), this.playerGroup); - } - - @Override - @NotNull - public String saveToSymbolLink() { - - return symbolLink; - } - - /** - * Sells amount of item to Player p. Does NOT check our inventory, or balances - * - * @param seller The seller - * @param sellerInventory The seller's inventory - * @param loc2Drop Location to drop items if inventory are full - * @param amount The amount to sell - */ - @Override - 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 - public void setDirty() { - - this.dirty = true; - } - - /** - * Save the extra data to the shop. - * - * @param plugin Plugin instace - * @param data The data table - */ - @Override - public void setExtra(@NotNull final Plugin plugin, @NotNull final ConfigurationSection data) { - - if(this.extra == null) { - this.extra = new YamlConfiguration(); - } - extra.set(plugin.getName(), data); - // compress extra to null if possible - boolean anyValid = false; - for(final String key : extra.getKeys(false)) { - if(!extra.isConfigurationSection(key)) { - anyValid = true; - break; - } - final ConfigurationSection section = extra.getConfigurationSection(key); - if(section == null) continue; - if(!section.getKeys(false).isEmpty()) { - anyValid = true; - break; - } - } - if(!anyValid) { - this.extra = null; - } - setDirty(); - } - - @Override - public void setInventory(@NotNull final InventoryWrapper wrapper, @NotNull final InventoryWrapperManager manager) { - - final String provider = plugin.getInventoryWrapperRegistry().find(manager); - if(provider == null) { - throw new IllegalArgumentException("The manager " + manager.getClass().getName() + " not registered in registry."); - } - this.inventoryWrapperProvider = provider; - this.symbolLink = manager.mklink(wrapper); - setDirty(); - Log.debug("Inventory changed: " + this.symbolLink + ", wrapper provider:" + inventoryWrapperProvider); - new ShopInventoryChangedEvent(wrapper, manager).callEvent(); - } - - @Override - public void setPlayerGroup(@NotNull final UUID player, @Nullable String group) { - - if(group == null) { - group = BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode(); - } - - ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.PRE, this, player, getPlayerGroup(player), group); - event.callEvent(); - - if(group.equals(BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode())) { - - this.playerGroup.remove(player); - } else { - - this.playerGroup.put(player, group); - } - event = event.clone(Phase.POST); - event.callEvent(); - - setDirty(); - } - - @Override - public void setPlayerGroup(@NotNull final UUID player, @Nullable BuiltInShopPermissionGroup group) { - - if(group == null) { - group = BuiltInShopPermissionGroup.EVERYONE; - } - - ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.PRE, this, player, getPlayerGroup(player), group.getNamespacedNode()); - event.callEvent(); - if(group == BuiltInShopPermissionGroup.EVERYONE) { - - this.playerGroup.remove(player); - } else { - - setPlayerGroup(player, group.getNamespacedNode()); - } - - event = event.clone(Phase.POST); - event.callEvent(); - setDirty(); - } - - /** - * Updates signs attached to the shop - */ - @Override - public void setSignText() { - - //Util.ensureThread(false); - if(!Util.isLoaded(this.location)) { - return; - } - QuickShop.folia().getScheduler().runAtLocation(this.location, (consumer)->{ - this.setSignText(getSignText(plugin.getTextManager().findRelativeLanguages(MsgUtil.getDefaultGameLanguageCode()))); - }); - } - - /** - * Changes all lines of text on a sign near the shop - * - * @param lines The array of lines to change. Index is line number. - */ - @Override - public void setSignText(@NotNull final List lines) { - - Util.ensureThread(false); - Log.debug("Globally sign text setting..."); - final List signs = this.getSigns(); - - final ShopSignLinesEvent event = new ShopSignLinesEvent(Phase.POST, this, lines); - event.callEvent(); - - for(final Sign sign : signs) { - - final DyeColor dyeColor = Util.getDyeColor(); - if(dyeColor != null) { - sign.setColor(dyeColor); - } - final boolean isGlowing = plugin.getConfig().getBoolean("shop.sign-glowing", false); - final boolean isWaxed = plugin.getConfig().getBoolean("shop.sign-wax", false); - - sign.setGlowingText(isGlowing); - sign.setWaxed(isWaxed); - sign.update(true); - plugin.platform().setLines(sign, event.updated()); - - new ShopSignUpdateEvent(this, sign).callEvent(); - } - if(plugin.getSignHooker() != null) { - Log.debug("Start sign broadcast..."); - plugin.getSignHooker().updatePerPlayerShopSignBroadcast(this.bukkitLocation(), this); - Log.debug("Sign broadcast completed."); - } - } - - /** - * Updates signs attached to the shop - * - * @param locale The locale to use for the sign text - */ - @Override - public void setSignText(@NotNull final ProxiedLocale locale) { - - //Util.ensureThread(false); - if(!Util.isLoaded(this.location)) { - return; - } - - QuickShop.folia().getScheduler().runAtLocation(this.location, (consumer)->{ - this.setSignText(getSignText(locale)); - }); - } - - /** - * Updates the shop into the database. - */ - @Override - @NotNull - public CompletableFuture update() { - - //Warning! This method can be run in async thread. - if(updating) { - return CompletableFuture.completedFuture(null); - } - - if(this.shopId == -1) { - Log.debug("Skip shop database update because it not fully setup!"); - return CompletableFuture.completedFuture(null); - } - - ShopDatabaseEvent event = new ShopDatabaseEvent(Phase.PRE_CANCELLABLE, this); - - if(event.callCancellableEvent()) { - - Log.debug("The Shop update action was canceled by a plugin."); - return CompletableFuture.completedFuture(null); - } - - event = event.clone(Phase.POST); - event.callEvent(); - - //If already updating, just return the same future - if(!updatingAtomic.compareAndSet(false, true)) { - return inFlightUpdate != null ? inFlightUpdate : CompletableFuture.completedFuture(null); - } - - //Start a new update - final CompletableFuture f = plugin.getDatabaseHelper().updateShop(this) - .whenComplete((r, th) -> { - updatingAtomic.set(false); - if (th == null) { - dirty = false; - } else { - plugin.logger().warn("Could not update shop in DB!", th); - } - }); - - inFlightUpdate = f; - return f; - } - - @Override - public void updateSync() throws RuntimeException { - final CompletableFuture future = update(); - - waitForFuture(future, 15, TimeUnit.SECONDS, "updateShop(" + shopId + ")"); - } - - @Override - public @NotNull BenefitProvider getShopBenefit() { - - final ShopBenefitEvent event = ShopBenefitEvent.RETRIEVE(this, this.benefit); - event.callEvent(); - - return event.updated(); - } - - @Override - public void setShopBenefit(@NotNull final BenefitProvider benefit) { - - this.benefit = benefit; - setDirty(); - } - - public @NotNull SimpleDataRecord createDataRecord() { - - return new SimpleDataRecord( - getOwner(), - plugin.platform().encodeStack(getItem()), - plugin.platform().encodeStack(getItem()), - getShopName(), - shopType().id(), - shopState().identifier(), - getCurrency(), - getPrice(), - isUnlimited(), - isDisableDisplay(), - getTaxAccount(), - JsonUtil.getGson().toJson(getPermissionAudiences()), - saveExtraToYaml(), - getInventoryWrapperProvider(), - saveToSymbolLink(), - new Date(), - getShopBenefit().serialize() - ); - } - - /** - * Returns the display item associated with this shop. - * - * @return The display item associated with this shop. - */ - @Nullable - public AbstractDisplayItem getDisplayItem() { - - return this.displayItem; - } - - /** - * @return The enchantments the shop has on its items. - */ - public @NotNull Map getEnchants() { - - if(this.item.hasItemMeta() && this.item.getItemMeta().hasEnchants()) { - return Objects.requireNonNull(this.item.getItemMeta()).getEnchants(); - } - return Collections.emptyMap(); - } - - /** - * @return The ItemStack type of this shop - */ - public @NotNull Material getMaterial() { - - return this.item.getType(); - } - private @NotNull InventoryWrapper locateInventory(@Nullable final String symbolLink) { if(symbolLink == null || symbolLink.isEmpty()) { @@ -1976,53 +464,4 @@ public AbstractDisplayItem getDisplayItem() { throw new IllegalStateException("Failed load shop data, the InventoryWrapper provider " + getInventoryWrapperProvider() + " returns error: " + e.getMessage(), e); } } - - @Override - public ReloadResult reloadModule() throws Exception { - - if(!plugin.isAllowStack()) { - this.item.setAmount(1); - } else { - this.item.setAmount(this.originalItem.getAmount()); - } - return Reloadable.super.reloadModule(); - } - - private ShopSignStorage saveToShopSignStorage() { - - return new ShopSignStorage(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); - } - - @Override - public String toString() { - - return "ContainerShop{" + - "location=" + location + - ", plugin=" + plugin + - ", runtimeRandomUniqueId=" + runtimeRandomUniqueId + - ", playerGroup=" + playerGroup + - ", isDeleted=" + isDeleted + - ", extra=" + extra + - ", shopId=" + shopId + - ", owner=" + owner + - ", price=" + price + - ", shopType=" + shopType + - ", unlimited=" + unlimited + - ", item=" + item + - ", originalItem=" + originalItem + - ", displayItem=" + displayItem + - ", isLoaded=" + isLoaded + - ", createBackup=" + createBackup + - ", inventoryPreview=" + inventoryPreview + - ", dirty=" + dirty + - ", updating=" + updating + - ", currency='" + currency + '\'' + - ", disableDisplay=" + disableDisplay + - ", taxAccount=" + taxAccount + - ", inventoryWrapperProvider='" + inventoryWrapperProvider + '\'' + - ", symbolLink='" + symbolLink + '\'' + - ", shopName='" + shopName + '\'' + - ", benefit=" + benefit + - '}'; - } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ModernContainerShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ModernContainerShop.java new file mode 100644 index 0000000000..0bf4774b57 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ModernContainerShop.java @@ -0,0 +1,280 @@ +package com.ghostchu.quickshop.shop; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.database.bean.DataRecord; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopInfoStorage; +import com.ghostchu.quickshop.api.shop.builder.ShopBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopInteraction; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import com.ghostchu.quickshop.api.shop.components.ShopLifecycle; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; +import com.ghostchu.quickshop.api.shop.components.ShopTrading; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.common.util.JsonUtil; +import com.ghostchu.quickshop.database.bean.SimpleDataRecord; +import com.ghostchu.quickshop.shop.components.SimpleShopInteraction; +import com.ghostchu.quickshop.api.shop.ShopSignStorage; +import com.ghostchu.quickshop.shop.components.SimpleShopItem; +import com.ghostchu.quickshop.shop.components.SimpleShopLifecycle; +import com.ghostchu.quickshop.shop.components.SimpleShopMeta; +import com.ghostchu.quickshop.shop.components.SimpleShopPermission; +import com.ghostchu.quickshop.shop.components.SimpleShopPrice; +import com.ghostchu.quickshop.shop.components.SimpleShopTrading; +import com.ghostchu.simplereloadlib.ReloadResult; +import com.ghostchu.simplereloadlib.Reloadable; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Date; +import java.util.EnumSet; +import java.util.Objects; +import java.util.UUID; + +@SuppressWarnings({"deprecation", "removal"}) +public class ModernContainerShop implements ModernShop, Reloadable { + + private final UUID runtimeRandomUniqueId = UUID.randomUUID(); + protected final Location location; + + @NotNull + protected String symbolLink; + + + protected SimpleShopItem item; + protected SimpleShopInteraction interaction; + protected SimpleShopLifecycle lifecycle; + protected SimpleShopMeta meta; + protected SimpleShopPermission permission; + protected SimpleShopPrice price; + protected SimpleShopTrading trading; + + public ModernContainerShop(final Location location) { + + this.location = location; + } + + @Override + public @NotNull UUID getRuntimeRandomUniqueId() { + + return this.runtimeRandomUniqueId; + } + + @Override + public ShopItem item() { + + return item; + } + + @Override + public ShopInteraction interaction() { + + return interaction; + } + + @Override + public ShopLifecycle lifecycle() { + + return lifecycle; + } + + @Override + public ShopMeta meta() { + + return meta; + } + + @Override + public ShopPermission permission() { + + return permission; + } + + @Override + public ShopPrice price() { + + return price; + } + + @Override + public ShopTrading trading() { + + return trading; + } + + @Override + public Location getLocation() { + + return location; + } + + @Override + public Location bukkitLocation() { + + return location; + } + + @Override + public ShopSignStorage asShopSignStorage() { + + return new ShopSignStorage(this.bukkitLocation().getWorld().getName(), + this.bukkitLocation().getBlockX(), + this.bukkitLocation().getBlockY(), + this.bukkitLocation().getBlockZ()); + } + + @Override + public @NotNull DataRecord asDataRecord() { + + return new SimpleDataRecord( + meta.getOwner(), + item.encodedItem(), + item.encodedItem(), + meta.getShopName(), + meta.shopType().id(), + meta.shopState().identifier(), + price.getCurrency(), + price.price(), + meta.isUnlimited(), + item.isDisableDisplay(), + meta.getTaxAccount(), + JsonUtil.getGson().toJson(permission.getPermissionAudiences()), + meta.saveExtraToYaml(), + meta.getInventoryWrapperProvider(), + asSymbolLink(), + new Date(), + meta.getShopBenefit().serialize() + ); + } + + /** + * Getting ShopInfoStorage that you can use for storage the shop data + * + * @return ShopInfoStorage + */ + @Override + public ShopInfoStorage asInfoStorage() { + + return ShopInfoStorage.fromShop(this); + } + + /** + * Gets the symbol link that created by InventoryWrapperManager + * + * @return InventoryWrapper + */ + @Override + public @NotNull String asSymbolLink() { + + return symbolLink; + } + + /** + * Compares the current {@code ModernShop} instance with another provided instance and determines + * the set of differences between them. These differences are represented as a set of + * {@code ShopChangeType} values, where each type corresponds to a category of change (e.g., item, + * price, owner, etc.). + * + * @param compare The {@code ModernShop} instance to compare against. If {@code null}, the method + * assumes comparison with a non-existent or empty shop. + * + * @return An {@code EnumSet} of {@code ShopChangeType} values that represent the changes detected + * between the current shop instance and the provided shop. If no changes are detected, an empty + * set is returned. + */ + @Override + public EnumSet diff(final @Nullable ModernShop compare) { + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + changes.addAll(compare.item().diff((compare == null)? null : compare.item())); + changes.addAll(compare.meta().diff((compare == null)? null : compare.meta())); + changes.addAll(compare.permission().diff((compare == null)? null : compare.permission())); + changes.addAll(compare.price().diff((compare == null)? null : compare.price())); + + if(!Objects.equals(compare.asSymbolLink(), this.asSymbolLink())) { + changes.add(ShopChangeType.SYMBOL_LINK); + } + + if(!this.location.equals(compare.getLocation())) { + changes.add(ShopChangeType.LOCATION); + } + + if(!Objects.equals(compare.getRuntimeRandomUniqueId(), this.getRuntimeRandomUniqueId())) { + changes.add(ShopChangeType.RUNTIME_ID); + } + + return changes; + } + + @Override + public ShopBuilder builder() { + + return null; + } + + @Override + public ReloadResult reloadModule() throws Exception { + + if(!QuickShop.getInstance().isAllowStack()) { + this.item.setAmount(1); + } else { + this.item.setAmount(this.originalItem.getAmount()); + } + return Reloadable.super.reloadModule(); + } + + @Override + public boolean equals(final Object o) { + + if(!(o instanceof final ModernContainerShop that)) return false; + return Objects.equals(location, that.location) && Objects.equals(symbolLink, that.symbolLink) + && Objects.equals(item, that.item) && Objects.equals(interaction, that.interaction) + && Objects.equals(lifecycle, that.lifecycle) && Objects.equals(meta, that.meta) + && Objects.equals(permission, that.permission) && Objects.equals(price, that.price) + && Objects.equals(trading, that.trading); + } + + @Override + public int hashCode() { + + return Objects.hash(location, symbolLink, item, interaction, lifecycle, meta, permission, price, trading); + } + + @Override + public String toString() { + + return "ContainerShop{" + + "location=" + location + + ", plugin=" + QuickShop.getInstance() + + ", runtimeRandomUniqueId=" + runtimeRandomUniqueId + + ", playerGroup=" + permission.getPermissionAudiences() + + ", isDeleted=" + lifecycle.isDeleted() + + ", extra=" + meta.getExtra(QuickShop.getInstance().getJavaPlugin()) + + ", shopId=" + meta.getShopId() + + ", owner=" + meta.getOwner() + + ", price=" + price + + ", shopType=" + meta.shopType() + + ", shopState=" + meta.shopState() + + ", unlimited=" + meta.isUnlimited() + + ", item=" + item.getItem() + + ", originalItem=" + item.getItem() + + ", displayItem=" + item.getDisplayItem() + + ", isLoaded=" + lifecycle.isLoaded() + + ", createBackup=" + createBackup + + ", inventoryPreview=" + inventoryPreview + + ", dirty=" + lifecycle.isDirty() + + ", updating=" + updating + + ", currency='" + price.getCurrency() + '\'' + + ", disableDisplay=" + item.isDisableDisplay() + + ", taxAccount=" + meta.getTaxAccount() + + ", inventoryWrapperProvider='" + meta.getInventoryWrapperProvider() + '\'' + + ", symbolLink='" + symbolLink + '\'' + + ", shopName='" + meta.getShopName() + '\'' + + ", benefit=" + meta.getShopBenefit() + + '}'; + } +} 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 a0a8eac362..9a0faa2f65 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 @@ -7,7 +7,6 @@ import com.ghostchu.quickshop.database.SimpleDatabaseHelperV2; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; -import com.ghostchu.quickshop.util.performance.BatchBukkitExecutor; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiter.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiter.java index c17bfa86b7..bb7c316a69 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiter.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiter.java @@ -4,17 +4,18 @@ import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.registry.BuiltInRegistry; import com.ghostchu.quickshop.api.registry.builtin.itemexpression.ItemExpressionRegistry; -import com.ghostchu.quickshop.api.shop.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; +import com.ghostchu.quickshop.api.shop.limit.RuleSet; import com.ghostchu.quickshop.common.util.CommonUtil; +import com.ghostchu.quickshop.shop.limit.SimpleRuleSet; import com.ghostchu.quickshop.util.ItemContainerUtil; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.paste.item.SubPasteItem; import com.ghostchu.quickshop.util.paste.util.HTMLTable; import com.ghostchu.simplereloadlib.ReloadResult; import com.ghostchu.simplereloadlib.Reloadable; -import lombok.Data; import org.bukkit.command.CommandSender; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; @@ -30,18 +31,21 @@ import java.math.RoundingMode; import java.nio.file.Files; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; import java.util.function.Function; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -public class SimplePriceLimiter implements Reloadable, PriceLimiter, SubPasteItem { +public class SimplePriceLimiter implements Reloadable, PriceLimiter, SubPasteItem { + + private final Map rules = new LinkedHashMap<>(); private final QuickShop plugin; - private final Map rules = new LinkedHashMap<>(); private boolean wholeNumberOnly = false; private double undefinedMin = 0.0d; private double undefinedMax = Double.MAX_VALUE; @@ -86,7 +90,7 @@ public void loadConfiguration() { return; } for(final String ruleName : rules.getKeys(false)) { - final RuleSet rule = readRule(ruleName, rules.getConfigurationSection(ruleName)); + final SimpleRuleSet rule = readRule(ruleName, rules.getConfigurationSection(ruleName)); if(rule == null) { plugin.logger().warn("Failed to read rule {}, syntax invalid! Skipping...", ruleName); continue; @@ -127,7 +131,7 @@ private boolean performMigrate(@NotNull final FileConfiguration configuration) { @Nullable @Contract("_,null -> null") - private RuleSet readRule(@NotNull final String ruleName, @Nullable final ConfigurationSection section) { + private SimpleRuleSet readRule(@NotNull final String ruleName, @Nullable final ConfigurationSection section) { if(section == null) { return null; @@ -149,7 +153,91 @@ private RuleSet readRule(@NotNull final String ruleName, @Nullable final Configu plugin.logger().warn("Failed to read rule {}'s a Currency option, invalid pattern {}! Skipping...", ruleName, currencyStr1); } } - return new RuleSet(items, bypassPermission, currency, min, max); + return new SimpleRuleSet(items, bypassPermission, currency, min, max); + } + + @Override + public Set> findApplicableRules(@NotNull final ItemStack stack) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(stack)) { + applicableRules.add(rule); + } + } + return applicableRules; + } + + @Override + public Set> findApplicableRules(@NotNull final CommandSender sender, @NotNull final ItemStack stack) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(sender, stack)) { + applicableRules.add(rule); + } + } + return applicableRules; + } + + @Override + public Set> findApplicableRules(@NotNull final QUser user, @NotNull final ItemStack stack) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(user, stack)) { + applicableRules.add(rule); + } + } + return applicableRules; + } + + @Override + public Set> findApplicableRules(@NotNull final ItemStack stack, @NotNull final String currency) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(stack, currency)) { + applicableRules.add(rule); + } + } + return applicableRules; + } + + @Override + public Set> findApplicableRules(@NotNull final CommandSender sender, @NotNull final ItemStack stack, @NotNull final String currency) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(sender, stack, currency)) { + applicableRules.add(rule); + } + } + return applicableRules; + } + + @Override + public Set> findApplicableRules(@NotNull final QUser user, @NotNull final ItemStack stack, @NotNull final String currency) { + + final Set> applicableRules = new HashSet<>(); + + for(final SimpleRuleSet rule : rules.values()) { + + if(rule.isApplicable(user, stack, currency)) { + applicableRules.add(rule); + } + } + return applicableRules; } /** @@ -186,25 +274,25 @@ public PriceLimiterCheckResult check(@NotNull final CommandSender sender, @NotNu boolean hasMaxPrice = false; final List flattenedItems = ItemContainerUtil.flattenContents(itemStack, true, false); - for(final RuleSet rule : rules.values()) { + for(final RuleSet rule : rules.values()) { if(rule.canBypass(sender) || !rule.isApplicableCurrency(currency)) { continue; } // we'll manually add the fist item, as we calculate on a single item basis for the parent item. // otherwise we would be adding up all the items a player is holding, rather than one. - int itemTally = rule.isApply(itemStack)? 1 : 0; + int itemTally = rule.isApplicable(itemStack)? 1 : 0; itemTally += rule.tallyApplicableItems(flattenedItems); if(itemTally == 0) { continue; } if(rule.hasMinPrice()) { - minPrice += rule.getMin() * itemTally; + minPrice += rule.minPrice() * itemTally; } if(rule.hasMaxPrice()) { hasMaxPrice = true; - maxPrice += rule.getMax() * itemTally; + maxPrice += rule.maxPrice() * itemTally; } } @@ -254,25 +342,25 @@ public PriceLimiterCheckResult check(@NotNull final QUser user, @NotNull final I boolean hasMaxPrice = false; final List flattenedItems = ItemContainerUtil.flattenContents(itemStack, true, false); - for(final RuleSet rule : rules.values()) { + for(final RuleSet rule : rules.values()) { if(rule.canBypass(user) || !rule.isApplicableCurrency(currency)) { continue; } // we'll manually add the fist item, as we calculate on a single item basis for the parent item. // otherwise we would be adding up all the items a player is holding, rather than one. - int itemTally = rule.isApply(itemStack)? 1 : 0; + int itemTally = rule.isApplicable(itemStack)? 1 : 0; itemTally += rule.tallyApplicableItems(flattenedItems); if(itemTally == 0) { continue; } if(rule.hasMinPrice()) { - minPrice += rule.getMin() * itemTally; + minPrice += rule.minPrice() * itemTally; } if(rule.hasMaxPrice()) { hasMaxPrice = true; - maxPrice += rule.getMax() * itemTally; + maxPrice += rule.maxPrice() * itemTally; } } @@ -309,13 +397,15 @@ public ReloadResult reloadModule() throws Exception { joiner.add("

Rules
"); final HTMLTable rules = new HTMLTable(5); rules.setTableTitle("Rule Name", "Bypass Permission", "Items", "Currency", "Price Range"); - for(final Map.Entry entry : this.rules.entrySet()) { - final RuleSet rule = entry.getValue(); - String currencies = CommonUtil.list2String(rule.getCurrency()); + + for(final Map.Entry entry : this.rules.entrySet()) { + + final SimpleRuleSet rule = entry.getValue(); + String currencies = CommonUtil.list2String(rule.currencies()); if(CommonUtil.isEmptyString(currencies)) { currencies = "*"; } - rules.insert(entry.getKey(), rule.getBypassPermission(), rule.getItems().size(), currencies, rule.getMin() + " - " + rule.getMax()); + rules.insert(entry.getKey(), rule.bypassPermission(), rule.items().size(), currencies, rule.minPrice() + " - " + rule.maxPrice()); } joiner.add(rules.render()); return joiner.toString(); @@ -326,166 +416,4 @@ public ReloadResult reloadModule() throws Exception { return "Price Limiter"; } - - @Data - static class RuleSet { - - private final List> items; - private final String bypassPermission; - private final List currency; - private final double min; - private final double max; - - public RuleSet(final List> items, final String bypassPermission, final List currency, final double min, final double max) { - - this.items = items; - this.bypassPermission = bypassPermission; - this.currency = currency; - this.min = min; - this.max = max; - } - - /** - * Check if the rule is allowed to apply to the given price. - * - * @param price the price - * - * @return true if the rule is allowed for given price - */ - public boolean isAllowed(final double price) { - - if(hasMaxPrice() && price > getMax()) { - return false; - } - if(hasMinPrice()) { - return price >= getMin(); - } - return true; - } - - /** - * @return if this rule has a min price set. - */ - public boolean hasMinPrice() { - - return getMin() > 0; - } - - /** - * @return if this rule has a max price set. - */ - public boolean hasMaxPrice() { - - return getMax() >= 0; - } - - /** - * Tallies the number of items this rules applies to. - * - * @param stacks the items to tally - * - * @return the sum of the item counts this rules applies to - */ - public int tallyApplicableItems(@NotNull final Iterable stacks) { - - int tally = 0; - for(final ItemStack is : stacks) { - if(isApply(is)) { - tally += is.getAmount(); - } - } - return tally; - } - - /** - * Checks if the provided CommandSender can bypass restrictions - * - * @param sender the CommandSender to check - * - * @return true if they can bypass, otherwise false. - */ - public boolean canBypass(@NotNull final CommandSender sender) { - - return QuickShop.getPermissionManager().hasPermission(sender, this.bypassPermission); - } - - /** - * Checks if the provided QUser can bypass restrictions - * - * @param user the QUser to check - * - * @return true if they can bypass, otherwise false. - */ - public boolean canBypass(@NotNull final QUser user) { - - return QuickShop.getPermissionManager().hasPermission(user, this.bypassPermission); - } - - /** - * Checks if the currency applies to this rule. Will return true if the currency is null - * - * @param currency the currency to check - * - * @return true if the currency either applies, or is null. false otherwise. - */ - public boolean isApplicableCurrency(@Nullable final String currency) { - - if(currency != null) { - return this.currency.stream().anyMatch(pattern->pattern.matcher(currency).matches()); - } - return true; - } - - /** - * Check if the rule is allowed to apply to the given price. - * - * @param sender the sender - * @param item the item - * @param currency the currency - * - * @return true if the rule is allowed to apply - */ - public boolean isApply(@NotNull final CommandSender sender, @NotNull final ItemStack item, @Nullable final String currency) { - - if(canBypass(sender) || !isApplicableCurrency(currency)) { - return false; - } - return isApply(item); - } - - /** - * Check if the rule is allowed to apply to the given price. - * - * @param user the user - * @param item the item - * @param currency the currency - * - * @return true if the rule is allowed to apply - */ - public boolean isApply(@NotNull final QUser user, @NotNull final ItemStack item, @Nullable final String currency) { - - if(canBypass(user) || !isApplicableCurrency(currency)) { - return false; - } - return isApply(item); - } - - /** - * Check if a rule applies to an ItemStack - * - * @param stack the stack to check - * - * @return true if it applies, otherwise false. - */ - public boolean isApply(@NotNull final ItemStack stack) { - - for(final Function fun : items) { - if(fun.apply(stack)) { - return true; - } - } - return false; - } - - } } \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiterCheckResult.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiterCheckResult.java index 4e94d640af..dc8116d1a6 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiterCheckResult.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimplePriceLimiterCheckResult.java @@ -1,7 +1,7 @@ package com.ghostchu.quickshop.shop; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.PriceLimiterStatus; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; import lombok.Data; @Data 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 78ae5db758..419aaddbfb 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 @@ -19,12 +19,12 @@ import com.ghostchu.quickshop.api.shop.IShopLayoutProvider; import com.ghostchu.quickshop.api.shop.IShopType; import com.ghostchu.quickshop.api.shop.Info; -import com.ghostchu.quickshop.api.shop.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.ShopChunk; import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.cache.ShopCacheNamespacedKey; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; 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; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopService.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopService.java new file mode 100644 index 0000000000..64ae42110d --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopService.java @@ -0,0 +1,515 @@ +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.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopBuilderFactory; +import com.ghostchu.quickshop.api.shop.ShopChunk; +import com.ghostchu.quickshop.api.shop.ShopService; +import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; +import com.ghostchu.quickshop.api.shop.service.ShopActionResult; +import com.ghostchu.quickshop.api.shop.service.request.ShopCreateRequest; +import com.ghostchu.quickshop.api.shop.service.request.ShopDeleteRequest; +import com.ghostchu.quickshop.api.shop.service.request.ShopUpdateRequest; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.api.shop.service.result.ShopCreateResult; +import com.ghostchu.quickshop.api.shop.service.result.ShopDeleteResult; +import com.ghostchu.quickshop.api.shop.service.result.ShopUpdateResult; +import com.ghostchu.quickshop.util.performance.PerfMonitor; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * SimpleShopService + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SimpleShopService implements ShopService { + + + protected final Map>> shops = Maps.newConcurrentMap(); + protected final Set loadedShops = Sets.newConcurrentHashSet(); + + /** + * Retrieves the ShopBuilderFactory instance associated with the ShopService implementation. + * + * @return ShopBuilderFactory instance responsible for providing builder objects for shop-related + * components, such as ShopItemBuilder, ShopMetaBuilder, ShopPermissionBuilder, and + * ShopPriceBuilder. + */ + @Override + public ShopBuilderFactory shopBuilderFactory() { + + return null; + } + + /** + * Creates a new shop based on the provided creation request. + * + * @param request The request object containing all necessary information for shop creation, + * including the actor initiating the operation and the shop details. + * + * @return A result object encapsulating details about the shop creation operation, including the + * success status, the created shop instance (if successful), and potential failure reasons if the + * operation failed. + */ + @Override + public @NotNull ShopActionResult createShop(final @NotNull ShopCreateRequest request) { + + return null; + } + + /** + * Updates an existing shop based on the provided update request. + * + * @param request The request object containing the necessary details to update a shop, including + * the actor performing the operation and the shop to be updated. + * + * @return A result object containing details about the update operation, including the success + * status, any changes made, and potential failure reasons if the operation was unsuccessful. + */ + @Override + public @NotNull ShopActionResult updateShop(final @NotNull ShopUpdateRequest request) { + + //TODO: shopManager.findShop(request.shop().meta().shopId()) + final ModernContainerShop originalShop = null; + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + changes.addAll(request.shop().item().diff((originalShop == null)? null : originalShop.item())); + changes.addAll(request.shop().meta().diff((originalShop == null)? null : originalShop.meta())); + changes.addAll(request.shop().permission().diff((originalShop == null)? null : originalShop.permission())); + changes.addAll(request.shop().price().diff((originalShop == null)? null : originalShop.price())); + + changes.addAll(originalShop.diff((ModernShop)request.shop())); + + final ShopUpdateResult result = new ShopUpdateResult(changes, originalShop, request.shop()); + + if(!request.options().checkPermissions()) { + return new ShopActionResult<>(result, true, null); + } + + final CommandSender actor = request.actor(); + if(actor == null) { + return new ShopActionResult<>(result, true, null); + } + // return result + + return null; + } + + /** + * Deletes a shop based on the provided delete request. + * + * @param request The request object containing the necessary details to delete a shop, such as + * the actor performing the operation and the target shop ID. + * + * @return A result object containing information about the outcome of the delete operation, + * including success status and possible failure reasons. + */ + @Override + public @NotNull ShopActionResult deleteShop(final @NotNull ShopDeleteRequest request) { + + return null; + } + + /** + * Retrieves a list of all the shops available across all shop chunks and locations. + * This method collects and consolidates the shops from the internal data structure + * into a single unmodifiable list. + * + * Performance is monitored while the method executes using a performance monitor. + * + * @return a list of all {@link ModernShop} instances across all shop chunks and locations, + * wrapped in an unmodifiable list. + */ + @Override + public @NotNull List getAllShops() { + + try(final PerfMonitor ignored = new PerfMonitor("Getting all shops")) { + final List shopsCollected = new ArrayList<>(); + + for(final Map> shopMapData : getShops().values()) { + for(final Map shopData : shopMapData.values()) { + + shopsCollected.addAll(shopData.values()); + } + } + return Collections.unmodifiableList(shopsCollected); + } + } + + /** + * Retrieves an unmodifiable set of all currently loaded shops. + * + * @return a non-null unmodifiable set containing the loaded shops. + */ + @Override + public @NotNull Set getLoadedShops() { + + return Collections.unmodifiableSet(this.loadedShops); + } + + /** + * Retrieves all shops that belong to the specified user. + * + * @param user the user whose shops are to be retrieved; must not be null + * @return a list of shops owned by the specified user, never null + */ + @Override + public @NotNull List getAllShops(@NotNull final QUser user) { + + final List playerShops = new ArrayList<>(10); + for(final ModernContainerShop shop : getAllShops()) { + if(shop.meta().getOwner().equals(user)) { + playerShops.add(shop); + } + } + return playerShops; + } + /** + * Retrieves a list of all shops owned by the player with the given UUID. + * + * @param playerUUID the UUID of the player whose shops are to be retrieved. Must not be null. + * @return a list of ModernShop objects owned by the specified player. Never null but may be empty if no shops are found. + */ + @Override + public @NotNull List getAllShops(@NotNull final UUID playerUUID) { + + final List playerShops = new ArrayList<>(10); + for(final ModernContainerShop shop : getAllShops()) { + final UUID shopUuid = shop.meta().getOwner().getUniqueIdIfRealPlayer().orElse(null); + if(playerUUID.equals(shopUuid)) { + + playerShops.add(shop); + } + } + return playerShops; + } + + /** + * Gets a shop by shop Id + * + * @return The shop object + */ + @Override + public @Nullable ModernContainerShop getShop(final long shopId) { + + for(final ModernContainerShop shop : getAllShops()) { + if(shop.meta().getShopId() == shopId) { + return shop; + } + } + return null; + } + + /** + * Gets a shop in a specific location ATTENTION: This not include attached shops (double-chest) + * + * @param loc The location to get the shop from + * + * @return The shop at that location + */ + @Override + public @Nullable ModernContainerShop getShop(@NotNull final Location loc) { + + return null; + } + + /** + * Gets a shop in a specific location but via cache ATTENTION: This not include attached shops + * (double-chest) + * + * @param loc The location to get the shop from + * + * @return The shop at that location but via cache + */ + @Override + public @Nullable ModernContainerShop getShopViaCache(@NotNull final Location loc) { + + return null; + } + + /** + * Gets a shop in a specific location ATTENTION: This not include attached shops (double-chest) + * + * @param loc The location to get the shop from + * @param skipShopableChecking whether to check is shopable + * + * @return The shop at that location + */ + @Override + public @Nullable ModernContainerShop getShop(@NotNull final Location loc, final boolean skipShopableChecking) { + + return null; + } + + @Override + public @Nullable ModernContainerShop getShopFromRuntimeRandomUniqueId(@NotNull final UUID runtimeRandomUniqueId) { + + return null; + } + + @Override + public @Nullable ModernContainerShop getShopFromRuntimeRandomUniqueId(@NotNull final UUID runtimeRandomUniqueId, final boolean includeInvalid) { + + return null; + } + + /** + * Gets a shop in a specific location Include the attached shop, e.g DoubleChest shop. + * + * @param loc The location to get the shop from + * + * @return The shop at that location + */ + @Override + public @Nullable ModernContainerShop getShopIncludeAttached(@Nullable final Location loc) { + + return null; + } + + /** + * Gets a shop in a specific location Include the attached shop, e.g DoubleChest shop. but via + * cache + * + * @param loc The location to get the shop from + * + * @return The shop at that location but via cache + */ + @Override + public @Nullable ModernContainerShop getShopIncludeAttachedViaCache(@Nullable final Location loc) { + + return null; + } + + /** + * Returns a new shop iterator object, allowing iteration over shops easily, instead of sorting + * through a 3D map. + * + * @return a new shop iterator object. + */ + @Override + public @NotNull Iterator getShopIterator() { + + return null; + } + + /** + * Returns a map of World - Chunk - Shop + * + * @return a map of World - Chunk - Shop + */ + @Override + public @NotNull Map>> getShops() { + + return Map.of(); + } + + /** + * Returns a map of Shops + * + * @param c The chunk to search. Referencing doesn't matter, only coordinates and world are used. + * + * @return Shops + */ + @Override + public @NotNull Map getShops(@NotNull final Chunk c) { + + return Map.of(); + } + + /** + * Gets the shop at the world and specific chunk. + * + * @param world The world to get the shop from + * @param chunkX The chunk x coordinate + * @param chunkZ The chunk z coordinate + * + * @return The shop at the world and specific chunk. + */ + @Override + public @NotNull Map getShops(@NotNull final String world, final int chunkX, final int chunkZ) { + + return Map.of(); + } + + /** + * Gets the shop at the world and specific chunk. + * + * @param shopChunk The shop chunk + * + * @return The shop at the world and specific chunk. + */ + @Override + public @NotNull Map getShops(@NotNull final ShopChunk shopChunk) { + + return Map.of(); + } + + /** + * Returns a map of Chunk - Shop + * + * @param world The name of the world (case sensitive) to get the list of shops from + * + * @return a map of Chunk - Shop + */ + @Override + public @NotNull Map> getShops(@NotNull final String world) { + + return Map.of(); + } + + /** + * Get the all shops in the world. + * + * @param world The world you want get the shops. + * + * @return The list have this world all shops + */ + @Override + public @NotNull List getShopsInWorld(@NotNull final World world) { + + return List.of(); + } + + /** + * Get the all shops in the world. + * + * @param worldName The world you want get the shops. + * + * @return The list have this world all shops + */ + @Override + public @NotNull List getShopsInWorld(@NotNull final String worldName) { + + return List.of(); + } + + /** + * Queries the database to retrieve the inventory cache for the specified shop. + * + * @param shop The shop instance for which the inventory cache is being queried. Must not be + * null. + * + * @return A CompletableFuture that completes with the ShopInventoryCountCache containing + * inventory information for the given shop. The result is guaranteed to be non-null. + */ + @Override + public @NotNull CompletableFuture<@NotNull ShopInventoryCountCache> queryShopInventoryCacheInDatabase(@NonNull final ModernContainerShop shop) { + + return null; + } + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + */ + @Override + public void loadShop(@NonNull final ModernContainerShop shop) { + + } + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + */ + @Override + public void unloadShop(@NonNull final ModernContainerShop shop) { + + } + + /** + * Load shop method for loading shop into mapping, so getShops method will can find it. It also + * effects a lots of feature, make sure load it after create it. + * + * @param shop The shop to load + * @param chunkUnloading If unloadShop called caused by chunk unloading, when this is true, + * QuickShop will try avoid any main-thread opreations to avoid + * load-unload-load loop + */ + @Override + public void unloadShop(@NonNull final ModernContainerShop shop, final boolean chunkUnloading) { + + } + + @Override + public ShopActionResult handleLoading() { + + return null; + } + + @Override + public ShopActionResult handleUnloading(final boolean dontTouchWorld) { + + return null; + } + + /** + * Registers a shop with the system and optionally persists the shop information. + * + * @param shop the shop object to be registered; must not be null + * @param persist a flag indicating whether the shop should be persisted to storage + * + * @return a CompletableFuture representing the asynchronous operation of registering the shop + */ + @Override + public CompletableFuture registerShop(@NonNull final ModernContainerShop shop, final boolean persist) { + + return null; + } + + /** + * Unregisters the specified shop from the system. If persistence is enabled, the removal will be + * reflected in the underlying storage to ensure the shop is no longer persisted. + * + * @param shop the shop instance to be unregistered; must not be null + * @param persist indicates whether the unregister operation should be persisted in the storage + * + * @return a CompletableFuture representing the asynchronous operation of unregistering the shop + */ + @Override + public CompletableFuture unregisterShop(@NonNull final ModernContainerShop shop, final boolean persist) { + + return null; + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopWorldAdapter.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopWorldAdapter.java new file mode 100644 index 0000000000..ac2c8a0c0e --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopWorldAdapter.java @@ -0,0 +1,367 @@ +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.event.Phase; +import com.ghostchu.quickshop.api.event.general.ShopSignUpdateEvent; +import com.ghostchu.quickshop.api.event.settings.type.ShopSignLinesEvent; +import com.ghostchu.quickshop.api.localization.text.ProxiedLocale; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.Info; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.ShopWorldAdapter; +import com.ghostchu.quickshop.shop.datatype.ShopSignPersistentDataType; +import com.ghostchu.quickshop.util.MsgUtil; +import com.ghostchu.quickshop.util.Util; +import com.ghostchu.quickshop.util.logger.Log; +import net.kyori.adventure.text.Component; +import org.bukkit.DyeColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import static com.ghostchu.quickshop.api.shop.Shop.SHOP_NAMESPACED_KEY; + +/** + * SimpleShopWorldAdapter + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SimpleShopWorldAdapter implements ShopWorldAdapter { + + // 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"); + private static final String LEGACY_SHOP_SIGN_RECOGNIZE_PATTERN = "§d§o "; + + /** + * Checks whether the specified shop instance is valid. This method determines if the given shop + * meets the necessary criteria for being considered a valid shop within the system. + * + * @param shop The shop instance to validate, represented by a {@code ModernShop}. + * + * @return {@code true} if the shop is valid, {@code false} otherwise. + * + * @since 6.3.0.0 + */ + @Override + public boolean isValidShop(final @NotNull ModernShop shop) { + + Util.ensureThread(false); + if(shop.lifecycle().isDeleted()) { + return false; + } + return Util.canBeShop(shop.bukkitLocation().getBlock()); + } + + /** + * Check if shop is not valided for specific player + * + * @param uuid The uuid of the player + * @param info The info of the shop + * @param shop The shop + * + * @return If the shop is not valided for the player + */ + @Override + public boolean shopIsNotValid(@NotNull final QUser uuid, @NotNull final Info info, @NotNull final ModernShop shop) { + + return false; + } + + /** + * Determines whether the specified block is attached to the given shop instance. + * + * This method checks if the provided block is associated with the given shop based on certain + * conditions, which could involve physical proximity, structural linkage, or other criteria + * defined within the system. + * + * @param shop The shop instance to check against, represented by a + * {@code ModernShop}. Must not be null. + * @param b The block to verify as being attached. Must not be null. + * + * @return {@code true} if the block is attached to the specified shop, {@code false} otherwise. + * + * @since 6.3.0.0 + */ + @Override + public boolean isAttached(final @NotNull ModernShop shop, final @NotNull Block b) { + + Util.ensureThread(false); + return shop.bukkitLocation().getBlock().equals(Util.getAttached(b)); + } + + /** + * Ensures that the display location for the specified shop is correctly handled. This method may + * involve verifying the shop's display location, teleporting entities, or respawning display + * objects as required to maintain consistency. + * + * @param shop The shop instance whose display location is to be checked and updated, represented + * by a {@code ModernShop}. + * + * @since 6.3.0.0 + */ + @Override + public void checkDisplay(final @NotNull ModernShop shop) { + + } + + /** + * Claim a sign as shop sign (modern method) + * + * @param sign The shop sign + */ + @Override + public void claimShopSign(final @NotNull ModernShop shop, @NotNull final Sign sign) { + + if(!sign.getPersistentDataContainer().has(SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE)) { + sign.getPersistentDataContainer().set(SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE, shop.asShopSignStorage()); + sign.update(); + } + } + + @Override + public @NotNull BlockState makeShopSign(@NotNull final Block container, @NotNull final Block signBlock, @Nullable final Material signMaterial) { + + return null; + } + + /** + * Get shop signs, may have multi signs + * + * @return Signs for the shop + * + * @since 6.3.0.0 + */ + @Override + public @NotNull List getSigns(final @NotNull ModernShop shop) { + + Util.ensureThread(false); + final List signs = new ArrayList<>(4); + if(shop.bukkitLocation().getWorld() == null) { + + return Collections.emptyList(); + } + + final Block[] blocks = new Block[4]; + blocks[0] = shop.bukkitLocation().getBlock().getRelative(BlockFace.EAST); + blocks[1] = shop.bukkitLocation().getBlock().getRelative(BlockFace.NORTH); + blocks[2] = shop.bukkitLocation().getBlock().getRelative(BlockFace.SOUTH); + blocks[3] = shop.bukkitLocation().getBlock().getRelative(BlockFace.WEST); + for(final Block b : blocks) { + + if(b == null) { + continue; + } + final BlockState state = b.getState(false); + if(!(state instanceof final Sign sign)) { + continue; + } + if(!shop.bukkitLocation().getBlock().equals(Util.getAttached(b))) { + continue; + } + if(isShopSign(shop, sign)) { + claimShopSign(shop, sign); + signs.add(sign); + } + } + + return signs; + } + + /** + * Checks if a Sign is a ShopSign + * + * @param shop The shop instance that the sign belongs to, represented by a + * {@code ModernShop}. + * @param sign Target {@link Sign} + * + * @return Is shop info sign + * + * @since 6.3.0.0 + */ + @Override + public boolean isShopSign(final @NotNull ModernShop shop, @NotNull final Sign sign) { + // Check for new shop sign + final Component[] lines = new Component[sign.getLines().length]; + for(int i = 0; i < sign.getLines().length; i++) { + lines[i] = QuickShop.getInstance().platform().getLine(sign, i); + } + // Can be claim + + boolean empty = true; + for(final Component line : lines) { + if(!Util.isEmptyComponent(line)) { + empty = false; + break; + } + } + + if(empty) { + return true; + } + + // Check for exists shop sign (modern) + com.ghostchu.quickshop.api.shop.ShopSignStorage shopSignStorage = sign.getPersistentDataContainer().get(SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE); + if(shopSignStorage == null) { + // Try to read Reremake sign namespaced key + shopSignStorage = sign.getPersistentDataContainer().get(LEGACY_SHOP_NAMESPACED_KEY, ShopSignPersistentDataType.INSTANCE); + } + if(shopSignStorage == null) { + // Try more hard to read Reremake sign namespaced key + if(sign.getLine(1).startsWith(LEGACY_SHOP_SIGN_RECOGNIZE_PATTERN)) { + return true; + } + } + if(shopSignStorage != null) { + return shopSignStorage.equals(shop.bukkitLocation().getWorld().getName(), + shop.bukkitLocation().getBlockX(), + shop.bukkitLocation().getBlockY(), + shop.bukkitLocation().getBlockZ()); + } + return false; + } + + /** + * Retrieves the localized text to be displayed on a shop's sign. This method provides the sign + * text in the form of a list of {@link Component}, customized according to the specified shop + * instance and locale. + * + * @param shop The shop instance for which the sign text is to be retrieved, represented by a + * {@code ModernShop}. + * @param locale The locale to be used for generating the sign text, represented by a + * {@code ProxiedLocale}. + * + * @return A list of {@link Component} objects representing the text of the shop's sign, with each + * list entry corresponding to a line of text. + * + * @since 6.3.0.0 + */ + @Override + public List getSignText(final @NotNull ModernShop shop, final @NotNull ProxiedLocale locale) { + + + Util.ensureThread(false); + + final LinkedList lines = QuickShop.getInstance().getShopManager().shopLayoutProvider().render(shop, locale); + + final ShopSignLinesEvent event = new ShopSignLinesEvent(Phase.RETRIEVE, shop, lines); + event.callEvent(); + + return event.updated(); + } + + /** + * Generate new sign texts on shop's sign. + * + * @since 6.3.0.0 + */ + @Override + public void setSignText(final @NotNull ModernShop shop) { + + Util.ensureThread(false); + if(!Util.isLoaded(shop.bukkitLocation())) { + return; + } + QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (consumer)->{ + this.setSignText(shop, getSignText(shop, QuickShop.getInstance().getTextManager().findRelativeLanguages(MsgUtil.getDefaultGameLanguageCode()))); + }); + } + + /** + * Sets the text displayed on a shop's sign. This method allows for updating the sign text of the + * specified shop using a list of {@link Component} objects where each entry represents a line of + * text. + * + * @param shop The shop instance whose sign text is to be updated, represented by a + * {@code ModernShop}. + * @param lines A list of {@link Component} objects representing the new sign text, + * with each list entry corresponding to a line of text. + * + * @since 6.3.0.0 + */ + @Override + public void setSignText(final @NotNull ModernShop shop, @NotNull final List lines) { + + Util.ensureThread(false); + Log.debug("Globally sign text setting..."); + final List signs = this.getSigns(shop); + + final ShopSignLinesEvent event = new ShopSignLinesEvent(Phase.POST, shop, lines); + event.callEvent(); + + for(final Sign sign : signs) { + + final DyeColor dyeColor = Util.getDyeColor(); + if(dyeColor != null) { + sign.setColor(dyeColor); + } + final boolean isGlowing = QuickShop.getInstance().getConfig().getBoolean("shop.sign-glowing", false); + final boolean isWaxed = QuickShop.getInstance().getConfig().getBoolean("shop.sign-wax", false); + + sign.setGlowingText(isGlowing); + sign.setWaxed(isWaxed); + sign.update(true); + QuickShop.getInstance().platform().setLines(sign, event.updated()); + + new ShopSignUpdateEvent(shop, sign).callEvent(); + } + if(QuickShop.getInstance().getSignHooker() != null) { + Log.debug("Start sign broadcast..."); + QuickShop.getInstance().getSignHooker().updatePerPlayerShopSignBroadcast(shop.bukkitLocation(), shop); + Log.debug("Sign broadcast completed."); + } + } + + /** + * Updates the text displayed on a shop's sign with content localized to the given locale. This + * method dynamically adjusts the shop sign's text based on the specified shop instance and target + * locale for display purposes. + * + * @param shop The shop instance whose sign text is to be updated, represented by a + * {@code ModernShop}. + * @param locale The locale used to localize the text displayed on the shop's sign, represented by + * a {@code ProxiedLocale}. + * + * @since 6.3.0.0 + */ + @Override + public void setSignText(final @NotNull ModernShop shop, @NotNull final ProxiedLocale locale) { + + //Util.ensureThread(false); + if(!Util.isLoaded(shop.bukkitLocation())) { + return; + } + + QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (consumer)->{ + this.setSignText(shop, getSignText(shop, locale)); + }); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopItemBuilder.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopItemBuilder.java new file mode 100644 index 0000000000..b077e36706 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopItemBuilder.java @@ -0,0 +1,45 @@ +package com.ghostchu.quickshop.shop.builder; + +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopItemBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import com.ghostchu.quickshop.shop.components.SimpleShopItem; +import org.bukkit.inventory.ItemStack; + +public class SimpleShopItemBuilder implements ShopItemBuilder { + + protected ItemStack item; + protected boolean disableDisplay; + + @Override + public ItemStack item() { + + return item; + } + + @Override + public ShopItemBuilder item(final ItemStack item) { + + this.item = item; + return this; + } + + @Override + public boolean isDisableDisplay() { + + return disableDisplay; + } + + @Override + public ShopItemBuilder disableDisplay(final boolean disabled) { + + this.disableDisplay = disabled; + return this; + } + + @Override + public ShopItem build(final ModernShop shop) { + + return new SimpleShopItem(shop, this.item, this.disableDisplay); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopMetaBuilder.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopMetaBuilder.java new file mode 100644 index 0000000000..48728bf944 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopMetaBuilder.java @@ -0,0 +1,182 @@ +package com.ghostchu.quickshop.shop.builder; + +import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.IShopType; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopMetaBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import com.ghostchu.quickshop.shop.components.SimpleShopMeta; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.function.Consumer; + +public class SimpleShopMetaBuilder implements ShopMetaBuilder { + + protected long shopId; + private QUser owner; + @Nullable + private String shopName; + private boolean unlimited; + + private IShopType shopType; + private ShopState shopState; + + private QUser taxAccount; + + private BenefitProvider benefit; + + private String inventoryWrapperProvider; + + protected YamlConfiguration extra; + + @Override + public long shopId() { + + return shopId; + } + + @Override + public ShopMetaBuilder shopId(final long shopId) { + + this.shopId = shopId; + return this; + } + + @Override + public @Nullable String shopName() { + + return shopName; + } + + @Override + public ShopMetaBuilder shopName(final @Nullable String shopName) { + + this.shopName = shopName; + return null; + } + + @Override + public ShopState shopState() { + + return this.shopState; + } + + @Override + public ShopMetaBuilder shopState(final @NotNull ShopState shopState) { + + this.shopState = shopState; + return this; + } + + @Override + public IShopType shopType() { + + return shopType; + } + + @Override + public ShopMetaBuilder shopType(final @NotNull IShopType shopType) { + + this.shopType = shopType; + return this; + } + + @Override + public QUser owner() { + + return this.owner; + } + + @Override + public ShopMetaBuilder owner(final @NotNull QUser owner) { + + this.owner = owner; + return this; + } + + @Override + public @Nullable QUser taxAccount() { + + return this.taxAccount; + } + + @Override + public ShopMetaBuilder taxAccount(final @Nullable QUser taxAccount) { + + this.taxAccount = taxAccount; + return this; + } + + @Override + public boolean isUnlimited() { + + return this.unlimited; + } + + @Override + public ShopMetaBuilder isUnlimited(final boolean unlimited) { + + this.unlimited = unlimited; + return this; + } + + @Override + public BenefitProvider benefit() { + + return this.benefit; + } + + @Override + public ShopMetaBuilder withBenefit(final @NotNull QUser user, final BigDecimal percent, final @NotNull Consumer result) { + + try { + + result.accept(benefit.add(user, percent)); + } catch(final Exception ignore) { + + result.accept(false); + } + return this; + } + + @Override + public ShopMetaBuilder lessBenefit(final @NotNull QUser user) { + + benefit.remove(user); + return this; + } + + @Override + public ShopMetaBuilder benefit(final @NotNull BenefitProvider benefit) { + + this.benefit = benefit; + return this; + } + + @Override + public ShopMetaBuilder extra(final @NotNull YamlConfiguration extra) { + + this.extra = extra; + return this; + } + + @Override + public ShopMetaBuilder inventoryWrapperProvider(final @NotNull String inventoryWrapperProvider) { + + this.inventoryWrapperProvider = inventoryWrapperProvider; + return this; + } + + @Override + public ShopMeta build(final ModernShop shop) { + + return new SimpleShopMeta(shop, this.shopId, this.owner, this.shopName, this.unlimited, + this.shopType, this.shopState, this.taxAccount, this.benefit, + this.inventoryWrapperProvider, this.extra); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPermissionsBuilder.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPermissionsBuilder.java new file mode 100644 index 0000000000..fbc1519ddb --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPermissionsBuilder.java @@ -0,0 +1,42 @@ +package com.ghostchu.quickshop.shop.builder; + +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopPermissionBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.shop.components.SimpleShopPermission; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SimpleShopPermissionsBuilder implements ShopPermissionBuilder { + + protected Map permissions = new HashMap<>(); + + @Override + public @NotNull Map permissions() { + + return permissions; + } + + @Override + public ShopPermissionBuilder permissions(@NotNull final Map permissions) { + + this.permissions = permissions; + return this; + } + + @Override + public ShopPermissionBuilder permission(@NotNull final UUID uuid, @NotNull final String group) { + + this.permissions.put(uuid, group); + return this; + } + + @Override + public ShopPermission build(final ModernShop shop) { + + return new SimpleShopPermission(shop, this.permissions); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPriceBuilder.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPriceBuilder.java new file mode 100644 index 0000000000..a3798b8aa2 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/builder/SimpleShopPriceBuilder.java @@ -0,0 +1,46 @@ +package com.ghostchu.quickshop.shop.builder; + +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopPriceBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; +import com.ghostchu.quickshop.shop.components.SimpleShopPrice; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SimpleShopPriceBuilder implements ShopPriceBuilder { + + protected Double price; + protected String currency; + + @Override + public Double price() { + + return price; + } + + @Override + public ShopPriceBuilder price(final @NotNull Double price) { + + this.price = price; + return this; + } + + @Override + public @Nullable String currency() { + + return this.currency; + } + + @Override + public ShopPriceBuilder currency(final @Nullable String currency) { + + this.currency = currency; + return this; + } + + @Override + public ShopPrice build(final ModernShop shop) { + + return new SimpleShopPrice(shop, this.currency, this.price); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopInteraction.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopInteraction.java new file mode 100644 index 0000000000..09f65ffb90 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopInteraction.java @@ -0,0 +1,97 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.event.Phase; +import com.ghostchu.quickshop.api.event.management.ShopClickEvent; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopInteraction; +import com.ghostchu.quickshop.obj.QUserImpl; +import com.ghostchu.quickshop.shop.InventoryPreview; +import com.ghostchu.quickshop.util.Util; +import com.ghostchu.quickshop.util.logger.Log; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +/** + * SimpleShopIteraction + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SimpleShopInteraction implements ShopInteraction { + + private final ModernShop shop; + private InventoryPreview inventoryPreview = null; + + public SimpleShopInteraction(@NotNull final ModernShop shop) { + this.shop = shop; + } + + /** + * Handles the action triggered when a player interacts with an element in the shop. + * + * @param player the player object interacting with the shop + */ + @Override + public void onClick(@NonNull final Player player) { + + Util.ensureThread(false); + ShopClickEvent event = new ShopClickEvent(this.shop, QUserImpl.createFullFilled(player)); + event.callEvent(); + + event = event.clone(Phase.MAIN); + if(event.callCancellableEvent()) { + + Log.debug("Ignore shop click, because some plugin cancelled it."); + return; + } + + //setSignText(plugin.getTextManager().findRelativeLanguages(clicker)); + // not sure if we need the above + + event = event.clone(Phase.POST); + event.callEvent(); + } + + @Override + public InventoryPreview preview() { + + return inventoryPreview; + } + + /** + * Opens a preview for the specified player. This is typically used to display shop items to the + * player in a preview interface. + * + * @param player the player object for whom the preview will be displayed; must not be null + */ + @Override + public void openPreview(@NonNull final Player player) { + + if(inventoryPreview == null) { + inventoryPreview = new InventoryPreview(QuickShop.getInstance(), + shop.item().getItem().clone(), + player.getLocale()); + } + inventoryPreview.show(player); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopItem.java new file mode 100644 index 0000000000..6b477e0047 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopItem.java @@ -0,0 +1,329 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.event.Phase; +import com.ghostchu.quickshop.api.event.settings.type.ShopDisplayEvent; +import com.ghostchu.quickshop.api.event.settings.type.ShopItemEvent; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopItemBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopItem; +import com.ghostchu.quickshop.api.shop.display.DisplayItem; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.shop.builder.SimpleShopItemBuilder; +import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; +import com.ghostchu.quickshop.util.Util; +import com.ghostchu.quickshop.util.logger.Log; +import lombok.EqualsAndHashCode; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; + +/** + * SimpleShopDisplay + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +@SuppressWarnings({ "removal"}) +public class SimpleShopItem implements ShopItem { + + private final ModernShop shop; + + @NotNull + private ItemStack item; + @NotNull + private ItemStack originalItem; + + @Nullable + @EqualsAndHashCode.Exclude + private AbstractDisplayItem displayItem = null; + + private boolean disableDisplay = false; + + public SimpleShopItem(@NotNull final ModernShop shop) { + this.shop = shop; + } + + public SimpleShopItem(@NotNull final ModernShop shop, + @NotNull final ItemStack item, final boolean disableDisplay) { + + this.shop = shop; + this.item = item; + this.originalItem = item.clone(); + this.disableDisplay = disableDisplay; + } + + /** + * Get shop item's ItemStack + * + * @return The shop's ItemStack + */ + @Override + public @NotNull ItemStack getItem() { + + final ShopItemEvent event = new ShopItemEvent(Phase.RETRIEVE, shop, this.item.clone()); + + return event.updated(); + } + + /** + * @return The enchantments the shop has on its items. + */ + public @NotNull Map getEnchants() { + + final ItemStack item = getItem(); + + if(item.hasItemMeta() && item.getItemMeta().hasEnchants()) { + return Objects.requireNonNull(item.getItemMeta()).getEnchants(); + } + return Collections.emptyMap(); + } + + /** + * @return The ItemStack type of this shop + */ + public @NotNull Material getMaterial() { + + return this.getItem().getType(); + } + + /** + * Set shop item's ItemStack + * + * @param item ItemStack to set + */ + @Override + public void setItem(@NotNull final ItemStack item) { + + //Create our shop event with Pre Phase and call + ShopItemEvent event = new ShopItemEvent(Phase.PRE, shop, this.item, item); + event.callEvent(); + + //Call our Main Phase + event = event.clone(Phase.MAIN); + if(event.callCancellableEvent()) { + + Log.debug("A plugin cancelled the item change event."); + return; + } + + + this.item = event.updated().clone(); + this.originalItem = item.clone(); + + //call our Post Phase + event.clone(Phase.POST).callEvent(); + } + + /** + * Encodes and retrieves information related to the shop's item. + * + * @return a string representation of the encoded item data + */ + @Override + public String encodedItem() { + + return QuickShop.getInstance().platform().encodeStack(this.originalItem); + } + + /** + * Checks whether the provided {@code ItemStack} matches the current shop item. + * + * @param compare The {@code ItemStack} to compare against the shop's item. The parameter can be + * {@code null}. + * + * @return {@code true} if the provided {@code ItemStack} matches the shop's item, {@code false} + * otherwise. + */ + @Override + public boolean matches(final @Nullable ItemStack compare) { + + if(compare == null) { + return false; + } + final ItemStack givenItem = compare.clone(); + givenItem.setAmount(1); + final ItemStack shopItem = this.getItem(); + shopItem.setAmount(1); + return QuickShop.getInstance().getItemMatcher().matches(shopItem, givenItem); + } + + /** + * Gets shop status is stacking shop + * + * @return The shop stacking status + */ + @Override + public boolean isStackingShop() { + + return QuickShop.getInstance().isAllowStack() && this.item.getAmount() > 1; + } + + /** + * Getting the item stacking amount of the shop. + * + * @return The item stacking amount of the shop. + */ + @Override + public int getShopStackingAmount() { + + if(isStackingShop()) { + return this.item.getAmount(); + } + return 1; + } + + /** + * Getting if this shop has been disabled the display + * + * @return Does display has been disabled + */ + @Override + public boolean isDisableDisplay() { + + final ShopDisplayEvent event = ShopDisplayEvent.RETRIEVE(shop, this.disableDisplay); + event.callEvent(); + + return event.updated(); + } + + /** + * Set the display disable state + * + * @param disabled Has been disabled + */ + @Override + public void setDisableDisplay(final boolean disabled) { + + if(this.disableDisplay == disabled) { + return; + } + + this.disableDisplay = disabled; + } + + /** + * Get the display item + * + * @return The display item + */ + @Override + public DisplayItem getDisplayItem() { + + return displayItem; + } + + /** + * Determines whether a custom item name should be used. + * + * @return true if a custom item name is enabled, false otherwise + */ + @Override + public boolean useCustomItemName() { + + if(!QuickShop.getInstance().getConfig().getBoolean("shop.force-use-item-original-name")) { + return false; + } + + final ItemMeta itemMeta = this.item.getItemMeta(); + if(itemMeta == null) { + return false; + } + + try { + if(itemMeta.hasItemName()) { + return true; + } + } catch(final NoSuchMethodError ignore) { + //old version + } + + try { + if(itemMeta.hasCustomName()) { + return true; + } + } catch(final NoSuchMethodError ignore) { + //old version + } + + return itemMeta.hasDisplayName(); + } + + /** + * Customizes and returns a Component representing an item name. + * + * @return a Component representing the customized item name + */ + @Override + public Component customItemName() { + + return Util.getItemStackName(getItem()); + } + + @Override + public EnumSet diff(final @Nullable ShopItem compare) { + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + + if(compare == null || this.disableDisplay != compare.isDisableDisplay()) { + changes.add(ShopChangeType.DISPLAY_TOGGLE); + } + + if(compare == null || !this.item.equals(compare.getItem())) { + changes.add(ShopChangeType.ITEM); + } + + if(compare == null || this.getShopStackingAmount() != compare.getShopStackingAmount()) { + changes.add(ShopChangeType.AMOUNT); + } + + return changes; + } + + @Override + public ShopItemBuilder builder() { + + return new SimpleShopItemBuilder().item(this.item.clone()).disableDisplay(this.disableDisplay); + } + + @Override + public boolean equals(final Object o) { + + if(!(o instanceof final SimpleShopItem that)) return false; + return disableDisplay == that.disableDisplay && Objects.equals(item, that.item) + && Objects.equals(originalItem, that.originalItem); + } + + @Override + public int hashCode() { + + return Objects.hash(item, originalItem, disableDisplay); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopLifecycle.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopLifecycle.java new file mode 100644 index 0000000000..412b99e619 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopLifecycle.java @@ -0,0 +1,233 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.event.Phase; +import com.ghostchu.quickshop.api.event.management.ShopDatabaseEvent; +import com.ghostchu.quickshop.api.event.management.ShopLoadEvent; +import com.ghostchu.quickshop.api.event.management.ShopUnloadEvent; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopLifecycle; +import com.ghostchu.quickshop.util.Util; +import com.ghostchu.quickshop.util.logger.Log; +import com.ghostchu.quickshop.util.performance.PerfMonitor; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.ghostchu.quickshop.util.Util.waitForFuture; + +/** + * SimpleShopLifecycle + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SimpleShopLifecycle implements ShopLifecycle { + + protected final QuickShop plugin; + protected final ModernShop shop; + + protected boolean dirty = false; + protected boolean updating = false; + protected boolean isLoaded = false; + private final boolean isDeleted = false; + private volatile boolean createBackup = false; + + //updating objects + private final AtomicBoolean updatingAtomic = new AtomicBoolean(false); + private volatile CompletableFuture inFlightUpdate; + + public SimpleShopLifecycle(@NotNull final ModernShop shop) { + this.shop = shop; + this.plugin = QuickShop.getInstance(); + } + + /** + * Gets if shop is dirty (so shop will be save) + * + * @return Is dirty + */ + @Override + public boolean isDirty() { + + return dirty; + } + + /** + * Sets dirty status + * + * @param isDirty Shop is dirty + */ + @Override + public void setDirty(final boolean isDirty) { + this.dirty = isDirty; + } + + /** + * Sets shop is dirty + */ + @Override + public void markDirty() { + this.dirty = true; + } + + /** + * Get this container shop is loaded or unloaded. + * + * @return Loaded + */ + @Override + public boolean isLoaded() { + + return this.isLoaded; + } + + /** + * Checks whether the shop is marked as deleted. + * + * @return {@code true} if the shop is deleted, {@code false} otherwise + */ + @Override + public boolean isDeleted() { + + return isDeleted; + } + + @Override + public void handleLoading() { + + Util.ensureThread(false); + if(this.isLoaded) { + Log.debug("Dupe load request, canceled."); + return; + } + try(final PerfMonitor ignored = new PerfMonitor("Shop Inventory Locate", Duration.of(1, ChronoUnit.SECONDS))) { + if(this.shop.meta().getInventory() == null) { + + plugin.logger().warn("Failed to load shop: {}: {}: {}", shop.asSymbolLink(), this.getClass().getName(), "Inventory is null"); + if(plugin.getConfig().getBoolean("debug.delete-corrupt-shops")) { + plugin.logger().warn("Deleting corrupt shop..."); + Util.regionThread(this.shop.bukkitLocation(), () -> plugin.getShopManager().deleteShop(this.shop)); + } else { + + plugin.logger().warn("Unloading shops from memory, set `debug.delete-corrupt-shops` to true to delete corrupted shops."); + plugin.getShopManager().unloadShop(this.shop); + } + return; + } + } + if(Util.fireCancellableEvent(new ShopLoadEvent(this.shop))) { + return; + } + this.isLoaded = true; + + try(final PerfMonitor ignored = new PerfMonitor("Shop Display Check", Duration.of(1, ChronoUnit.SECONDS))) { + checkDisplay(); + } + if(plugin.getConfig().getBoolean("shop.update-sign-on-load", false)) { + + Log.debug("Scheduled sign update for shop " + this + " because updateShopSignOnLoad has been enabled."); + plugin.getSignUpdateWatcher().scheduleSignUpdate(this.shop); + } + } + + @Override + public void handleUnloading(final boolean dontTouchWorld) { + + Util.ensureThread(false); + if(!this.isLoaded) { + Log.debug("Dupe unload request, canceled."); + return; + } + if(this.shop.interaction().preview() != null) { + this.shop.interaction().preview().close(); + } + if(this.shop.item().getDisplayItem() != null) { + this.shop.item().getDisplayItem().remove(dontTouchWorld); + } + this.isLoaded = false; + QuickShop.getInstance().getShopManager().getLoadedShops().remove(this.shop); + new ShopUnloadEvent(Phase.POST, this.shop).callEvent(); + } + + /** + * Update shop data to database + */ + @Override + public @NotNull CompletableFuture update() { + + //Warning! This method can be run in async thread. + if(updating) { + return CompletableFuture.completedFuture(null); + } + + if(this.shop.meta().getShopId() == -1) { + Log.debug("Skip shop database update because it not fully setup!"); + return CompletableFuture.completedFuture(null); + } + + ShopDatabaseEvent event = new ShopDatabaseEvent(Phase.PRE_CANCELLABLE, this.shop); + + if(event.callCancellableEvent()) { + + Log.debug("The Shop update action was canceled by a plugin."); + return CompletableFuture.completedFuture(null); + } + + event = event.clone(Phase.POST); + event.callEvent(); + + //If already updating, just return the same future + if(!updatingAtomic.compareAndSet(false, true)) { + return inFlightUpdate != null ? inFlightUpdate : CompletableFuture.completedFuture(null); + } + + //Start a new update + final CompletableFuture f = QuickShop.getInstance().getDatabaseHelper().updateShop(this.shop) + .whenComplete((r, th) -> { + updatingAtomic.set(false); + if (th == null) { + dirty = false; + } else { + QuickShop.getInstance().logger().warn("Could not update shop in DB!", th); + } + }); + + inFlightUpdate = f; + return f; + } + + /** + * Update shop data to database synchronously. This will create the completeable future for the + * save function, and wait for it to complete. DON'T USE IF YOU DON'T KNOW WHAT YOU'RE DOING! + */ + @Override + public void updateSync() throws RuntimeException { + + final CompletableFuture future = update(); + + waitForFuture(future, 15, TimeUnit.SECONDS, "updateShop(" + shop.meta().getShopId() + ")"); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopMeta.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopMeta.java new file mode 100644 index 0000000000..86473c8505 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopMeta.java @@ -0,0 +1,685 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.economy.benefit.BenefitProvider; +import com.ghostchu.quickshop.api.event.Phase; +import com.ghostchu.quickshop.api.event.settings.type.ShopOwnerNameEvent; +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; +import com.ghostchu.quickshop.api.inventory.InventoryWrapper; +import com.ghostchu.quickshop.api.localization.text.ProxiedLocale; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.IShopType; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopMetaBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopMeta; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import com.ghostchu.quickshop.common.util.CommonUtil; +import com.ghostchu.quickshop.shop.SimpleShopManager; +import com.ghostchu.quickshop.shop.builder.SimpleShopMetaBuilder; +import com.ghostchu.quickshop.util.MsgUtil; +import com.ghostchu.quickshop.util.logger.Log; +import net.kyori.adventure.text.Component; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * SimpleShopMeta + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +@SuppressWarnings({"deprecation", "removal"}) +public class SimpleShopMeta implements ShopMeta { + + private final ModernShop shop; + + protected long shopId; + private QUser owner; + @Nullable + private String shopName; + private boolean unlimited; + + private IShopType shopType; + private ShopState shopState; + + private QUser taxAccount; + + @NotNull + private BenefitProvider benefit; + + @NotNull + private String inventoryWrapperProvider; + + protected YamlConfiguration extra; + + public SimpleShopMeta(@NotNull final ModernShop shop) { + this.shop = shop; + } + + public SimpleShopMeta(@NotNull final ModernShop shop, + @NotNull final QUser owner, + @Nullable final String shopName, + final boolean unlimited, + @NotNull final IShopType shopType, + @NotNull final ShopState shopState, + @Nullable final QUser taxAccount, + @NotNull final BenefitProvider benefit, + @NotNull final String inventoryWrapperProvider, + @NotNull final YamlConfiguration extra) { + + this(shop, shop.meta().getShopId(), owner, shopName, unlimited, shopType, shopState, taxAccount, benefit, inventoryWrapperProvider, extra); + } + + public SimpleShopMeta(@NotNull final ModernShop shop, + final long shopId, + @NotNull final QUser owner, + @Nullable final String shopName, + final boolean unlimited, + @NotNull final IShopType shopType, + @NotNull final ShopState shopState, + @Nullable final QUser taxAccount, + @NotNull final BenefitProvider benefit, + @NotNull final String inventoryWrapperProvider, + @NotNull final YamlConfiguration extra) { + + this.shop = shop; + this.shopId = shopId; + this.owner = owner; + this.shopName = shopName; + this.unlimited = unlimited; + this.shopType = shopType; + this.shopState = shopState; + this.taxAccount = taxAccount; + this.benefit = benefit; + this.inventoryWrapperProvider = inventoryWrapperProvider; + this.extra = extra; + } + + /** + * Gets the Shop ID to identify the shop. + * + * @return Shop ID -1 if shop in creating state. + */ + @Override + public long getShopId() { + + return this.shopId; + } + + /** + * Internal Only: Give shop that under id_waiting state an ShopId. + * + * @param newId The new shop id, once set will cannot change anymore. + */ + @Override + public void setShopId(final long newId) { + + if(this.shopId != -1) { + throw new IllegalStateException("Cannot set shop id once it fully created."); + } + this.shopId = newId; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * Gets this shop name that set by player + * + * @return Shop name, or null if not set + */ + @Override + public @Nullable String getShopName() { + + return this.shopName; + } + + /** + * Sets shop name + * + * @param shopName shop name, null to remove currently name + */ + @Override + public void setShopName(@Nullable final String shopName) { + + if(CommonUtil.strEquals(this.shopName, shopName)) { + return; + } + this.shopName = shopName; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * 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, shop, this.shopState); + event.callEvent(); + + return event.updated(); + } + + /** + * Updates the current state of the shop based on the provided {@code ShopState}. + * + * @param state the new state to set for the shop; must not be null + */ + @Override + public void shopState(@NotNull final ShopState state) { + + if(this.shopState.identifier().equalsIgnoreCase(state.identifier())) { + + return; + } + + ShopStateEvent event = new ShopStateEvent(Phase.PRE, shop, this.shopState, state); + 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(); + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + //setSignText(); + } + + /** + * 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 void shopState(@NotNull final String shopStateIdentifier) { + + shopState(QuickShop.getInstance().getShopManager().shopStateOrDefault(shopStateIdentifier)); + } + + /** + * Retrieves the type of shop associated with this entity. + * + * @return an instance of IShopType representing the shop type + */ + @Override + public IShopType shopType() { + + final ShopTypeEnhancedEvent event = new ShopTypeEnhancedEvent(Phase.RETRIEVE, shop, this.shopType); + event.callEvent(); + + return event.updated(); + } + + /** + * Sets the type of shop using the provided shop type parameter. + * + * @param newShopType the shop type to set, must not be null + */ + @Override + public void shopType(@NotNull final IShopType newShopType) { + + if(this.shopType.identifier().equalsIgnoreCase(newShopType.identifier())) { + + return; + } + + ShopTypeEnhancedEvent event = new ShopTypeEnhancedEvent(Phase.PRE, shop, this.shopType, newShopType); + event.callEvent(); + + event = event.clone(Phase.MAIN); + + if(event.callCancellableEvent()) { + + Log.debug("Some addon cancelled shop type changes, target shop: " + this); + return; + } + + this.shopType = event.updated(); + + event = event.clone(Phase.POST); + event.callEvent(); + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + //setSignText(); + } + + /** + * Specifies the type of shop based on the given identifier. + * + * @param shopTypeIdentifier the identifier representing the type of shop. Must not be null. + */ + @Override + public void shopType(@NotNull final String shopTypeIdentifier) { + + shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(shopTypeIdentifier)); + } + + /** + * Get shop's owner name, it will return owner name or Admin Shop(i18n) when it is unlimited + * + * @param forceUsername Force returns username of shop + * @param locale The locale to parse the message + * + * @return owner name + */ + @Override + public @NotNull Component ownerName(final boolean forceUsername, @NotNull final ProxiedLocale locale) { + + Component name; + if(!forceUsername && isUnlimited()) { + name = QuickShop.getInstance().text().of("admin-shop").forLocale(locale.getLocale()); + } else { + final String playerName = this.getOwner().getUsername(); + if(playerName == null) { + name = QuickShop.getInstance().text().of("unknown-owner").forLocale(locale.getLocale()); + } else { + name = Component.text(playerName); + } + } + if(getOwner().isRealPlayer()) { + name = name.hoverEvent( + QuickShop.getInstance().text().of("real-player-component-hover", getOwner().getUniqueId(), getOwner().getUsername(), getOwner().getDisplay()).forLocale(locale.getLocale()) + ); + } else { + name = name.hoverEvent( + QuickShop.getInstance().text().of("virtual-player-component-hover", getOwner().getUniqueId(), getOwner().getUsername(), getOwner().getDisplay()).forLocale(locale.getLocale()) + ); + + } + + final ShopOwnerNameEvent event = new ShopOwnerNameEvent(Phase.RETRIEVE, this.shop, name); + event.callEvent(); + + name = event.updated(); + return name; + } + + /** + * Get shop's owner name, it will return owner name or Admin Shop(i18n) when it is unlimited + * + * @param locale The locale to parse the message + * + * @return owner name + */ + @Override + public @NotNull Component ownerName(@NotNull final ProxiedLocale locale) { + + return ownerName(false, locale); + } + + /** + * Get shop's owner name, it will return owner name or Admin Shop(i18n) when it is unlimited + * + * @return owner name + */ + @Override + public @NotNull Component ownerName() { + + return ownerName(false, MsgUtil.getDefaultGameLanguageLocale()); + } + + /** + * Get shop's owner QUser + * + * @return Shop's owner QUser object, can use Bukkit.getOfflinePlayer to convert to the + * OfflinePlayer. + */ + @Override + public @NotNull QUser getOwner() { + + return this.owner; + } + + /** + * Set new owner to the shop's owner + * + * @param owner New owner user + */ + @Override + public void setOwner(@NotNull final QUser owner) { + + if(this.owner.equals(owner)) { + return; + } + this.owner = owner; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + //setSignText(plugin.getTextManager().findRelativeLanguages(owner, false)); + } + + /** + * Getting the shop tax account for using, it can be specific uuid or general tax account + * + * @return Shop Tax Account or fallback to general tax account + */ + @Override + public @Nullable QUser getTaxAccount() { + + QUser uuid = null; + if(taxAccount != null) { + uuid = taxAccount; + } else { + if(((SimpleShopManager)QuickShop.getInstance().getShopManager()).getCacheTaxAccount() != null) { + uuid = ((SimpleShopManager)QuickShop.getInstance().getShopManager()).getCacheTaxAccount(); + } + } + final ShopTaxAccountEvent event = new ShopTaxAccountEvent(Phase.RETRIEVE, this.shop, uuid); + event.callEvent(); + + return event.updated(); + } + + /** + * Sets shop taxAccount + * + * @param taxAccount tax account, null to use general tax account + */ + @Override + public void setTaxAccount(@Nullable final QUser taxAccount) { + + if(Objects.equals(taxAccount, this.taxAccount)) { + return; + } + + ShopTaxAccountEvent event = new ShopTaxAccountEvent(Phase.PRE, this.shop, this.taxAccount, taxAccount); + event.callEvent(); + + event = event.clone(Phase.MAIN); + if(event.callCancellableEvent()) { + + return; + } + + this.taxAccount = event.updated(); + + event = event.clone(Phase.POST); + event.callEvent(); + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * Getting the shop tax account, it can be specific uuid or general tax account + * + * @return Shop Tax Account, null if use general tax account + */ + @Override + public @Nullable QUser getTaxAccountActual() { + + return taxAccount; + } + + /** + * Get shop is or not in Unlimited Mode (Admin Shop) + * + * @return yes or not + */ + @Override + public boolean isUnlimited() { + + return this.unlimited; + } + + /** + * Set shop is or not Unlimited Mode (Admin Shop) + * + * @param unlimited status + */ + @Override + public void setUnlimited(final boolean unlimited) { + + if(this.unlimited == unlimited) { + return; + } + this.unlimited = unlimited; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + //this.setSignText(); + } + + /** + * Gets the benefit in this shop + */ + @Override + public @NotNull BenefitProvider getShopBenefit() { + + final ShopBenefitEvent event = ShopBenefitEvent.RETRIEVE(this.shop, this.benefit); + event.callEvent(); + + return event.updated(); + } + + /** + * Sets the benefit in this shop + */ + @Override + public void setShopBenefit(@NotNull final BenefitProvider benefit) { + + this.benefit = benefit; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * Getting ConfigurationSection (extra data) instance of your plugin namespace) + * + * @param plugin The plugin and plugin name will used for namespace + * + * @return ExtraSection, save it through Shop#setExtra. If you don't save it, it may randomly lose + * or save + */ + @Override + public @NotNull ConfigurationSection getExtra(@NotNull final Plugin plugin) { + + if(this.extra == null) { + this.extra = new YamlConfiguration(); + } + ConfigurationSection section = extra.getConfigurationSection(plugin.getName()); + if(section == null) { + section = extra.createSection(plugin.getName()); + } + return section; + } + + /** + * Save the extra data to the shop. + * + * @param plugin Plugin instace + * @param data The data table + */ + @Override + public void setExtra(@NotNull final Plugin plugin, @Nullable final ConfigurationSection data) { + + if(data == null && this.extra == null) { + return; + } + + if(this.extra == null) { + this.extra = new YamlConfiguration(); + } + extra.set(plugin.getName(), data); + // compress extra to null if possible + boolean anyValid = false; + for(final String key : extra.getKeys(false)) { + if(!extra.isConfigurationSection(key)) { + anyValid = true; + break; + } + final ConfigurationSection section = extra.getConfigurationSection(key); + if(section == null) continue; + if(!section.getKeys(false).isEmpty()) { + anyValid = true; + break; + } + } + if(!anyValid) { + this.extra = null; + } + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * Returns the name of the inventory wrapper provider used in the application. + * + * @return A non-null string representing the inventory wrapper provider's name. + */ + @Override + public @NotNull String getInventoryWrapperProvider() { + + return inventoryWrapperProvider; + } + + /** + * Retrieves the current inventory encapsulated within an InventoryWrapper object. If no inventory + * is available, this method may return null. + * + * @return the InventoryWrapper containing inventory data, or null if no inventory is present. + */ + @Override + public @Nullable InventoryWrapper getInventory() { + + return null; + } + + /** + * Save the plugin extra data to Json format + * + * @return The json string + */ + @Override + public @NotNull String saveExtraToYaml() { + + return extra == null? "" : extra.saveToString(); + } + + @Override + public EnumSet diff(final @Nullable ShopMeta compare) { + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + + if(compare == null || this.owner.getUniqueId() != compare.getOwner().getUniqueId()) { + changes.add(ShopChangeType.OWNER); + } + + if(compare == null && this.shopName != null + || compare != null && this.shopName == null && compare.getShopName() != null + || compare != null && this.shopName != null && !this.shopName.equals(compare.getShopName())) { + changes.add(ShopChangeType.NAME); + } + + if(compare == null || this.unlimited != compare.isUnlimited()) { + changes.add(ShopChangeType.ADMIN_STATUS); + } + + if(compare == null || !Objects.equals(this.shopType.identifier(), compare.shopType().identifier())) { + changes.add(ShopChangeType.TYPE); + } + + if(compare == null || !Objects.equals(this.shopState.identifier(), compare.shopState().identifier())) { + changes.add(ShopChangeType.STATE); + } + + if(compare == null || compare.getTaxAccount() == null && this.taxAccount != null + || compare.getTaxAccount() != null && !Objects.equals(this.taxAccount.getUniqueId(), compare.getTaxAccount().getUniqueId())) { + changes.add(ShopChangeType.TAX_ACCOUNT); + } + + if(compare == null || !compare.getShopBenefit().serialize().equals(this.benefit.serialize())) { + changes.add(ShopChangeType.BENEFITS); + } + + if(compare == null || !compare.saveExtraToYaml().equals(this.saveExtraToYaml())) { + changes.add(ShopChangeType.EXTRA); + } + + if(compare == null || !compare.getInventoryWrapperProvider().equals(this.getInventoryWrapperProvider())) { + changes.add(ShopChangeType.INVENTORY_WRAPPER); + } + return changes; + } + + @Override + public ShopMetaBuilder builder() { + + return new SimpleShopMetaBuilder() + .shopId(shopId) + .owner(owner) + .shopName(shopName) + .isUnlimited(unlimited) + .shopType(shopType) + .shopState(shopState) + .taxAccount(taxAccount) + .benefit(benefit) + .inventoryWrapperProvider(inventoryWrapperProvider) + .extra(extra); + } + + @Override + public boolean equals(final Object o) { + + if(!(o instanceof final SimpleShopMeta that)) return false; + return shopId == that.shopId && unlimited == that.unlimited && Objects.equals(owner, that.owner) + && Objects.equals(shopName, that.shopName) && Objects.equals(shopType, that.shopType) + && Objects.equals(shopState, that.shopState) && Objects.equals(taxAccount, that.taxAccount) + && Objects.equals(benefit, that.benefit) + && Objects.equals(inventoryWrapperProvider, that.inventoryWrapperProvider) + && Objects.equals(extra, that.extra); + } + + @Override + public int hashCode() { + + return Objects.hash(shopId, owner, shopName, unlimited, shopType, shopState, taxAccount, benefit, + inventoryWrapperProvider, extra); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPermission.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPermission.java new file mode 100644 index 0000000000..c351b5f0bf --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPermission.java @@ -0,0 +1,301 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.event.Phase; +import com.ghostchu.quickshop.api.event.management.ShopPermissionCheckEvent; +import com.ghostchu.quickshop.api.event.settings.type.ShopPlayerGroupEvent; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopPermissionBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopPermission; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.common.util.CommonUtil; +import com.ghostchu.quickshop.shop.builder.SimpleShopPermissionsBuilder; +import com.ghostchu.quickshop.util.logger.Log; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * SimpleShopPermission + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +@SuppressWarnings({"deprecation", "removal"}) +public class SimpleShopPermission implements ShopPermission { + + private final ModernShop shop; + + //TODO: modification methods for this in order to add entries/set all entries + @NotNull + private final Map groups = new ConcurrentHashMap<>(); + + public SimpleShopPermission(@NotNull final ModernShop shop) { + this.shop = shop; + } + + public SimpleShopPermission(@NotNull final ModernShop shop, + @NotNull final Map groups) { + this.shop = shop; + this.groups.putAll(groups); + } + + + + /** + * Gets all player and their group on this shop + * + * @return Map of UUID and group + */ + @Override + public @NotNull Map getPermissionAudiences() { + + final Map clonedPlayerGroup = new HashMap<>(groups); + final Optional uuid = shop.meta().getOwner().getUniqueIdOptional(); + if(uuid.isPresent()) { + clonedPlayerGroup.put(shop.meta().getOwner().getUniqueId(), BuiltInShopPermissionGroup.ADMINISTRATOR.getNamespacedNode()); + } + return clonedPlayerGroup; + } + + /** + * Gets specific player group on specific shop + * + * @param player player + * + * @return namespaced group + */ + @Override + public @NotNull String getPlayerGroup(@NotNull final UUID player) { + + if(player.equals(shop.meta().getOwner().getUniqueId())) { + return BuiltInShopPermissionGroup.ADMINISTRATOR.getNamespacedNode(); + } + + final String group = getPermissionAudiences().getOrDefault(player, BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode()); + if(QuickShop.getInstance().getShopPermissionManager().hasGroup(group)) { + + final ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.RETRIEVE, shop, player, group); + event.callEvent(); + + return event.updated(); + } + return BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode(); + } + + /** + * Check if player have authorized for specific permission on specific shop + * + * @param player player + * @param namespace permission namespace + * @param permission permission + * + * @return true if player have authorized + */ + @Override + public boolean playerAuthorize(@NotNull final UUID player, @NotNull final Plugin namespace, @NotNull final String permission) { + + if(player.equals(shop.meta().getOwner().getUniqueId())) { + Log.permission("Check permission " + namespace.getName().toLowerCase(Locale.ROOT) + "." + permission + " for " + player + " -> " + "true"); + return true; + } + + final String group = getPlayerGroup(player); + final boolean hasPermission = QuickShop.getInstance().getShopPermissionManager().hasPermission(group, namespace, permission); + + final ShopPermissionCheckEvent event = new ShopPermissionCheckEvent(Phase.MAIN, shop, player, namespace.getName(), permission, hasPermission); + event.callEvent(); + + Log.permission("Check permission " + namespace.getName().toLowerCase(Locale.ROOT) + "." + permission + ": " + player + " -> " + event.hasPermission()); + + return event.hasPermission(); + } + + /** + * Check if player have authorized for specific permission on specific shop + * + * @param player player + * @param permission namespaced permission + * + * @return true if player have authorized + */ + @Override + public boolean playerAuthorize(@NotNull final UUID player, @NotNull final BuiltInShopPermission permission) { + + return playerAuthorize(player, QuickShop.getInstance().getJavaPlugin(), permission.getRawNode()); + } + + /** + * Gets the player list of who can authorize specific permission on this shop + * + * @param permission permission + * + * @return Collection of UUID + */ + @Override + public List playersCanAuthorize(@NotNull final BuiltInShopPermission permission) { + + return playersCanAuthorize(QuickShop.getInstance().getJavaPlugin(), permission.getRawNode()); + } + + /** + * Gets the player list of who can authorize specific group on this shop + * + * @param permissionGroup group + * + * @return Collection of UUID + */ + @Override + public List playersCanAuthorize(@NotNull final BuiltInShopPermissionGroup permissionGroup) { + + return getPermissionAudiences().entrySet().stream().filter(entry->entry.getValue().equals(permissionGroup.getNamespacedNode())).map(Map.Entry::getKey).toList(); + } + + /** + * Gets the player list of who can authorize specific permission on this shop + * + * @param plugin namespace of permission + * @param permission raw permission + * + * @return Collection of UUID + */ + @Override + public List playersCanAuthorize(@NotNull final Plugin plugin, @NotNull final String permission) { + + final List result = new ArrayList<>(); + for(final Map.Entry uuidStringEntry : this.getPermissionAudiences().entrySet()) { + + final String group = uuidStringEntry.getValue(); + final boolean hasPermission = QuickShop.getInstance().getShopPermissionManager().hasPermission(group, plugin, permission); + + final ShopPermissionCheckEvent event = new ShopPermissionCheckEvent(Phase.MAIN, shop, uuidStringEntry.getKey(), plugin.getName(), permission, hasPermission); + event.callEvent(); + + if(event.hasPermission()) { + result.add(uuidStringEntry.getKey()); + } + } + Log.permission("Check permission " + plugin.getName().toLowerCase(Locale.ROOT) + "." + permission + ": " + CommonUtil.list2String(result.stream().map(UUID::toString).toList())); + return result; + } + + /** + * Sets specific player permission on specfic shop + * + * @param player player + * @param group namespaced group name + */ + @Override + public void setPlayerGroup(@NotNull final UUID player, @Nullable String group) { + + if(group == null) { + group = BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode(); + } + + ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.PRE, shop, player, getPlayerGroup(player), group); + event.callEvent(); + + if(group.equals(BuiltInShopPermissionGroup.EVERYONE.getNamespacedNode())) { + + this.groups.remove(player); + } else { + + this.groups.put(player, group); + } + event = event.clone(Phase.POST); + event.callEvent(); + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + /** + * Sets specific player permission on specfic shop + * + * @param player player + * @param group group + */ + @Override + public void setPlayerGroup(@NotNull final UUID player, @Nullable BuiltInShopPermissionGroup group) { + + if(group == null) { + group = BuiltInShopPermissionGroup.EVERYONE; + } + + ShopPlayerGroupEvent event = new ShopPlayerGroupEvent(Phase.PRE, shop, player, getPlayerGroup(player), group.getNamespacedNode()); + event.callEvent(); + if(group == BuiltInShopPermissionGroup.EVERYONE) { + + this.groups.remove(player); + } else { + + setPlayerGroup(player, group.getNamespacedNode()); + } + + event = event.clone(Phase.POST); + event.callEvent(); + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + } + + @Override + public EnumSet diff(final @Nullable ShopPermission compare) { + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + + //TODO: check maps for equality + + return changes; + } + + @Override + public ShopPermissionBuilder builder() { + + return new SimpleShopPermissionsBuilder().permissions(Map.copyOf(this.groups)); + } + + @Override + public boolean equals(final Object o) { + + if(!(o instanceof final SimpleShopPermission that)) return false; + return Objects.equals(groups, that.groups); + } + + @Override + public int hashCode() { + + return Objects.hashCode(groups); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPrice.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPrice.java new file mode 100644 index 0000000000..93dec6be98 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopPrice.java @@ -0,0 +1,285 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.economy.EconomyProvider; +import com.ghostchu.quickshop.api.event.settings.type.ShopCurrencyEvent; +import com.ghostchu.quickshop.api.shop.ModernShop; +import com.ghostchu.quickshop.api.shop.builder.ShopPriceBuilder; +import com.ghostchu.quickshop.api.shop.components.ShopPrice; +import com.ghostchu.quickshop.api.shop.service.result.ShopChangeType; +import com.ghostchu.quickshop.shop.builder.SimpleShopPriceBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Objects; + +import static java.math.BigDecimal.ZERO; + +/** + * SimpleShopPrice + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +@SuppressWarnings({ "removal"}) +public class SimpleShopPrice implements ShopPrice { + + private final ModernShop shop; + + @Nullable + protected String currency; + protected double price; + + public SimpleShopPrice(@NotNull final ModernShop shop) { + this.shop = shop; + } + + public SimpleShopPrice(@NotNull final ModernShop shop, + @Nullable final String currency, final double price) { + + this.shop = shop; + this.currency = currency; + this.price = price; + } + + /** + * 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; + + //TODO: Determine how to mark as dirty. Maybe through shop service? + //setDirty(); + //setSignText(); + } + + /** + * Gets the currency that shop use + * + * @return The currency name + */ + @Override + public @Nullable String getCurrency() { + + final ShopCurrencyEvent event = ShopCurrencyEvent.RETRIEVE(this.shop, this.currency); + event.callEvent(); + + return event.updated(); + } + + /** + * Sets the currency that shop use + * + * @param currency The currency name; null to use default currency + */ + @Override + public void setCurrency(@Nullable final String currency) { + + if(Objects.equals(this.currency, currency)) { + return; + } + this.currency = currency; + } + + /** + * Check if this shop is free shop + * + * @return Free Shop + */ + @Override + public boolean isFreeShop() { + + return price == 0.0d; + } + + /** + * 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 QuickShop.getInstance().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 QuickShop.getInstance().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(shop.meta().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(shop.meta().getOwner(), shop.bukkitLocation().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(shop.meta().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(shop.meta().getOwner(), shop.bukkitLocation().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; + } + + @Override + public EnumSet diff(final @Nullable ShopPrice compare) { + + final EnumSet changes = EnumSet.noneOf(ShopChangeType.class); + + if(compare == null || !Objects.equals(this.currency, compare.getCurrency())) { + changes.add(ShopChangeType.CURRENCY); + } + + if(compare == null || compare.price() instanceof Double && this.price != (Double)compare.price()) { + changes.add(ShopChangeType.PRICE); + } + return changes; + } + + @Override + public ShopPriceBuilder builder() { + + return new SimpleShopPriceBuilder().price(this.price).currency(this.currency); + } + + @Override + public boolean equals(final Object o) { + + if(!(o instanceof final SimpleShopPrice that)) return false; + return Double.compare(price, that.price) == 0 && Objects.equals(currency, that.currency); + } + + @Override + public int hashCode() { + + return Objects.hash(currency, price); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopTrading.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopTrading.java new file mode 100644 index 0000000000..7c43ce57c2 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/SimpleShopTrading.java @@ -0,0 +1,121 @@ +package com.ghostchu.quickshop.shop.components; + +/* + * 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.ModernShop; +import com.ghostchu.quickshop.api.shop.components.ShopTrading; +import com.ghostchu.quickshop.api.shop.trading.TradeResult; +import com.ghostchu.quickshop.api.shop.trading.TradeService; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +/** + * SimpleShopTrading + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +@SuppressWarnings({ "removal"}) +public class SimpleShopTrading implements ShopTrading { + + private final ModernShop shop; + + public SimpleShopTrading(@NotNull final ModernShop shop) { + this.shop = shop; + } + + /** + * Get shop is or not in buying mode + * + * @return yes or no + * + * @deprecated Use the #shopType() method instead. + */ + @Override + public boolean isBuying() { + + return shop.meta().shopType().isBuying(); + } + + /** + * Get shop is frozen or not + * + * @return yes or no + * + * @deprecated Use the #shopState() method instead. + */ + @Override + public boolean isFrozen() { + + return this.shop.meta().shopType().isTradingBlocked() || !this.shop.meta().shopState().isTradingAllowed(); + } + + /** + * Get shop is or not in selling mode + * + * @return yes or no + * + * @deprecated Use the #shopType() method instead. + */ + @Override + public boolean isSelling() { + + return !shop.meta().shopType().isBuying(); + } + + /** + * Execute buy action for player with x items. + * + * @param buyer The player buying + * @param buyerInventory The buyer inventory ( may not a player inventory ) + * @param loc2Drop The location to drops items if player inventory are full + * @param amount The amount to buy + * + * @throws Exception Possible exception thrown if anything wrong. + * @deprecated Use the {@link TradeService} instead. + */ + @Override + public TradeResult buy(@NotNull final QUser buyer, @NotNull final InventoryWrapper buyerInventory, + @NotNull final Location loc2Drop, final int amount) { + + return QuickShop.getInstance().getShopManager().tradeService().executeSellToShop(shop, buyer, buyerInventory, loc2Drop, amount);; + } + + /** + * Execute sell action for player with x items. + * + * @param seller Seller + * @param sellerInventory Seller's inventory ( may not a player inventory ) + * @param loc2Drop The location to be drop if buyer inventory full ( if player enter a + * number that < 0, it will turn to buying item) + * @param amount The amount to sell + * + * @throws Exception Possible exception thrown if anything wrong. + * @deprecated Use the {@link TradeService} instead. + */ + @Override + public TradeResult sell(@NotNull final QUser seller, @NotNull final InventoryWrapper sellerInventory, + @NotNull final Location loc2Drop, final int amount) { + + return QuickShop.getInstance().getShopManager().tradeService().executeBuyFromShop(shop, seller, sellerInventory, loc2Drop, amount); + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/implementation details.md b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/implementation details.md new file mode 100644 index 0000000000..dd480864c5 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/components/implementation details.md @@ -0,0 +1,65 @@ +```java + +ShopUpdateRequest request = ShopUpdateRequest.builder() + .actor(player/commandsender) + .shop(ContainerShop.builder() + .meta(SimpleMeta.builder() + .name(name) + .owner(example) + .item(itemStack) + .build()) + .price(SimpleShopPrice.builder() + .price(100.0d) + .build()) + .build()) + .options(ShopUpdateOptions.builder() + .updateSign(true) + .updateDisplay(true) + .updateDatabase(true) + .build()) + .build(); + +ShopActionResult result = QuickShop.getInstance().shopManager().service().update(request); + +worldAdapter.apply(result); + +ShopItem.asBuilder().... + +ShopBuilder toBuilder(); + +default Shop withChanges(Consumer changes) { + ShopBuilder builder = toBuilder(); + changes.accept(builder); + return builder.build(); +} + +public ActionResult updateShop( + UUID shopId, + Consumer updater) { + Shop updated = shops.compute(shopId, (id, existing)->{ + if (existing == null) { + return null; + } + + ShopBuilder builder = existing.toBuilder(); + updater.accept(builder); + return builder.build(); + }); + + if(updated == null) { + return ActionResult.failure("Shop not found"); + } + + return ActionResult.success(updated); +} + +ActionResult result = shopService.updateShop(shopId, builder->builder + .price(500) + .stock(32)); + +Shop updated = shop.withChanges(builder->builder + .price(500) + .stock(32)); + + +``` \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/datatype/ShopSignPersistentDataType.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/datatype/ShopSignPersistentDataType.java index 812582b607..a8377a48a7 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/datatype/ShopSignPersistentDataType.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/datatype/ShopSignPersistentDataType.java @@ -1,7 +1,7 @@ package com.ghostchu.quickshop.shop.datatype; import com.ghostchu.quickshop.common.util.JsonUtil; -import com.ghostchu.quickshop.shop.ShopSignStorage; +import com.ghostchu.quickshop.api.shop.ShopSignStorage; import org.bukkit.persistence.PersistentDataAdapterContext; import org.bukkit.persistence.PersistentDataType; import org.jetbrains.annotations.NotNull; 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 7f4ecbb7cd..280ea3bad6 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 @@ -19,10 +19,13 @@ */ import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.shop.ModernShop; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.display.DisplayItem; import com.ghostchu.quickshop.api.shop.display.DisplayType; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.shop.datatype.ShopProtectionFlag; +import com.ghostchu.quickshop.shop.display.virtual.VirtualDisplayItemManager; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.simplereloadlib.ReloadResult; @@ -30,7 +33,6 @@ import com.ghostchu.simplereloadlib.Reloadable; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.NamespacedKey; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -39,25 +41,26 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static com.ghostchu.quickshop.shop.display.virtual.VirtualDisplayItemManager.DISPLAY_MARK_NAMESPACE; + /** * @author Netherfoam A display item, that spawns a block above the chest and cannot be interacted * with. */ -public abstract class AbstractDisplayItem implements Reloadable { +public abstract class AbstractDisplayItem implements DisplayItem, Reloadable { protected static final QuickShop PLUGIN = QuickShop.getInstance(); - private static final NamespacedKey DISPLAY_MARK_NAMESPACE = new NamespacedKey(QuickShop.getInstance().getJavaPlugin(), "display_protection"); - private static boolean virtualDisplayDoesntWork = false; + protected final ItemStack originalItemStack; - protected final Shop shop; + protected final ModernShop shop; @Nullable protected ItemStack guardedStack; private boolean pendingRemoval; - protected AbstractDisplayItem(final Shop shop) { + protected AbstractDisplayItem(final ModernShop shop) { this.shop = shop; - this.originalItemStack = shop.getItem().clone(); + this.originalItemStack = shop.item().getItem(); PLUGIN.getReloadManager().register(this); init(); } @@ -71,7 +74,7 @@ protected AbstractDisplayItem(final Shop shop) { public static DisplayType getNowUsing() { final DisplayType displayType = DisplayType.fromID(PLUGIN.getConfig().getInt("shop.display-type")); - if(displayType == DisplayType.VIRTUALITEM && virtualDisplayDoesntWork) { + if(displayType == DisplayType.VIRTUALITEM && VirtualDisplayItemManager.isVirtualDisplayDoesntWork()) { return DisplayType.CUSTOM; } return displayType; @@ -152,16 +155,6 @@ public static ShopProtectionFlag createShopProtectionFlag( return new ShopProtectionFlag(shop.bukkitLocation().toString(), Util.serialize(itemStack)); } - public static boolean isVirtualDisplayDoesntWork() { - - return virtualDisplayDoesntWork; - } - - public static void setVirtualDisplayDoesntWork(final boolean shouldDisable) { - - virtualDisplayDoesntWork = shouldDisable; - } - protected void init() { if(PLUGIN.getConfig().getBoolean("shop.display-allow-stacks")) { @@ -321,7 +314,7 @@ public ReloadResult reloadModule() { * @return The shop that display holding */ @NotNull - public Shop getShop() { + public ModernShop shop() { return shop; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItemManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItemManager.java index af4176ea16..ad82085073 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItemManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItemManager.java @@ -30,6 +30,7 @@ import com.ghostchu.quickshop.util.logger.Log; import lombok.Getter; import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -45,6 +46,10 @@ public class VirtualDisplayItemManager { private static VirtualDisplayItemManager instance; + + public static final NamespacedKey DISPLAY_MARK_NAMESPACE = new NamespacedKey(QuickShop.getInstance().getJavaPlugin(), "display_protection"); + private boolean virtualDisplayDoesntWork = false; + public final Map shopEntities = new ConcurrentHashMap<>(); protected final Map> packetHandlers = new LinkedHashMap<>(); @Getter @@ -172,6 +177,16 @@ public VirtualDisplayItem createVirtualDisplayItem(@NotNull final Shop shop) return new VirtualDisplayItem<>(this, packetFactory, shop); } + public static boolean isVirtualDisplayDoesntWork() { + + return instance.virtualDisplayDoesntWork; + } + + public static void setVirtualDisplayDoesntWork(final boolean shouldDisable) { + + instance.virtualDisplayDoesntWork = shouldDisable; + } + public boolean isTestPassed() { return testPassed; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/limit/SimpleRuleSet.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/limit/SimpleRuleSet.java new file mode 100644 index 0000000000..cfac962d21 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/limit/SimpleRuleSet.java @@ -0,0 +1,98 @@ +package com.ghostchu.quickshop.shop.limit; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.limit.RuleSet; +import org.bukkit.command.CommandSender; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class SimpleRuleSet implements RuleSet { + + private final List> items; + private final String bypassPermission; + private final List currencies; + private final double minPrice; + private final double maxPrice; + + + public SimpleRuleSet(final List> items, final String bypassPermission, + final List currencies, final double minPrice, final double maxPrice) { + + this.items = items; + this.bypassPermission = bypassPermission; + this.currencies = currencies; + this.minPrice = minPrice; + this.maxPrice = maxPrice; + } + + @Override + public List> items() { + + return items; + } + + @Override + public List currencies() { + + return currencies; + } + + @Override + public String bypassPermission() { + + return bypassPermission; + } + + @Override + public Double minPrice() { + + return minPrice; + } + + @Override + public Double maxPrice() { + + return maxPrice; + } + + @Override + public boolean hasMinPrice() { + + return minPrice > 0; + } + + @Override + public boolean hasMaxPrice() { + + return maxPrice >= 0; + } + + @Override + public boolean isAllowed(final Double price) { + + if(hasMaxPrice() && price > maxPrice) { + return false; + } + if(hasMinPrice()) { + return price >= minPrice; + } + return true; + } + + @Override + public boolean canBypass(final @NotNull CommandSender sender) { + + return QuickShop.getPermissionManager().hasPermission(sender, this.bypassPermission); + } + + @Override + public boolean canBypass(final @NotNull QUser user) { + + return QuickShop.getPermissionManager().hasPermission(user, this.bypassPermission); + } +} \ No newline at end of file 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 93de0b0a22..19ea7317db 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 @@ -25,8 +25,8 @@ import com.ghostchu.quickshop.api.inventory.InventoryWrapper; import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.Info; -import com.ghostchu.quickshop.api.shop.PriceLimiter; -import com.ghostchu.quickshop.api.shop.PriceLimiterCheckResult; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiter; +import com.ghostchu.quickshop.api.shop.limit.PriceLimiterCheckResult; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.ShopAction; import com.ghostchu.quickshop.api.shop.ShopManager;