From 29324c4767d5ffef88b6d478e4b89b8c27d11fd6 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Mon, 2 Mar 2026 19:07:55 -0500 Subject: [PATCH 01/29] Start work on tagging addon, expanding upon the tagging database methods. --- .changelog/6.3.0.0.md | 5 +- addon/tags/pom.xml | 62 ++++ .../ghostchu/quickshop/addon/tags/Main.java | 46 +++ .../quickshop/addon/tags/TagService.java | 80 +++++ .../ghostchu/quickshop/addon/tags/Util.java | 169 +++++++++++ .../addon/tags/command/SubCommand_Avoid.java | 95 ++++++ .../tags/command/SubCommand_Favorite.java | 95 ++++++ .../addon/tags/command/SubCommand_Tag.java | 120 ++++++++ .../addon/tags/command/SubCommand_Watch.java | 74 +++++ addon/tags/src/main/resources/config.yml | 6 + addon/tags/src/main/resources/plugin.yml | 16 + pom.xml | 1 + .../api/database/DatabaseHelper.java | 14 +- .../command/subcommand/SubCommand_Tag.java | 280 ++++++++++++++++++ .../database/SimpleDatabaseHelperV2.java | 65 +++- .../src/main/resources/lang/messages.yml | 4 + 16 files changed, 1120 insertions(+), 12 deletions(-) create mode 100644 addon/tags/pom.xml create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java create mode 100644 addon/tags/src/main/resources/config.yml create mode 100644 addon/tags/src/main/resources/plugin.yml create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index 6448b5cfe5..29ce9c2395 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -94,9 +94,12 @@ This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) - Replaced startup parameter "skipDatabaseVersionCheck" with config option "database.skip-version-check" - Bumped config version to 1037 - started including tax amount in shop failure message "you-cant-afford-to-buy" +- Update configuration file comments and standardize formatting(thanks to YuanYuanOwO) ## Fixes - Fix issue where disabling tax for unlimited shops wasn't properly working. - Fix issue where FAWE on 2.11.2 and lower wasn't working correctly. - Fix issue where FAWE would remove shops outside of selection zone. -- Fix issue where limits configs were not working properly(thanks to YuanYuanOwO) \ No newline at end of file +- Fix issue where limits configs were not working properly(thanks to YuanYuanOwO) +- Fix Folia thread violations in /qs info stock stats(thanks to GoodrichDev) +- Fix Towny Compat not working in Folia(thanks to GoodrichDev) \ No newline at end of file diff --git a/addon/tags/pom.xml b/addon/tags/pom.xml new file mode 100644 index 0000000000..6cbac8251d --- /dev/null +++ b/addon/tags/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + com.ghostchu + quickshop-hikari + 6.3.0.0-SNAPSHOT-5 + ../../pom.xml + + com.ghostchu.quickshop.addon + tags + takari-jar + + Addon-Tags + + QuickShop-Tags adds a lightweight, powerful tagging system to QuickShop-Hikari, allowing players to categorize, favorite, and track shops without altering core shop behavior. + + + + dv8tion + m2-dv8tion + https://m2.dv8tion.net/releases + + + Scarsz-Nexus + https://nexus.scarsz.me/content/groups/public/ + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + src/main/resources + true + + + + + + + + io.papermc.paper + paper-api + ${depend.paper} + + + com.ghostchu + quickshop-bukkit + ${project.parent.version} + provided + + + diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java new file mode 100644 index 0000000000..5fad56e48e --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java @@ -0,0 +1,46 @@ +package com.ghostchu.quickshop.addon.tags; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.command.SubCommand_Tag; +import com.ghostchu.quickshop.api.command.CommandContainer; +import org.bukkit.Bukkit; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; + +public final class Main extends JavaPlugin implements Listener { + + static Main instance; + private QuickShop plugin; + + @Override + public void onLoad() { + + instance = this; + } + + @Override + public void onDisable() { + + HandlerList.unregisterAll((Plugin)this); + } + + @Override + public void onEnable() { + + saveDefaultConfig(); + plugin = QuickShop.getInstance(); + getLogger().info("Registering tags commands..."); + Bukkit.getPluginManager().registerEvents(this, this); + plugin.getCommandManager().registerCmd( + CommandContainer + .builder() + .prefix("tag") + .description((locale)->plugin.text().of("addon.list.commands.list").forLocale(locale)) + .selectivePermission("quickshopaddon.list.self") + .selectivePermission("quickshopaddon.list.other") + .executor(new SubCommand_Tag(plugin)) + .build()); + } +} diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java new file mode 100644 index 0000000000..00d531e54e --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java @@ -0,0 +1,80 @@ +package com.ghostchu.quickshop.addon.tags; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +/** + * TagService + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class TagService { + + private static final int MAX_TAG_LENGTH = 32; + private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); + + private final Main tagsMain; + private final QuickShop plugin; + + public TagService(final Main tagsMain, final QuickShop plugin) { + this.tagsMain = tagsMain; + this.plugin = plugin; + } + + public @Nullable String normalizePlayerTag(String input) { + if (input == null) return null; + + if (input.startsWith("#")) { + input = input.substring(1); + } + + input = input.trim().toLowerCase(Locale.ROOT); + + if (input.isEmpty()) return null; + if (input.length() > MAX_TAG_LENGTH) return null; + if (input.startsWith("@")) return null; + + if (!VALID_TAG_PATTERN.matcher(input).matches()) { + return null; + } + + return input; + } + + public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { + final var db = plugin.getDatabaseHelper(); + + return db.tagShop(player, shopId, tag).thenCompose(result -> { + if (result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return db.removeShopTag(player, shopId, tag) + .thenApply(r -> false); + }); + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java new file mode 100644 index 0000000000..7957b1db07 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java @@ -0,0 +1,169 @@ +package com.ghostchu.quickshop.addon.tags; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Util + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class Util { + + public static final String SYS_FAV = "@fav"; + public static final String SYS_WATCH = "@watch"; + public static final String SYS_AVOID = "@avoid"; + public static final int MAX_TAG_LENGTH = 32; + //we only want letters, underscores and dashes + public static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); + + //our shop-specific methods + public static void handleAdd(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser, final int tagPosition) { + if(parser.getArgs().size() < tagPosition) { + //sendUsage(sender); + return; + } + + final String tag = normalizeTag(parser.getArgs().get(tagPosition - 1), false); + if(tag == null) { + QuickShop.getInstance().text().of(sender, "tag-invalid").send(); + return; + } + com.ghostchu.quickshop.util.Util.regionThread(sender.getLocation(), () -> { + QuickShop.getInstance().getDatabaseHelper().tagShop(sender.getUniqueId(), shop.getShopId(), tag); + QuickShop.getInstance().text().of(sender, "tag-added", tag).send(); + }); + } + + public static void handleRemove(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser) { + if(parser.getArgs().size() < 2) { + //sendUsage(sender); + return; + } + } + + public static void handleClear(final Player sender, @NotNull final Shop shop) { + + } + + public static boolean handleToggleSystem(final Player sender, @NotNull final Shop shop, final String tag) { + + } + + public static void handleUnsetSystem(final Player sender, @NotNull final Shop shop, final String tag) { + + } + + public static void handleClearAll(final Player sender, @NotNull final Shop shop) { + + //TODO: User parameter + if(!sender.hasPermission("quickshop.tag.clearall")) { + QuickShop.getInstance().text().of(sender, "no-permission").send(); + return; + } + + + } + + //our global methods + public static void handleTaggedList(final Player sender, @NotNull final CommandParser parser) { + } + + public static void handleTaggedList(final Player sender, @NotNull final ArrayDeque tags) { + } + + public static void handleTags(final Player sender) { + + } + + public static void handleTags(final Player sender, @NotNull final Shop shop) { + + } + + public static void handleRemoveTagFromAllShops(final Player sender, @NotNull final CommandParser parser) { + + } + + public static String displayTag(final Player sender, final String stored) { + if(stored == null) { + return ""; + } + + return switch (stored) { + case SYS_FAV -> "Favorite"; + case SYS_WATCH -> "Watch"; + case SYS_AVOID -> "Avoid"; + default -> "#" + stored; + }; + } + + public static String displayTagOrHash(final String stored) { + if(stored == null) { + return ""; + } + + if(stored.startsWith("@")) { + return stored; + } + return "#" + stored; + } + + @Nullable + public static String normalizeTag(@Nullable String input, final boolean allowSystem) { + + if (input == null) { + return null; + } + + if (input.startsWith("#")) { + input = input.substring(1); + } + + input = input.trim().toLowerCase(Locale.ROOT); + + if (input.isEmpty()) { + return null; + } + + if (input.length() > MAX_TAG_LENGTH) { + return null; + } + + if (!allowSystem && input.startsWith("@")) { + return null; + } + + if (!VALID_TAG_PATTERN.matcher(input).matches()) { + return null; + } + + return input; + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java new file mode 100644 index 0000000000..129197c62d --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java @@ -0,0 +1,95 @@ +package com.ghostchu.quickshop.addon.tags.command; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static com.ghostchu.quickshop.addon.tags.Util.SYS_AVOID; +import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; +import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; + +/** + * SubCommand_Avoid + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SubCommand_Avoid implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Avoid(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + if(parser.getArgs().size() == 0) { + + final Shop shop = getLookingShop(sender); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + //TODO: Toggle avoid for the shop + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + + case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_AVOID))); + } + } + + @NotNull + @Override + public List onTabComplete( + @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("list"); + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", + "/quickshop avoid [list]") + .send(); + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java new file mode 100644 index 0000000000..2dd8c65f52 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java @@ -0,0 +1,95 @@ +package com.ghostchu.quickshop.addon.tags.command; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import static com.ghostchu.quickshop.addon.tags.Util.SYS_FAV; +import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; +import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; + +/** + * SubCommand_Favorite + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class SubCommand_Favorite implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Favorite(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + if(parser.getArgs().size() == 0) { + + final Shop shop = getLookingShop(sender); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + //TODO: Toggle favorite for the shop + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + + case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_FAV))); + } + } + + @NotNull + @Override + public List onTabComplete( + @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("list"); + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", + "/quickshop fav [list]") + .send(); + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java new file mode 100644 index 0000000000..f6024bfad7 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java @@ -0,0 +1,120 @@ +package com.ghostchu.quickshop.addon.tags.command; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.util.Util; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import static com.ghostchu.quickshop.addon.tags.Util.SYS_AVOID; +import static com.ghostchu.quickshop.addon.tags.Util.SYS_FAV; +import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; +import static com.ghostchu.quickshop.addon.tags.Util.handleAdd; +import static com.ghostchu.quickshop.addon.tags.Util.handleClear; +import static com.ghostchu.quickshop.addon.tags.Util.handleClearAll; +import static com.ghostchu.quickshop.addon.tags.Util.handleRemove; +import static com.ghostchu.quickshop.addon.tags.Util.handleRemoveTagFromAllShops; +import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; +import static com.ghostchu.quickshop.addon.tags.Util.handleTags; +import static com.ghostchu.quickshop.addon.tags.Util.normalizeTag; + +public class SubCommand_Tag implements CommandHandler { + + private final QuickShop plugin; + //our system tags + + public SubCommand_Tag(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + final String sub = parser.getArgs().getFirst(); + + //These are our global tag-related commands. They don't require looking at a shop. + switch (sub) { + // /qs tag tags + case "tags" -> { + handleTags(sender); + return; + } + // /qs tag tagged + case "tagged" -> { + handleTaggedList(sender, parser); + return; + } + // /qs tag purge + case "purge", "removefromall", "untagall" -> { + handleRemoveTagFromAllShops(sender, parser); + return; + } + default -> {} + } + + final Shop shop = getLookingShop(sender); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + // Check permission + if(!shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_BENEFIT) + && !plugin.perm().hasPermission(sender, "quickshop.other.benefit")) { + plugin.text().of(sender, "not-managed-shop").send(); + return; + } + + switch(parser.getArgs().getFirst()) { + case "add" -> handleAdd(sender, shop, parser, 2); + case "remove", "del", "delete" -> handleRemove(sender, shop, parser); + case "clear" -> handleClear(sender, shop); + + // Optional: admin "clear all tags from this shop for everyone" + case "clearall" -> handleClearAll(sender, shop); + + case "list" -> handleTags(sender, shop); + + default -> handleAdd(sender, shop, parser, 1); + } + } + + @NotNull + @Override + public List onTabComplete( + @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("add", "remove", "query"); + } + if(parser.getArgs().size() == 2) { + return null; + } + if(parser.getArgs().size() == 3) { + return Collections.singletonList(plugin.text().of(sender, "tabcomplete.percentage").legacy()); + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", + "/quickshop tag <[tag]/remove/list/clear/tagged/tags> [player]") + .send(); + } +} diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java new file mode 100644 index 0000000000..a646d90527 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java @@ -0,0 +1,74 @@ +package com.ghostchu.quickshop.addon.tags.command; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.util.Util; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; +import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; + +public class SubCommand_Watch implements CommandHandler { + + private final QuickShop plugin; + + public SubCommand_Watch(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + if(parser.getArgs().size() == 0) { + + final Shop shop = getLookingShop(sender); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + //TODO: Toggle watch for the shop + return; + } + + final String sub = parser.getArgs().getFirst(); + switch(sub.toLowerCase(Locale.ROOT)) { + + case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_WATCH))); + } + } + + @NotNull + @Override + public List onTabComplete( + @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("list"); + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", + "/quickshop watch [list]") + .send(); + } +} \ No newline at end of file diff --git a/addon/tags/src/main/resources/config.yml b/addon/tags/src/main/resources/config.yml new file mode 100644 index 0000000000..41aafdb045 --- /dev/null +++ b/addon/tags/src/main/resources/config.yml @@ -0,0 +1,6 @@ +config-version: 1 + +tag: + max-length: 32 + max-per-shop: 20 + max-total-per-player: 200 \ No newline at end of file diff --git a/addon/tags/src/main/resources/plugin.yml b/addon/tags/src/main/resources/plugin.yml new file mode 100644 index 0000000000..7c41a27300 --- /dev/null +++ b/addon/tags/src/main/resources/plugin.yml @@ -0,0 +1,16 @@ +name: qsaddon-${project.artifactId} +version: '${project.version}' +main: com.ghostchu.quickshop.addon.${project.artifactId}.Main +#Has to be included for folia support +folia-supported: true +api-version: '1.20' +depend: + - QuickShop-Hikari +authors: [ creatorfromhell ] +permissions: + quickshopaddon.list.self: + description: Allow player to use /quickshop list to query self + default: true + quickshopaddon.list.other: + description: Allow player to use /quickshop list to query other + default: op \ No newline at end of file diff --git a/pom.xml b/pom.xml index b9021944d1..c3f1ccdb59 100644 --- a/pom.xml +++ b/pom.xml @@ -484,6 +484,7 @@ addon/reremake-migrator addon/squaremap addon/shopitemonly + addon/tags compatibility/advancedregionmarket compatibility/bentobox diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java index e06447fb7e..760e6833b9 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java @@ -125,20 +125,30 @@ public interface DatabaseHelper { @NotNull List listShops(@Nullable String worldFilter, boolean deleteIfCorrupt); + + List listShopsByTag(@NotNull String tag); + @NotNull List listShopsTaggedBy(@NotNull UUID tagger, @NotNull String tag); @NotNull List listTags(@NotNull UUID tagger); + @NotNull + List listTags(@NotNull UUID tagger, @NotNull Long shopId); + + CompletableFuture<@Nullable Integer> tagShop(@NotNull UUID tagger, @NotNull Long shopId, @NotNull String tag); + + CompletableFuture<@Nullable Integer> removeAllShopTags(@NotNull Long shopId); + + CompletableFuture<@Nullable Integer> removeAllTagsBy(@NotNull UUID tagger); + CompletableFuture<@Nullable Integer> removeShopTag(@NotNull UUID tagger, @NotNull Long shopId, @NotNull String tag); CompletableFuture<@Nullable Integer> removeShopAllTag(@NotNull UUID tagger, @NotNull Long shopId); CompletableFuture<@Nullable Integer> removeTagFromShops(@NotNull UUID tagger, @NotNull String tag); - CompletableFuture<@Nullable Integer> tagShop(@NotNull UUID tagger, @NotNull Long shopId, @NotNull String tag); - /** * Locate a shop record from database by shop record id * diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java new file mode 100644 index 0000000000..60df8815df --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java @@ -0,0 +1,280 @@ +package com.ghostchu.quickshop.command.subcommand; + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.command.CommandHandler; +import com.ghostchu.quickshop.api.command.CommandParser; +import com.ghostchu.quickshop.api.economy.benefit.BenefitOverflowException; +import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; +import com.ghostchu.quickshop.api.economy.benefit.BenefitsAlreadyException; +import com.ghostchu.quickshop.api.event.Phase; +import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitAddEvent; +import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitRemoveEvent; +import com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.common.util.CommonUtil; +import com.ghostchu.quickshop.obj.QUserImpl; +import com.ghostchu.quickshop.util.MsgUtil; +import com.ghostchu.quickshop.util.Util; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SubCommand_Tag implements CommandHandler { + + private final QuickShop plugin; + //our system tags + private static final String SYS_FAV = "@fav"; + private static final String SYS_WATCH = "@watch"; + private static final String SYS_AVOID = "@avoid"; + + private static final int MAX_TAG_LENGTH = 32; + //we only want letters, underscores and dashes + private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); + + public SubCommand_Tag(final QuickShop plugin) { + + this.plugin = plugin; + } + + @Override + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().isEmpty()) { + sendUsage(sender); + return; + } + + final String sub = parser.getArgs().getFirst(); + + //These are our global tag-related commands. They don't require looking at a shop. + switch (sub) { + // /qs tag tags + case "tags" -> { + handleTags(sender); + return; + } + // /qs tag tagged + case "tagged" -> { + handleTaggedList(sender, parser); + return; + } + // /qs tag favs + case "favs" -> { + handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_FAV))); + return; + } + // /qs tag watched + case "watched" -> { + handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_WATCH))); + return; + } + // /qs tag avoided + case "avoided" -> { + handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_AVOID))); + return; + } + // /qs tag purge + case "purge", "removefromall", "untagall" -> { + handleRemoveTagFromAllShops(sender, parser); + return; + } + default -> {} + } + + final Shop shop = getLookingShop(sender); + if(shop == null) { + plugin.text().of(sender, "not-looking-at-shop").send(); + return; + } + + // Check permission + if(!shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_BENEFIT) + && !plugin.perm().hasPermission(sender, "quickshop.other.benefit")) { + plugin.text().of(sender, "not-managed-shop").send(); + return; + } + + switch(parser.getArgs().getFirst()) { + case "add" -> handleAdd(sender, shop, parser, 2); + case "remove", "del", "delete" -> handleRemove(sender, shop, parser); + case "clear" -> handleClear(sender, shop); + + // Optional: admin "clear all tags from this shop for everyone" + case "clearall" -> handleClearAll(sender, shop); + + // System tag shortcuts (looked-at shop) + case "fav" -> handleToggleSystem(sender, shop, SYS_FAV); + case "unfav" -> handleUnsetSystem(sender, shop, SYS_FAV); + + case "watch" -> handleToggleSystem(sender, shop, SYS_WATCH); + case "unwatch" -> handleUnsetSystem(sender, shop, SYS_WATCH); + + case "avoid" -> handleToggleSystem(sender, shop, SYS_AVOID); + case "unavoid" -> handleUnsetSystem(sender, shop, SYS_AVOID); + + case "list" -> handleTags(sender, shop); + + default -> handleAdd(sender, shop, parser, 1); + } + } + + @NotNull + @Override + public List onTabComplete( + @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("add", "remove", "query"); + } + if(parser.getArgs().size() == 2) { + return null; + } + if(parser.getArgs().size() == 3) { + return Collections.singletonList(plugin.text().of(sender, "tabcomplete.percentage").legacy()); + } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", + "/quickshop tag <[tag]/remove/list/clear/tagged/tags/fav/unfav/favs/watch/unwatch/watched/avoid/unavoid/avoided>") + .send(); + } + + //our shop-specific methods + private void handleAdd(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser, final int tagPosition) { + if(parser.getArgs().size() < tagPosition) { + sendUsage(sender); + return; + } + + final String tag = normalizeTag(parser.getArgs().get(tagPosition - 1), false); + if(tag == null) { + plugin.text().of(sender, "tag-invalid").send(); + return; + } + Util.regionThread(sender.getLocation(), () -> { + plugin.getDatabaseHelper().tagShop(sender.getUniqueId(), shop.getShopId(), tag); + plugin.text().of(sender, "tag-added", tag).send(); + }); + } + + private void handleRemove(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser) { + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + } + + private void handleClear(final Player sender, @NotNull final Shop shop) { + + } + + private void handleToggleSystem(final Player sender, @NotNull final Shop shop, final String tag) { + + } + + private void handleUnsetSystem(final Player sender, @NotNull final Shop shop, final String tag) { + + } + + private void handleClearAll(final Player sender, @NotNull final Shop shop) { + + //TODO: User parameter + if(!sender.hasPermission("quickshop.tag.clearall")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + + } + + //our global methods + private void handleTaggedList(final Player sender, @NotNull final CommandParser parser) { + } + + private void handleTaggedList(final Player sender, @NotNull final ArrayDeque tags) { + } + + private void handleTags(final Player sender) { + + } + + private void handleTags(final Player sender, @NotNull final Shop shop) { + + } + + private void handleRemoveTagFromAllShops(final Player sender, @NotNull final CommandParser parser) { + + } + + private String displayTag(final Player sender, final String stored) { + if(stored == null) { + return ""; + } + + return switch (stored) { + case SYS_FAV -> "Favorite"; + case SYS_WATCH -> "Watch"; + case SYS_AVOID -> "Avoid"; + default -> "#" + stored; + }; + } + + private String displayTagOrHash(final String stored) { + if(stored == null) { + return ""; + } + + if(stored.startsWith("@")) { + return stored; + } + return "#" + stored; + } + + @Nullable + private String normalizeTag(@Nullable String input, final boolean allowSystem) { + + if (input == null) { + return null; + } + + if (input.startsWith("#")) { + input = input.substring(1); + } + + input = input.trim().toLowerCase(Locale.ROOT); + + if (input.isEmpty()) { + return null; + } + + if (input.length() > MAX_TAG_LENGTH) { + return null; + } + + if (!allowSystem && input.startsWith("@")) { + return null; + } + + if (!VALID_TAG_PATTERN.matcher(input).matches()) { + return null; + } + + return input; + } +} diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index 3af198a584..ff321265df 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -526,6 +526,21 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina return shopRecords; } + @Override + public @NotNull List listShopsByTag(@NotNull final String tag) { + + final List shopIds = new ArrayList<>(); + try(final SQLQuery query = DataTables.TAGS.createQuery() + .addCondition("tag", tag) + .build().execute()) { + final ResultSet set = query.getResultSet(); + shopIds.add(set.getLong("shop")); + } catch(final SQLException e) { + plugin.logger().error("Failed to list shops by with tag " + tag, e); + } + return shopIds; + } + @Override public @NotNull List listShopsTaggedBy(@NotNull final UUID tagger, @NotNull final String tag) { @@ -557,6 +572,47 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina return tags; } + @Override + public @NotNull List listTags(@NotNull final UUID tagger, @NotNull final Long shopId) { + + final List tags = new ArrayList<>(); + try(final SQLQuery query = DataTables.TAGS.createQuery() + .addCondition("tagger", tagger.toString()) + .addCondition("shop", shopId) + .build().execute()) { + final ResultSet set = query.getResultSet(); + tags.add(set.getString("tag")); + } catch(final SQLException e) { + plugin.logger().error("Failed to list tags by " + tagger, e); + } + return tags; + } + + @Override + public @NotNull CompletableFuture<@Nullable Integer> tagShop(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { + + return DataTables.TAGS.createInsert() + .setColumnNames("tagger", "shop", "tag") + .setParams(tagger.toString(), shopId, tag) + .executeFuture(i->i); + } + + @Override + public CompletableFuture<@Nullable Integer> removeAllShopTags(@NotNull final Long shopId) { + + return DataTables.TAGS.createDelete() + .addCondition("shop", shopId) + .build().executeFuture(i->i); + } + + @Override + public CompletableFuture<@Nullable Integer> removeAllTagsBy(@NotNull final UUID tagger) { + + return DataTables.TAGS.createDelete() + .addCondition("tagger", tagger.toString()) + .build().executeFuture(i->i); + } + @Override public CompletableFuture<@Nullable Integer> removeShopTag(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { @@ -584,15 +640,6 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina .build().executeFuture(i->i); } - @Override - public @NotNull CompletableFuture<@Nullable Integer> tagShop(@NotNull final UUID tagger, @NotNull final Long shopId, @NotNull final String tag) { - - return DataTables.TAGS.createInsert() - .setColumnNames("tagger", "shop", "tag") - .setParams(tagger.toString(), shopId, tag) - .executeFuture(i->i); - } - @Override public @NotNull CompletableFuture<@Nullable Long> locateShopDataId(final long shopId) { diff --git a/quickshop-bukkit/src/main/resources/lang/messages.yml b/quickshop-bukkit/src/main/resources/lang/messages.yml index 6787af8d32..6f52346445 100644 --- a/quickshop-bukkit/src/main/resources/lang/messages.yml +++ b/quickshop-bukkit/src/main/resources/lang/messages.yml @@ -879,6 +879,9 @@ benefit-added: Player {0} has been added into shop benefits! benefit-updated: Player {0}'s benefits has been updated! benefit-query: This shop have {0} players in benefits list! benefit-query-list: - Player {0}, Benefit {1}% +tag-favorite: Favorite +tag-watch: Watch +tag-avoid: Avoid tag-added: Successfully added #{0} to this shop! tag-add-duplicate: The tag #{0} already exists at this shop! tag-removed: Successfully removed #{0} from this shop! @@ -891,6 +894,7 @@ tag-query-listing: - #{0} tag-query-no-tag: This shop have no tags. tag-query-shops: 'This tag contains {0} shops:' tag-query-shops-listing: - {0} +tag-invalid: 'Invalid tag. Only letters, dash (-), and underscore (_) are allowed. Max length: {0}' batch-operations-based-on-tags-no-failure: Successfully batch processed {0} shops. batch-operations-based-on-tags-have-failure: Total {0} shops in batch processing From d115487af2ce5b098301f5a099ee5650c7f7a90d Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sat, 7 Mar 2026 16:07:01 -0500 Subject: [PATCH 02/29] Fully implement the underlying logical methods for the tagging system. This includes the storage system, and database calls. This leaves the commands left to complete as the last item. --- .../quickshop/addon/tags/TagManager.java | 123 ++++++++++++++++++ .../quickshop/addon/tags/TagService.java | 74 +++++++++-- .../quickshop/addon/tags/tag/PlayerTags.java | 50 +++++++ .../quickshop/addon/tags/tag/ShopTags.java | 84 ++++++++++++ .../addon/tags/tag/TaggingResult.java | 32 +++++ .../api/database/DatabaseHelper.java | 2 +- .../database/SimpleDatabaseHelperV2.java | 2 +- .../quickshop/shop/AbstractShopManager.java | 2 +- 8 files changed, 356 insertions(+), 13 deletions(-) create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java create mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java new file mode 100644 index 0000000000..a56092827b --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java @@ -0,0 +1,123 @@ +package com.ghostchu.quickshop.addon.tags; + +/* + * 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.addon.tags.tag.ShopTags; +import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; + +import java.util.Iterator; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * TagManager + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class TagManager { + + private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); + + private final TagService service; + + public TagManager(final Main main, final QuickShop plugin) { + this.service = new TagService(main, plugin); + } + + public TaggingResult addTag(final long shopId, final UUID player, final String tag) { + + final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + if(shopTags.hasTag(player, tag)) { + return TaggingResult.ALREADY_EXISTS; + } + + shopTags.addTag(player, tag); + service.addShopTag(player, shopId, tag); + return TaggingResult.SUCCESS; + } + + public boolean removeAllTags() { + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + removeAllShopTags(shopId); + } + return true; + } + + public boolean removeAllShopTags(final long shopId) { + final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + if(shopTags.isEmpty()) { + return false; + } + shopTags.clear(); + service.removeAllShopTags(shopId); + tags.remove(shopId); + return true; + } + + public boolean removeAllShopTagsBy(final long shopId, final UUID player) { + final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + if(shopTags.isEmpty()) { + return false; + } + shopTags.removeAllTags(player); + service.removeAllShopTagsBy(shopId, player); + return true; + } + + public boolean removeAllPlayerTags(final UUID player) { + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + removeAllShopTagsBy(shopId, player); + } + return true; + } + + public boolean hasTag(final long shopId, final UUID player, final String tag) { + final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + return shopTags.hasTag(player, tag); + } + + public TaggingResult removeTag(final long shopId, final UUID player, final String tag) { + + final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + if(!shopTags.hasTag(player, tag)) { + return TaggingResult.NOT_FOUND; + } + + shopTags.removeTag(player, tag); + service.removeShopTag(player, shopId, tag); + + //remove our shop object if empty to save memory + if(shopTags.isEmpty()) { + tags.remove(shopId); + } + + return TaggingResult.SUCCESS; + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java index 00d531e54e..2dc9411e5c 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java @@ -19,6 +19,7 @@ */ import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.api.database.DatabaseHelper; import org.jetbrains.annotations.Nullable; import java.util.Locale; @@ -41,24 +42,76 @@ public class TagService { private final QuickShop plugin; public TagService(final Main tagsMain, final QuickShop plugin) { + this.tagsMain = tagsMain; this.plugin = plugin; } + public CompletableFuture addShopTag(final UUID player, final long shopId, final String tag) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + + return db.tagShop(player, shopId, tag).thenCompose(result->{ + + if(result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return CompletableFuture.completedFuture(false); + }); + } + + public CompletableFuture removeShopTag(final UUID player, final long shopId, final String tag) { + + final DatabaseHelper db = plugin.getDatabaseHelper(); + + return db.removeShopTag(player, shopId, tag).thenCompose(result->{ + + if(result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return CompletableFuture.completedFuture(false); + }); + } + + public CompletableFuture removeAllShopTags(final long shopId) { + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.removeAllShopTags(shopId).thenCompose(result->{ + if(result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return CompletableFuture.completedFuture(false); + }); + } + + public CompletableFuture removeAllShopTagsBy(final long shopId, final UUID player) { + final DatabaseHelper db = plugin.getDatabaseHelper(); + return db.removeAllShopTagsBy(player, shopId).thenCompose(result->{ + if(result != null && result > 0) { + return CompletableFuture.completedFuture(true); + } + + return CompletableFuture.completedFuture(false); + }); + } + public @Nullable String normalizePlayerTag(String input) { - if (input == null) return null; - if (input.startsWith("#")) { + if(input == null) return null; + + if(input.startsWith("#")) { input = input.substring(1); } input = input.trim().toLowerCase(Locale.ROOT); - if (input.isEmpty()) return null; - if (input.length() > MAX_TAG_LENGTH) return null; - if (input.startsWith("@")) return null; + if(input.isEmpty()) return null; + if(input.length() > MAX_TAG_LENGTH) return null; + if(input.startsWith("@")) return null; - if (!VALID_TAG_PATTERN.matcher(input).matches()) { + if(!VALID_TAG_PATTERN.matcher(input).matches()) { return null; } @@ -66,15 +119,16 @@ public TagService(final Main tagsMain, final QuickShop plugin) { } public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { - final var db = plugin.getDatabaseHelper(); - return db.tagShop(player, shopId, tag).thenCompose(result -> { - if (result != null && result > 0) { + final DatabaseHelper db = plugin.getDatabaseHelper(); + + return db.tagShop(player, shopId, tag).thenCompose(result->{ + if(result != null && result > 0) { return CompletableFuture.completedFuture(true); } return db.removeShopTag(player, shopId, tag) - .thenApply(r -> false); + .thenApply(r->false); }); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java new file mode 100644 index 0000000000..98a95faab2 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java @@ -0,0 +1,50 @@ +package com.ghostchu.quickshop.addon.tags.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * PlayerTags + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class PlayerTags { + + private final Set tags = ConcurrentHashMap.newKeySet(); + + public boolean hasTag(final String tag) { + return tags.contains(tag); + } + + public void addTag(final String tag) { + tags.add(tag); + } + + public void removeTag(final String tag) { + tags.remove(tag); + } + + public Set getTags() { + return tags; + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java new file mode 100644 index 0000000000..1393a33925 --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java @@ -0,0 +1,84 @@ +package com.ghostchu.quickshop.addon.tags.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * ShopTags + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopTags { + + private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); + + public void addTag(final UUID player, final String tag) { + if(!tags.containsKey(player)) { + tags.put(player, new PlayerTags()); + } + tags.get(player).addTag(tag); + } + + public void removeTag(final UUID player, final String tag) { + if(!tags.containsKey(player)) { + return; + } + tags.get(player).removeTag(tag); + + //remove our player object if empty to save memory + if(tags.get(player).getTags().isEmpty()) { + tags.remove(player); + } + } + + public boolean removeAllTags(final UUID player) { + if(!tags.containsKey(player)) { + return false; + } + tags.remove(player); + return true; + } + + public boolean hasTag(final UUID player, final String tag) { + if(!tags.containsKey(player)) { + return false; + } + return getTags(player).hasTag(tag); + } + + public PlayerTags getTags(final UUID player) { + return tags.get(player); + } + + public void setTags(final UUID player, final PlayerTags tags) { + this.tags.put(player, tags); + } + + public boolean isEmpty() { + return tags.isEmpty(); + } + + public void clear() { + tags.clear(); + } + +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java new file mode 100644 index 0000000000..e5b231c10e --- /dev/null +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java @@ -0,0 +1,32 @@ +package com.ghostchu.quickshop.addon.tags.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * TaggingResult + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public enum TaggingResult { + + SUCCESS, + NOT_FOUND, + ALREADY_EXISTS, +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java index 760e6833b9..26571d609d 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/DatabaseHelper.java @@ -145,7 +145,7 @@ public interface DatabaseHelper { CompletableFuture<@Nullable Integer> removeShopTag(@NotNull UUID tagger, @NotNull Long shopId, @NotNull String tag); - CompletableFuture<@Nullable Integer> removeShopAllTag(@NotNull UUID tagger, @NotNull Long shopId); + CompletableFuture<@Nullable Integer> removeAllShopTagsBy(@NotNull UUID tagger, @NotNull Long shopId); CompletableFuture<@Nullable Integer> removeTagFromShops(@NotNull UUID tagger, @NotNull String tag); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index ff321265df..aabe36888f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -623,7 +623,7 @@ public void insertTransactionRecord(@Nullable UUID from, @Nullable UUID to, fina } @Override - public CompletableFuture<@Nullable Integer> removeShopAllTag(@NotNull final UUID tagger, @NotNull final Long shopId) { + public CompletableFuture<@Nullable Integer> removeAllShopTagsBy(@NotNull final UUID tagger, @NotNull final Long shopId) { return DataTables.TAGS.createDelete() .addCondition("tagger", tagger.toString()) 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 cc2269c0b7..0a3475d754 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 @@ -218,7 +218,7 @@ protected void processCreationFail(@NotNull final Shop shop, @NotNull final QUse @Override public CompletableFuture<@Nullable Integer> clearShopTags(@NotNull final UUID tagger, @NotNull final Shop shop) { - return plugin.getDatabaseHelper().removeShopAllTag(tagger, shop.getShopId()); + return plugin.getDatabaseHelper().removeAllShopTagsBy(tagger, shop.getShopId()); } @Override From 520d66e8cafdfad58fc517c47c2e138b343acf93 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sat, 7 Mar 2026 16:09:34 -0500 Subject: [PATCH 03/29] Remove util.java since we moved the logic parts into TagService. --- .../quickshop/addon/tags/TagService.java | 30 ++++ .../ghostchu/quickshop/addon/tags/Util.java | 169 ------------------ 2 files changed, 30 insertions(+), 169 deletions(-) delete mode 100644 addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java index 2dc9411e5c..d55ce6030e 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java @@ -20,6 +20,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.database.DatabaseHelper; +import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; import java.util.Locale; @@ -35,6 +36,11 @@ */ public class TagService { + + public static final String SYS_FAV = "@fav"; + public static final String SYS_WATCH = "@watch"; + public static final String SYS_AVOID = "@avoid"; + private static final int MAX_TAG_LENGTH = 32; private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); @@ -118,6 +124,30 @@ public CompletableFuture removeAllShopTagsBy(final long shopId, final U return input; } + public static String displayTag(final Player sender, final String stored) { + if(stored == null) { + return ""; + } + + return switch (stored) { + case SYS_FAV -> "Favorite"; + case SYS_WATCH -> "Watch"; + case SYS_AVOID -> "Avoid"; + default -> "#" + stored; + }; + } + + public static String displayTagOrHash(final String stored) { + if(stored == null) { + return ""; + } + + if(stored.startsWith("@")) { + return stored; + } + return "#" + stored; + } + public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java deleted file mode 100644 index 7957b1db07..0000000000 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Util.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.ghostchu.quickshop.addon.tags; - -/* - * QuickShop-Hikari - * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.api.command.CommandParser; -import com.ghostchu.quickshop.api.shop.Shop; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayDeque; -import java.util.Locale; -import java.util.regex.Pattern; - -/** - * Util - * - * @author creatorfromhell - * @since 6.3.0.0 - */ -public class Util { - - public static final String SYS_FAV = "@fav"; - public static final String SYS_WATCH = "@watch"; - public static final String SYS_AVOID = "@avoid"; - public static final int MAX_TAG_LENGTH = 32; - //we only want letters, underscores and dashes - public static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); - - //our shop-specific methods - public static void handleAdd(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser, final int tagPosition) { - if(parser.getArgs().size() < tagPosition) { - //sendUsage(sender); - return; - } - - final String tag = normalizeTag(parser.getArgs().get(tagPosition - 1), false); - if(tag == null) { - QuickShop.getInstance().text().of(sender, "tag-invalid").send(); - return; - } - com.ghostchu.quickshop.util.Util.regionThread(sender.getLocation(), () -> { - QuickShop.getInstance().getDatabaseHelper().tagShop(sender.getUniqueId(), shop.getShopId(), tag); - QuickShop.getInstance().text().of(sender, "tag-added", tag).send(); - }); - } - - public static void handleRemove(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser) { - if(parser.getArgs().size() < 2) { - //sendUsage(sender); - return; - } - } - - public static void handleClear(final Player sender, @NotNull final Shop shop) { - - } - - public static boolean handleToggleSystem(final Player sender, @NotNull final Shop shop, final String tag) { - - } - - public static void handleUnsetSystem(final Player sender, @NotNull final Shop shop, final String tag) { - - } - - public static void handleClearAll(final Player sender, @NotNull final Shop shop) { - - //TODO: User parameter - if(!sender.hasPermission("quickshop.tag.clearall")) { - QuickShop.getInstance().text().of(sender, "no-permission").send(); - return; - } - - - } - - //our global methods - public static void handleTaggedList(final Player sender, @NotNull final CommandParser parser) { - } - - public static void handleTaggedList(final Player sender, @NotNull final ArrayDeque tags) { - } - - public static void handleTags(final Player sender) { - - } - - public static void handleTags(final Player sender, @NotNull final Shop shop) { - - } - - public static void handleRemoveTagFromAllShops(final Player sender, @NotNull final CommandParser parser) { - - } - - public static String displayTag(final Player sender, final String stored) { - if(stored == null) { - return ""; - } - - return switch (stored) { - case SYS_FAV -> "Favorite"; - case SYS_WATCH -> "Watch"; - case SYS_AVOID -> "Avoid"; - default -> "#" + stored; - }; - } - - public static String displayTagOrHash(final String stored) { - if(stored == null) { - return ""; - } - - if(stored.startsWith("@")) { - return stored; - } - return "#" + stored; - } - - @Nullable - public static String normalizeTag(@Nullable String input, final boolean allowSystem) { - - if (input == null) { - return null; - } - - if (input.startsWith("#")) { - input = input.substring(1); - } - - input = input.trim().toLowerCase(Locale.ROOT); - - if (input.isEmpty()) { - return null; - } - - if (input.length() > MAX_TAG_LENGTH) { - return null; - } - - if (!allowSystem && input.startsWith("@")) { - return null; - } - - if (!VALID_TAG_PATTERN.matcher(input).matches()) { - return null; - } - - return input; - } -} \ No newline at end of file From f27602b7214e27a77081bec662a12adf57c75102 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sat, 7 Mar 2026 16:19:12 -0500 Subject: [PATCH 04/29] Various performance improvements, including not creating an empty cache for shops without any tags already on removal calls. --- .../quickshop/addon/tags/TagManager.java | 23 +++++++++++++------ .../quickshop/addon/tags/tag/PlayerTags.java | 4 ++-- .../quickshop/addon/tags/tag/ShopTags.java | 17 +++++++++----- .../addon/tags/tag/TaggingResult.java | 2 ++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java index a56092827b..6caf7e584f 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java @@ -27,6 +27,8 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import static com.ghostchu.quickshop.addon.tags.tag.TaggingResult.NOT_FOUND; + /** * TagManager * @@ -67,8 +69,8 @@ public boolean removeAllTags() { } public boolean removeAllShopTags(final long shopId) { - final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); - if(shopTags.isEmpty()) { + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { return false; } shopTags.clear(); @@ -78,8 +80,8 @@ public boolean removeAllShopTags(final long shopId) { } public boolean removeAllShopTagsBy(final long shopId, final UUID player) { - final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); - if(shopTags.isEmpty()) { + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { return false; } shopTags.removeAllTags(player); @@ -99,15 +101,22 @@ public boolean removeAllPlayerTags(final UUID player) { } public boolean hasTag(final long shopId, final UUID player, final String tag) { - final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return false; + } return shopTags.hasTag(player, tag); } public TaggingResult removeTag(final long shopId, final UUID player, final String tag) { - final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return NOT_FOUND; + } + if(!shopTags.hasTag(player, tag)) { - return TaggingResult.NOT_FOUND; + return NOT_FOUND; } shopTags.removeTag(player, tag); diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java index 98a95faab2..c88b3df6eb 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java @@ -18,8 +18,8 @@ * along with this program. If not, see . */ +import java.util.Collections; import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** @@ -45,6 +45,6 @@ public void removeTag(final String tag) { } public Set getTags() { - return tags; + return Collections.unmodifiableSet(tags); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java index 1393a33925..173c468300 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java @@ -18,6 +18,7 @@ * along with this program. If not, see . */ +import java.util.Collections; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -39,10 +40,15 @@ public void addTag(final UUID player, final String tag) { } public void removeTag(final UUID player, final String tag) { - if(!tags.containsKey(player)) { + final PlayerTags playerTags = tags.get(player); + if(playerTags == null) { return; } - tags.get(player).removeTag(tag); + + playerTags.removeTag(tag); + if(playerTags.getTags().isEmpty()) { + tags.remove(player, playerTags); + } //remove our player object if empty to save memory if(tags.get(player).getTags().isEmpty()) { @@ -56,13 +62,12 @@ public boolean removeAllTags(final UUID player) { } tags.remove(player); return true; + } public boolean hasTag(final UUID player, final String tag) { - if(!tags.containsKey(player)) { - return false; - } - return getTags(player).hasTag(tag); + final PlayerTags playerTags = tags.get(player); + return playerTags != null && playerTags.hasTag(tag); } public PlayerTags getTags(final UUID player) { diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java index e5b231c10e..38972e9bf3 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java @@ -29,4 +29,6 @@ public enum TaggingResult { SUCCESS, NOT_FOUND, ALREADY_EXISTS, + DATABASE_ERROR, + INVALID_TAG } \ No newline at end of file From 9287790ff2d92e7fb9e0e1cff48e76a03654024b Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sun, 8 Mar 2026 13:46:44 -0400 Subject: [PATCH 05/29] Finalized functionality for commands, and implemented remaining needed methods. Tag management functionality is now ready for testing. --- addon/tags/pom.xml | 5 +- .../ghostchu/quickshop/addon/tags/Main.java | 64 ++++- .../quickshop/addon/tags/TagManager.java | 145 ++++++++++- .../quickshop/addon/tags/TagService.java | 106 +++++---- .../addon/tags/command/SubCommand_Avoid.java | 57 +++-- .../tags/command/SubCommand_Favorite.java | 59 +++-- .../addon/tags/command/SubCommand_Tag.java | 225 ++++++++++++++---- .../addon/tags/command/SubCommand_Watch.java | 59 +++-- .../quickshop/addon/tags/tag/PlayerTags.java | 18 +- .../quickshop/addon/tags/tag/ShopTags.java | 26 +- pom.xml | 5 + .../src/main/resources/lang/messages.yml | 69 ++++-- 12 files changed, 661 insertions(+), 177 deletions(-) diff --git a/addon/tags/pom.xml b/addon/tags/pom.xml index 6cbac8251d..49c662236d 100644 --- a/addon/tags/pom.xml +++ b/addon/tags/pom.xml @@ -16,7 +16,10 @@ Addon-Tags - QuickShop-Tags adds a lightweight, powerful tagging system to QuickShop-Hikari, allowing players to categorize, favorite, and track shops without altering core shop behavior. + QuickShop-Tags adds a lightweight, powerful tagging system to QuickShop-Hikari, + allowing players to categorize, favorite, and track shops without altering core shop + behavior. + diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java index 5fad56e48e..fa3374b114 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/Main.java @@ -1,7 +1,10 @@ package com.ghostchu.quickshop.addon.tags; import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.command.SubCommand_Avoid; +import com.ghostchu.quickshop.addon.tags.command.SubCommand_Favorite; import com.ghostchu.quickshop.addon.tags.command.SubCommand_Tag; +import com.ghostchu.quickshop.addon.tags.command.SubCommand_Watch; import com.ghostchu.quickshop.api.command.CommandContainer; import org.bukkit.Bukkit; import org.bukkit.event.HandlerList; @@ -11,8 +14,25 @@ public final class Main extends JavaPlugin implements Listener { - static Main instance; + private static Main instance; + private QuickShop plugin; + private TagManager tagManager; + + public static Main instance() { + + return instance; + } + + public QuickShop quickShop() { + + return plugin; + } + + public TagManager tagManager() { + + return tagManager; + } @Override public void onLoad() { @@ -31,16 +51,44 @@ public void onEnable() { saveDefaultConfig(); plugin = QuickShop.getInstance(); - getLogger().info("Registering tags commands..."); + tagManager = new TagManager(this, plugin); + Bukkit.getPluginManager().registerEvents(this, this); + + getLogger().info("Registering QuickShop-Tags commands..."); + + + final SubCommand_Avoid avoidCommand = new SubCommand_Avoid(this, plugin); + final SubCommand_Favorite favoriteCommand = new SubCommand_Favorite(this, plugin); + final SubCommand_Tag tagCommand = new SubCommand_Tag(this, plugin, tagManager); + final SubCommand_Watch watchCommand = new SubCommand_Watch(this, plugin); + + plugin.getCommandManager().registerCmd( + CommandContainer.builder() + .prefix("avoid") + .description((locale)->plugin.text().of("addon.tags.commands.avoid").forLocale(locale)) + .executor(avoidCommand) + .build()); + plugin.getCommandManager().registerCmd( - CommandContainer - .builder() + CommandContainer.builder() + .prefix("favorite") + .description((locale)->plugin.text().of("addon.tags.commands.favorite").forLocale(locale)) + .executor(favoriteCommand) + .build()); + + plugin.getCommandManager().registerCmd( + CommandContainer.builder() .prefix("tag") - .description((locale)->plugin.text().of("addon.list.commands.list").forLocale(locale)) - .selectivePermission("quickshopaddon.list.self") - .selectivePermission("quickshopaddon.list.other") - .executor(new SubCommand_Tag(plugin)) + .description((locale)->plugin.text().of("addon.tags.commands.tag").forLocale(locale)) + .executor(tagCommand) + .build()); + + plugin.getCommandManager().registerCmd( + CommandContainer.builder() + .prefix("watch") + .description((locale)->plugin.text().of("addon.tags.commands.watch").forLocale(locale)) + .executor(watchCommand) .build()); } } diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java index 6caf7e584f..e998ef634d 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java @@ -19,14 +19,22 @@ */ import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.tag.PlayerTags; import com.ghostchu.quickshop.addon.tags.tag.ShopTags; import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; +import org.bukkit.entity.Player; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import static com.ghostchu.quickshop.addon.tags.TagService.TOTAL_INDEX; import static com.ghostchu.quickshop.addon.tags.tag.TaggingResult.NOT_FOUND; /** @@ -42,12 +50,13 @@ public class TagManager { private final TagService service; public TagManager(final Main main, final QuickShop plugin) { + this.service = new TagService(main, plugin); } public TaggingResult addTag(final long shopId, final UUID player, final String tag) { - final ShopTags shopTags = tags.computeIfAbsent(shopId, id -> new ShopTags()); + final ShopTags shopTags = tags.computeIfAbsent(shopId, id->new ShopTags()); if(shopTags.hasTag(player, tag)) { return TaggingResult.ALREADY_EXISTS; } @@ -57,6 +66,14 @@ public TaggingResult addTag(final long shopId, final UUID player, final String t return TaggingResult.SUCCESS; } + public TaggingResult toggleTag(final long shopId, final UUID player, final String tag) { + + if(hasTag(shopId, player, tag)) { + return removeTag(shopId, player, tag); + } + return addTag(shopId, player, tag); + } + public boolean removeAllTags() { final Iterator iterator = tags.keySet().iterator(); @@ -69,6 +86,7 @@ public boolean removeAllTags() { } public boolean removeAllShopTags(final long shopId) { + final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return false; @@ -80,6 +98,7 @@ public boolean removeAllShopTags(final long shopId) { } public boolean removeAllShopTagsBy(final long shopId, final UUID player) { + final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return false; @@ -100,7 +119,116 @@ public boolean removeAllPlayerTags(final UUID player) { return true; } + public int totalTags() { + + int total = 0; + for(final Map.Entry entry : tags.entrySet()) { + + for(final PlayerTags tags : entry.getValue().getTags()) { + total += tags.getTags().size(); + } + } + return total; + } + + public int totalTagsByPlayer(final UUID player) { + + int total = 0; + for(final Map.Entry entry : tags.entrySet()) { + + final PlayerTags tags = entry.getValue().getTags(player); + if(tags == null || tags.getTags().isEmpty()) { + continue; + } + total += tags.getTags().size(); + } + //our total count for all tags by player. + return total; + } + + public TreeMap tagsCount(final UUID player) { + + int total = 0; + final TreeMap count = new TreeMap<>(); + for(final Map.Entry entry : tags.entrySet()) { + + final PlayerTags tags = entry.getValue().getTags(player); + if(tags == null || tags.getTags().isEmpty()) { + continue; + } + + count.put(entry.getKey(), tags.getTags().size()); + total += tags.getTags().size(); + } + //our total count for all tags by player. + count.put(TOTAL_INDEX, total); + return count; + } + + public Set tagsFilteredByShop(final UUID player, final long shopId) { + + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + return Collections.emptySet(); + } + return shopTags.getTags(player).getTags(); + } + + public List shopsFilteredByTag(final UUID player, final String tag) { + + final List shopIds = new ArrayList<>(); + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + continue; + } + + if(shopTags.hasTag(player, tag)) { + shopIds.add(shopId); + } + } + return shopIds; + } + + public List shopsFilteredByTags(final UUID player, final List filterTags) { + + final List shopIds = new ArrayList<>(); + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + + final Long shopId = iterator.next(); + final ShopTags shopTags = tags.get(shopId); + if(shopTags == null) { + continue; + } + + if(shopTags.hasTags(player, filterTags)) { + shopIds.add(shopId); + } + } + return shopIds; + } + + public void listShopsByFilter(final Player player, final List filterTags, final String titleNode, + final String noEntries) { + + final List shopIds = shopsFilteredByTags(player.getUniqueId(), filterTags); + if(shopIds.isEmpty()) { + Main.instance().quickShop().text().of(player, noEntries).send(); + return; + } + + Main.instance().quickShop().text().of(player, titleNode).send(); + for(final Long shopId : shopIds) { + Main.instance().quickShop().text().of(player, "addon.tags.general.list-entry", shopId).send(); + } + } + public boolean hasTag(final long shopId, final UUID player, final String tag) { + final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return false; @@ -108,6 +236,16 @@ public boolean hasTag(final long shopId, final UUID player, final String tag) { return shopTags.hasTag(player, tag); } + public boolean removeTag(final UUID player, final String tag) { + + final Iterator iterator = tags.keySet().iterator(); + while(iterator.hasNext()) { + final Long shopId = iterator.next(); + removeTag(shopId, player, tag); + } + return true; + } + public TaggingResult removeTag(final long shopId, final UUID player, final String tag) { final ShopTags shopTags = tags.get(shopId); @@ -129,4 +267,9 @@ public TaggingResult removeTag(final long shopId, final UUID player, final Strin return TaggingResult.SUCCESS; } + + public TagService service() { + + return service; + } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java index d55ce6030e..3115350855 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java @@ -37,6 +37,7 @@ public class TagService { + public static final long TOTAL_INDEX = -1L; public static final String SYS_FAV = "@fav"; public static final String SYS_WATCH = "@watch"; public static final String SYS_AVOID = "@avoid"; @@ -53,6 +54,64 @@ public TagService(final Main tagsMain, final QuickShop plugin) { this.plugin = plugin; } + public static String displayTag(final Player sender, final String stored) { + + if(stored == null) { + return ""; + } + + return switch(stored) { + case SYS_FAV -> "Favorite"; + case SYS_WATCH -> "Watch"; + case SYS_AVOID -> "Avoid"; + default -> "#" + stored; + }; + } + + public static String displayTagOrHash(final String stored) { + + if(stored == null) { + return ""; + } + + if(stored.startsWith("@")) { + return stored; + } + return "#" + stored; + } + + @Nullable + public static String normalizeTag(@Nullable String input, final boolean allowSystem) { + + if(input == null) { + return null; + } + + if(input.startsWith("#")) { + input = input.substring(1); + } + + input = input.trim().toLowerCase(Locale.ROOT); + + if(input.isEmpty()) { + return null; + } + + if(input.length() > MAX_TAG_LENGTH) { + return null; + } + + if(!allowSystem && input.startsWith("@")) { + return null; + } + + if(!VALID_TAG_PATTERN.matcher(input).matches()) { + return null; + } + + return input; + } + public CompletableFuture addShopTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); @@ -82,6 +141,7 @@ public CompletableFuture removeShopTag(final UUID player, final long sh } public CompletableFuture removeAllShopTags(final long shopId) { + final DatabaseHelper db = plugin.getDatabaseHelper(); return db.removeAllShopTags(shopId).thenCompose(result->{ if(result != null && result > 0) { @@ -93,6 +153,7 @@ public CompletableFuture removeAllShopTags(final long shopId) { } public CompletableFuture removeAllShopTagsBy(final long shopId, final UUID player) { + final DatabaseHelper db = plugin.getDatabaseHelper(); return db.removeAllShopTagsBy(player, shopId).thenCompose(result->{ if(result != null && result > 0) { @@ -103,51 +164,6 @@ public CompletableFuture removeAllShopTagsBy(final long shopId, final U }); } - public @Nullable String normalizePlayerTag(String input) { - - if(input == null) return null; - - if(input.startsWith("#")) { - input = input.substring(1); - } - - input = input.trim().toLowerCase(Locale.ROOT); - - if(input.isEmpty()) return null; - if(input.length() > MAX_TAG_LENGTH) return null; - if(input.startsWith("@")) return null; - - if(!VALID_TAG_PATTERN.matcher(input).matches()) { - return null; - } - - return input; - } - - public static String displayTag(final Player sender, final String stored) { - if(stored == null) { - return ""; - } - - return switch (stored) { - case SYS_FAV -> "Favorite"; - case SYS_WATCH -> "Watch"; - case SYS_AVOID -> "Avoid"; - default -> "#" + stored; - }; - } - - public static String displayTagOrHash(final String stored) { - if(stored == null) { - return ""; - } - - if(stored.startsWith("@")) { - return stored; - } - return "#" + stored; - } - public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java index 129197c62d..5863b9a49b 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java @@ -19,21 +19,20 @@ */ import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.Main; +import com.ghostchu.quickshop.addon.tags.TagService; +import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_AVOID; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; -import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; - /** * SubCommand_Avoid * @@ -42,22 +41,29 @@ */ public class SubCommand_Avoid implements CommandHandler { + private final Main main; private final QuickShop plugin; - public SubCommand_Avoid(final QuickShop plugin) { + public SubCommand_Avoid(final Main main, final QuickShop plugin) { + this.main = main; this.plugin = plugin; } @Override - public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { - if(parser.getArgs().isEmpty()) { - sendUsage(sender); + final String tag = TagService.normalizeTag(TagService.SYS_AVOID, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "addon.tags.general.invalid").send(); return; } - if(parser.getArgs().size() == 0) { + if(parser.getArgs().isEmpty()) { final Shop shop = getLookingShop(sender); if(shop == null) { @@ -65,21 +71,39 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } - //TODO: Toggle avoid for the shop + final TaggingResult result = main.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case SUCCESS -> { + if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "addon.tags.avoid.added").send(); + } else { + plugin.text().of(sender, "addon.tags.avoid.removed").send(); + } + } + case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); + default -> plugin.text().of(sender, "addon.tags.avoid.unable").send(); + } return; } final String sub = parser.getArgs().getFirst(); switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { - case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_AVOID))); + Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "addon.tags.avoid.list-title", + "addon.tags.avoid.none"); + } + default -> sendUsage(sender); } } @NotNull @Override - public List onTabComplete( - @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().size() == 1) { return List.of("list"); @@ -88,8 +112,7 @@ public List onTabComplete( } private void sendUsage(final Player sender) { - plugin.text().of(sender, "command-incorrect", - "/quickshop avoid [list]") - .send(); + + plugin.text().of(sender, "command-incorrect", "/quickshop avoid [list]").send(); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java index 2dd8c65f52..78ea68b56c 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java @@ -19,21 +19,20 @@ */ import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.Main; +import com.ghostchu.quickshop.addon.tags.TagService; +import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_FAV; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; -import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; - /** * SubCommand_Favorite * @@ -42,44 +41,69 @@ */ public class SubCommand_Favorite implements CommandHandler { + private final Main main; private final QuickShop plugin; - public SubCommand_Favorite(final QuickShop plugin) { + public SubCommand_Favorite(final Main main, final QuickShop plugin) { + this.main = main; this.plugin = plugin; } @Override - public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { - if(parser.getArgs().isEmpty()) { - sendUsage(sender); + final String tag = TagService.normalizeTag(TagService.SYS_FAV, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "addon.tags.general.invalid").send(); return; } - if(parser.getArgs().size() == 0) { - + if(parser.getArgs().isEmpty()) { final Shop shop = getLookingShop(sender); if(shop == null) { plugin.text().of(sender, "not-looking-at-shop").send(); return; } - //TODO: Toggle favorite for the shop + final TaggingResult result = main.tagManager() + .toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case SUCCESS -> { + if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "addon.tags.favorite.added").send(); + } else { + plugin.text().of(sender, "addon.tags.favorite.removed").send(); + } + } + case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); + default -> plugin.text().of(sender, "addon.tags.favorite.unable").send(); + } return; } final String sub = parser.getArgs().getFirst(); switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { - case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_FAV))); + Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "addon.tags.favorite.list-title", + "addon.tags.favorite.none"); + } + default -> sendUsage(sender); } } @NotNull @Override - public List onTabComplete( - @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().size() == 1) { return List.of("list"); @@ -88,8 +112,7 @@ public List onTabComplete( } private void sendUsage(final Player sender) { - plugin.text().of(sender, "command-incorrect", - "/quickshop fav [list]") - .send(); + + plugin.text().of(sender, "command-incorrect", "/quickshop fav [list]").send(); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java index f6024bfad7..b4b9e901e7 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Tag.java @@ -1,71 +1,66 @@ package com.ghostchu.quickshop.addon.tags.command; import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.Main; +import com.ghostchu.quickshop.addon.tags.TagManager; +import com.ghostchu.quickshop.addon.tags.TagService; +import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; -import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.ArrayDeque; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.regex.Pattern; - -import static com.ghostchu.quickshop.addon.tags.Util.SYS_AVOID; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_FAV; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; -import static com.ghostchu.quickshop.addon.tags.Util.handleAdd; -import static com.ghostchu.quickshop.addon.tags.Util.handleClear; -import static com.ghostchu.quickshop.addon.tags.Util.handleClearAll; -import static com.ghostchu.quickshop.addon.tags.Util.handleRemove; -import static com.ghostchu.quickshop.addon.tags.Util.handleRemoveTagFromAllShops; -import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; -import static com.ghostchu.quickshop.addon.tags.Util.handleTags; -import static com.ghostchu.quickshop.addon.tags.Util.normalizeTag; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static com.ghostchu.quickshop.addon.tags.TagService.TOTAL_INDEX; public class SubCommand_Tag implements CommandHandler { + private final Main main; private final QuickShop plugin; - //our system tags + private final TagManager tagManager; - public SubCommand_Tag(final QuickShop plugin) { + public SubCommand_Tag(final Main main, final QuickShop plugin, final TagManager tagManager) { + this.main = main; this.plugin = plugin; + this.tagManager = tagManager; } @Override - public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().isEmpty()) { sendUsage(sender); return; } - final String sub = parser.getArgs().getFirst(); + final String sub = parser.getArgs().getFirst().toLowerCase(Locale.ROOT); - //These are our global tag-related commands. They don't require looking at a shop. - switch (sub) { - // /qs tag tags + // Global commands that do not require looking at a shop. + switch(sub) { case "tags" -> { handleTags(sender); return; } - // /qs tag tagged case "tagged" -> { handleTaggedList(sender, parser); return; } - // /qs tag purge case "purge", "removefromall", "untagall" -> { handleRemoveTagFromAllShops(sender, parser); return; } - default -> {} + default -> { + } } final Shop shop = getLookingShop(sender); @@ -74,47 +69,191 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } - // Check permission if(!shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_BENEFIT) && !plugin.perm().hasPermission(sender, "quickshop.other.benefit")) { plugin.text().of(sender, "not-managed-shop").send(); return; } - switch(parser.getArgs().getFirst()) { + switch(sub) { case "add" -> handleAdd(sender, shop, parser, 2); case "remove", "del", "delete" -> handleRemove(sender, shop, parser); case "clear" -> handleClear(sender, shop); + case "clearall" -> + handleClearAll(sender, parser); //todo: confirmation maybe for clear and clearall? + case "list" -> handleTags(sender, shop); + default -> handleAdd(sender, shop, parser, 1); + } + } - // Optional: admin "clear all tags from this shop for everyone" - case "clearall" -> handleClearAll(sender, shop); + private void handleAdd(final Player sender, final Shop shop, + final CommandParser parser, final int tagIndex) { - case "list" -> handleTags(sender, shop); + if(parser.getArgs().size() <= tagIndex) { + sendUsage(sender); + return; + } - default -> handleAdd(sender, shop, parser, 1); + final String normalized = TagService.normalizeTag(parser.getArgs().get(tagIndex), false); + if(normalized == null) { + plugin.text().of(sender, "addon.tags.general.invalid").send(); + return; + } + + final TaggingResult result = tagManager.addTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "addon.tags.tag.added", TagService.displayTag(sender, normalized)).send(); + case ALREADY_EXISTS -> + plugin.text().of(sender, "addon.tags.tag.duplicate", TagService.displayTag(sender, normalized)).send(); + case INVALID_TAG -> plugin.text().of(sender, "addon.tags.general.invalid").send(); + case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); + default -> + plugin.text().of(sender, "addon.tags.tag.failed-add", TagService.displayTag(sender, normalized)).send(); } } + private void handleRemove(final Player sender, final Shop shop, final CommandParser parser) { + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final String normalized = TagService.normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "addon.tags.general.invalid").send(); + return; + } + + final TaggingResult result = tagManager.removeTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "addon.tags.tag.removed", TagService.displayTag(sender, normalized)).send(); + case NOT_FOUND -> + plugin.text().of(sender, "addon.tags.tag.does-not-exist", TagService.displayTag(sender, normalized)).send(); + case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); + default -> + plugin.text().of(sender, "addon.tags.tag.failed-remove", TagService.displayTag(sender, normalized)).send(); + } + } + + private void handleClear(final Player sender, final Shop shop) { + + final boolean removed = tagManager.removeAllShopTagsBy(shop.getShopId(), sender.getUniqueId()); + if(removed) { + plugin.text().of(sender, "addon.tags.tag.cleared", shop.getShopId()).send(); + } else { + plugin.text().of(sender, "addon.tags.tag.no-tags", shop.getShopId()).send(); + } + } + + private void handleClearAll(final Player sender, final CommandParser parser) { + + if(!plugin.perm().hasPermission(sender, "quickshop.tag.admin.clearall")) { + plugin.text().of(sender, "no-permission").send(); + return; + } + + final boolean removed = tagManager.removeAllTags(); + if(removed) { + plugin.text().of(sender, "addon.tags.tag.cleared-all").send(); + } else { + plugin.text().of(sender, "addon.tags.tag.no-tags-all").send(); + } + } + + private void handleTags(final Player sender) { + + final TreeMap count = tagManager.tagsCount(sender.getUniqueId()); + if(count.isEmpty()) { + plugin.text().of(sender, "addon.tags.tag.no-tagged-shops").send(); + return; + } + + plugin.text().of(sender, "addon.tags.tag.list-player-shops-title", count.get(TOTAL_INDEX), count.size()).send(); + for(final Map.Entry entry : count.entrySet()) { + + plugin.text().of(sender, "addon.tags.tag.list-player-shop-entry", entry.getKey(), entry.getValue()).send(); + } + } + + private void handleTags(final Player sender, final Shop shop) { + + final Set tags = Collections.unmodifiableSet(tagManager.tagsFilteredByShop(sender.getUniqueId(), shop.getShopId())); + if(tags.isEmpty()) { + plugin.text().of(sender, "addon.tags.tag.no-tagged-shops-player", shop.getShopId()).send(); + return; + } + + plugin.text().of(sender, "addon.tags.tag.list-player-shop-title", tags.size(), shop.getShopId()).send(); + for(final String tag : tags) { + plugin.text().of(sender, "addon.tags.general.list-entry", tag).send(); + } + } + + private void handleTaggedList(final Player sender, final CommandParser parser) { + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final String normalized = TagService.normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "addon.tags.general.invalid").send(); + return; + } + + final List shopIds = tagManager.shopsFilteredByTag(sender.getUniqueId(), normalized); + if(shopIds.isEmpty()) { + plugin.text().of(sender, "addon.tags.tag.no-tagged-shops-tag", normalized).send(); + return; + } + + plugin.text().of(sender, "addon.tags.tag.list-tag-title", shopIds.size()).send(); + for(final Long shopId : shopIds) { + plugin.text().of(sender, "addon.tags.general.list-entry", shopId).send(); + } + } + + private void handleRemoveTagFromAllShops(final Player sender, final CommandParser parser) { + + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; + } + + final String normalized = TagService.normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "addon.tags.general.invalid").send(); + return; + } + + final boolean cleared = tagManager.removeTag(sender.getUniqueId(), normalized); + if(!cleared) { + plugin.text().of(sender, "addon.tags.general.database-error", normalized).send(); + return; + } + + plugin.text().of(sender, "addon.tags.tag.clearing-tag", normalized).send(); + } + @NotNull @Override - public List onTabComplete( - @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public List onTabComplete(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().size() == 1) { - return List.of("add", "remove", "query"); - } - if(parser.getArgs().size() == 2) { - return null; - } - if(parser.getArgs().size() == 3) { - return Collections.singletonList(plugin.text().of(sender, "tabcomplete.percentage").legacy()); + return List.of("add", "remove", "clear", "clearall", "list", "tags", "tagged", "purge"); } return Collections.emptyList(); } private void sendUsage(final Player sender) { + plugin.text().of(sender, "command-incorrect", - "/quickshop tag <[tag]/remove/list/clear/tagged/tags> [player]") - .send(); + "/quickshop tag [tag]") + .send(); } } diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java index a646d90527..1de8474732 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java @@ -1,42 +1,46 @@ package com.ghostchu.quickshop.addon.tags.command; import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.addon.tags.Main; +import com.ghostchu.quickshop.addon.tags.TagService; +import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; -import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; -import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.regex.Pattern; -import static com.ghostchu.quickshop.addon.tags.Util.SYS_WATCH; -import static com.ghostchu.quickshop.addon.tags.Util.handleTaggedList; public class SubCommand_Watch implements CommandHandler { + private final Main main; private final QuickShop plugin; - public SubCommand_Watch(final QuickShop plugin) { + public SubCommand_Watch(final Main main, final QuickShop plugin) { + this.main = main; this.plugin = plugin; } @Override - public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public void onCommand(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { - if(parser.getArgs().isEmpty()) { - sendUsage(sender); + final String tag = TagService.normalizeTag(TagService.SYS_WATCH, true); + if(tag == null) { + + //should never happen, but we'll catch just in case. + plugin.text().of(sender, "addon.tags.general.invalid").send(); return; } - if(parser.getArgs().size() == 0) { + if(parser.getArgs().isEmpty()) { final Shop shop = getLookingShop(sender); if(shop == null) { @@ -44,21 +48,39 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } - //TODO: Toggle watch for the shop + final TaggingResult result = main.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + + switch(result) { + case SUCCESS -> { + if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "addon.tags.watch.added").send(); + } else { + plugin.text().of(sender, "addon.tags.watch.removed").send(); + } + } + case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); + default -> plugin.text().of(sender, "addon.tags.watch.unable").send(); + } return; } final String sub = parser.getArgs().getFirst(); switch(sub.toLowerCase(Locale.ROOT)) { + case "list" -> { - case "list": handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_WATCH))); + Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "addon.tags.watch.list-title", + "addon.tags.watch.none"); + } + default -> sendUsage(sender); } } @NotNull @Override - public List onTabComplete( - @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public List onTabComplete(@NotNull final Player sender, + @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().size() == 1) { return List.of("list"); @@ -67,8 +89,7 @@ public List onTabComplete( } private void sendUsage(final Player sender) { - plugin.text().of(sender, "command-incorrect", - "/quickshop watch [list]") - .send(); + + plugin.text().of(sender, "command-incorrect", "/quickshop watch [list]").send(); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java index c88b3df6eb..453c7092ba 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java @@ -19,6 +19,7 @@ */ import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -33,18 +34,27 @@ public class PlayerTags { private final Set tags = ConcurrentHashMap.newKeySet(); public boolean hasTag(final String tag) { + return tags.contains(tag); } - public void addTag(final String tag) { - tags.add(tag); + public boolean hasTags(final List filterTags) { + + return tags.containsAll(filterTags); } - public void removeTag(final String tag) { - tags.remove(tag); + public boolean addTag(final String tag) { + + return tags.add(tag); + } + + public boolean removeTag(final String tag) { + + return tags.remove(tag); } public Set getTags() { + return Collections.unmodifiableSet(tags); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java index 173c468300..215e37183f 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java +++ b/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java @@ -18,7 +18,9 @@ * along with this program. If not, see . */ +import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -33,6 +35,7 @@ public class ShopTags { private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); public void addTag(final UUID player, final String tag) { + if(!tags.containsKey(player)) { tags.put(player, new PlayerTags()); } @@ -40,23 +43,21 @@ public void addTag(final UUID player, final String tag) { } public void removeTag(final UUID player, final String tag) { + final PlayerTags playerTags = tags.get(player); if(playerTags == null) { return; } playerTags.removeTag(tag); - if(playerTags.getTags().isEmpty()) { - tags.remove(player, playerTags); - } - //remove our player object if empty to save memory - if(tags.get(player).getTags().isEmpty()) { + if(playerTags.getTags().isEmpty()) { tags.remove(player); } } public boolean removeAllTags(final UUID player) { + if(!tags.containsKey(player)) { return false; } @@ -66,24 +67,39 @@ public boolean removeAllTags(final UUID player) { } public boolean hasTag(final UUID player, final String tag) { + final PlayerTags playerTags = tags.get(player); return playerTags != null && playerTags.hasTag(tag); } + public boolean hasTags(final UUID player, final List filterTags) { + + final PlayerTags playerTags = tags.get(player); + return playerTags != null && playerTags.hasTags(filterTags); + } + public PlayerTags getTags(final UUID player) { + return tags.get(player); } public void setTags(final UUID player, final PlayerTags tags) { + this.tags.put(player, tags); } public boolean isEmpty() { + return tags.isEmpty(); } public void clear() { + tags.clear(); } + public Collection getTags() { + + return Collections.unmodifiableCollection(tags.values()); + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index c3f1ccdb59..5e9cfb3db0 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,7 @@ 0.4.7 1.3.0 1.3.0 + 0.17.2 2.15.0 2.9.0-SNAPSHOT 1.21-R0.1-SNAPSHOT @@ -280,6 +281,10 @@ com.rollbar. ${qs.relocation}.com.rollbar. + + dev.faststats + ${qs.relocation}.faststats + compatibility/bentobox 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 7683052031..0d2eeea8a8 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 @@ -13,6 +13,7 @@ import com.ghostchu.quickshop.api.shop.ShopItemBlackList; import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.interaction.InteractionManager; +import com.ghostchu.quickshop.api.shop.tag.TagManager; import com.vdurmont.semver4j.Semver; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; @@ -148,6 +149,16 @@ default void removeHook(final String identifier) { */ EconomyManager getEconomyManager(); + /** + * Retrieves the TagManager associated with the QuickShop system. The TagManager + * is responsible for handling operations related to managing tags, which may + * include inventory item metadata and custom item tags used by the system. + * + * @return The TagManager instance that provides functionality for tag-related + * operations within the QuickShop system. + */ + TagManager tagManager(); + /** * Getting the control panel manager * diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/PlayerTagIndex.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/PlayerTagIndex.java new file mode 100644 index 0000000000..661597a3df --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/PlayerTagIndex.java @@ -0,0 +1,117 @@ +package com.ghostchu.quickshop.api.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import java.util.Set; + +/** + * Represents a per-player tag index used for fast lookup of tags and shops. + * + *

This index maintains a bidirectional mapping between shops and tags:

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

Implementations are expected to be thread-safe.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface PlayerTagIndex { + + /** + * Adds a tag entry to the index. + * + * @param shopId the shop ID + * @param tag the tag + * + * @since 6.3.0.0 + */ + void addTag(long shopId, String tag); + + /** + * Removes a tag entry from the index. + * + * @param shopId the shop ID + * @param tag the tag + * + * @since 6.3.0.0 + */ + void removeTag(long shopId, String tag); + + /** + * Returns the tags applied to a shop. + * + * @param shopId the shop ID + * + * @return tags associated with the shop + * + * @since 6.3.0.0 + */ + Set getTags(long shopId); + + /** + * Returns all shops associated with a tag. + * + * @param tag the tag + * + * @return shop IDs containing the tag + * + * @since 6.3.0.0 + */ + Set getShops(String tag); + + /** + * Returns all shop IDs currently tracked by this index. + * + * @return tracked shop IDs + * + * @since 6.3.0.0 + */ + Set shops(); + + /** + * Returns the total number of tags tracked by this index. + * + * @return total tag count + * + * @since 6.3.0.0 + */ + int totalTags(); + + /** + * Checks whether a shop has a specific tag. + * + * @param shopId the shop ID + * @param tag the tag + * + * @return true if the tag exists + * + * @since 6.3.0.0 + */ + boolean hasTag(long shopId, String tag); + + /** + * Clears all cached tag data. + * + * @since 6.3.0.0 + */ + void clear(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagManager.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagManager.java new file mode 100644 index 0000000000..35df00eef6 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagManager.java @@ -0,0 +1,255 @@ +package com.ghostchu.quickshop.api.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.UUID; + +/** + * Provides an API for managing tags associated with shops and players. + * + *

The {@code TagManager} allows plugins and internal systems to:

+ *
    + *
  • Add, remove, or toggle tags on shops
  • + *
  • Query tags associated with a shop or player
  • + *
  • Filter shops based on tag criteria
  • + *
  • Clear tags globally, per shop, or per player
  • + *
  • Retrieve tag statistics and counts
  • + *
+ * + *

Tags are stored per player per shop, meaning each player maintains + * their own tagging view of shops. Tags may be used for systems such as + * favorites, watchlists, or custom player categorization.

+ * + *

Implementations are expected to maintain an in-memory cache for + * fast lookup while delegating persistence operations to {@link TagService}.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface TagManager { + + /** + * Adds a tag to a shop for a specific player. + * + * @param shopId the ID of the shop + * @param player the player applying the tag + * @param tag the tag to apply + * + * @return the result of the tagging operation + * + * @since 6.3.0.0 + */ + TaggingResult addTag(long shopId, UUID player, String tag); + + /** + * Toggles a tag on a shop for a player. + * + *

If the tag already exists it will be removed, + * otherwise it will be added.

+ * + * @param shopId the ID of the shop + * @param player the player toggling the tag + * @param tag the tag to toggle + * + * @return the result of the operation + * + * @since 6.3.0.0 + */ + TaggingResult toggleTag(long shopId, UUID player, String tag); + + /** + * Removes all tags from all shops for all players. + * + * @return true if the operation completed successfully + * + * @since 6.3.0.0 + */ + boolean removeAllTags(); + + /** + * Removes all tags associated with a specific shop. + * + * @param shopId the shop ID + * + * @return true if tags were removed + * + * @since 6.3.0.0 + */ + boolean removeAllShopTags(long shopId); + + /** + * Removes all tags applied by a specific player to a shop. + * + * @param shopId the shop ID + * @param player the player whose tags should be removed + * + * @return true if tags were removed + * + * @since 6.3.0.0 + */ + boolean removeAllShopTagsBy(long shopId, UUID player); + + /** + * Removes all tags applied by a player across every shop. + * + * @param player the player whose tags should be removed + * + * @return true if the operation completed + * + * @since 6.3.0.0 + */ + boolean removeAllPlayerTags(UUID player); + + /** + * Gets the total number of tags currently stored. + * + * @return total tag count + * + * @since 6.3.0.0 + */ + int totalTags(); + + /** + * Gets the total number of tags applied by a specific player. + * + * @param player the player + * + * @return number of tags applied by the player + * + * @since 6.3.0.0 + */ + int totalTagsByPlayer(UUID player); + + /** + * Returns a count of tags grouped by shop for a player. + * + *

The returned {@link TreeMap} maps shop IDs to the number of + * tags the player has applied to each shop.

+ * + * @param player the player + * + * @return a map containing tag counts per shop + * + * @since 6.3.0.0 + */ + TreeMap tagsCount(UUID player); + + /** + * Retrieves the tags applied by a player to a specific shop. + * + * @param player the player + * @param shopId the shop ID + * + * @return a set of tags applied by the player + * + * @since 6.3.0.0 + */ + Set tagsFilteredByShop(UUID player, long shopId); + + /** + * Retrieves all shop IDs that match a given tag for a player. + * + * @param player the player + * @param tag the tag to filter by + * + * @return list of shop IDs containing the tag + * + * @since 6.3.0.0 + */ + List shopsFilteredByTag(UUID player, String tag); + + /** + * Retrieves shop IDs matching all provided tags for a player. + * + * @param player the player + * @param filterTags the tags used as filter criteria + * + * @return list of shop IDs matching the filter + * + * @since 6.3.0.0 + */ + List shopsFilteredByTags(UUID player, List filterTags); + + /** + * Sends a formatted list of shops matching a tag filter to a player. + * + *

This is typically used by commands such as favorites or watchlists.

+ * + * @param player the player to send the results to + * @param filterTags the tags to filter shops by + * @param titleNode the message node used for the list title + * @param noEntries the message node used when no results exist + * + * @since 6.3.0.0 + */ + void listShopsByFilter(Player player, List filterTags, String titleNode, String noEntries); + + /** + * Checks whether a player has applied a specific tag to a shop. + * + * @param shopId the shop ID + * @param player the player + * @param tag the tag to check + * + * @return true if the tag exists + * + * @since 6.3.0.0 + */ + boolean hasTag(long shopId, UUID player, String tag); + + /** + * Removes a specific tag from all shops for a player. + * + * @param player the player + * @param tag the tag to remove + * + * @return true if the operation completed + * + * @since 6.3.0.0 + */ + boolean removeTag(UUID player, String tag); + + /** + * Removes a specific tag from a single shop for a player. + * + * @param shopId the shop ID + * @param player the player + * @param tag the tag to remove + * + * @return the result of the removal operation + * + * @since 6.3.0.0 + */ + TaggingResult removeTag(long shopId, UUID player, String tag); + + /** + * Returns the underlying {@link TagService} responsible for persistence and normalization + * operations. + * + * @return the tag service + * + * @since 6.3.0.0 + */ + TagService service(); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagService.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagService.java new file mode 100644 index 0000000000..9914d92f50 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TagService.java @@ -0,0 +1,183 @@ +package com.ghostchu.quickshop.api.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * Represents a service for managing shop tags. + * + *

This service is responsible for:

+ *
    + *
  • Normalizing and displaying tags
  • + *
  • Persisting tags to the database
  • + *
  • Removing tags from shops
  • + *
  • Toggling system-defined tags
  • + *
+ * + *

Implementations are expected to connect directly to the database layer, + * while a tag manager handles in-memory indexing and caching.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface TagService { + + /** + * Represents the index key used for total counts in tag count maps. + * + * @since 6.3.0.0 + */ + long TOTAL_INDEX = -1L; + + /** + * Represents the reserved system tag for favorites. + * + * @since 6.3.0.0 + */ + String SYS_FAV = "@fav"; + + /** + * Represents the reserved system tag for watch tracking. + * + * @since 6.3.0.0 + */ + String SYS_WATCH = "@watch"; + + /** + * Represents the reserved system tag for avoided shops. + * + * @since 6.3.0.0 + */ + String SYS_AVOID = "@avoid"; + + /** + * Formats a stored tag for display to a player. + * + *

System tags are converted into friendly display names, while + * custom tags are prefixed with a hash character.

+ * + * @param sender the player receiving the display value + * @param stored the stored tag value + * + * @return the formatted display value, or an empty string if the tag is null + * + * @since 6.3.0.0 + */ + String displayTag(Player sender, String stored); + + /** + * Formats a stored tag for display while preserving system tags. + * + *

System tags beginning with {@code @} are returned unchanged, + * while custom tags are prefixed with a hash character.

+ * + * @param stored the stored tag value + * + * @return the formatted tag, or an empty string if the tag is null + * + * @since 6.3.0.0 + */ + String displayTagOrHash(String stored); + + /** + * Normalizes a tag into its stored form. + * + *

This includes trimming whitespace, lowercasing the value, + * removing a leading hash character, enforcing a maximum length, and validating allowed + * characters.

+ * + * @param input the input tag + * @param allowSystem whether reserved system tags are allowed + * + * @return the normalized tag, or {@code null} if invalid + * + * @since 6.3.0.0 + */ + @Nullable String normalizeTag(@Nullable String input, boolean allowSystem); + + /** + * Adds a tag to a shop for a player. + * + * @param player the player applying the tag + * @param shopId the shop id + * @param tag the tag to add + * + * @return a future containing {@code true} if the tag was added + * + * @since 6.3.0.0 + */ + CompletableFuture addShopTag(UUID player, long shopId, String tag); + + /** + * Removes a tag from a shop for a player. + * + * @param player the player removing the tag + * @param shopId the shop id + * @param tag the tag to remove + * + * @return a future containing {@code true} if the tag was removed + * + * @since 6.3.0.0 + */ + CompletableFuture removeShopTag(UUID player, long shopId, String tag); + + /** + * Removes all tags from a shop. + * + * @param shopId the shop id + * + * @return a future containing {@code true} if tags were removed + * + * @since 6.3.0.0 + */ + CompletableFuture removeAllShopTags(long shopId); + + /** + * Removes all tags applied by a specific player to a shop. + * + * @param shopId the shop id + * @param player the player whose tags should be removed + * + * @return a future containing {@code true} if tags were removed + * + * @since 6.3.0.0 + */ + CompletableFuture removeAllShopTagsBy(long shopId, UUID player); + + /** + * Toggles a reserved system tag on a shop for a player. + * + *

If the tag does not exist it will be added. If it already exists + * it will be removed.

+ * + * @param player the player toggling the tag + * @param shopId the shop id + * @param tag the system tag + * + * @return a future containing {@code true} if the tag is enabled after the operation + * + * @since 6.3.0.0 + */ + CompletableFuture toggleSystemTag(UUID player, long shopId, String tag); +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TaggingResult.java similarity index 90% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java rename to quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TaggingResult.java index 38972e9bf3..32ff885f50 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/TaggingResult.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/tag/TaggingResult.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags.tag; +package com.ghostchu.quickshop.api.shop.tag; /* * QuickShop-Hikari @@ -30,5 +30,6 @@ public enum TaggingResult { NOT_FOUND, ALREADY_EXISTS, DATABASE_ERROR, - INVALID_TAG + INVALID_TAG, + PLAYER_MAX_TAG_LIMIT_REACHED } \ No newline at end of file diff --git a/quickshop-bukkit/pom.xml b/quickshop-bukkit/pom.xml index 7f4ac4a915..98f4177227 100644 --- a/quickshop-bukkit/pom.xml +++ b/quickshop-bukkit/pom.xml @@ -128,19 +128,6 @@ true - - com.github.MilkBowl - VaultAPI - ${depend.vault} - provided - - - * - * - - - true - me.clip diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java index ac7d54bef8..200c521c1d 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java @@ -27,6 +27,7 @@ import com.ghostchu.quickshop.api.shop.ShopItemBlackList; import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.display.DisplayType; +import com.ghostchu.quickshop.api.shop.tag.TagManager; import com.ghostchu.quickshop.command.QuickShopCommand; import com.ghostchu.quickshop.command.SimpleCommandManager; import com.ghostchu.quickshop.common.util.CommonUtil; @@ -81,6 +82,7 @@ import com.ghostchu.quickshop.shop.interaction.QuickShopInteractionManager; import com.ghostchu.quickshop.shop.inventory.BukkitInventoryWrapperManager; import com.ghostchu.quickshop.shop.sign.SignHooker; +import com.ghostchu.quickshop.shop.tag.QuickShopTagManager; import com.ghostchu.quickshop.util.FastPlayerFinder; import com.ghostchu.quickshop.util.ItemMarker; import com.ghostchu.quickshop.util.MsgUtil; @@ -218,6 +220,7 @@ public class QuickShop implements QuickShopAPI, Reloadable { @Getter private final EconomyLoader economyLoader = new EconomyLoader(this); private final EconomyManager economyManager = new QSEconomyManager(); + private final QuickShopTagManager tagManager; private UpdateManager updateManager; @Getter private final PasteManager pasteManager = new PasteManager(); @@ -349,6 +352,7 @@ public QuickShop(final QuickShopBukkit javaPlugin, final Logger logger, final Pl this.logger = logger; this.platform = platform; this.helperMethods = new BukkitHelper(); + this.tagManager = new QuickShopTagManager(this); } /** @@ -634,6 +638,19 @@ public EconomyManager getEconomyManager() { return economyManager; } + /** + * Retrieves the TagManager associated with the QuickShop system. The TagManager is responsible + * for handling operations related to managing tags, which may include inventory item metadata and + * custom item tags used by the system. + * + * @return The TagManager instance that provides functionality for tag-related operations within + * the QuickShop system. + */ + @Override + public TagManager tagManager() { + return tagManager; + } + public UpdateManager updateManager() { return this.updateManager; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java index 27e6407db3..1ba5a81ee2 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/SimpleCommandManager.java @@ -5,6 +5,7 @@ import com.ghostchu.quickshop.api.command.CommandManager; import com.ghostchu.quickshop.command.subcommand.SubCommand_About; import com.ghostchu.quickshop.command.subcommand.SubCommand_Amount; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Avoid; import com.ghostchu.quickshop.command.subcommand.SubCommand_Benefit; import com.ghostchu.quickshop.command.subcommand.SubCommand_Browse; import com.ghostchu.quickshop.command.subcommand.SubCommand_Buy; @@ -16,6 +17,7 @@ import com.ghostchu.quickshop.command.subcommand.SubCommand_Debug; import com.ghostchu.quickshop.command.subcommand.SubCommand_Empty; import com.ghostchu.quickshop.command.subcommand.SubCommand_Export; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Favorite; import com.ghostchu.quickshop.command.subcommand.SubCommand_FetchMessage; import com.ghostchu.quickshop.command.subcommand.SubCommand_Find; import com.ghostchu.quickshop.command.subcommand.SubCommand_Freeze; @@ -45,12 +47,14 @@ import com.ghostchu.quickshop.command.subcommand.SubCommand_StaffAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_SuggestPrice; import com.ghostchu.quickshop.command.subcommand.SubCommand_SuperCreate; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Tag; import com.ghostchu.quickshop.command.subcommand.SubCommand_TaxAccount; import com.ghostchu.quickshop.command.subcommand.SubCommand_ToggleDisplay; import com.ghostchu.quickshop.command.subcommand.SubCommand_ToggleDisplayAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_TransferAll; import com.ghostchu.quickshop.command.subcommand.SubCommand_TransferOwnership; import com.ghostchu.quickshop.command.subcommand.SubCommand_Unlimited; +import com.ghostchu.quickshop.command.subcommand.SubCommand_Watch; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentBuy; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentEmpty; import com.ghostchu.quickshop.command.subcommand.silent.SubCommand_SilentFreeze; @@ -470,6 +474,41 @@ public SimpleCommandManager(final QuickShop plugin) { .permission("quickshop.suggestprice") .executor(new SubCommand_SuggestPrice(plugin)) .build()); + + final SubCommand_Avoid avoidCommand = new SubCommand_Avoid(plugin); + final SubCommand_Favorite favoriteCommand = new SubCommand_Favorite(plugin); + final SubCommand_Tag tagCommand = new SubCommand_Tag(plugin); + final SubCommand_Watch watchCommand = new SubCommand_Watch(plugin); + + registerCmd( + CommandContainer.builder() + .prefix("avoid") + .selectivePermission("quickshop.avoid") + .selectivePermission("quickshop.avoid.list") + .description((locale)->plugin.text().of("tags.commands.avoid").forLocale(locale)) + .executor(avoidCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("favorite") + .description((locale)->plugin.text().of("tags.commands.favorite").forLocale(locale)) + .executor(favoriteCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("tag") + .description((locale)->plugin.text().of("tags.commands.tag").forLocale(locale)) + .executor(tagCommand) + .build()); + + registerCmd( + CommandContainer.builder() + .prefix("watch") + .description((locale)->plugin.text().of("tags.commands.watch").forLocale(locale)) + .executor(watchCommand) + .build()); init(); } diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java similarity index 67% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java index 5863b9a49b..e94458ff5c 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Avoid.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Avoid.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags.command; +package com.ghostchu.quickshop.command.subcommand; /* * QuickShop-Hikari @@ -19,12 +19,10 @@ */ import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.addon.tags.Main; -import com.ghostchu.quickshop.addon.tags.TagService; -import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -33,6 +31,8 @@ import java.util.List; import java.util.Locale; +import static com.ghostchu.quickshop.api.shop.tag.TagService.SYS_AVOID; + /** * SubCommand_Avoid * @@ -41,12 +41,10 @@ */ public class SubCommand_Avoid implements CommandHandler { - private final Main main; private final QuickShop plugin; - public SubCommand_Avoid(final Main main, final QuickShop plugin) { + public SubCommand_Avoid(final QuickShop plugin) { - this.main = main; this.plugin = plugin; } @@ -55,11 +53,11 @@ public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { - final String tag = TagService.normalizeTag(TagService.SYS_AVOID, true); + final String tag = plugin.tagManager().service().normalizeTag(SYS_AVOID, true); if(tag == null) { //should never happen, but we'll catch just in case. - plugin.text().of(sender, "addon.tags.general.invalid").send(); + plugin.text().of(sender, "tags.general.invalid").send(); return; } @@ -71,18 +69,19 @@ public void onCommand(@NotNull final Player sender, return; } - final TaggingResult result = main.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); switch(result) { - case SUCCESS -> { - if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { - plugin.text().of(sender, "addon.tags.avoid.added").send(); + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.avoid.added").send(); } else { - plugin.text().of(sender, "addon.tags.avoid.removed").send(); + plugin.text().of(sender, "tags.avoid.removed").send(); } } - case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); - default -> plugin.text().of(sender, "addon.tags.avoid.unable").send(); + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.avoid.unable").send(); } return; } @@ -91,9 +90,9 @@ public void onCommand(@NotNull final Player sender, switch(sub.toLowerCase(Locale.ROOT)) { case "list" -> { - Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), - "addon.tags.avoid.list-title", - "addon.tags.avoid.none"); + plugin.tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "tags.avoid.list-title", + "tags.avoid.none"); } default -> sendUsage(sender); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java index c33ae74748..eca4c4e29a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Benefit.java @@ -15,7 +15,6 @@ import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.obj.QUserImpl; import com.ghostchu.quickshop.util.MsgUtil; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java index 4707fc5d6b..f697c4ffbf 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java @@ -50,7 +50,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } for(final Shop shop : pendingRemoval) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.getLocation(), ()->{ plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop clean", shop.saveToInfoStorage())); plugin.getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java index 1a3f172b5b..0d87ef227c 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java @@ -44,7 +44,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final List> pendingTasks = new CopyOnWriteArrayList<>(); for(final Shop shop : plugin.getShopManager().getAllShops()) { - final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc) -> { + final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc)->{ if(shop == null) { return; // WTF } @@ -81,14 +81,13 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String plugin.getShopManager().deleteShop(shop); deletionCounter.incrementAndGet(); plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); - return; } }); pendingTasks.add(task); } CompletableFuture.allOf(pendingTasks.toArray(new CompletableFuture[0])) - .whenComplete((v, t) -> plugin.text().of(sender, "cleanghost-deleted", deletionCounter.get()).send()); + .whenComplete((v, t)->plugin.text().of(sender, "cleanghost-deleted", deletionCounter.get()).send()); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java index 7c51f5b1d6..fd4346f897 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Debug.java @@ -135,8 +135,8 @@ private void handleItemInfo(final CommandSender sender, final List subPa return; } if(player.getInventory().getItemInMainHand().getType().isAir()) { - plugin.text().of(sender, "no-anythings-in-your-hand").send(); - return; + plugin.text().of(sender, "no-anythings-in-your-hand").send(); + return; } final String hand = player.getInventory().getItemInMainHand().getItemMeta().getAsString(); plugin.text().of(sender, "debug.item-info-hand-as-string", hand, Hashing.crc32().hashString(hand, StandardCharsets.UTF_8).toString()).send(); @@ -186,7 +186,7 @@ private void handleDbConnectionTest(final CommandSender sender, final List subParams) { - final long stopped = plugin.getSqlManager().getActiveQuery().values().size(); + final long stopped = plugin.getSqlManager().getActiveQuery().size(); plugin.getSqlManager().getActiveQuery().values().forEach(SQLQuery::close); plugin.text().of(sender, "debug.queries-stopped", stopped).send(); } diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java similarity index 66% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java index 78ea68b56c..7b0f9a07df 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Favorite.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Favorite.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags.command; +package com.ghostchu.quickshop.command.subcommand; /* * QuickShop-Hikari @@ -19,12 +19,10 @@ */ import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.addon.tags.Main; -import com.ghostchu.quickshop.addon.tags.TagService; -import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -33,6 +31,8 @@ import java.util.List; import java.util.Locale; +import static com.ghostchu.quickshop.api.shop.tag.TagService.SYS_FAV; + /** * SubCommand_Favorite * @@ -41,12 +41,10 @@ */ public class SubCommand_Favorite implements CommandHandler { - private final Main main; private final QuickShop plugin; - public SubCommand_Favorite(final Main main, final QuickShop plugin) { + public SubCommand_Favorite(final QuickShop plugin) { - this.main = main; this.plugin = plugin; } @@ -55,11 +53,11 @@ public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { - final String tag = TagService.normalizeTag(TagService.SYS_FAV, true); + final String tag = plugin.tagManager().service().normalizeTag(SYS_FAV, true); if(tag == null) { //should never happen, but we'll catch just in case. - plugin.text().of(sender, "addon.tags.general.invalid").send(); + plugin.text().of(sender, "tags.general.invalid").send(); return; } @@ -70,19 +68,19 @@ public void onCommand(@NotNull final Player sender, return; } - final TaggingResult result = main.tagManager() - .toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); switch(result) { - case SUCCESS -> { - if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { - plugin.text().of(sender, "addon.tags.favorite.added").send(); + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.favorite.added").send(); } else { - plugin.text().of(sender, "addon.tags.favorite.removed").send(); + plugin.text().of(sender, "tags.favorite.removed").send(); } } - case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); - default -> plugin.text().of(sender, "addon.tags.favorite.unable").send(); + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.favorite.unable").send(); } return; } @@ -91,9 +89,9 @@ public void onCommand(@NotNull final Player sender, switch(sub.toLowerCase(Locale.ROOT)) { case "list" -> { - Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), - "addon.tags.favorite.list-title", - "addon.tags.favorite.none"); + plugin.tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "tags.favorite.list-title", + "tags.favorite.none"); } default -> sendUsage(sender); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java index 0c1c1877d0..1d406820e8 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java @@ -123,7 +123,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } //Okay now all shops is our wanted shop in Map - final List> sortedShops = aroundShops.entrySet().stream().sorted(Map.Entry.comparingByValue(Double::compare).reversed()).toList(); + final List> sortedShops = aroundShops.entrySet().stream().sorted(Map.Entry. comparingByValue(Double::compare).reversed()).toList(); //Function if(usingOldLogic) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java index f7f196705e..7046855197 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java @@ -74,7 +74,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final AtomicInteger noStockCounter = new AtomicInteger(0); final List> futures = new ArrayList<>(outOfStockCheckQueue.size()); for(final Shop shop : outOfStockCheckQueue) { - final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), task -> { + final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), task->{ try { if(shop.getRemainingStock() == 0) { noStockCounter.incrementAndGet(); @@ -91,7 +91,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final int worldsFinal = worlds; CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .whenComplete((unused, throwable)->QuickShop.folia().getScheduler().runLater(()-> - sendStats(sender, buyingFinal, sellingFinal, chunksFinal, worldsFinal, noStockCounter.get()), 1)); + sendStats(sender, buyingFinal, sellingFinal, chunksFinal, worldsFinal, noStockCounter.get()), 1)); } private void sendStats(@NotNull final CommandSender sender, final int buying, final int selling, final int chunks, final int worlds, final int nostock) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java index 60df8815df..92e289d161 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Tag.java @@ -3,47 +3,24 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; -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.event.Phase; -import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitAddEvent; -import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitRemoveEvent; -import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; -import com.ghostchu.quickshop.common.util.CommonUtil; -import com.ghostchu.quickshop.obj.QUserImpl; -import com.ghostchu.quickshop.util.MsgUtil; -import com.ghostchu.quickshop.util.Util; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.math.BigDecimal; -import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Collections; -import java.util.Deque; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; -import java.util.UUID; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import java.util.Set; +import java.util.TreeMap; + +import static com.ghostchu.quickshop.api.shop.tag.TagService.TOTAL_INDEX; public class SubCommand_Tag implements CommandHandler { private final QuickShop plugin; - //our system tags - private static final String SYS_FAV = "@fav"; - private static final String SYS_WATCH = "@watch"; - private static final String SYS_AVOID = "@avoid"; - - private static final int MAX_TAG_LENGTH = 32; - //we only want letters, underscores and dashes - private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); public SubCommand_Tag(final QuickShop plugin) { @@ -51,48 +28,32 @@ public SubCommand_Tag(final QuickShop plugin) { } @Override - public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { if(parser.getArgs().isEmpty()) { sendUsage(sender); return; } - final String sub = parser.getArgs().getFirst(); + final String sub = parser.getArgs().getFirst().toLowerCase(Locale.ROOT); - //These are our global tag-related commands. They don't require looking at a shop. - switch (sub) { - // /qs tag tags + // Global commands that do not require looking at a shop. + switch(sub) { case "tags" -> { handleTags(sender); return; } - // /qs tag tagged case "tagged" -> { handleTaggedList(sender, parser); return; } - // /qs tag favs - case "favs" -> { - handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_FAV))); - return; - } - // /qs tag watched - case "watched" -> { - handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_WATCH))); - return; - } - // /qs tag avoided - case "avoided" -> { - handleTaggedList(sender, new ArrayDeque<>(List.of(SYS_AVOID))); - return; - } - // /qs tag purge case "purge", "removefromall", "untagall" -> { handleRemoveTagFromAllShops(sender, parser); return; } - default -> {} + default -> { + } } final Shop shop = getLookingShop(sender); @@ -101,180 +62,191 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } - // Check permission if(!shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_BENEFIT) && !plugin.perm().hasPermission(sender, "quickshop.other.benefit")) { plugin.text().of(sender, "not-managed-shop").send(); return; } - switch(parser.getArgs().getFirst()) { + switch(sub) { case "add" -> handleAdd(sender, shop, parser, 2); case "remove", "del", "delete" -> handleRemove(sender, shop, parser); case "clear" -> handleClear(sender, shop); - - // Optional: admin "clear all tags from this shop for everyone" - case "clearall" -> handleClearAll(sender, shop); - - // System tag shortcuts (looked-at shop) - case "fav" -> handleToggleSystem(sender, shop, SYS_FAV); - case "unfav" -> handleUnsetSystem(sender, shop, SYS_FAV); - - case "watch" -> handleToggleSystem(sender, shop, SYS_WATCH); - case "unwatch" -> handleUnsetSystem(sender, shop, SYS_WATCH); - - case "avoid" -> handleToggleSystem(sender, shop, SYS_AVOID); - case "unavoid" -> handleUnsetSystem(sender, shop, SYS_AVOID); - + case "clearall" -> + handleClearAll(sender, parser); //todo: confirmation maybe for clear and clearall? case "list" -> handleTags(sender, shop); - default -> handleAdd(sender, shop, parser, 1); } } - @NotNull - @Override - public List onTabComplete( - @NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { + private void handleAdd(final Player sender, final Shop shop, + final CommandParser parser, final int tagIndex) { - if(parser.getArgs().size() == 1) { - return List.of("add", "remove", "query"); - } - if(parser.getArgs().size() == 2) { - return null; - } - if(parser.getArgs().size() == 3) { - return Collections.singletonList(plugin.text().of(sender, "tabcomplete.percentage").legacy()); - } - return Collections.emptyList(); - } - - private void sendUsage(final Player sender) { - plugin.text().of(sender, "command-incorrect", - "/quickshop tag <[tag]/remove/list/clear/tagged/tags/fav/unfav/favs/watch/unwatch/watched/avoid/unavoid/avoided>") - .send(); - } - - //our shop-specific methods - private void handleAdd(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser, final int tagPosition) { - if(parser.getArgs().size() < tagPosition) { + if(parser.getArgs().size() <= tagIndex) { sendUsage(sender); return; } - final String tag = normalizeTag(parser.getArgs().get(tagPosition - 1), false); - if(tag == null) { - plugin.text().of(sender, "tag-invalid").send(); + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(tagIndex), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); return; } - Util.regionThread(sender.getLocation(), () -> { - plugin.getDatabaseHelper().tagShop(sender.getUniqueId(), shop.getShopId(), tag); - plugin.text().of(sender, "tag-added", tag).send(); - }); + + final TaggingResult result = plugin.tagManager().addTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "tags.tag.added", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case ALREADY_EXISTS -> + plugin.text().of(sender, "tags.tag.duplicate", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case INVALID_TAG -> plugin.text().of(sender, "tags.general.invalid").send(); + case DATABASE_ERROR -> plugin.text().of(sender, "tags.general.database-error").send(); + default -> + plugin.text().of(sender, "tags.tag.failed-add", plugin.tagManager().service().displayTag(sender, normalized)).send(); + } } - private void handleRemove(final Player sender, @NotNull final Shop shop, @NotNull final CommandParser parser) { + private void handleRemove(final Player sender, final Shop shop, final CommandParser parser) { + if(parser.getArgs().size() < 2) { sendUsage(sender); return; } - } - - private void handleClear(final Player sender, @NotNull final Shop shop) { - - } - private void handleToggleSystem(final Player sender, @NotNull final Shop shop, final String tag) { + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } + final TaggingResult result = plugin.tagManager().removeTag(shop.getShopId(), sender.getUniqueId(), normalized); + switch(result) { + case SUCCESS -> + plugin.text().of(sender, "tags.tag.removed", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case NOT_FOUND -> + plugin.text().of(sender, "tags.tag.does-not-exist", plugin.tagManager().service().displayTag(sender, normalized)).send(); + case DATABASE_ERROR -> plugin.text().of(sender, "tags.general.database-error").send(); + default -> + plugin.text().of(sender, "tags.tag.failed-remove", plugin.tagManager().service().displayTag(sender, normalized)).send(); + } } - private void handleUnsetSystem(final Player sender, @NotNull final Shop shop, final String tag) { + private void handleClear(final Player sender, final Shop shop) { + final boolean removed = plugin.tagManager().removeAllShopTagsBy(shop.getShopId(), sender.getUniqueId()); + if(removed) { + plugin.text().of(sender, "tags.tag.cleared", shop.getShopId()).send(); + } else { + plugin.text().of(sender, "tags.tag.no-tags", shop.getShopId()).send(); + } } - private void handleClearAll(final Player sender, @NotNull final Shop shop) { + private void handleClearAll(final Player sender, final CommandParser parser) { - //TODO: User parameter - if(!sender.hasPermission("quickshop.tag.clearall")) { + if(!plugin.perm().hasPermission(sender, "quickshop.tag.admin.clearall")) { plugin.text().of(sender, "no-permission").send(); return; } - - } - - //our global methods - private void handleTaggedList(final Player sender, @NotNull final CommandParser parser) { - } - - private void handleTaggedList(final Player sender, @NotNull final ArrayDeque tags) { + final boolean removed = plugin.tagManager().removeAllTags(); + if(removed) { + plugin.text().of(sender, "tags.tag.cleared-all").send(); + } else { + plugin.text().of(sender, "tags.tag.no-tags-all").send(); + } } private void handleTags(final Player sender) { - } + final TreeMap count = plugin.tagManager().tagsCount(sender.getUniqueId()); + if(count.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops").send(); + return; + } - private void handleTags(final Player sender, @NotNull final Shop shop) { + plugin.text().of(sender, "tags.tag.list-player-shops-title", count.get(TOTAL_INDEX), count.size()).send(); + for(final Map.Entry entry : count.entrySet()) { + plugin.text().of(sender, "tags.tag.list-player-shop-entry", entry.getKey(), entry.getValue()).send(); + } } - private void handleRemoveTagFromAllShops(final Player sender, @NotNull final CommandParser parser) { + private void handleTags(final Player sender, final Shop shop) { - } - - private String displayTag(final Player sender, final String stored) { - if(stored == null) { - return ""; + final Set tags = Collections.unmodifiableSet(plugin.tagManager().tagsFilteredByShop(sender.getUniqueId(), shop.getShopId())); + if(tags.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops-player", shop.getShopId()).send(); + return; } - return switch (stored) { - case SYS_FAV -> "Favorite"; - case SYS_WATCH -> "Watch"; - case SYS_AVOID -> "Avoid"; - default -> "#" + stored; - }; + plugin.text().of(sender, "tags.tag.list-player-shop-title", tags.size(), shop.getShopId()).send(); + for(final String tag : tags) { + plugin.text().of(sender, "tags.general.list-entry", tag).send(); + } } - private String displayTagOrHash(final String stored) { - if(stored == null) { - return ""; - } + private void handleTaggedList(final Player sender, final CommandParser parser) { - if(stored.startsWith("@")) { - return stored; + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; } - return "#" + stored; - } - @Nullable - private String normalizeTag(@Nullable String input, final boolean allowSystem) { + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; + } - if (input == null) { - return null; + final List shopIds = plugin.tagManager().shopsFilteredByTag(sender.getUniqueId(), normalized); + if(shopIds.isEmpty()) { + plugin.text().of(sender, "tags.tag.no-tagged-shops-tag", normalized).send(); + return; } - if (input.startsWith("#")) { - input = input.substring(1); + plugin.text().of(sender, "tags.tag.list-tag-title", shopIds.size()).send(); + for(final Long shopId : shopIds) { + plugin.text().of(sender, "tags.general.list-entry", shopId).send(); } + } - input = input.trim().toLowerCase(Locale.ROOT); + private void handleRemoveTagFromAllShops(final Player sender, final CommandParser parser) { - if (input.isEmpty()) { - return null; + if(parser.getArgs().size() < 2) { + sendUsage(sender); + return; } - if (input.length() > MAX_TAG_LENGTH) { - return null; + final String normalized = plugin.tagManager().service().normalizeTag(parser.getArgs().get(1), false); + if(normalized == null) { + plugin.text().of(sender, "tags.general.invalid").send(); + return; } - if (!allowSystem && input.startsWith("@")) { - return null; + final boolean cleared = plugin.tagManager().removeTag(sender.getUniqueId(), normalized); + if(!cleared) { + plugin.text().of(sender, "tags.general.database-error", normalized).send(); + return; } - if (!VALID_TAG_PATTERN.matcher(input).matches()) { - return null; + plugin.text().of(sender, "tags.tag.clearing-tag", normalized).send(); + } + + @NotNull + @Override + public List onTabComplete(@NotNull final Player sender, @NotNull final String commandLabel, + @NotNull final CommandParser parser) { + + if(parser.getArgs().size() == 1) { + return List.of("add", "remove", "clear", "clearall", "list", "tags", "tagged", "purge"); } + return Collections.emptyList(); + } + + private void sendUsage(final Player sender) { - return input; + plugin.text().of(sender, "command-incorrect", + "/quickshop tag [tag]") + .send(); } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java index e06658220e..efe27d44b1 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_TransferOwnership.java @@ -77,8 +77,8 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman plugin.text().of(sender, "not-looking-at-shop").send(); return; } - if (!targetShop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.OWNERSHIP_TRANSFER) - && !plugin.perm().hasPermission(sender, "quickshop.transferownership.other")) { + if(!targetShop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.OWNERSHIP_TRANSFER) + && !plugin.perm().hasPermission(sender, "quickshop.transferownership.other")) { plugin.text().of(sender, "no-permission").send(); return; } diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java similarity index 58% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java index 1de8474732..ae56797148 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/command/SubCommand_Watch.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Watch.java @@ -1,12 +1,11 @@ -package com.ghostchu.quickshop.addon.tags.command; +package com.ghostchu.quickshop.command.subcommand; import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.addon.tags.Main; -import com.ghostchu.quickshop.addon.tags.TagService; -import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; import com.ghostchu.quickshop.api.command.CommandHandler; import com.ghostchu.quickshop.api.command.CommandParser; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.tag.TagService; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -18,12 +17,10 @@ public class SubCommand_Watch implements CommandHandler { - private final Main main; private final QuickShop plugin; - public SubCommand_Watch(final Main main, final QuickShop plugin) { + public SubCommand_Watch(final QuickShop plugin) { - this.main = main; this.plugin = plugin; } @@ -32,11 +29,11 @@ public void onCommand(@NotNull final Player sender, @NotNull final String commandLabel, @NotNull final CommandParser parser) { - final String tag = TagService.normalizeTag(TagService.SYS_WATCH, true); + final String tag = plugin.tagManager().service().normalizeTag(TagService.SYS_WATCH, true); if(tag == null) { //should never happen, but we'll catch just in case. - plugin.text().of(sender, "addon.tags.general.invalid").send(); + plugin.text().of(sender, "tags.general.invalid").send(); return; } @@ -48,18 +45,19 @@ public void onCommand(@NotNull final Player sender, return; } - final TaggingResult result = main.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); + final TaggingResult result = plugin.tagManager().toggleTag(shop.getShopId(), sender.getUniqueId(), tag); switch(result) { - case SUCCESS -> { - if(main.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { - plugin.text().of(sender, "addon.tags.watch.added").send(); + case TaggingResult.SUCCESS -> { + if(plugin.tagManager().hasTag(shop.getShopId(), sender.getUniqueId(), tag)) { + plugin.text().of(sender, "tags.watch.added").send(); } else { - plugin.text().of(sender, "addon.tags.watch.removed").send(); + plugin.text().of(sender, "tags.watch.removed").send(); } } - case DATABASE_ERROR -> plugin.text().of(sender, "addon.tags.general.database-error").send(); - default -> plugin.text().of(sender, "addon.tags.watch.unable").send(); + case TaggingResult.DATABASE_ERROR -> + plugin.text().of(sender, "tags.general.database-error").send(); + default -> plugin.text().of(sender, "tags.watch.unable").send(); } return; } @@ -68,9 +66,9 @@ public void onCommand(@NotNull final Player sender, switch(sub.toLowerCase(Locale.ROOT)) { case "list" -> { - Main.instance().tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), - "addon.tags.watch.list-title", - "addon.tags.watch.none"); + plugin.tagManager().listShopsByFilter(sender, new ArrayList<>(List.of(tag)), + "tags.watch.list-title", + "tags.watch.none"); } default -> sendUsage(sender); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java index 45204da69c..211dd520cd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentInventory.java @@ -33,7 +33,7 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N Log.debug("Inventory is empty! " + cs); return; } - + if(plugin.getConfig().getBoolean("shop.lock") && !shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.ACCESS_INVENTORY)) { if(plugin.perm().hasPermission(sender, "quickshop.other.open")) { if(LockListener.lockCoolDown.getIfPresent(sender.getUniqueId()) == null) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java index 6a6e064eeb..90aa2a0657 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentRemove.java @@ -5,7 +5,6 @@ import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.obj.QUserImpl; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.logging.container.ShopRemoveLog; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java similarity index 87% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java index 453c7092ba..944f178aac 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/PlayerTags.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/PlayerTags.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags.tag; +package com.ghostchu.quickshop.shop.tag; /* * QuickShop-Hikari @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** @@ -33,6 +34,13 @@ public class PlayerTags { private final Set tags = ConcurrentHashMap.newKeySet(); + private final UUID player; + + public PlayerTags(final UUID player) { + + this.player = player; + } + public boolean hasTag(final String tag) { return tags.contains(tag); @@ -57,4 +65,9 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + + public UUID player() { + + return player; + } } \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java new file mode 100644 index 0000000000..bf85fa30fb --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopPlayerTagIndex.java @@ -0,0 +1,120 @@ +package com.ghostchu.quickshop.shop.tag; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.api.shop.tag.PlayerTagIndex; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Default QuickShop implementation of {@link PlayerTagIndex}. + * + *

This implementation maintains two concurrent indexes:

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

This allows fast lookups in both directions.

+ * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class QuickShopPlayerTagIndex implements PlayerTagIndex { + + private final Map> shopToTags = new ConcurrentHashMap<>(); + private final Map> tagToShops = new ConcurrentHashMap<>(); + + @Override + public void addTag(final long shopId, final String tag) { + + shopToTags + .computeIfAbsent(shopId, id->ConcurrentHashMap.newKeySet()) + .add(tag); + + tagToShops + .computeIfAbsent(tag, t->ConcurrentHashMap.newKeySet()) + .add(shopId); + } + + @Override + public void removeTag(final long shopId, final String tag) { + + final Set tags = shopToTags.get(shopId); + if(tags != null) { + tags.remove(tag); + if(tags.isEmpty()) { + shopToTags.remove(shopId); + } + } + + final Set shops = tagToShops.get(tag); + if(shops != null) { + shops.remove(shopId); + if(shops.isEmpty()) { + tagToShops.remove(tag); + } + } + } + + @Override + public Set getTags(final long shopId) { + + return shopToTags.getOrDefault(shopId, Collections.emptySet()); + } + + @Override + public Set getShops(final String tag) { + + return tagToShops.getOrDefault(tag, Collections.emptySet()); + } + + @Override + public Set shops() { + + return Collections.unmodifiableSet(shopToTags.keySet()); + } + + @Override + public int totalTags() { + + int total = 0; + for(final Set tags : shopToTags.values()) { + total += tags.size(); + } + return total; + } + + @Override + public boolean hasTag(final long shopId, final String tag) { + + final Set tags = shopToTags.get(shopId); + return tags != null && tags.contains(tag); + } + + @Override + public void clear() { + + shopToTags.clear(); + tagToShops.clear(); + } +} \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java similarity index 51% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java index e998ef634d..f813b1c30d 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagManager.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags; +package com.ghostchu.quickshop.shop.tag; /* * QuickShop-Hikari @@ -19,41 +19,64 @@ */ import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.addon.tags.tag.PlayerTags; -import com.ghostchu.quickshop.addon.tags.tag.ShopTags; -import com.ghostchu.quickshop.addon.tags.tag.TaggingResult; +import com.ghostchu.quickshop.api.shop.tag.PlayerTagIndex; +import com.ghostchu.quickshop.api.shop.tag.TagManager; +import com.ghostchu.quickshop.api.shop.tag.TagService; +import com.ghostchu.quickshop.api.shop.tag.TaggingResult; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import static com.ghostchu.quickshop.addon.tags.TagService.TOTAL_INDEX; -import static com.ghostchu.quickshop.addon.tags.tag.TaggingResult.NOT_FOUND; +import static com.ghostchu.quickshop.api.shop.tag.TagService.TOTAL_INDEX; +import static com.ghostchu.quickshop.api.shop.tag.TaggingResult.NOT_FOUND; /** - * TagManager + * The TagManager class handles the management of tags associated with shops and players. It allows + * adding, removing, and querying tags for shops, as well as filtering shops by tags. The data is + * stored in memory with support for persistent operations via the TagService. * * @author creatorfromhell * @since 6.3.0.0 */ -public class TagManager { +public class QuickShopTagManager implements TagManager { private final ConcurrentHashMap tags = new ConcurrentHashMap<>(); + private final ConcurrentHashMap playerIndexes = new ConcurrentHashMap<>(); + private final QuickShop plugin; private final TagService service; - public TagManager(final Main main, final QuickShop plugin) { + public QuickShopTagManager(final QuickShop plugin) { - this.service = new TagService(main, plugin); + this.plugin = plugin; + this.service = new QuickShopTagService(plugin); } + private PlayerTagIndex playerIndex(final UUID player) { + + return playerIndexes.computeIfAbsent(player, id->new QuickShopPlayerTagIndex()); + } + + private void cleanupPlayerIndex(final UUID player) { + + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return; + } + + if(index.totalTags() <= 0) { + playerIndexes.remove(player); + } + } + + @Override public TaggingResult addTag(final long shopId, final UUID player, final String tag) { final ShopTags shopTags = tags.computeIfAbsent(shopId, id->new ShopTags()); @@ -62,10 +85,14 @@ public TaggingResult addTag(final long shopId, final UUID player, final String t } shopTags.addTag(player, tag); + //update our index service + playerIndex(player).addTag(shopId, tag); + //update our database service.addShopTag(player, shopId, tag); return TaggingResult.SUCCESS; } + @Override public TaggingResult toggleTag(final long shopId, final UUID player, final String tag) { if(hasTag(shopId, player, tag)) { @@ -74,6 +101,7 @@ public TaggingResult toggleTag(final long shopId, final UUID player, final Strin return addTag(shopId, player, tag); } + @Override public boolean removeAllTags() { final Iterator iterator = tags.keySet().iterator(); @@ -85,148 +113,212 @@ public boolean removeAllTags() { return true; } + @Override public boolean removeAllShopTags(final long shopId) { final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return false; } + + for(final PlayerTags playerTags : shopTags.getTags()) { + final UUID player = playerTags.player(); + + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + for(final String tag : playerTags.getTags()) { + index.removeTag(shopId, tag); + } + cleanupPlayerIndex(player); + } + } + shopTags.clear(); service.removeAllShopTags(shopId); tags.remove(shopId); return true; } + @Override public boolean removeAllShopTagsBy(final long shopId, final UUID player) { final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return false; } + + final PlayerTags playerTags = shopTags.getTags(player); + if(playerTags == null || playerTags.getTags().isEmpty()) { + return false; + } + + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + for(final String tag : playerTags.getTags()) { + index.removeTag(shopId, tag); + } + cleanupPlayerIndex(player); + } + shopTags.removeAllTags(player); + //remove our shop object if empty to save memory + if(shopTags.isEmpty()) { + tags.remove(shopId); + } + service.removeAllShopTagsBy(shopId, player); return true; } + @Override public boolean removeAllPlayerTags(final UUID player) { + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null || index.totalTags() <= 0) { + return false; + } + final Iterator iterator = tags.keySet().iterator(); while(iterator.hasNext()) { final Long shopId = iterator.next(); removeAllShopTagsBy(shopId, player); } + + //update our index service + playerIndexes.remove(player); return true; } + @Override public int totalTags() { int total = 0; - for(final Map.Entry entry : tags.entrySet()) { - - for(final PlayerTags tags : entry.getValue().getTags()) { - total += tags.getTags().size(); - } + for(final PlayerTagIndex index : playerIndexes.values()) { + total += index.totalTags(); } return total; } + @Override public int totalTagsByPlayer(final UUID player) { - int total = 0; - for(final Map.Entry entry : tags.entrySet()) { - - final PlayerTags tags = entry.getValue().getTags(player); - if(tags == null || tags.getTags().isEmpty()) { - continue; - } - total += tags.getTags().size(); + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return 0; } - //our total count for all tags by player. - return total; + return index.totalTags(); } + @Override public TreeMap tagsCount(final UUID player) { - int total = 0; final TreeMap count = new TreeMap<>(); - for(final Map.Entry entry : tags.entrySet()) { + final PlayerTagIndex index = playerIndexes.get(player); + //check out index service + if(index == null) { + count.put(TOTAL_INDEX, 0); + return count; + } - final PlayerTags tags = entry.getValue().getTags(player); - if(tags == null || tags.getTags().isEmpty()) { + int total = 0; + for(final Long shopId : index.shops()) { + final int size = index.getTags(shopId).size(); + if(size <= 0) { continue; } - count.put(entry.getKey(), tags.getTags().size()); - total += tags.getTags().size(); + count.put(shopId, size); + total += size; } - //our total count for all tags by player. + + //our total count count.put(TOTAL_INDEX, total); return count; } + @Override public Set tagsFilteredByShop(final UUID player, final long shopId) { + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + return index.getTags(shopId); + } + final ShopTags shopTags = tags.get(shopId); if(shopTags == null) { return Collections.emptySet(); } - return shopTags.getTags(player).getTags(); - } - public List shopsFilteredByTag(final UUID player, final String tag) { + final PlayerTags playerTags = shopTags.getTags(player); + if(playerTags == null) { + return Collections.emptySet(); + } - final List shopIds = new ArrayList<>(); - final Iterator iterator = tags.keySet().iterator(); - while(iterator.hasNext()) { + return playerTags.getTags(); + } - final Long shopId = iterator.next(); - final ShopTags shopTags = tags.get(shopId); - if(shopTags == null) { - continue; - } + @Override + public List shopsFilteredByTag(final UUID player, final String tag) { - if(shopTags.hasTag(player, tag)) { - shopIds.add(shopId); - } + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null) { + return Collections.emptyList(); } - return shopIds; + + return new ArrayList<>(index.getShops(tag)); } + @Override public List shopsFilteredByTags(final UUID player, final List filterTags) { - final List shopIds = new ArrayList<>(); - final Iterator iterator = tags.keySet().iterator(); - while(iterator.hasNext()) { + final PlayerTagIndex index = playerIndexes.get(player); + if(index == null || filterTags.isEmpty()) { + return Collections.emptyList(); + } - final Long shopId = iterator.next(); - final ShopTags shopTags = tags.get(shopId); - if(shopTags == null) { + Set result = null; + + for(final String tag : filterTags) { + final Set shops = index.getShops(tag); + if(shops.isEmpty()) { + return Collections.emptyList(); + } + + if(result == null) { + result = ConcurrentHashMap.newKeySet(); + result.addAll(shops); continue; } - if(shopTags.hasTags(player, filterTags)) { - shopIds.add(shopId); + result.retainAll(shops); + if(result.isEmpty()) { + return Collections.emptyList(); } } - return shopIds; + + return result == null? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(result)); } + @Override public void listShopsByFilter(final Player player, final List filterTags, final String titleNode, final String noEntries) { final List shopIds = shopsFilteredByTags(player.getUniqueId(), filterTags); if(shopIds.isEmpty()) { - Main.instance().quickShop().text().of(player, noEntries).send(); + plugin.text().of(player, noEntries).send(); return; } - Main.instance().quickShop().text().of(player, titleNode).send(); + plugin.text().of(player, titleNode).send(); for(final Long shopId : shopIds) { - Main.instance().quickShop().text().of(player, "addon.tags.general.list-entry", shopId).send(); + plugin.text().of(player, "tags.general.list-entry", shopId).send(); } } + @Override public boolean hasTag(final long shopId, final UUID player, final String tag) { final ShopTags shopTags = tags.get(shopId); @@ -236,6 +328,7 @@ public boolean hasTag(final long shopId, final UUID player, final String tag) { return shopTags.hasTag(player, tag); } + @Override public boolean removeTag(final UUID player, final String tag) { final Iterator iterator = tags.keySet().iterator(); @@ -246,6 +339,7 @@ public boolean removeTag(final UUID player, final String tag) { return true; } + @Override public TaggingResult removeTag(final long shopId, final UUID player, final String tag) { final ShopTags shopTags = tags.get(shopId); @@ -258,6 +352,13 @@ public TaggingResult removeTag(final long shopId, final UUID player, final Strin } shopTags.removeTag(player, tag); + //update our index service + final PlayerTagIndex index = playerIndexes.get(player); + if(index != null) { + + index.removeTag(shopId, tag); + } + //update our database service.removeShopTag(player, shopId, tag); //remove our shop object if empty to save memory @@ -268,6 +369,7 @@ public TaggingResult removeTag(final long shopId, final UUID player, final Strin return TaggingResult.SUCCESS; } + @Override public TagService service() { return service; diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java similarity index 63% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java index 3115350855..65b5e3fc84 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/TagService.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/QuickShopTagService.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags; +package com.ghostchu.quickshop.shop.tag; /* * QuickShop-Hikari @@ -20,6 +20,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.api.database.DatabaseHelper; +import com.ghostchu.quickshop.api.shop.tag.TagService; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; @@ -29,32 +30,54 @@ import java.util.regex.Pattern; /** - * TagService + * Default QuickShop implementation of {@link TagService}. + * + *

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

* * @author creatorfromhell * @since 6.3.0.0 */ -public class TagService { - - - public static final long TOTAL_INDEX = -1L; - public static final String SYS_FAV = "@fav"; - public static final String SYS_WATCH = "@watch"; - public static final String SYS_AVOID = "@avoid"; +public class QuickShopTagService implements TagService { private static final int MAX_TAG_LENGTH = 32; private static final Pattern VALID_TAG_PATTERN = Pattern.compile("^[a-z_-]+$"); - private final Main tagsMain; + private static QuickShopTagService instance; + private final QuickShop plugin; - public TagService(final Main tagsMain, final QuickShop plugin) { + /** + * Creates a new tag service implementation. + * + * @param plugin the QuickShop plugin instance + * + * @since 6.3.0.0 + */ + public QuickShopTagService(final QuickShop plugin) { - this.tagsMain = tagsMain; this.plugin = plugin; + instance = this; } - public static String displayTag(final Player sender, final String stored) { + /** + * Returns the active tag service implementation instance. + * + * @return the active tag service implementation + * + * @throws IllegalStateException if the tag service has not been initialized + * @since 6.3.0.0 + */ + public static QuickShopTagService instance() { + + if(instance == null) { + throw new IllegalStateException("QuickShopTagService has not been initialized."); + } + return instance; + } + + @Override + public String displayTag(final Player sender, final String stored) { if(stored == null) { return ""; @@ -68,7 +91,8 @@ public static String displayTag(final Player sender, final String stored) { }; } - public static String displayTagOrHash(final String stored) { + @Override + public String displayTagOrHash(final String stored) { if(stored == null) { return ""; @@ -80,8 +104,8 @@ public static String displayTagOrHash(final String stored) { return "#" + stored; } - @Nullable - public static String normalizeTag(@Nullable String input, final boolean allowSystem) { + @Override + public @Nullable String normalizeTag(@Nullable String input, final boolean allowSystem) { if(input == null) { return null; @@ -112,58 +136,35 @@ public static String normalizeTag(@Nullable String input, final boolean allowSys return input; } + @Override public CompletableFuture addShopTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); - - return db.tagShop(player, shopId, tag).thenCompose(result->{ - - if(result != null && result > 0) { - return CompletableFuture.completedFuture(true); - } - - return CompletableFuture.completedFuture(false); - }); + return db.tagShop(player, shopId, tag).thenApply(result->result != null && result > 0); } + @Override public CompletableFuture removeShopTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); - - return db.removeShopTag(player, shopId, tag).thenCompose(result->{ - - if(result != null && result > 0) { - return CompletableFuture.completedFuture(true); - } - - return CompletableFuture.completedFuture(false); - }); + return db.removeShopTag(player, shopId, tag).thenApply(result->result != null && result > 0); } + @Override public CompletableFuture removeAllShopTags(final long shopId) { final DatabaseHelper db = plugin.getDatabaseHelper(); - return db.removeAllShopTags(shopId).thenCompose(result->{ - if(result != null && result > 0) { - return CompletableFuture.completedFuture(true); - } - - return CompletableFuture.completedFuture(false); - }); + return db.removeAllShopTags(shopId).thenApply(result->result != null && result > 0); } + @Override public CompletableFuture removeAllShopTagsBy(final long shopId, final UUID player) { final DatabaseHelper db = plugin.getDatabaseHelper(); - return db.removeAllShopTagsBy(player, shopId).thenCompose(result->{ - if(result != null && result > 0) { - return CompletableFuture.completedFuture(true); - } - - return CompletableFuture.completedFuture(false); - }); + return db.removeAllShopTagsBy(player, shopId).thenApply(result->result != null && result > 0); } + @Override public CompletableFuture toggleSystemTag(final UUID player, final long shopId, final String tag) { final DatabaseHelper db = plugin.getDatabaseHelper(); @@ -173,8 +174,7 @@ public CompletableFuture toggleSystemTag(final UUID player, final long return CompletableFuture.completedFuture(true); } - return db.removeShopTag(player, shopId, tag) - .thenApply(r->false); + return db.removeShopTag(player, shopId, tag).thenApply(r->false); }); } } \ No newline at end of file diff --git a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java similarity index 96% rename from addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java index 215e37183f..9d51926544 100644 --- a/addon/tags/src/main/java/com/ghostchu/quickshop/addon/tags/tag/ShopTags.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tag/ShopTags.java @@ -1,4 +1,4 @@ -package com.ghostchu.quickshop.addon.tags.tag; +package com.ghostchu.quickshop.shop.tag; /* * QuickShop-Hikari @@ -37,7 +37,7 @@ public class ShopTags { public void addTag(final UUID player, final String tag) { if(!tags.containsKey(player)) { - tags.put(player, new PlayerTags()); + tags.put(player, new PlayerTags(player)); } tags.get(player).addTag(tag); } @@ -63,7 +63,6 @@ public boolean removeAllTags(final UUID player) { } tags.remove(player); return true; - } public boolean hasTag(final UUID player, final String tag) { diff --git a/quickshop-bukkit/src/main/resources/lang/messages.yml b/quickshop-bukkit/src/main/resources/lang/messages.yml index 92cd923398..fdb705657e 100644 --- a/quickshop-bukkit/src/main/resources/lang/messages.yml +++ b/quickshop-bukkit/src/main/resources/lang/messages.yml @@ -133,6 +133,57 @@ shop-staff-empty: This shop have no staff members. shops-recovering: Recovering shops from backup... virtual-player-component-hover: 'This is a virtual player.\nRefers to a system account of this name with the same name.\nUUID: {0}\nUsername: {1}\nDisplay as: {2}' real-player-component-hover: 'This is a real exists player.\nUUID: {0}\nUsername: {1}\nDisplay as: {2}\nIf you wish to use a virtual system account with the same name, add "[]" to the both side of the username: [{1}].' +tags: + commands: + tag: Manage your shop tags. + favorite: Toggle favorite on looked-at shop. + watch: Toggle watch on looked-at shop. + avoid: Toggle avoid on looked-at shop. + general: + database-error: An error occurred while execution tags operation. + list-entry: ' - {0}' + invalid: 'Invalid tag. Only letters, dash (-), and underscore (_) are allowed. Max length: {0}' + avoid: + added: Shop has been added to your avoid list. + removed: Shop has been removed from your avoid list. + unable: Unable to toggle avoid on this shop. + none: You don't have any shop in your avoid list. + list-title: 'Avoided Shops:' + label: Avoid + favorite: + added: Shop has been added to your favorite list. + removed: Shop has been removed from your favorite list. + unable: Unable to add shop to your favorite list. + none: You have no favorite shops. + list-title: 'Favorited Shops:' + label: Favorite + watch: + added: You are watching this shop now. + removed: You are no longer watching this shop. + unable: Unable to toggle watch on this shop. + none: You are not watching any shops. + list-title: 'Watched Shops:' + label: Watching + tag: + added: Successfully added tag {0} to this shop! + duplicate: This shop already has tag {0}! + failed-add: An error occurred while adding tag {0} to this shop. + failed-remove: An error occurred while removing tag {0} from this shop. + removed: Successfully removed tag {0} from this shop! + does-not-exist: This shop does not have tag {0}! + list-player-shop-title: 'You have {0} tags on shop #{1}:' + list-player-shops-title: 'You have {0} tags on {1} shops:' + list-player-shop-entry: 'Shop #{0} - {1} tags' + list-title: 'This shop has {0} tags:' + list-tag-title: 'This tag is applied to {0} shops:' + cleared: Successfully cleared all tags from this shop! + cleared-all: Successfully cleared all tags from all shops! + cleared-tag: Successfully cleared tag {0} from all your tagged shops! + no-tags-all: There are no tags on any shop! + no-tags: This shop has no tags! + no-tagged-shops-player: You have no tags on this shop! + no-tagged-shops: You have no tagged shops! + no-tagged-shops-tag: You have no shops with tag {0}! menu: sell-tax: You paid {0} in taxes. owner: 'Owner: {0}' @@ -1636,59 +1687,6 @@ addon: shopitemonly: message: You can't put non-shop items into shop container, all non-shop items will dropped at your location. - tags: - commands: - tag: Manage your shop tags. - favorite: Toggle favorite on looked-at shop. - watch: Toggle watch on looked-at shop. - avoid: Toggle avoid on looked-at shop. - general: - database-error: An error occurred while execution tags operation. - list-entry: ' - {0}' - invalid: 'Invalid tag. Only letters, dash (-), and underscore (_) are allowed. Max length: {0}' - avoid: - added: Shop has been added to your avoid list. - removed: Shop has been removed from your avoid list. - unable: Unable to toggle avoid on this shop. - none: You don't have any shop in your avoid list. - list-title: 'Avoided Shops:' - label: Avoid - favorite: - added: Shop has been added to your favorite list. - removed: Shop has been removed from your favorite list. - unable: Unable to add shop to your favorite list. - none: You have no favorite shops. - list-title: 'Favorited Shops:' - label: Favorite - watch: - added: You are watching this shop now. - removed: You are no longer watching this shop. - unable: Unable to toggle watch on this shop. - none: You are not watching any shops. - list-title: 'Watched Shops:' - label: Watching - tag: - added: Successfully added tag {0} to this shop! - duplicate: This shop already has tag {0}! - failed-add: An error occurred while adding tag {0} to this shop. - failed-remove: An error occurred while removing tag {0} from this shop. - removed: Successfully removed tag {0} from this shop! - does-not-exist: This shop does not have tag {0}! - list-player-shop-title: 'You have {0} tags on shop #{1}:' - list-player-shops-title: 'You have {0} tags on {1} shops:' - list-player-shop-entry: 'Shop #{0} - {1} tags' - list-title: 'This shop has {0} tags:' - list-tag-title: 'This tag is applied to {0} shops:' - cleared: Successfully cleared all tags from this shop! - cleared-all: Successfully cleared all tags from all shops! - cleared-tag: Successfully cleared tag {0} from all your tagged shops! - no-tags-all: There are no tags on any shop! - no-tags: This shop has no tags! - no-tagged-shops-player: You have no tags on this shop! - no-tagged-shops: You have no tagged shops! - no-tagged-shops-tag: You have no shops with tag {0}! - - limited: commands: limit: Set a limit that restrict the purchase that player can make in From 9b7f0d7135578672988399a0a364d593990da3cf Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sun, 8 Mar 2026 17:29:33 -0400 Subject: [PATCH 07/29] Introduce metrics system modularization and add bStats/FastStats integration for improved tracking and research. --- .changelog/6.3.0.0.md | 30 +++ quickshop-bukkit/pom.xml | 10 + .../com/ghostchu/quickshop/QuickShop.java | 4 +- .../command/subcommand/SubCommand_Paste.java | 4 +- .../quickshop/util/metric/MetricManager.java | 88 ++++----- .../quickshop/util/metric/MetricPlatform.java | 118 ++++++++++++ .../quickshop/util/metric/StatCollector.java | 163 ++++++++++++++++ .../quickshop/util/metric/bstats/BStats.java | 90 +++++++++ .../util/metric/bstats/BStatsCollector.java | 115 +++++++++++ .../util/metric/faststats/FastStats.java | 86 +++++++++ .../FastStatsCollector.java} | 179 ++++++------------ 11 files changed, 709 insertions(+), 178 deletions(-) create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java rename quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/{collect/BuiltInCollects.java => faststats/FastStatsCollector.java} (51%) diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index 29ce9c2395..508f49f14a 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -67,6 +67,35 @@ updater-source: "modrinth" This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) without core rewrites. +### New: Shop Tagging System + +A new player-driven tagging system has been introduced, allowing players to organize and track shops using custom tags. + +**Highlights** +- Players can add custom tags to shops they are looking at. +- Tags support letters, underscores (`_`), and dashes (`-`). +- Tags are normalized and limited in length to ensure consistency and performance. +- Reserved system tags power built-in features such as: + - `@fav` — Favorites + - `@watch` — Watchlist + - `@avoid` — Avoided shops +- Tags are stored per-player per-shop, allowing each player to maintain their own organization system. + +**Commands** +- `/qs tag ` — Add a tag to the shop you are looking at +- `/qs tag remove ` — Remove a tag from the current shop +- `/qs tag clear` — Remove all of your tags from the current shop +- `/qs tag tags` — List all tags you have created +- `/qs tag tagged ` — List shops you have tagged with a specific tag +- `/qs tag purge ` — Remove a tag from all shops +- `/qs favorite` - Add the current shop to your favorites +- `/qs watch` - Add the current shop to your watchlist +- `/qs avoid` - Add the current shop to your avoid list + +**Performance** +- Uses a dual-index caching system (`shop → tags` and `player → tag → shops`) for fast lookups. +- Tag filtering and listing operations are now **O(1)** instead of scanning all shops. + ## Minor Changes - Replaced startup parameter "playerDBFindUUIDTask" with config option "uuid-lookup.allow-playerdb-uuid" - Replaced startup parameter "playerDBFindNameTask" with config option "uuid-lookup.allow-playerdb-name" @@ -95,6 +124,7 @@ This lays the groundwork for future providers (CurseForge, Hangar, GitHub, etc.) - Bumped config version to 1037 - started including tax amount in shop failure message "you-cant-afford-to-buy" - Update configuration file comments and standardize formatting(thanks to YuanYuanOwO) +- Split metrics into separate files and added FastStats metric tracking. ## Fixes - Fix issue where disabling tax for unlimited shops wasn't properly working. diff --git a/quickshop-bukkit/pom.xml b/quickshop-bukkit/pom.xml index 98f4177227..4b2b09f5be 100644 --- a/quickshop-bukkit/pom.xml +++ b/quickshop-bukkit/pom.xml @@ -73,6 +73,11 @@ enginehub-repo https://maven.enginehub.org/repo/
+ + faststats + TheNextLvl + https://repo.thenextlvl.net/releases +
@@ -324,6 +329,11 @@ compile 0.4.7 + + dev.faststats.metrics + bukkit + ${depend.faststats} + org.apache.commons commons-compress diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java index 200c521c1d..ec79d03a3d 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java @@ -99,7 +99,6 @@ import com.ghostchu.quickshop.util.matcher.item.BukkitItemMatcherImpl; import com.ghostchu.quickshop.util.matcher.item.ModernCustomMatcher; import com.ghostchu.quickshop.util.matcher.item.QuickShopItemMatcherImpl; -import com.ghostchu.quickshop.util.matcher.item.TNEItemMatcherImpl; import com.ghostchu.quickshop.util.metric.MetricManager; import com.ghostchu.quickshop.util.paste.PasteManager; import com.ghostchu.quickshop.util.performance.PerfMonitor; @@ -421,7 +420,8 @@ public final void onLoad() { this.registry = new SimpleRegistryManager(); this.registry.registerRegistry(BuiltInRegistry.ITEM_EXPRESSION.getName(), new SimpleItemExpressionRegistry(this)); logger.info("Setting up metrics manager..."); - this.metricManager = new MetricManager(this); + this.metricManager = new MetricManager(); + this.metricManager.initPlatforms(); logger.info("Loading player name and unique id mapping..."); this.playerFinder = new FastPlayerFinder(this); loadTextManager(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java index 99b1214f78..df84c37966 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Paste.java @@ -72,13 +72,13 @@ private boolean pasteToLocalFile(@NotNull final CommandSender sender) { try { final boolean createResult = file.createNewFile(); Log.debug("Create paste file: " + file.getCanonicalPath() + " " + createResult); - try(FileWriter fwriter = new FileWriter(file)) { + try(final FileWriter fwriter = new FileWriter(file)) { fwriter.write(string); fwriter.flush(); } plugin.text().of(sender, "paste-created-local", file.getAbsolutePath()).send(); return true; - } catch(IOException e) { + } catch(final IOException e) { if(plugin.getSentryErrorReporter() != null) { plugin.getSentryErrorReporter().ignoreThrow(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java index c3bf1d42b3..99003d877d 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricManager.java @@ -1,61 +1,49 @@ package com.ghostchu.quickshop.util.metric; -import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.util.logger.Log; -import com.ghostchu.quickshop.util.metric.collect.BuiltInCollects; -import org.bstats.bukkit.Metrics; -import org.bstats.charts.CustomChart; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Method; -import java.util.Locale; +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.bstats.BStats; +import com.ghostchu.quickshop.util.metric.faststats.FastStats; + +/** + * MetricManager + * + * @author creatorfromhell + * @since 6.3.0.0 + */ public class MetricManager { - private final QuickShop plugin; - private final Metrics metrics; - - public MetricManager(final QuickShop plugin) { + private final BStats bStats; + private final FastStats fastStats; + private final StatCollector collector; - this.plugin = plugin; - this.metrics = new Metrics(plugin.getJavaPlugin(), 14281); - initCollects(); - } - - public void registerChart(final MetricDataType dataType, final String moduleName, final String reason, final CustomChart chart) { - - if(chart == null) { - return; // ignore - } - plugin.getPrivacyController().privacyReview(dataType, moduleName.replace(" ", "_").replace("-", "_").toUpperCase(Locale.ROOT), reason, ()->this.metrics.addCustomChart(chart), ()->Log.debug("Blocked chart register: failed privacy reviewing.")); - } + public MetricManager() { - public void initCollects() { + this.collector = new StatCollector(QuickShop.getInstance()); - registerMetricCollector(new BuiltInCollects(plugin)); + this.bStats = new BStats(collector); + this.fastStats = new FastStats(collector); } - public void registerMetricCollector(@NotNull final Object object) { - - for(final Method method : object.getClass().getDeclaredMethods()) { - final MetricCollectEntry collectEntry = method.getAnnotation(MetricCollectEntry.class); - if(collectEntry == null) { - continue; - } - if(method.getReturnType() != CustomChart.class) { - plugin.logger().warn("Failed loading MetricCollectEntry [{}]: Illegal test returns", method.getName()); - continue; - } - try { - final Object result = method.invoke(object, (Object[])null); - if(result != null) { - registerChart(collectEntry.dataType(), collectEntry.moduleName(), collectEntry.description(), - (CustomChart)result); - Log.debug("Registered metrics collector: " + collectEntry.moduleName()); - } - } catch(Throwable th) { - plugin.logger().warn("Failed to register metrics chart", th); - } - } + public void initPlatforms() { + bStats.register(); + fastStats.register(); } -} +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java new file mode 100644 index 0000000000..5ea2081939 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/MetricPlatform.java @@ -0,0 +1,118 @@ +package com.ghostchu.quickshop.util.metric; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.logger.Log; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.Locale; + +/** + * MetricPlatform + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface MetricPlatform { + + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + String projectID(); + + /** + * Registers this MetricPlatform implementation for tracking or activation within the system. + * This method should be invoked to enable the platform's functionality and integrate it + * with the underlying metrics or analytics framework. + */ + void register(); + + /** + * Retrieves the validated class associated with this MetricPlatform. + * The validated class represents the type used for compliance or correctness + * validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + Class validatedClass(); + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform + * to the expectations of the underlying system or validation logic. + */ + void addMetric(final T metric); + + /** + * Registers a metric to be tracked or processed by the system, subject to a privacy review + * based on the provided data type, module name, and reason. + * + * @param dataType the data type of the metric, specifying its classification (e.g., STATISTIC, RESEARCH, DIAGNOSTIC) + * @param moduleName the name of the module associated with the metric, which will be reformatted internally + * for compatibility and privacy review purposes + * @param reason the reason or justification for collecting this metric, used for transparency and approval + * @param metric the metric of type {@code T} to be registered; if null, registration will be ignored + */ + default void registerMetric(final MetricDataType dataType, final String moduleName, final String reason, final T metric) { + + //Don't register if the metric is null + if(metric == null) { + return; + } + QuickShop.getInstance().getPrivacyController().privacyReview(dataType, moduleName.replace(" ", "_").replace("-", "_").toUpperCase(Locale.ROOT), reason, ()->addMetric(metric), ()->Log.debug("Blocked metric registration: failed privacy reviewing.")); + } + + /** + * Registers a metric collector by analyzing the provided object's methods for annotated + * metric collection entries. For each method annotated with {@link MetricCollectEntry}, the + * method's return value is used as the metric to register, provided it matches the expected + * validated class. Logs warnings for invalid configurations or failed registrations. + * + * @param object the target object to scan for methods annotated with {@link MetricCollectEntry}. + * Must not be {@code null}. + */ + default void registerMetricCollector(@NotNull final Object object) { + + for(final Method method : object.getClass().getDeclaredMethods()) { + final MetricCollectEntry collectEntry = method.getAnnotation(MetricCollectEntry.class); + if(collectEntry == null) { + continue; + } + if(!method.getReturnType().isAssignableFrom(validatedClass())) { + QuickShop.getInstance().logger().warn("Failed loading MetricCollectEntry [{}]: Illegal test returns", method.getName()); + continue; + } + try { + final Object result = method.invoke(object, (Object[])null); + if(result != null) { + registerMetric(collectEntry.dataType(), collectEntry.moduleName(), collectEntry.description(), validatedClass().cast(result)); + Log.debug("Registered metrics collector: " + collectEntry.moduleName()); + } + } catch(final Throwable th) { + QuickShop.getInstance().logger().warn("Failed to register metrics chart", th); + } + } + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java new file mode 100644 index 0000000000..eb2e8e6f72 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/StatCollector.java @@ -0,0 +1,163 @@ +package com.ghostchu.quickshop.util.metric; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; + +/** + * StatCollector + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class StatCollector { + + private final QuickShop plugin; + + public StatCollector(final QuickShop plugin) { + this.plugin = plugin; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") + public Map researchAddonsCompats() { + final String myName = plugin.getJavaPlugin().getDescription().getName(); + final Map data = new HashMap<>(); + for(final Plugin discoverPlugin : Bukkit.getPluginManager().getPlugins()) { + final PluginDescriptionFile descriptionFile = discoverPlugin.getDescription(); + if(descriptionFile.getDepend().contains(myName) || descriptionFile.getSoftDepend().contains(myName)) { + data.put(descriptionFile.getName(), 1); + } + } + if(data.isEmpty()) { + data.put("None", 1); + } + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") + public boolean researchItemStackingShop() { + return plugin.isAllowStack(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") + public Map researchProtectionListenerBlacklist() { + final Map data = new HashMap<>(); + plugin.getConfig().getStringList("shop.protection-checking-listener-blacklist").forEach(s->data.put(s, 1)); + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") + public Map researchCommandAlias() { + final Map data = new HashMap<>(); + plugin.getConfig().getStringList("custom-commands").forEach(s->data.put(s, 1)); + + return data; + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") + public String statisticDatabaseType() { + return plugin.getDatabaseDriverType().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") + public String statisticEconomyType() { + return plugin.getEconomyManager().provider().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") + public String statisticItemMatcher() { + return plugin.getItemMatcher().getName(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public String statisticDisplayImpl() { + return AbstractDisplayItem.getNowUsing().name(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") + public int statisticAllShops() { + return plugin.getShopManager().getAllShops().size(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public int statisticAllTags() { + return plugin.tagManager().totalTags(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public String statisticBackgroundDebugLogger() { + return plugin.getConfig().getBoolean("debug.disable-debuglogger") ? "Disabled" : "Enabled"; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") + public String researchProtocolLibVersion() { + final Plugin protocolLib = Bukkit.getPluginManager().getPlugin("ProtocolLib"); + if(protocolLib == null) { + return "Not Installed"; + } + return protocolLib.getDescription().getVersion(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") + public String researchPacketEventsVersion() { + final Plugin packetevents = Bukkit.getPluginManager().getPlugin("packetevents"); + if(packetevents == null) { + return "Not Installed"; + } + return packetevents.getDescription().getVersion(); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") + public Map> statisticServerSoftwareBuildVersion() { + final Map> map = new HashMap<>(); + final Map entry = new HashMap<>(); + entry.put(Bukkit.getServer().getVersion(), 1); + map.put(Bukkit.getServer().getName(), entry); + return map; + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") + public String statisticPublisher() { + return plugin.getBuildInfo().getGitInfo().getCommitUsername() + "@" + plugin.getFork(); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") + public String researchGeyser() { + + final StringJoiner joiner = new StringJoiner("+"); + joiner.setEmptyValue("Not detected"); + final Plugin geyser = Bukkit.getPluginManager().getPlugin("Geyser-Spigot"); + if(geyser != null) { + joiner.add("Geyser-Spigot"); + } + final Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); + if(floodgate != null) { + joiner.add("Floodgate"); + } + return joiner.toString(); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java new file mode 100644 index 0000000000..9932081bfe --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStats.java @@ -0,0 +1,90 @@ +package com.ghostchu.quickshop.util.metric.bstats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.MetricPlatform; +import com.ghostchu.quickshop.util.metric.StatCollector; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.CustomChart; + +/** + * BStats + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class BStats implements MetricPlatform { + + private final Metrics metrics; + private final StatCollector collector; + + public BStats(final StatCollector collector) { + + this.collector = collector; + this.metrics = new Metrics(QuickShop.getInstance().getJavaPlugin(), Integer.parseInt(projectID())); + } + + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + @Override + public String projectID() { + + return "14281"; + } + + /** + * Registers this MetricPlatform implementation for tracking or activation within the system. This + * method should be invoked to enable the platform's functionality and integrate it with the + * underlying metrics or analytics framework. + */ + @Override + public void register() { + + registerMetricCollector(new BStatsCollector(collector)); + } + + /** + * Retrieves the validated class associated with this MetricPlatform. The validated class + * represents the type used for compliance or correctness validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + @Override + public Class validatedClass() { + + return CustomChart.class; + } + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform to + * the expectations of the underlying system or validation logic. + */ + @Override + public void addMetric(final CustomChart metric) { + + metrics.addCustomChart(metric); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java new file mode 100644 index 0000000000..da8fb1e094 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/bstats/BStatsCollector.java @@ -0,0 +1,115 @@ +package com.ghostchu.quickshop.util.metric.bstats; + +import com.ghostchu.quickshop.util.metric.MetricCollectEntry; +import com.ghostchu.quickshop.util.metric.MetricDataType; +import com.ghostchu.quickshop.util.metric.StatCollector; +import org.bstats.charts.AdvancedPie; +import org.bstats.charts.CustomChart; +import org.bstats.charts.DrilldownPie; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; + +public class BStatsCollector {//Statistic + + private final StatCollector collector; + + public BStatsCollector(final StatCollector collector) { + this.collector = collector; + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") + public CustomChart researchAddonsCompacts() { + + return new AdvancedPie("research_addons_or_compacts_discovered", collector::researchAddonsCompats); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") + public CustomChart researchItemStackingShop() { + + return new SimplePie("research_item_stacking_shop", ()->String.valueOf(collector.researchItemStackingShop())); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") + public CustomChart researchProtectionListenerBlacklist() { + + return new AdvancedPie("research_protection_checker_blacklist", collector::researchProtectionListenerBlacklist); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") + public CustomChart researchCommandAlias() { + + return new AdvancedPie("research_command_alias", collector::researchCommandAlias); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") + public CustomChart statisticDatabaseTypes() { + + return new SimplePie("statistic_database_types", collector::statisticDatabaseType); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") + public CustomChart statisticEconomyTypes() { + + return new SimplePie("statistic_economy_types", collector::statisticEconomyType); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") + public CustomChart statisticItemMatcher() { + + return new SimplePie("statistic_item_matcher", collector::statisticItemMatcher); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public CustomChart statisticDisplayImpl() { + + return new SimplePie("statistic_displayimpl", collector::statisticDisplayImpl); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") + public CustomChart statisticAllShops() { + + return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", collector::statisticAllShops); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public CustomChart statisticAllTags() { + + return new SingleLineChart("statistic_all_tags_hosting_across_all_servers", collector::statisticAllTags); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") + public CustomChart statisticBackgroundDebugLogger() { + + return new SimplePie("statistic_background_debug_logger", collector::statisticBackgroundDebugLogger); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") + public CustomChart researchProtocolLibVersion() { + + return new SimplePie("research_protocollib_version", collector::researchProtocolLibVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") + public CustomChart researchPacketEventsVersion() { + + return new SimplePie("research_packetevents_version", collector::researchPacketEventsVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") + public CustomChart statisticServerSoftwareBuildVersion() { + + return new DrilldownPie("statistic_server_software_build_version", collector::statisticServerSoftwareBuildVersion); + } + + @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") + public CustomChart statisticPublisher() { + + return new SimplePie("statistic_publisher", collector::statisticPublisher); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") + public CustomChart researchGeyser() { + + return new SimplePie("research_geyser", collector::researchGeyser); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java new file mode 100644 index 0000000000..0f25bf6348 --- /dev/null +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStats.java @@ -0,0 +1,86 @@ +package com.ghostchu.quickshop.util.metric.faststats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import com.ghostchu.quickshop.QuickShop; +import com.ghostchu.quickshop.util.metric.MetricPlatform; +import com.ghostchu.quickshop.util.metric.StatCollector; +import dev.faststats.bukkit.BukkitMetrics; +import dev.faststats.core.ErrorTracker; +import dev.faststats.core.data.Metric; + +/** + * FastStats + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class FastStats implements MetricPlatform { + + public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); + + private BukkitMetrics.Factory metrics = BukkitMetrics.factory().token(projectID()); + private final StatCollector collector; + + public FastStats(final StatCollector collector) { + + this.collector = collector; + } + /** + * Retrieves the unique identifier for the project associated with this MetricPlatform. + * + * @return a {@code String} representing the unique project identifier. + */ + @Override + public String projectID() { + + return "7883edc919d2a4fa1bec3d78e190d9a5"; + } + + @Override + public void register() { + + registerMetricCollector(new FastStatsCollector(collector)); + metrics.create(QuickShop.getInstance().getJavaPlugin()); + } + + /** + * Retrieves the validated class associated with this MetricPlatform. The validated class + * represents the type used for compliance or correctness validation within the system's context. + * + * @return an instance of type {@code T} representing the validated class. + */ + @Override + public Class validatedClass() { + + return Metric.class; + } + + /** + * Adds a metric to the system for tracking or processing. The metric must be of the validated + * type associated with this MetricPlatform. + * + * @param metric the metric of type {@code T} to be added; must not be null and should conform to + * the expectations of the underlying system or validation logic. + */ + @Override + public void addMetric(final Metric metric) { + metrics = metrics.addMetric(metric); + } +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java similarity index 51% rename from quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java rename to quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java index 5933d830dd..d1d5a7e0b2 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/collect/BuiltInCollects.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/metric/faststats/FastStatsCollector.java @@ -1,209 +1,140 @@ -package com.ghostchu.quickshop.util.metric.collect; +package com.ghostchu.quickshop.util.metric.faststats; + +/* + * QuickShop-Hikari + * Copyright (C) 2026 Daniel "creatorfromhell" Vidmar + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ -import com.ghostchu.quickshop.QuickShop; -import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; import com.ghostchu.quickshop.util.metric.MetricCollectEntry; import com.ghostchu.quickshop.util.metric.MetricDataType; +import com.ghostchu.quickshop.util.metric.StatCollector; import org.bstats.charts.AdvancedPie; import org.bstats.charts.CustomChart; import org.bstats.charts.DrilldownPie; import org.bstats.charts.SimplePie; import org.bstats.charts.SingleLineChart; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import java.util.HashMap; -import java.util.Map; -import java.util.StringJoiner; +/** + * FastStatsCollector + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class FastStatsCollector { -public class BuiltInCollects {//Statistic - private final QuickShop plugin; + private final StatCollector collector; - public BuiltInCollects(final QuickShop plugin) { - - this.plugin = plugin; + //TODO: Fully implement this class. + public FastStatsCollector(final StatCollector collector) { + this.collector = collector; } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Addons or Compacts Discovered", description = "QuickShop collects the QuickShop's addons/compacts (including 3rd-party) list that installed on your server to discover new addons/compacts so we can contact authors when we have major API changes, or use for improve exists official addons/compacts who have most of users using.") public CustomChart researchAddonsCompacts() { - return new AdvancedPie("research_addons_or_compacts_discovered", ()->{ - final String myName = plugin.getJavaPlugin().getDescription().getName(); - final Map data = new HashMap<>(); - for(final Plugin discoverPlugin : Bukkit.getPluginManager().getPlugins()) { - final PluginDescriptionFile descriptionFile = discoverPlugin.getDescription(); - if(descriptionFile.getDepend().contains(myName) || descriptionFile.getSoftDepend().contains(myName)) { - data.put(descriptionFile.getName(), 1); - } - } - if(data.isEmpty()) { - data.put("None", 1); - } - return data; - }); + return new AdvancedPie("research_addons_or_compacts_discovered", collector::researchAddonsCompats); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Item Stacking Shop", description = "We collect this for determine if we should push Item Stacking Shop to a feature that default enabled.") public CustomChart researchItemStackingShop() { - return new SimplePie("research_item_stacking_shop", ()->String.valueOf(plugin.isAllowStack())); + return new SimplePie("research_item_stacking_shop", ()->String.valueOf(collector.researchItemStackingShop())); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Protection Listener Blacklist", description = "We collect this for determine if we should add common listener blacklist entry to default configuration.") public CustomChart researchProtectionListenerBlacklist() { - return new AdvancedPie("research_protection_checker_blacklist", ()->{ - final Map data = new HashMap<>(); - plugin.getConfig().getStringList("shop.protection-checking-listener-blacklist").forEach(s->data.put(s, 1)); - return data; - }); + return new AdvancedPie("research_protection_checker_blacklist", collector::researchProtectionListenerBlacklist); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Command Alias", description = "We collect this for determine if we should add/remove alias to default configuration.") public CustomChart researchCommandAlias() { - return new AdvancedPie("research_command_alias", ()->{ - final Map data = new HashMap<>(); - plugin.getConfig().getStringList("custom-commands").forEach(s->data.put(s, 1)); - return data; - }); + return new AdvancedPie("research_command_alias", collector::researchCommandAlias); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Types", description = "We collect this so we can know the percent of different database types users.") public CustomChart statisticDatabaseTypes() { - return new SimplePie("statistic_database_types", ()->plugin.getDatabaseDriverType().name()); - } -// -// private String databaseProduct = null; -// @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Product", description = "We collect this so we can know the database vendor that users using, and provide dedicated driver if possible.") -// public CustomChart statisticDatabaseProject() { -// return new SimplePie("statistic_database_product", () -> { -// if(databaseProduct != null){ -// return databaseProduct; -// } -// CompletableFuture future = CompletableFuture.supplyAsync(()->{ -// try (Connection connection = plugin.getSqlManager().getConnection()) { -// DatabaseMetaData metaData = connection.getMetaData(); -// return metaData.getDatabaseProductName(); -// } catch (Throwable throwable) { -// Log.debug("Populate statistic database version failed: " + throwable.getClass().getName() + ": " + throwable.getMessage()); -// return "Error"; -// } -// }); -// databaseProduct = future.get(3, TimeUnit.SECONDS); -// return databaseProduct; -// }); -// } -// -// @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Database Version", description = "We collect this so we can know the database versions that users using, and provide/update dedicated driver or drop the support for too old database versions.") -// public CustomChart statisticDatabaseVersion() { -// return new SimplePie("statistic_database_version", () -> { -// try (Connection connection = plugin.getSqlManager().getConnection()) { -// DatabaseMetaData metaData = connection.getMetaData(); -// return metaData.getDatabaseProductName() + "@" + metaData.getDatabaseProductVersion(); -// } catch (Throwable throwable) { -// Log.debug("Populate statistic database version failed: " + throwable.getClass().getName() + ": " + throwable.getMessage()); -// return "Error"; -// } -// }); -// } + return new SimplePie("statistic_database_types", collector::statisticDatabaseType); + } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Economy Types", description = "We collect this so we can know the percent of different economy types users.") public CustomChart statisticEconomyTypes() { - return new SimplePie("statistic_economy_types", ()->plugin.getEconomyManager().provider().name()); + return new SimplePie("statistic_economy_types", collector::statisticEconomyType); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - ItemMatcher", description = "We collect this so we can know the item matcher that users using, and improve it.") public CustomChart statisticItemMatcher() { - return new SimplePie("statistic_item_matcher", ()->plugin.getItemMatcher().getName()); + return new SimplePie("statistic_item_matcher", collector::statisticItemMatcher); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - DisplayImpl", description = "We collect this so we can know the which one item display impl most using, and improve it.") public CustomChart statisticDisplayImpl() { - return new SimplePie("statistic_displayimpl", ()->AbstractDisplayItem.getNowUsing().name()); + return new SimplePie("statistic_displayimpl", collector::statisticDisplayImpl); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All shops hosting across all servers", description = "How many shops we can power across all servers? This research will used for performance tweak for components like shop managing/looking up/caching size etc.") public CustomChart statisticAllShops() { - return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", ()->plugin.getShopManager().getAllShops().size()); + return new SingleLineChart("statistic_all_shops_hosting_across_all_servers", collector::statisticAllShops); + } + + @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Statistic - All tags hosted across all servers", description = "How many tags we power across all servers? This research will used for performance tweak for components like tag managing/looking up/caching size etc.") + public CustomChart statisticAllTags() { + + return new SingleLineChart("statistic_all_tags_hosting_across_all_servers", collector::statisticAllTags); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Background Debug Logger", description = "We collect this so we can know the which one item display impl most using, and improve it.") public CustomChart statisticBackgroundDebugLogger() { - return new SimplePie("statistic_background_debug_logger", ()->{ - if(plugin.getConfig().getBoolean("debug.disable-debuglogger")) { - return "Disabled"; - } else { - return "Enabled"; - } - }); + return new SimplePie("statistic_background_debug_logger", collector::statisticBackgroundDebugLogger); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - ProtocolLib Version", description = "We collect this so we can know the which one ProtocolLib is popular. ProtocolLib sometimes releases destructive updates, so we collect this metric to know the distribution of ProtocolLib versions among users and remove unused ProtocolLib workaround code to improve code maintainability and program performance.") public CustomChart researchProtocolLibVersion() { - return new SimplePie("research_protocollib_version", ()->{ - final Plugin protocolLib = Bukkit.getPluginManager().getPlugin("ProtocolLib"); - if(protocolLib == null) { - return "Not Installed"; - } - return protocolLib.getDescription().getVersion(); - }); + return new SimplePie("research_protocollib_version", collector::researchProtocolLibVersion); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - PacketEvents Version", description = "We collect this so we can know the which one PacketEvents is popular. PacketEvents sometimes releases destructive updates, so we collect this metric to know the distribution of PacketEvents versions among users and remove unused PacketEvents workaround code to improve code maintainability and program performance.") public CustomChart researchPacketEventsVersion() { - return new SimplePie("research_packetevents_version", ()->{ - final Plugin packetevents = Bukkit.getPluginManager().getPlugin("packetevents"); - if(packetevents == null) { - - return "Not Installed"; - } - return packetevents.getDescription().getVersion(); - }); + return new SimplePie("research_packetevents_version", collector::researchPacketEventsVersion); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Server Software Build Version", description = "Spigot and Paper always release updates during their version support cycles. Counting the server-side software versions used by users lets us know which builds are popular. And it allows us to be more aggressive with newly added APIs, This can improve code maintainability, stability and program performance.") public CustomChart statisticServerSoftwareBuildVersion() { - return new DrilldownPie("statistic_server_software_build_version", ()->{ - final Map> map = new HashMap<>(); - final Map entry = new HashMap<>(); - entry.put(Bukkit.getServer().getVersion(), 1); - map.put(Bukkit.getServer().getName(), entry); - return map; - }); + return new DrilldownPie("statistic_server_software_build_version", collector::statisticServerSoftwareBuildVersion); } @MetricCollectEntry(dataType = MetricDataType.STATISTIC, moduleName = "Statistic - Publisher", description = "We count the name of the publisher (in BuildInfo) so that we know if someone else is recompiling our plugin without changing the fork name. if you are a QuickShop-Hikari fork developer, please change the return value of your getFork() to something else in order to separate it from the stats. This value is usually fixed to Ghost-chu@Hikari.") public CustomChart statisticPublisher() { - return new SimplePie("statistic_publisher", ()->plugin.getBuildInfo().getGitInfo().getCommitUsername() + "@" + plugin.getFork()); + return new SimplePie("statistic_publisher", collector::statisticPublisher); } @MetricCollectEntry(dataType = MetricDataType.RESEARCH, moduleName = "Research - Geyser", description = "We've released the Suspension Closure expansion for Geyser, but we're ultimately undecided about a Geyser-specific update. The data collected from this study allows us to analyze the QuickShop-Hikari user base to check if Geyser or Floodgate is installed, and with the percentage of users who have the statistics, we will decide whether to add support for Geyser GUIs and the like. We also welcome your feedback on our Discord server.") public CustomChart researchGeyser() { - return new SimplePie("research_geyser", ()->{ - final StringJoiner joiner = new StringJoiner("+"); - joiner.setEmptyValue("Not detected"); - final Plugin geyser = Bukkit.getPluginManager().getPlugin("Geyser-Spigot"); - if(geyser != null) { - joiner.add("Geyser-Spigot"); - } - final Plugin floodgate = Bukkit.getPluginManager().getPlugin("floodgate"); - if(floodgate != null) { - joiner.add("Floodgate"); - } - return joiner.toString(); - }); - } -} + return new SimplePie("research_geyser", collector::researchGeyser); + } +} \ No newline at end of file From 06a9e9599aa486be17794a2039f87e6575a94f07 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Mon, 16 Mar 2026 23:12:15 -0400 Subject: [PATCH 08/29] Introduce shop states, add shop state column, add active and frozen state. --- .changelog/6.3.0.0.md | 8 ++ .../migratecomponent/ShopMigrate.java | 1 + .../api/database/bean/DataRecord.java | 2 + .../transaction/EconomyTransaction.java | 19 --- .../event/settings/type/ShopStateEvent.java | 128 ++++++++++++++++++ .../com/ghostchu/quickshop/api/shop/Shop.java | 34 +++-- .../quickshop/api/shop/ShopManager.java | 46 +++++++ .../ghostchu/quickshop/api/shop/ShopType.java | 51 ------- .../quickshop/api/shop/state/ShopState.java | 65 +++++++++ .../api/shop/state/impl/ActiveState.java | 49 +++++++ .../api/shop/state/impl/FrozenState.java | 79 +++++++++++ .../quickshop/api/shop/type/FrozenType.java | 1 + .../command/subcommand/SubCommand_Freeze.java | 8 +- .../silent/SubCommand_SilentFreeze.java | 8 +- .../quickshop/database/DataTables.java | 1 + .../database/SimpleDatabaseHelperV2.java | 24 +++- .../database/bean/SimpleDataRecord.java | 12 +- .../transaction/QSEconomyTransaction.java | 22 --- .../quickshop/shop/ContainerShop.java | 69 +++++++++- .../ghostchu/quickshop/shop/ShopLoader.java | 4 + .../shop/SimpleShopLayoutProvider.java | 6 +- .../quickshop/shop/SimpleShopManager.java | 41 +++++- 22 files changed, 558 insertions(+), 120 deletions(-) create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopStateEvent.java delete mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopType.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/ShopState.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/ActiveState.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/FrozenState.java diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index 508f49f14a..4cee359a02 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -126,6 +126,14 @@ A new player-driven tagging system has been introduced, allowing players to orga - Update configuration file comments and standardize formatting(thanks to YuanYuanOwO) - Split metrics into separate files and added FastStats metric tracking. +## Internals +- Deprecated FrozenType in favor of a new ShopState system. +- Added new ShopState system to handle shop state changes. + +## Deprecation Removals +- Removed ShopType enum and methods +- Removed deprecated tax methods. + ## Fixes - Fix issue where disabling tax for unlimited shops wasn't properly working. - Fix issue where FAWE on 2.11.2 and lower wasn't working correctly. diff --git a/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java b/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java index b45a4a73c2..41d7d97bf5 100644 --- a/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java +++ b/addon/reremake-migrator/src/main/java/com/ghostchu/quickshop/addon/reremakemigrator/migratecomponent/ShopMigrate.java @@ -80,6 +80,7 @@ public boolean migrate() { QUserImpl.createSync(getHikari().getPlayerFinder(), reremakeShop.getOwner()), reremakeShop.isUnlimited(), QuickShop.getInstance().getShopManager().shopTypeOrDefault(reremakeShop.getShopType().toID()), + QuickShop.getInstance().getShopManager().shopStateOrDefault("active"), getReremakeShopExtra(reremakeShop), reremakeShop.getCurrency(), reremakeShop.isDisableDisplay(), diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/bean/DataRecord.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/bean/DataRecord.java index 927776e6bf..2783ff6071 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/bean/DataRecord.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/database/bean/DataRecord.java @@ -45,6 +45,8 @@ public interface DataRecord { int getType(); + String getState(); + boolean isHologram(); boolean isUnlimited(); diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/transaction/EconomyTransaction.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/transaction/EconomyTransaction.java index 33963d2a48..5c2ffc470d 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/transaction/EconomyTransaction.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/transaction/EconomyTransaction.java @@ -127,25 +127,6 @@ public interface EconomyTransaction { */ void taxer(final @Nullable QUser user); - /** - * Retrieves the tax amount associated with this transaction. - * - * @return a BigDecimal value representing the tax amount of the transaction - * @deprecated no longer apart of the enhanced tax system - */ - @NotNull - @Deprecated(since = "6.2.0.11", forRemoval = true) - BigDecimal tax(); - - /** - * Sets the tax for the transaction. - * - * @param tax the amount of tax to be set for the transaction - * @deprecated no longer apart of the enhanced tax system - */ - @Deprecated(since = "6.2.0.11", forRemoval = true) - void tax(final @NotNull BigDecimal tax); - /** * Calculates and retrieves the tax amount associated with this transaction * based on the defined tax rules or system configuration. diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopStateEvent.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopStateEvent.java new file mode 100644 index 0000000000..6af1046597 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopStateEvent.java @@ -0,0 +1,128 @@ +package com.ghostchu.quickshop.api.event.settings.type; + +/* + * QuickShop-Hikari + * Copyright (C) 2025 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.event.Phase; +import com.ghostchu.quickshop.api.event.settings.ShopSettingEvent; +import com.ghostchu.quickshop.api.shop.IShopType; +import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import org.jetbrains.annotations.NotNull; + +/** + * ShopStateEvent + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ShopStateEvent extends ShopSettingEvent { + + public ShopStateEvent(final @NotNull Phase phase, final @NotNull Shop shop, + final @NotNull ShopState old) { + + super(phase, shop, old); + } + + public ShopStateEvent(final @NotNull Phase phase, final @NotNull Shop shop, + final @NotNull ShopState old, final @NotNull ShopState updated) { + + super(phase, shop, old, updated); + } + + public static ShopStateEvent PRE(final @NotNull Shop shop, + final ShopState old) { + + return new ShopStateEvent(Phase.PRE, shop, old); + } + + public static ShopStateEvent PRE(final @NotNull Shop shop, + final ShopState old, final ShopState updated) { + + return new ShopStateEvent(Phase.PRE, shop, old, updated); + } + + public static ShopStateEvent MAIN(final @NotNull Shop shop, + final ShopState old) { + + return new ShopStateEvent(Phase.MAIN, shop, old); + } + + public static ShopStateEvent MAIN(final @NotNull Shop shop, + final ShopState old, final ShopState updated) { + + return new ShopStateEvent(Phase.MAIN, shop, old, updated); + } + + public static ShopStateEvent POST(final @NotNull Shop shop, + final ShopState old) { + + return new ShopStateEvent(Phase.POST, shop, old); + } + + public static ShopStateEvent POST(final @NotNull Shop shop, + final ShopState old, final ShopState updated) { + + return new ShopStateEvent(Phase.POST, shop, old, updated); + } + + public static ShopStateEvent RETRIEVE(final @NotNull Shop shop, + final ShopState old) { + + return new ShopStateEvent(Phase.RETRIEVE, shop, old); + } + + public static ShopStateEvent RETRIEVE(final @NotNull Shop shop, + final ShopState old, final ShopState updated) { + + return new ShopStateEvent(Phase.RETRIEVE, shop, old, updated); + } + + /** + * Creates a new instance of PhasedEvent with the specified newPhase. + * + * @param newPhase The new Phase for the cloned PhasedEvent + * + * @return A new instance of PhasedEvent with the specified newPhase + */ + @Override + public ShopStateEvent clone(final Phase newPhase) { + + if(this.updated != null) { + + return new ShopStateEvent(newPhase, this.shop, this.old, this.updated); + } + return new ShopStateEvent(newPhase, this.shop, this.old); + } + + /** + * Creates a clone of the ShopSettingEvent with the provided newPhase, old value, and updated + * value. + * + * @param newPhase The new phase for the cloned ShopSettingEvent + * @param old The old value for the cloned ShopSettingEvent + * @param updated The updated value for the cloned ShopSettingEvent + * + * @return A new instance of ShopSettingEvent with the specified newPhase, old, and updated values + */ + @Override + public ShopStateEvent clone(final Phase newPhase, final ShopState old, final ShopState updated) { + + return new ShopStateEvent(newPhase, this.shop, old, updated); + } +} \ No newline at end of file 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 445223e6bb..1d9334e665 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 @@ -8,6 +8,7 @@ import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; +import com.ghostchu.quickshop.api.shop.state.ShopState; import net.kyori.adventure.text.Component; import org.bukkit.Location; import org.bukkit.NamespacedKey; @@ -235,14 +236,26 @@ public interface Shop extends Locatable { int getShopStackingAmount(); /** - * Get shop type + * Retrieves the current state of the shop. * - * @return shop type - * @deprecated Use shopType() instead + * @return the current state of the shop as a ShopState object */ - @NotNull - @Deprecated(since = "6.2.0.11", forRemoval = true) - ShopType getShopType(); + ShopState shopState(); + + /** + * 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 + */ + void shopState(@NotNull ShopState state); + + /** + * 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. + */ + void shopState(@NotNull String shopStateIdentifier); /** * Retrieves the type of shop associated with this entity. @@ -265,15 +278,6 @@ public interface Shop extends Locatable { */ void shopType(@NotNull String shopTypeIdentifier); - /** - * Set new shop type for this shop - * - * @param paramShopType New {@link ShopType} - * @deprecated Use shopType(IShopType shopType) or shopType(String shopTypeIdentifier) instead - */ - @Deprecated(since = "6.2.0.11", forRemoval = true) - void setShopType(@NotNull ShopType paramShopType); - /** * Get sign texts on shop's sign. * 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 da4a06eca1..07a3adcf53 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.state.ShopState; import com.ghostchu.quickshop.api.shop.tax.TaxManager; import org.bukkit.Chunk; import org.bukkit.Location; @@ -125,6 +126,51 @@ default Optional shopType(final String identifier) { */ @NotNull IShopType shopTypeOrDefault(final String identifier); + /** + * Retrieves a map of shop states where the keys are shop identifiers and the values are the corresponding shop states. + * + * @return a non-null map containing shop identifiers as keys and their corresponding {@link ShopState} objects as values. + */ + @NotNull Map shopStates(); + + /** + * Adds a shop state to the collection by associating its identifier with the given ShopState instance. + * + * @param type the ShopState object to be added, which contains the identifier and related state information + */ + default void addShopState(final ShopState type) { + shopStates().put(type.identifier(), type); + } + + /** + * Removes the shop state associated with the given identifier. + * + * @param identifier the unique identifier of the shop state to be removed + */ + default void removeShopState(final String identifier) { + shopStates().remove(identifier); + } + + + /** + * Retrieves the shop state associated with the given identifier. + * + * @param identifier the unique identifier of the shop state to retrieve + * @return an {@code Optional} containing the {@code ShopState} if found, or an empty {@code Optional} if no match is found + */ + default Optional shopState(final String identifier) { + return Optional.ofNullable(shopStates().get(identifier)); + } + + /** + * Retrieves the ShopState associated with the specified identifier. + * If no ShopState is found for the identifier, a default ShopState is returned. + * + * @param identifier the unique identifier for the shop state to retrieve + * @return the ShopState associated with the identifier, or a default ShopState if not found + */ + @NotNull ShopState shopStateOrDefault(final String identifier); + /** * Handle the player buying * diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopType.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopType.java deleted file mode 100644 index 06d79e694e..0000000000 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/ShopType.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ghostchu.quickshop.api.shop; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * The shop trading type *SELLING* or *BUYING* - */ -@Deprecated(since = "6.2.0.11", forRemoval = true) -public enum ShopType { - SELLING(0), // Sell Mode - BUYING(1), // Buy Mode - FROZEN(2); //Locked so no mode - private final int id; - - ShopType(final int id) { - - this.id = id; - } - - public static @Nullable ShopType fromString(@NotNull final String string) { - - for(final ShopType type : ShopType.values()) { - if(type.name().equalsIgnoreCase(string)) { - return type; - } - } - return null; - } - - - public static @NotNull ShopType fromID(final int id) { - - for(final ShopType type : ShopType.values()) { - if(type.id == id) { - return type; - } - } - return SELLING; - } - - public static int toID(@NotNull final ShopType shopType) { - - return shopType.id; - } - - public int toID() { - - return id; - } -} diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/ShopState.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/ShopState.java new file mode 100644 index 0000000000..79ce62b428 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/ShopState.java @@ -0,0 +1,65 @@ +package com.ghostchu.quickshop.api.shop.state; + +/* + * 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 . + */ + +/** + * ShopState + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopState { + + /** + * Stable string id, e.g. "active", "frozen", "disabled". + */ + String identifier(); + + /** + * Translation key for displaying the state name. + */ + String translationKey(); + + default boolean overrideShopTypeText() { + return false; + } + + /** + * Whether players are allowed to trade with the shop in this state. + */ + default boolean isTradingAllowed() { + return true; + } + + /** + * Whether this state should be treated as temporarily blocked. + * Useful for UX such as showing a paused/frozen icon. + */ + default boolean isFrozen() { + return false; + } + + /** + * Translation key to explain why trading is unavailable. + * Null if trading is allowed. + */ + default String blockedReasonTranslationKey() { + return null; + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/ActiveState.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/ActiveState.java new file mode 100644 index 0000000000..0d608efa6e --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/ActiveState.java @@ -0,0 +1,49 @@ +package com.ghostchu.quickshop.api.shop.state.impl; + +/* + * 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.state.ShopState; + +/** + * ActiveState + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class ActiveState implements ShopState { + + /** + * Stable string id, e.g. "active", "frozen", "disabled". + */ + @Override + public String identifier() { + + return "active"; + } + + @Override + public String translationKey() { + return "shop.state.active"; + } + + @Override + public boolean overrideShopTypeText() { + return false; + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/FrozenState.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/FrozenState.java new file mode 100644 index 0000000000..590d102075 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/state/impl/FrozenState.java @@ -0,0 +1,79 @@ +package com.ghostchu.quickshop.api.shop.state.impl; + +/* + * 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.state.ShopState; + +/** + * FrozenState + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public class FrozenState implements ShopState { + + /** + * Stable string id, e.g. "active", "frozen", "disabled". + */ + @Override + public String identifier() { + + return "frozen"; + } + + /** + * Translation key for displaying the state name. + */ + @Override + public String translationKey() { + + return "signs.freeze"; + } + + @Override + public boolean overrideShopTypeText() { + return true; + } + + /** + * Whether players are allowed to trade with the shop in this state. + */ + @Override + public boolean isTradingAllowed() { + return false; + } + + /** + * Whether this state should be treated as temporarily blocked. + * Useful for UX such as showing a paused/frozen icon. + */ + @Override + public boolean isFrozen() { + return true; + } + + /** + * Translation key to explain why trading is unavailable. + * Null if trading is allowed. + */ + @Override + public String blockedReasonTranslationKey() { + return "shop-cannot-trade-when-freezing"; + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/type/FrozenType.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/type/FrozenType.java index de07a4beae..732a6da232 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/type/FrozenType.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/type/FrozenType.java @@ -28,6 +28,7 @@ * @author creatorfromhell * @since 6.2.0.11 */ +@Deprecated(since = "6.3.0.0", forRemoval = true) public class FrozenType implements IShopType { @Override diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java index 10deef0682..701b383d2a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Freeze.java @@ -12,7 +12,9 @@ import java.util.Collections; import java.util.List; +import static com.ghostchu.quickshop.shop.SimpleShopManager.ACTIVE_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.BUYING_TYPE; +import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_TYPE; public class SubCommand_Freeze implements CommandHandler { @@ -32,14 +34,14 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(shop.playerAuthorize(sender.getUniqueId(), BuiltInShopPermission.SET_SHOPTYPE) || plugin.perm().hasPermission(sender, "quickshop.other.freeze")) { - if(shop.shopType().isTradingBlocked() && shop.shopType().identifier().equalsIgnoreCase("FROZEN")) { + if(!shop.shopState().isTradingAllowed() && shop.shopState().identifier().equalsIgnoreCase("FROZEN")) { - shop.shopType(BUYING_TYPE); + shop.shopState(ACTIVE_STATE); plugin.text().of(sender, "shop-nolonger-freezed", Util.getItemStackName(shop.getItem())).send(); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); } else { - shop.shopType(FROZEN_TYPE); + shop.shopState(FROZEN_STATE); plugin.text().of(sender, "shop-now-freezed", Util.getItemStackName(shop.getItem())).send(); } } else { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java index d19d593573..09d1ddc606 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentFreeze.java @@ -9,7 +9,9 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; +import static com.ghostchu.quickshop.shop.SimpleShopManager.ACTIVE_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.BUYING_TYPE; +import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_STATE; import static com.ghostchu.quickshop.shop.SimpleShopManager.FROZEN_TYPE; @@ -29,12 +31,12 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N return; } - if(shop.shopType().isTradingBlocked() && shop.shopType().identifier().equalsIgnoreCase("FROZEN")) { - shop.shopType(BUYING_TYPE); + if(!shop.shopState().isTradingAllowed() && shop.shopState().identifier().equalsIgnoreCase("FROZEN")) { + shop.shopState(ACTIVE_STATE); plugin.text().of(sender, "shop-nolonger-freezed", Util.getItemStackName(shop.getItem())).send(); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); } else { - shop.shopType(FROZEN_TYPE); + shop.shopState(FROZEN_STATE); plugin.text().of(sender, "shop-now-freezed", Util.getItemStackName(shop.getItem())).send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java index 0f2aa99d41..aabe478f64 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/DataTables.java @@ -31,6 +31,7 @@ public enum DataTables { table.addColumn("name", "TEXT"); // SHOP NAME table.addColumn("type", "INT NOT NULL DEFAULT 0"); // SHOP TYPE (see ShopType enum) + table.addColumn("shop_state", "VARCHAR(64)"); // shop state table.addColumn("currency", "VARCHAR(64)"); // CURRENCY (NULL means use the default currency) table.addColumn("price", "DECIMAL(32,2) NOT NULL"); // SHOP ITEM PRICE diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index aabe36888f..2d5f505186 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -58,7 +58,7 @@ public class SimpleDatabaseHelperV2 implements DatabaseHelper { @NotNull private final String prefix; - private final int LATEST_DATABASE_VERSION = 19; + private final int LATEST_DATABASE_VERSION = 20; public SimpleDatabaseHelperV2(@NotNull final QuickShop plugin, @NotNull final SQLManager manager, @NotNull final String prefix) throws Exception { @@ -212,6 +212,20 @@ private void upgradeBenefit() { return manager; } + private void addStateColumn() { + + fastBackup(); + try { + getManager().alterTable(DataTables.DATA.getName()) + .addColumn("shop_state", "VARCHAR(64)") + .execute(); + + } catch(final SQLException e) { + + Log.debug("Failed to add state " + DataTables.DATA.getName() + "! Err:" + e.getMessage()); + } + } + private void addEncodedColumn() { fastBackup(); @@ -1025,7 +1039,7 @@ public void upgrade() { int currentDatabaseVersion = parent.getDatabaseVersion(); if(currentDatabaseVersion == -1) { - currentDatabaseVersion = 19; + currentDatabaseVersion = 20; } logger.info("Database upgrade script running... Current Database Version: " + currentDatabaseVersion); @@ -1090,6 +1104,12 @@ public void upgrade() { currentDatabaseVersion = 19; } + if(currentDatabaseVersion == 16 || currentDatabaseVersion == 17 || currentDatabaseVersion == 18 || currentDatabaseVersion == 19) { + logger.info("Data upgrading: Creating a new column... shop_state for the new shop states system."); + parent.addStateColumn(); + currentDatabaseVersion = 20; + } + parent.setDatabaseVersion(currentDatabaseVersion).join(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java index 8fac3576ad..c51fc43915 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/bean/SimpleDataRecord.java @@ -23,6 +23,7 @@ public class SimpleDataRecord implements DataRecord { private final String encoded; private final String name; private final int type; + private final String state; private final String currency; private final double price; private final boolean unlimited; @@ -37,7 +38,7 @@ public class SimpleDataRecord implements DataRecord { private final String benefit; public SimpleDataRecord(final QUser owner, final String item, final String encoded, final String name, - final int type, final String currency, final double price, final boolean unlimited, + final int type, final String state, final String currency, final double price, final boolean unlimited, final boolean hologram, final QUser taxAccount, final String permissions, final String extra, final String inventoryWrapper, final String inventorySymbolLink, final Date createTime, final String benefit) { @@ -47,6 +48,7 @@ public SimpleDataRecord(final QUser owner, final String item, final String encod this.encoded = encoded; this.name = name; this.type = type; + this.state = state; this.currency = currency; this.price = price; this.unlimited = unlimited; @@ -74,6 +76,7 @@ public SimpleDataRecord(final PlayerFinder finder, final ResultSet set) throws S this.name = set.getString("name"); this.type = set.getInt("type"); + this.state = set.getString("shop_state"); this.currency = set.getString("currency"); this.price = set.getDouble("price"); this.unlimited = set.getBoolean("unlimited"); @@ -105,6 +108,7 @@ public Map generateParams() { map.put("encoded", encoded); map.put("name", name); map.put("type", type); + map.put("shop_state", state); map.put("currency", currency); map.put("price", price); map.put("unlimited", unlimited); @@ -201,6 +205,12 @@ public int getType() { return type; } + @Override + public String getState() { + + return state; + } + @Override public boolean isHologram() { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java index 87149f64bd..fce7078d6a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/transaction/QSEconomyTransaction.java @@ -264,28 +264,6 @@ public void taxer(final @Nullable QUser user) { this.taxer = user; } - /** - * Retrieves the tax amount associated with this transaction. - * - * @return a BigDecimal value representing the tax amount of the transaction - */ - @Override - public @NotNull BigDecimal tax() { - - return tax; - } - - /** - * Sets the tax for the transaction. - * - * @param tax the amount of tax to be set for the transaction - */ - @Override - public void tax(final @NotNull BigDecimal tax) { - - this.tax = tax; - } - /** * Calculates and retrieves the tax amount associated with this transaction based on the defined * tax rules or system configuration. diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java index 87d745dd3f..30b9c8809a 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 @@ -19,6 +19,7 @@ import com.ghostchu.quickshop.api.event.settings.type.ShopOwnerNameEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopPlayerGroupEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopSignLinesEvent; +import com.ghostchu.quickshop.api.event.settings.type.ShopStateEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopTaxAccountEvent; import com.ghostchu.quickshop.api.event.settings.type.ShopTypeEnhancedEvent; import com.ghostchu.quickshop.api.event.settings.type.benefit.ShopBenefitEvent; @@ -34,6 +35,7 @@ import com.ghostchu.quickshop.api.shop.display.DisplayType; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; +import com.ghostchu.quickshop.api.shop.state.ShopState; import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.database.bean.SimpleDataRecord; @@ -113,6 +115,7 @@ public class ContainerShop implements Shop, Reloadable { private QUser owner; private double price; private IShopType shopType; + private ShopState shopState; private boolean unlimited; @NotNull private ItemStack item; @@ -172,6 +175,7 @@ public ContainerShop( @NotNull final QUser owner, final boolean unlimited, @NotNull final IShopType type, + @NotNull final ShopState state, @Nullable final YamlConfiguration extra, @Nullable final String currency, final boolean disableDisplay, @@ -214,6 +218,7 @@ public ContainerShop( } } this.shopType = type; + this.shopState = state; this.unlimited = unlimited; this.extra = extra; this.currency = currency; @@ -777,6 +782,67 @@ public int getShopStackingAmount() { return ShopType.fromID(shopType.id()); } + /** + * 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. * @@ -986,7 +1052,7 @@ public boolean isBuying() { @Override public boolean isFrozen() { - return this.shopType.isTradingBlocked(); + return this.shopType.isTradingBlocked() || !this.shopState.isTradingAllowed(); } private boolean isDeleted() { @@ -1773,6 +1839,7 @@ public void setShopBenefit(@NotNull final BenefitProvider benefit) { plugin.platform().encodeStack(getItem()), getShopName(), shopType().id(), + shopState().identifier(), getCurrency(), getPrice(), isUnlimited(), diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java index 4f702939db..9a37f04372 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java @@ -8,6 +8,7 @@ import com.ghostchu.quickshop.api.obj.QUser; import com.ghostchu.quickshop.api.shop.IShopType; import com.ghostchu.quickshop.api.shop.Shop; +import com.ghostchu.quickshop.api.shop.state.ShopState; import com.ghostchu.quickshop.common.util.CommonUtil; import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.common.util.Timer; @@ -182,6 +183,7 @@ private ShopLoadResult loadSingleShop(final InfoRecord infoRecord, final DataRec rawInfo.getOwner(), rawInfo.isUnlimited(), rawInfo.getType(), + rawInfo.getState(), rawInfo.getExtra(), rawInfo.getCurrency(), rawInfo.isHologram(), @@ -302,6 +304,7 @@ public static class DataRawDatabaseInfo { private QUser owner; private String name; private IShopType type; + private ShopState state; private String currency; private double price; private boolean unlimited; @@ -324,6 +327,7 @@ public static class DataRawDatabaseInfo { this.owner = dataRecord.getOwner(); this.price = dataRecord.getPrice(); this.type = QuickShop.getInstance().getShopManager().shopTypeOrDefault(dataRecord.getType()); + this.state = QuickShop.getInstance().getShopManager().shopStateOrDefault(dataRecord.getState()); this.unlimited = dataRecord.isUnlimited(); final String extraStr = dataRecord.getExtra(); this.name = dataRecord.getName(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java index a7e9703424..7fd9c83c7b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopLayoutProvider.java @@ -140,15 +140,17 @@ public Component renderTrading(final @NotNull Shop shop, final @NotNull ProxiedL final String noRemainingStringKey = shop.shopType().outOfStockTranslationKey(); final int shopRemaining = shop.shopType().remainingStock(shop); + final String finalTradingStringKey = (shop.shopState().overrideShopTypeText())? shop.shopState().translationKey() : tradingStringKey; + final Component trading = switch(shopRemaining) { //Unlimited case -1 -> - plugin.text().of(tradingStringKey, plugin.text().of("signs.unlimited").forLocale(locale.getLocale())).forLocale(locale.getLocale()); + plugin.text().of(finalTradingStringKey, plugin.text().of("signs.unlimited").forLocale(locale.getLocale())).forLocale(locale.getLocale()); //No remaining case 0 -> plugin.text().of(noRemainingStringKey).forLocale(locale.getLocale()); //Has remaining default -> - plugin.text().of(tradingStringKey, Component.text(shopRemaining)).forLocale(locale.getLocale()); + plugin.text().of(finalTradingStringKey, Component.text(shopRemaining)).forLocale(locale.getLocale()); }; return trading; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java index 04c411c3c1..7d46feebba 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleShopManager.java @@ -26,6 +26,9 @@ import com.ghostchu.quickshop.api.shop.ShopManager; import com.ghostchu.quickshop.api.shop.cache.ShopCacheNamespacedKey; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; +import com.ghostchu.quickshop.api.shop.state.ShopState; +import com.ghostchu.quickshop.api.shop.state.impl.ActiveState; +import com.ghostchu.quickshop.api.shop.state.impl.FrozenState; import com.ghostchu.quickshop.api.shop.tax.TaxManager; import com.ghostchu.quickshop.api.shop.tax.TaxRates; import com.ghostchu.quickshop.api.shop.type.BuyingType; @@ -106,6 +109,7 @@ public class SimpleShopManager extends AbstractShopManager implements ShopManage protected final Map cooldowns = Maps.newConcurrentMap(); protected final Map shopTypes = Maps.newConcurrentMap(); + protected final Map shopStates = Maps.newConcurrentMap(); protected final ConcurrentLinkedQueue inDeletion = new ConcurrentLinkedQueue<>(); protected final InteractiveManager interactiveManager; @@ -137,6 +141,10 @@ public class SimpleShopManager extends AbstractShopManager implements ShopManage public static final SellingType SELLING_TYPE = new SellingType(); public static final FrozenType FROZEN_TYPE = new FrozenType(); + //Initialize our shop states + public static final ActiveState ACTIVE_STATE = new ActiveState(); + public static final FrozenState FROZEN_STATE = new FrozenState(); + public SimpleShopManager(@NotNull final QuickShop plugin) { super(plugin); @@ -165,6 +173,9 @@ public void init() { addShopType(SELLING_TYPE); addShopType(FROZEN_TYPE); + addShopState(ACTIVE_STATE); + addShopState(FROZEN_STATE); + Log.debug("Loading caching tax account..."); final String taxAccount = taxManager().taxAccount(); if(!taxAccount.isEmpty()) { @@ -287,6 +298,34 @@ public Map shopTypes() { return type.orElse(SELLING_TYPE); } + /** + * Retrieves a map of shop states where the keys are shop identifiers and the values are the + * corresponding shop states. + * + * @return a non-null map containing shop identifiers as keys and their corresponding + * {@link ShopState} objects as values. + */ + @Override + public @NotNull Map shopStates() { + + return shopStates; + } + + /** + * Retrieves the ShopState associated with the specified identifier. If no ShopState is found for + * the identifier, a default ShopState is returned. + * + * @param identifier the unique identifier for the shop state to retrieve + * + * @return the ShopState associated with the identifier, or a default ShopState if not found + */ + @Override + public @NotNull ShopState shopStateOrDefault(final String identifier) { + + final Optional type = shopState(identifier); + return type.orElse(ACTIVE_STATE); + } + @Override public boolean actionBuying(@NotNull final Player buyer, @NotNull final InventoryWrapper buyerInventory, @NotNull final EconomyProvider eco, @NotNull final Info info, @NotNull final Shop shop, final int amount) { @@ -496,7 +535,7 @@ public void actionCreate(@NotNull final Player p, final Info info, @NotNull fina } final ContainerShop shop = new ContainerShop(plugin, -1, info.getLocation(), priceDouble, info.getItem(), createQUser, false, - SELLING_TYPE, new YamlConfiguration(), null, !plugin.getConfig().getBoolean("shop.display-default", true), + SELLING_TYPE, ACTIVE_STATE, new YamlConfiguration(), null, !plugin.getConfig().getBoolean("shop.display-default", true), null, plugin.getJavaPlugin().getName(), symbolLink, null, Collections.emptyMap(), new QSBenefitProvider()); From 5834e576e22bef3e00266bfb1b142929f48dfae3 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Sun, 22 Mar 2026 21:25:44 -0400 Subject: [PATCH 09/29] Split Shop interface into multiple interfaces that are sorted by job. --- .../com/ghostchu/quickshop/api/shop/Shop.java | 522 +----------------- .../api/shop/inventory/ShopInventory.java | 101 ++++ .../quickshop/api/shop/meta/ShopDisplay.java | 119 ++++ .../quickshop/api/shop/meta/ShopMeta.java | 269 +++++++++ .../api/shop/permission/ShopPermission.java | 119 ++++ .../api/shop/trading/ShopTrading.java | 86 +++ .../api/shop/trading/TradeFailureReason.java | 47 ++ .../api/shop/trading/TradeOptions.java | 148 +++++ .../api/shop/trading/TradePreview.java | 36 ++ .../api/shop/trading/TradeResult.java | 35 ++ .../quickshop/api/shop/trading/TradeType.java | 30 + .../silent/SubCommand_SilentBuy.java | 3 +- .../database/SimpleDatabaseHelperV2.java | 13 + 13 files changed, 1010 insertions(+), 518 deletions(-) create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/inventory/ShopInventory.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopDisplay.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/permission/ShopPermission.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/ShopTrading.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeOptions.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradePreview.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeResult.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeType.java 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 1d9334e665..af49c38675 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 @@ -6,9 +6,14 @@ 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 org.bukkit.NamespacedKey; @@ -30,57 +35,10 @@ /** * A shop */ -public interface Shop extends Locatable { +public interface Shop extends Locatable, ShopInventory, ShopMeta, ShopTrading, ShopDisplay, ShopPermission { NamespacedKey SHOP_NAMESPACED_KEY = new NamespacedKey(QuickShopAPI.getPluginInstance(), "shopsign"); - /** - * Add x ItemStack to the shop inventory - * - * @param paramItemStack The ItemStack you want add - * @param paramInt How many you want add - */ - void add(@NotNull ItemStack paramItemStack, int paramInt); - - /** - * 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? - * - * @throws Exception Possible exception thrown if anything wrong. - */ - void buy(@NotNull QUser buyer, @NotNull InventoryWrapper buyerInventory, @NotNull Location loc2Drop, int paramInt) throws Exception; - - /** - * 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); - - /** - * 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 item durability, if have. * @@ -99,237 +57,6 @@ public interface Shop extends Locatable { @NotNull ConfigurationSection getExtra(@NotNull Plugin plugin); - /** - * Gets the shop Inventory - * - * @return Inventory - */ - @Nullable - InventoryWrapper getInventory(); - - /** - * Gets the InventoryWrapper provider name (the plugin name who register it), usually is - * QuickShop - * - * @return InventoryWrapper - */ - @NotNull - String getInventoryWrapperProvider(); - - /** - * 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); - - - /** - * Get shop's owner QUser - * - * @return Shop's owner QUser object, can use Bukkit.getOfflinePlayer to convert to the - * OfflinePlayer. - */ - @NotNull - QUser getOwner(); - - /** - * Set new owner to the shop's owner - * - * @param qUser New owner user - */ - void setOwner(@NotNull QUser qUser); - - /** - * Gets all player and their group on this shop - * - * @return Map of UUID and group - */ - @NotNull - Map getPermissionAudiences(); - - /** - * Gets specific player group on specific shop - * - * @param player player - * - * @return namespaced group - */ - @NotNull - String getPlayerGroup(@NotNull UUID player); - - /** - * Get shop's price - * - * @return Price - */ - double getPrice(); - - /** - * Set shop's new price - * - * @param paramDouble New price - */ - void setPrice(double paramDouble); - - /** - * Get shop remaining space. - * - * @return Remaining space. - */ - int getRemainingSpace(); - - /** - * Get shop remaining stock. - * - * @return Remaining stock. - */ - int getRemainingStock(); - - /** - * 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(); - - /** - * Gets the Shop ID to identify the shop. - * - * @return Shop ID -1 if shop in creating state. - */ - long getShopId(); - - /** - * Internal Only: Give shop that under id_waiting state an ShopId. - * - * @param newId The new shop id, once set will cannot change anymore. - */ - @ApiStatus.Internal - void setShopId(long newId); - - /** - * Gets this shop name that set by player - * - * @return Shop name, or null if not set - */ - @Nullable - String getShopName(); - - /** - * Sets shop name - * - * @param shopName shop name, null to remove currently name - */ - void setShopName(@Nullable String shopName); - - int getShopStackingAmount(); - - /** - * Retrieves the current state of the shop. - * - * @return the current state of the shop as a ShopState object - */ - ShopState shopState(); - - /** - * 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 - */ - void shopState(@NotNull ShopState state); - - /** - * 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. - */ - void shopState(@NotNull String shopStateIdentifier); - - /** - * Retrieves the type of shop associated with this entity. - * - * @return an instance of IShopType representing the shop type - */ - IShopType shopType(); - - /** - * Sets the type of shop using the provided shop type parameter. - * - * @param newShopType the shop type to set, must not be null - */ - 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. - */ - void shopType(@NotNull String shopTypeIdentifier); - - /** - * 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 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 - */ - @Nullable - QUser getTaxAccount(); - - /** - * Sets shop taxAccount - * - * @param taxAccount tax account, null to use general tax account - */ - void setTaxAccount(@Nullable QUser taxAccount); - - /** - * Getting the shop tax account, it can be specific uuid or general tax account - * - * @return Shop Tax Account, null if use general tax account - */ - - @Nullable - QUser getTaxAccountActual(); - - /** - * Check if shop out of space or out of stock - * - * @return true if out of space or out of stock - */ - boolean inventoryAvailable(); - /** * Check shop is or not attached the target block * @@ -339,20 +66,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ boolean isAttached(@NotNull Block paramBlock); - /** - * Get shop is or not in buying mode - * - * @return yes or no - */ - boolean isBuying(); - - /** - * Get shop is frozen or not - * - * @return yes or no - */ - boolean isFrozen(); - /** * Gets if shop is dirty (so shop will be save) * @@ -367,41 +80,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ void setDirty(boolean isDirty); - /** - * 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(); - - /** - * Check if this shop is free shop - * - * @return Free Shop - */ - boolean isFreeShop(); - /** * Get this container shop is loaded or unloaded. * @@ -409,43 +87,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ boolean isLoaded(); - /** - * Get shop is or not in selling mode - * - * @return yes or no - */ - boolean isSelling(); - - /** - * Checks if a Sign is a ShopSign - * - * @param sign Target {@link Sign} - * - * @return Is shop info sign - */ - boolean isShopSign(@NotNull Sign sign); - - /** - * Gets shop status is stacking shop - * - * @return The shop stacking status - */ - boolean isStackingShop(); - - /** - * Get shop is or not in Unlimited Mode (Admin Shop) - * - * @return yes or not - */ - boolean isUnlimited(); - - /** - * Set shop is or not Unlimited Mode (Admin Shop) - * - * @param paramBoolean status - */ - void setUnlimited(boolean paramBoolean); - /** * Whether Shop is valid * @@ -453,15 +94,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ boolean isValid(); - /** - * Check the target ItemStack is matches with this shop's item. - * - * @param paramItemStack Target ItemStack. - * - * @return Matches - */ - boolean matches(@NotNull ItemStack paramItemStack); - /** * Execute codes when player click the shop will did things */ @@ -483,92 +115,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ void openPreview(@NotNull Player player); - /** - * 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 - */ - @NotNull - Component ownerName(boolean forceUsername, @NotNull ProxiedLocale locale); - - /** - * 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 - */ - @NotNull - Component ownerName(@NotNull ProxiedLocale locale); - - /** - * Get shop's owner name, it will return owner name or Admin Shop(i18n) when it is unlimited - * - * @return owner name - */ - @NotNull - Component ownerName(); - - /** - * 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 - */ - boolean playerAuthorize(@NotNull UUID player, @NotNull Plugin namespace, @NotNull String permission); - - /** - * Check if player have authorized for specific permission on specific shop - * - * @param player player - * @param permission namespaced permission - * - * @return true if player have authorized - */ - boolean playerAuthorize(@NotNull UUID player, @NotNull BuiltInShopPermission permission); - - /** - * Gets the player list of who can authorize specific permission on this shop - * - * @param permission permission - * - * @return Collection of UUID - */ - List playersCanAuthorize(@NotNull BuiltInShopPermission permission); - - /** - * Gets the player list of who can authorize specific group on this shop - * - * @param permissionGroup group - * - * @return Collection of UUID - */ - List playersCanAuthorize(@NotNull BuiltInShopPermissionGroup permissionGroup); - - /** - * Gets the player list of who can authorize specific permission on this shop - * - * @param permission raw permission - * @param plugin namespace of permission - * - * @return Collection of UUID - */ - List playersCanAuthorize(@NotNull Plugin plugin, @NotNull String permission); - - /** - * Remove x ItemStack from the shop inventory - * - * @param paramItemStack Want removed ItemStack - * @param paramInt Want remove how many - */ - void remove(@NotNull ItemStack paramItemStack, int paramInt); - /** * Save the plugin extra data to Json format * @@ -592,19 +138,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { @NotNull String saveToSymbolLink(); - /** - * 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 paramInt How many sold? - * - * @throws Exception Possible exception thrown if anything wrong. - */ - void sell(@NotNull QUser seller, @NotNull InventoryWrapper sellerInventory, @NotNull Location loc2Drop, int paramInt) throws Exception; - /** * Sets shop is dirty */ @@ -618,38 +151,6 @@ default List getSignText(@NotNull final ProxiedLocale locale) { */ void setExtra(@NotNull Plugin plugin, @NotNull ConfigurationSection data); - void setInventory(@NotNull InventoryWrapper wrapper, @NotNull InventoryWrapperManager manager); - - /** - * Sets specific player permission on specfic shop - * - * @param player player - * @param group namespaced group name - */ - void setPlayerGroup(@NotNull UUID player, @Nullable String group); - - /** - * Sets specific player permission on specfic shop - * - * @param player player - * @param group group - */ - void setPlayerGroup(@NotNull UUID player, @Nullable BuiltInShopPermissionGroup group); - - /** - * 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); - /** * Update shop data to database */ @@ -663,15 +164,4 @@ default List getSignText(@NotNull final ProxiedLocale locale) { * @throws RuntimeException */ void updateSync() throws RuntimeException; - - /** - * Gets the benefit in this shop - */ - @NotNull - BenefitProvider getShopBenefit(); - - /** - * Sets the benefit in this shop - */ - void setShopBenefit(@NotNull BenefitProvider benefit); } \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/inventory/ShopInventory.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/inventory/ShopInventory.java new file mode 100644 index 0000000000..2ca3d3ece4 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/inventory/ShopInventory.java @@ -0,0 +1,101 @@ +package com.ghostchu.quickshop.api.shop.inventory; + +/* + * 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 com.ghostchu.quickshop.api.inventory.InventoryWrapperManager; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * ShopInventory + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopInventory { + + /** + * Add x ItemStack to the shop inventory + * + * @param paramItemStack The ItemStack you want add + * @param paramInt How many you want add + */ + void add(@NotNull ItemStack paramItemStack, int paramInt); + + /** + * Remove x ItemStack from the shop inventory + * + * @param paramItemStack Want removed ItemStack + * @param paramInt Want remove how many + */ + void remove(@NotNull ItemStack paramItemStack, int paramInt); + + /** + * Gets the shop Inventory + * + * @return Inventory + */ + @Nullable + InventoryWrapper getInventory(); + + /** + * Gets the InventoryWrapper provider name (the plugin name who register it), usually is + * QuickShop + * + * @return InventoryWrapper + */ + @NotNull + String getInventoryWrapperProvider(); + + /** + * Get shop remaining space. + * + * @return Remaining space. + */ + int getRemainingSpace(); + + /** + * Get shop remaining stock. + * + * @return Remaining stock. + */ + int getRemainingStock(); + + int getShopStackingAmount(); + + /** + * Check if shop out of space or out of stock + * + * @return true if out of space or out of stock + */ + boolean inventoryAvailable(); + + /** + * Check the target ItemStack is matches with this shop's item. + * + * @param paramItemStack Target ItemStack. + * + * @return Matches + */ + boolean matches(@NotNull ItemStack paramItemStack); + + void setInventory(@NotNull InventoryWrapper wrapper, @NotNull InventoryWrapperManager manager); +} \ 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 new file mode 100644 index 0000000000..95c9d0d2be --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopDisplay.java @@ -0,0 +1,119 @@ +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/meta/ShopMeta.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java new file mode 100644 index 0000000000..0e9b9cc483 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopMeta.java @@ -0,0 +1,269 @@ +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.economy.benefit.BenefitProvider; +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.state.ShopState; +import net.kyori.adventure.text.Component; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +/** + * ShopIdentity + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopMeta { + + /** + * 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(); + + /** + * Gets the Shop ID to identify the shop. + * + * @return Shop ID -1 if shop in creating state. + */ + long getShopId(); + + /** + * Internal Only: Give shop that under id_waiting state an ShopId. + * + * @param newId The new shop id, once set will cannot change anymore. + */ + @ApiStatus.Internal + void setShopId(long newId); + + /** + * Gets this shop name that set by player + * + * @return Shop name, or null if not set + */ + @Nullable + String getShopName(); + + /** + * 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 + */ + double getPrice(); + + /** + * Set shop's new price + * + * @param paramDouble New price + */ + void setPrice(double paramDouble); + + /** + * Retrieves the current state of the shop. + * + * @return the current state of the shop as a ShopState object + */ + ShopState shopState(); + + /** + * 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 + */ + void shopState(@NotNull ShopState state); + + /** + * 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. + */ + void shopState(@NotNull String shopStateIdentifier); + + /** + * Retrieves the type of shop associated with this entity. + * + * @return an instance of IShopType representing the shop type + */ + IShopType shopType(); + + /** + * Sets the type of shop using the provided shop type parameter. + * + * @param newShopType the shop type to set, must not be null + */ + 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. + */ + void shopType(@NotNull String 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 + */ + @NotNull + Component ownerName(boolean forceUsername, @NotNull ProxiedLocale locale); + + /** + * 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 + */ + @NotNull + Component ownerName(@NotNull ProxiedLocale locale); + + /** + * Get shop's owner name, it will return owner name or Admin Shop(i18n) when it is unlimited + * + * @return owner name + */ + @NotNull + Component ownerName(); + + + /** + * Get shop's owner QUser + * + * @return Shop's owner QUser object, can use Bukkit.getOfflinePlayer to convert to the + * OfflinePlayer. + */ + @NotNull + QUser getOwner(); + + /** + * Set new owner to the shop's owner + * + * @param qUser New owner user + */ + void setOwner(@NotNull QUser qUser); + + /** + * 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 + */ + @Nullable + QUser getTaxAccount(); + + /** + * Sets shop taxAccount + * + * @param taxAccount tax account, null to use general tax account + */ + void setTaxAccount(@Nullable QUser taxAccount); + + /** + * Getting the shop tax account, it can be specific uuid or general tax account + * + * @return Shop Tax Account, null if use general tax account + */ + + @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) + * + * @return yes or not + */ + boolean isUnlimited(); + + /** + * Set shop is or not Unlimited Mode (Admin Shop) + * + * @param paramBoolean status + */ + void setUnlimited(boolean paramBoolean); + + /** + * Gets the benefit in this shop + */ + @NotNull + BenefitProvider getShopBenefit(); + + /** + * Sets the benefit in this shop + */ + void setShopBenefit(@NotNull BenefitProvider benefit); +} \ 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/permission/ShopPermission.java new file mode 100644 index 0000000000..a80263cf1d --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/permission/ShopPermission.java @@ -0,0 +1,119 @@ +package com.ghostchu.quickshop.api.shop.permission; + +/* + * 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.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * ShopPermission + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopPermission { + + /** + * Gets all player and their group on this shop + * + * @return Map of UUID and group + */ + @NotNull + Map getPermissionAudiences(); + + /** + * Gets specific player group on specific shop + * + * @param player player + * + * @return namespaced group + */ + @NotNull + String getPlayerGroup(@NotNull UUID player); + + /** + * 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 + */ + boolean playerAuthorize(@NotNull UUID player, @NotNull Plugin namespace, @NotNull String permission); + + /** + * Check if player have authorized for specific permission on specific shop + * + * @param player player + * @param permission namespaced permission + * + * @return true if player have authorized + */ + boolean playerAuthorize(@NotNull UUID player, @NotNull BuiltInShopPermission permission); + + /** + * Gets the player list of who can authorize specific permission on this shop + * + * @param permission permission + * + * @return Collection of UUID + */ + List playersCanAuthorize(@NotNull BuiltInShopPermission permission); + + /** + * Gets the player list of who can authorize specific group on this shop + * + * @param permissionGroup group + * + * @return Collection of UUID + */ + List playersCanAuthorize(@NotNull BuiltInShopPermissionGroup permissionGroup); + + /** + * Gets the player list of who can authorize specific permission on this shop + * + * @param permission raw permission + * @param plugin namespace of permission + * + * @return Collection of UUID + */ + List playersCanAuthorize(@NotNull Plugin plugin, @NotNull String permission); + + /** + * Sets specific player permission on specfic shop + * + * @param player player + * @param group namespaced group name + */ + void setPlayerGroup(@NotNull UUID player, @Nullable String group); + + /** + * Sets specific player permission on specfic shop + * + * @param player player + * @param group group + */ + void setPlayerGroup(@NotNull UUID player, @Nullable BuiltInShopPermissionGroup group); +} \ 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/trading/ShopTrading.java new file mode 100644 index 0000000000..159b697961 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/ShopTrading.java @@ -0,0 +1,86 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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 com.ghostchu.quickshop.api.obj.QUser; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +/** + * ShopTrading + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface ShopTrading { + + /** + * Get shop is or not in buying mode + * + * @return yes or no + */ + boolean isBuying(); + + /** + * Get shop is frozen or not + * + * @return yes or no + */ + boolean isFrozen(); + + /** + * Get shop is or not in selling mode + * + * @return yes or no + */ + 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? + * + * @throws Exception Possible exception thrown if anything wrong. + */ + void buy(@NotNull QUser buyer, @NotNull InventoryWrapper buyerInventory, @NotNull Location loc2Drop, int paramInt) throws Exception; + + /** + * 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 paramInt How many sold? + * + * @throws Exception Possible exception thrown if anything wrong. + */ + void sell(@NotNull QUser seller, @NotNull InventoryWrapper sellerInventory, @NotNull Location loc2Drop, int paramInt) throws Exception; +} \ 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 new file mode 100644 index 0000000000..4470c30d6e --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeFailureReason.java @@ -0,0 +1,47 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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 . + */ + +/** + * TradeFailReason + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public enum TradeFailureReason { + + NO_PERMISSION, + SELF_TRADE_DENIED, + SHOP_INVALID, + SHOP_FROZEN, + SHOP_DISABLED, + INVALID_AMOUNT, + STOCK_TOO_LOW, + INVENTORY_FULL, + SHOP_NO_SPACE, + LIMIT_REACHED, + INSUFFICIENT_FUNDS, + ITEM_NOT_ENOUGH, + ECONOMY_TRANSACTION_NOT_COMPLETABLE, + ECONOMY_TRANSACTION_FAILED, + INVENTORY_TRANSACTION_FAILED, + SHOP_TRANSACTION_FAILED, + EVENT_CANCELLED, + INTERNAL_ERROR +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeOptions.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeOptions.java new file mode 100644 index 0000000000..a843da8e59 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeOptions.java @@ -0,0 +1,148 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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 . + */ + +/** + * TradeOptions + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public final class TradeOptions { + + public static final TradeOptions DEFAULT = builder().build(); + + public static final TradeOptions SILENT = builder() + .sendMessages(false) + .updateSigns(true) + .build(); + + public static final TradeOptions PREVIEW = builder() + .sendMessages(false) + .updateSigns(false) + .callEvents(false) + .commit(false) + .allowRollback(false) + .build(); + + private final boolean sendMessages; + private final boolean updateSigns; + private final boolean callEvents; + private final boolean allowRollback; + private final boolean commit; + + private TradeOptions(final Builder builder) { + + this.sendMessages = builder.sendMessages; + this.updateSigns = builder.updateSigns; + this.callEvents = builder.callEvents; + this.allowRollback = builder.allowRollback; + this.commit = builder.commit; + } + + public static Builder builder() { + + return new Builder(); + } + + public static Builder builder(final TradeOptions copy) { + + return new Builder(copy); + } + + public boolean sendMessages() { + + return sendMessages; + } + + public boolean updateSigns() { + + return updateSigns; + } + + public boolean callEvents() { + + return callEvents; + } + + public boolean allowRollback() { + + return allowRollback; + } + + public boolean commit() { + + return commit; + } + + public static final class Builder { + + private boolean sendMessages = true; + private boolean updateSigns = true; + private boolean callEvents = true; + private boolean allowRollback = true; + private boolean commit = true; + + private Builder() { } + + private Builder(final TradeOptions copy) { + + this.sendMessages = copy.sendMessages; + this.updateSigns = copy.updateSigns; + this.callEvents = copy.callEvents; + this.allowRollback = copy.allowRollback; + this.commit = copy.commit; + } + + public Builder sendMessages(final boolean sendMessages) { + + this.sendMessages = sendMessages; + return this; + } + + public Builder updateSigns(final boolean updateSigns) { + + this.updateSigns = updateSigns; + return this; + } + + public Builder callEvents(final boolean callEvents) { + + this.callEvents = callEvents; + return this; + } + + public Builder allowRollback(final boolean allowRollback) { + + this.allowRollback = allowRollback; + return this; + } + + public Builder commit(final boolean commit) { + + this.commit = commit; + return this; + } + + public TradeOptions build() { + + return new TradeOptions(this); + } + } +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradePreview.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradePreview.java new file mode 100644 index 0000000000..481e18489e --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradePreview.java @@ -0,0 +1,36 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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; + +import java.math.BigDecimal; + +/** + * TradePreview + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record TradePreview(TradeType tradeType, int requestedAmount, int allowedAmount, + BigDecimal unitPrice, + BigDecimal totalPrice, boolean allowed, + @Nullable TradeFailureReason limitingReason, + @Nullable String debugMessage) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeResult.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeResult.java new file mode 100644 index 0000000000..f00d05f17b --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeResult.java @@ -0,0 +1,35 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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; + +import java.math.BigDecimal; + +/** + * TradeResult + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public record TradeResult(boolean success, TradeType tradeType, int requestedAmount, int tradedAmount, + BigDecimal unitPrice, BigDecimal totalPrice, BigDecimal actorTax, + BigDecimal ownerTax, @Nullable TradeFailureReason failureReason, + @Nullable String messageKey, @Nullable String debugMessage) { +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeType.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeType.java new file mode 100644 index 0000000000..21681618aa --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeType.java @@ -0,0 +1,30 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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 . + */ + +/** + * TradeType + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public enum TradeType { + BUY_FROM_SHOP, + SELL_TO_SHOP +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java index b006b23163..c43375a720 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/silent/SubCommand_SilentBuy.java @@ -33,5 +33,4 @@ protected void doSilentCommand(final Player sender, @NotNull final Shop shop, @N MsgUtil.sendControlPanelInfo(sender, shop); plugin.text().of(sender, "command.now-buying", Util.getItemStackName(shop.getItem())).send(); } - -} +} \ No newline at end of file diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index 2d5f505186..daccdb4c6e 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -216,10 +216,23 @@ private void addStateColumn() { fastBackup(); try { + Log.debug("Adding state column to " + DataTables.DATA.getName()); getManager().alterTable(DataTables.DATA.getName()) .addColumn("shop_state", "VARCHAR(64)") .execute(); + Log.debug("Converting old frozen type shops to new frozen state."); + getManager().createUpdate(DataTables.SHOPS.getName()) + .setColumnValues("shop_state", "frozen") + .addCondition("type", 2) + .build().execute(); + + Log.debug("Converting old frozen type shops to buy type."); + getManager().createUpdate(DataTables.SHOPS.getName()) + .setColumnValues("type", 1) + .addCondition("type", 2) + .build().execute(); + } catch(final SQLException e) { Log.debug("Failed to add state " + DataTables.DATA.getName() + "! Err:" + e.getMessage()); From 19c3e6679e1d6db555112fed931626126fded262 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Thu, 2 Apr 2026 23:13:04 -0400 Subject: [PATCH 10/29] Introduce TradeService and ShopPrice interfaces, and refactor ShopMeta to extend ShopPrice. --- .../quickshop/api/shop/meta/ShopMeta.java | 6 +- .../quickshop/api/shop/meta/ShopPrice.java | 58 ++++++++++++++ .../api/shop/trading/TradeService.java | 76 +++++++++++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopPrice.java create mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeService.java 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/meta/ShopMeta.java index 0e9b9cc483..1e2a7940ac 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/meta/ShopMeta.java @@ -37,7 +37,7 @@ * @author creatorfromhell * @since 6.3.0.0 */ -public interface ShopMeta { +public interface ShopMeta extends ShopPrice { /** * WARNING: This UUID will changed after plugin reload, shop reload or server restart DO NOT USE @@ -112,14 +112,18 @@ public interface ShopMeta { * Get shop's price * * @return Price + * @deprecated Use {@link ShopPrice#price()} instead */ + @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); /** 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/meta/ShopPrice.java new file mode 100644 index 0000000000..716036cca3 --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/meta/ShopPrice.java @@ -0,0 +1,58 @@ +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 . + */ + +/** + * ShopPrice + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +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(); + + /** + * 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 + */ + void price(U price); + + /** + * 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. + */ + int getMaxAffordable(); + + /** + * 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 + */ + boolean canAfford(final int itemAmount); +} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeService.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeService.java new file mode 100644 index 0000000000..3ca86805cb --- /dev/null +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/trading/TradeService.java @@ -0,0 +1,76 @@ +package com.ghostchu.quickshop.api.shop.trading; + +/* + * 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 com.ghostchu.quickshop.api.obj.QUser; +import com.ghostchu.quickshop.api.shop.Shop; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +/** + * TradeService + * + * @author creatorfromhell + * @since 6.3.0.0 + */ +public interface TradeService { + + @NotNull + TradeResult executeBuyFromShop(@NotNull Shop shop, + @NotNull QUser buyer, + @NotNull InventoryWrapper buyerInventory, + @NotNull Location dropLocation, + int amount); + + @NotNull + TradeResult executeBuyFromShop(@NotNull Shop shop, + @NotNull QUser buyer, + @NotNull InventoryWrapper buyerInventory, + @NotNull Location dropLocation, + int amount, + @NotNull TradeOptions options); + + @NotNull + TradeResult executeSellToShop(@NotNull Shop shop, + @NotNull QUser seller, + @NotNull InventoryWrapper sellerInventory, + @NotNull Location dropLocation, + int amount); + + @NotNull + TradeResult executeSellToShop(@NotNull Shop shop, + @NotNull QUser seller, + @NotNull InventoryWrapper sellerInventory, + @NotNull Location dropLocation, + int amount, + @NotNull TradeOptions options); + + @NotNull + TradePreview previewBuyFromShop(@NotNull Shop shop, + @NotNull QUser buyer, + @NotNull InventoryWrapper buyerInventory, + int amount); + + @NotNull + TradePreview previewSellToShop(@NotNull Shop shop, + @NotNull QUser seller, + @NotNull InventoryWrapper sellerInventory, + int amount); +} \ No newline at end of file From b2f53eda54eff9923502210952ce6b8399f44962 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Fri, 3 Apr 2026 21:00:23 -0400 Subject: [PATCH 11/29] Refactor Shop and ShopMeta to support generic pricing system, implement placeholder methods for generic price handling, and update related API interfaces. --- .changelog/6.3.0.0.md | 1 + .../com/ghostchu/quickshop/api/shop/Shop.java | 2 +- .../quickshop/api/shop/meta/ShopMeta.java | 2 +- .../quickshop/shop/ContainerShop.java | 74 +++++++++++++------ 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index 4cee359a02..a6f2abf319 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -129,6 +129,7 @@ A new player-driven tagging system has been introduced, allowing players to orga ## Internals - Deprecated FrozenType in favor of a new ShopState system. - Added new ShopState system to handle shop state changes. +- Added new ShopPrice interface that accepts a generic type, which is an object for the shop's price. ## Deprecation Removals - Removed ShopType enum and methods 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 af49c38675..a7587f33c2 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 @@ -35,7 +35,7 @@ /** * A shop */ -public interface Shop extends Locatable, ShopInventory, ShopMeta, ShopTrading, ShopDisplay, ShopPermission { +public interface Shop extends Locatable, ShopInventory, ShopMeta, ShopTrading, ShopDisplay, ShopPermission { NamespacedKey SHOP_NAMESPACED_KEY = new NamespacedKey(QuickShopAPI.getPluginInstance(), "shopsign"); 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/meta/ShopMeta.java index 1e2a7940ac..8dc506e776 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/meta/ShopMeta.java @@ -37,7 +37,7 @@ * @author creatorfromhell * @since 6.3.0.0 */ -public interface ShopMeta extends ShopPrice { +public interface ShopMeta extends ShopPrice { /** * WARNING: This UUID will changed after plugin reload, shop reload or server restart DO NOT USE 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 30b9c8809a..183a2d4254 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 @@ -31,7 +31,6 @@ import com.ghostchu.quickshop.api.shop.IShopType; import com.ghostchu.quickshop.api.shop.Shop; import com.ghostchu.quickshop.api.shop.ShopInfoStorage; -import com.ghostchu.quickshop.api.shop.ShopType; import com.ghostchu.quickshop.api.shop.display.DisplayType; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermission; import com.ghostchu.quickshop.api.shop.permission.BuiltInShopPermissionGroup; @@ -43,7 +42,6 @@ import com.ghostchu.quickshop.shop.datatype.ShopSignPersistentDataType; import com.ghostchu.quickshop.shop.display.AbstractDisplayItem; import com.ghostchu.quickshop.util.MsgUtil; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.logging.container.ShopRemoveLog; @@ -95,7 +93,7 @@ * ChestShop core */ @EqualsAndHashCode -public class ContainerShop implements Shop, Reloadable { +public class ContainerShop implements Shop, Reloadable { // We use deprecated method to create a fake quickshop-reremake namespace to trick bukkit to access legacy data. private static final NamespacedKey LEGACY_SHOP_NAMESPACED_KEY = new NamespacedKey("quickshop", "shopsign"); @@ -633,6 +631,7 @@ public void setOwner(@NotNull final QUser owner) { /** * @return The price per item this shop is selling */ + @SuppressWarnings("removal") @Override public double getPrice() { @@ -644,6 +643,7 @@ public double getPrice() { * * @param price The new price of the shop. */ + @SuppressWarnings("removal") @Override public void setPrice(final double price) { @@ -653,6 +653,55 @@ public void setPrice(final double price) { setSignText(); } + //TODO: Implement the new price methods. + /** + * 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 0.0; + } + + /** + * 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) { + + } + + /** + * 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() { + + return 0; + } + + /** + * 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) { + + return false; + } + /** * Returns the number of free spots in the chest for the particular item. * @@ -775,13 +824,6 @@ public int getShopStackingAmount() { return 1; } - @SuppressWarnings("removal") - @Override - public @NotNull ShopType getShopType() { - - return ShopType.fromID(shopType.id()); - } - /** * Retrieves the current state of the shop. * @@ -903,18 +945,6 @@ public void shopType(@NotNull final String shopTypeIdentifier) { shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(shopTypeIdentifier)); } - /** - * Changes a shop type to Buying or Selling. Also updates the signs nearby. - * - * @param newShopType The new type (ShopType.BUYING or ShopType.SELLING) - */ - @SuppressWarnings("removal") - @Override - public void setShopType(@NotNull final ShopType newShopType) { - - shopType(QuickShop.getInstance().getShopManager().shopTypeOrDefault(newShopType.name())); - } - @Override public List getSignText(@NotNull final ProxiedLocale locale) { From 5dcae0193076981add7bfeb6d9db73aa9db5158a Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Wed, 15 Apr 2026 14:43:54 -0400 Subject: [PATCH 12/29] Remove deprecated `ShopTypeEvent`, refactor `getLocation` to `bukkitLocation` in shop-related classes, and add generic price comparison methods. --- .changelog/6.3.0.0.md | 3 + .../quickshop/addon/bluemap/Main.java | 10 +- .../discordsrv/message/MessageFactory.java | 12 +- .../addon/discount/listener/MainListener.java | 8 +- .../ghostchu/quickshop/addon/dynmap/Main.java | 18 +-- .../addon/list/command/SubCommand_List.java | 4 +- .../addon/pl3xmap/ShopMarkerManager.java | 8 +- .../addon/plan/HikariDataExtension.java | 8 +- .../quickshop/addon/plan/util/DataUtil.java | 4 +- .../addon/squaremap/ShopLayerProvider.java | 8 +- .../advancedregionmarket/Main.java | 4 +- .../compatibility/bentobox/Main.java | 2 +- .../compatibility/CompatibilityModule.java | 2 +- .../compatibility/dominion/Main.java | 4 +- .../compatibility/fabledskyblock/Main.java | 4 +- .../compatibility/griefprevention/Main.java | 20 +-- .../compatibility/husktowns/Main.java | 6 +- .../compatibility/iridiumskyblock/Main.java | 2 +- .../quickshop/compatibility/lands/Main.java | 8 +- .../compatibility/openinv/OpenInvCommand.java | 2 +- .../compatibility/plotsquared/Main.java | 6 +- .../compatibility/residence/Main.java | 4 +- .../compatibility/simpleclaimsystem/Main.java | 2 +- .../compatibility/superiorskyblock/Main.java | 2 +- .../quickshop/compatibility/towny/Main.java | 12 +- .../compatibility/towny/TownyShopUtil.java | 4 +- .../towny/command/NationCommand.java | 2 +- .../towny/command/TownCommand.java | 6 +- .../compatibility/ultimateclaims/Main.java | 2 +- .../compatibility/worldguard/Main.java | 6 +- .../event/settings/type/ShopTypeEvent.java | 130 ------------------ .../quickshop/api/shop/Locatable.java | 9 ++ .../com/ghostchu/quickshop/api/shop/Shop.java | 2 +- .../quickshop/api/shop/meta/ShopPrice.java | 25 ++++ .../com/ghostchu/quickshop/QuickShop.java | 9 +- .../command/subcommand/SubCommand_Browse.java | 4 +- .../command/subcommand/SubCommand_Clean.java | 4 +- .../subcommand/SubCommand_CleanGhost.java | 6 +- .../subcommand/SubCommand_Currency.java | 2 +- .../subcommand/SubCommand_Database.java | 4 +- .../command/subcommand/SubCommand_Find.java | 8 +- .../command/subcommand/SubCommand_Info.java | 2 +- .../command/subcommand/SubCommand_Name.java | 4 +- .../subcommand/SubCommand_RemoveWorld.java | 2 +- .../command/subcommand/SubCommand_Sign.java | 2 +- .../database/SimpleDatabaseHelperV2.java | 3 +- .../hook/fworldedit/ShopProcessor.java | 2 +- .../hook/fworldedit/ShopProcessorLegacy.java | 2 +- .../worldedit/WorldEditBlockListener.java | 2 +- .../quickshop/listener/InternalListener.java | 14 +- .../quickshop/listener/WorldListener.java | 4 +- .../quickshop/menu/browse/MainPage.java | 6 +- .../quickshop/menu/browse/MarketUtils.java | 8 +- .../quickshop/menu/browse/ShopListPage.java | 14 +- .../quickshop/menu/history/MainPage.java | 12 +- .../quickshop/menu/keeper/MainPage.java | 12 +- .../menu/staff/StaffSelectionPage.java | 2 +- .../quickshop/menu/trade/MainPage.java | 20 +-- .../ghostchu/quickshop/papi/PAPICache.java | 4 +- .../quickshop/shop/AbstractShopManager.java | 24 ++-- .../quickshop/shop/ContainerShop.java | 96 +++++++++++-- .../ghostchu/quickshop/shop/ShopLoader.java | 3 +- .../quickshop/shop/SimpleShopManager.java | 40 +++--- .../shop/display/AbstractDisplayItem.java | 4 +- .../display/virtual/VirtualDisplayItem.java | 6 +- .../quickshop/shop/sign/SignHooker.java | 6 +- .../shop/tax/ProgressiveProvider.java | 4 +- .../quickshop/util/MetricDataUtil.java | 2 +- .../com/ghostchu/quickshop/util/ShopUtil.java | 26 ++-- .../economyformatter/EconomyFormatter.java | 2 +- .../matcher/item/ModernCustomMatcher.java | 2 + .../util/paste/item/ShopsInfoItem.java | 6 +- .../watcher/DisplayAutoDespawnWatcher.java | 4 +- .../quickshop/watcher/OngoingFeeWatcher.java | 4 +- 74 files changed, 357 insertions(+), 373 deletions(-) delete mode 100644 quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopTypeEvent.java diff --git a/.changelog/6.3.0.0.md b/.changelog/6.3.0.0.md index a6f2abf319..ed050bdb52 100644 --- a/.changelog/6.3.0.0.md +++ b/.changelog/6.3.0.0.md @@ -130,6 +130,9 @@ A new player-driven tagging system has been introduced, allowing players to orga - Deprecated FrozenType in favor of a new ShopState system. - Added new ShopState system to handle shop state changes. - Added new ShopPrice interface that accepts a generic type, which is an object for the shop's price. + - This new interface also contains methods for comparison purposes, such as price comparison for sorting in UIs. +- Added bukkitLocation() to the Locatable interface, which returns a BukkitLocation object. +- Replaced internal calls to shop.getLocation with shop.bukkitLocation ## Deprecation Removals - Removed ShopType enum and methods diff --git a/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java b/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java index 64e3c93c87..cc9cae45a7 100644 --- a/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java +++ b/addon/bluemap/src/main/java/com/ghostchu/quickshop/addon/bluemap/Main.java @@ -82,7 +82,7 @@ private void updateAllMarkers() { public void updateShopMarker(final Shop shop) { - final Optional bWorld = blueMapAPI.getWorld(shop.getLocation().getWorld()); + final Optional bWorld = blueMapAPI.getWorld(shop.bukkitLocation().getWorld()); if(bWorld.isEmpty()) { return; } @@ -93,9 +93,9 @@ public void updateShopMarker(final Shop shop) { final POIMarker.Builder markerBuilder = POIMarker.builder() .label(markerName) - .position(shop.getLocation().getX(), - shop.getLocation().getY() + 1, - shop.getLocation().getZ()) + .position(shop.bukkitLocation().getX(), + shop.bukkitLocation().getY() + 1, + shop.bukkitLocation().getZ()) .maxDistance(getConfig().getDouble("max-distance")) .detail(desc); @@ -150,7 +150,7 @@ private String getShopColor(final Shop shop) { private String fillPlaceholders(String s, final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getX()); final String y = String.valueOf(loc.getY()); final String z = String.valueOf(loc.getZ()); diff --git a/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java b/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java index eb6c232252..4e11cffdf6 100644 --- a/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java +++ b/addon/discordsrv/src/main/java/com/ghostchu/quickshop/addon/discordsrv/message/MessageFactory.java @@ -55,10 +55,10 @@ public MessageEmbed modShopCreated(@NotNull final ShopCreateEvent event) { map.put("shop.name", ChatColor.stripColor(shop.getShopName())); map.put("shop.owner.name", wrap(shop.ownerName(plugin.text().findRelativeLanguages(langUser, false)))); - map.put("shop.location.world", shop.getLocation().getWorld().getName()); - map.put("shop.location.x", String.valueOf(shop.getLocation().getBlockX())); - map.put("shop.location.y", String.valueOf(shop.getLocation().getBlockY())); - map.put("shop.location.z", String.valueOf(shop.getLocation().getBlockZ())); + map.put("shop.location.world", shop.bukkitLocation().getWorld().getName()); + map.put("shop.location.x", String.valueOf(shop.bukkitLocation().getBlockX())); + map.put("shop.location.y", String.valueOf(shop.bukkitLocation().getBlockY())); + map.put("shop.location.z", String.valueOf(shop.bukkitLocation().getBlockZ())); map.put("shop.location.id", String.valueOf(shop.getShopId())); final Component customName = Util.getItemCustomName(shop.getItem()); if(customName != null) { @@ -77,7 +77,7 @@ public MessageEmbed modShopCreated(@NotNull final ShopCreateEvent event) { map.put("shop.display-name", shop.getShopName()); } else { // world, x, y, z - map.put("shop.display-name", String.format("%s %s, %s, %s", shop.getLocation().getWorld().getName(), shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ())); + map.put("shop.display-name", String.format("%s %s, %s, %s", shop.bukkitLocation().getWorld().getName(), shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ())); } map.put("shop.type", shop.shopType().identifier()); return map; @@ -126,7 +126,7 @@ private Map applyPlaceHoldersForPurchaseEvent(@NotNull final Map placeHolders.put("purchase.uuid", event.getPurchaser().toString()); placeHolders.put("purchase.name", getPlayerName(langUser)); //noinspection DataFlowIssue - placeHolders.put("purchase.world", shop.getLocation().getWorld().getName()); + placeHolders.put("purchase.world", shop.bukkitLocation().getWorld().getName()); placeHolders.put("purchase.amount", String.valueOf(event.getAmount())); placeHolders.put("purchase.balance", String.valueOf(event.getBalanceWithoutTax())); placeHolders.put("purchase.balance-formatted", purgeColors(plugin.getShopManager().format(event.getBalanceWithoutTax(), shop))); diff --git a/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java b/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java index 2f6d4965bd..c1981d7fa4 100644 --- a/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java +++ b/addon/discount/src/main/java/com/ghostchu/quickshop/addon/discount/listener/MainListener.java @@ -48,7 +48,7 @@ public void onPrePurchase(final ShopInfoPanelEvent event) { case APPLICABLE -> quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code).send(); case APPLICABLE_WITH_THRESHOLD -> - quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applicable", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); case NOT_APPLICABLE -> quickshop.text().of(purchaser, "addon.discount.discount-code-not-applicable", code).send(); case REACHED_THE_LIMIT -> @@ -84,16 +84,16 @@ public void onPurchase(final ShopPurchaseEvent event) { final double beforeDiscount = event.getTotal(); event.setTotal(codeInstalled.apply(purchaser, event.getTotal())); final double discounted = beforeDiscount - event.getTotal(); - quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } case APPLICABLE_WITH_THRESHOLD -> { if(event.getTotal() < codeInstalled.getThreshold()) { - quickshop.text().of(purchaser, "addon.discount.discount-code-under-threshold", quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-under-threshold", quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(codeInstalled.getThreshold()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } else { final double beforeDiscount = event.getTotal(); event.setTotal(codeInstalled.apply(purchaser, event.getTotal())); final double discounted = beforeDiscount - event.getTotal(); - quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.getLocation().getWorld().getName(), shop.getCurrency())).send(); + quickshop.text().of(purchaser, "addon.discount.discount-code-applied-in-purchase", code, quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(discounted), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())).send(); } } case NOT_APPLICABLE -> diff --git a/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java b/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java index da48fcadcd..302e9487d2 100644 --- a/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java +++ b/addon/dynmap/src/main/java/com/ghostchu/quickshop/addon/dynmap/Main.java @@ -207,7 +207,7 @@ public void updateShopMarker(final Shop shop) { final MarkerSet markerSet = getMarkerSet(); String shopName = shop.getShopName(); - final String posStr = String.format("%s %s, %s, %s", shop.getLocation().getWorld().getName(), shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()); + final String posStr = String.format("%s %s, %s, %s", shop.bukkitLocation().getWorld().getName(), shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()); if(shopName == null) { shopName = posStr; } @@ -229,16 +229,16 @@ public void updateShopMarker(final Shop shop) { if(marker == null) { marker = markerSet.createMarker("quickshop-hikari-shop-" + shop.getShopId() , markerName - , shop.getLocation().getWorld().getName(), - shop.getLocation().getX(), - shop.getLocation().getY(), - shop.getLocation().getZ(), + , shop.bukkitLocation().getWorld().getName(), + shop.bukkitLocation().getX(), + shop.bukkitLocation().getY(), + shop.bukkitLocation().getZ(), getShopMarkerIcon(), false); } else { - marker.setLocation(shop.getLocation().getWorld().getName(), - shop.getLocation().getX(), - shop.getLocation().getY(), - shop.getLocation().getZ()); + marker.setLocation(shop.bukkitLocation().getWorld().getName(), + shop.bukkitLocation().getX(), + shop.bukkitLocation().getY(), + shop.bukkitLocation().getZ()); } final String desc = plain(text().of("addon.dynmap.marker-description", shopName, diff --git a/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java b/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java index eda8dcd9f4..f3a75e7a41 100644 --- a/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java +++ b/addon/list/src/main/java/com/ghostchu/quickshop/addon/list/command/SubCommand_List.java @@ -111,7 +111,7 @@ private void lookup(@NotNull final Player sender, @NotNull final UUID lookupUser continue; } String shopName = shop.getShopName(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String combineLocation = location.getWorld().getName() + " " + location.getBlockX() + ", " + location.getBlockY() + ", " + location.getBlockZ(); if(CommonUtil.isEmptyString(shopName)) { shopName = combineLocation; @@ -125,7 +125,7 @@ private void lookup(@NotNull final Player sender, @NotNull final UUID lookupUser } else { shopTypeComponent = quickshop.text().of(sender, "menu.this-shop-is-frozen").forLocale(); } - Component component = quickshop.text().of(sender, "addon.list.entry", counter, shopNameComponent, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()), shop.getShopStackingAmount(), Util.getItemStackName(shop.getItem()), shopTypeComponent).forLocale(); + Component component = quickshop.text().of(sender, "addon.list.entry", counter, shopNameComponent, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ(), quickshop.getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()), shop.getShopStackingAmount(), Util.getItemStackName(shop.getItem()), shopTypeComponent).forLocale(); component = component.clickEvent(ClickEvent.runCommand(MsgUtil.fillArgs("/{0} {1} {2}", quickshop.getMainCommand(), quickshop.getCommandPrefix("silentpreview"), shop.getRuntimeRandomUniqueId().toString()))); printer.printLine(component); loopCounter++; diff --git a/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java b/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java index 8d606eeb45..7d6e23735b 100644 --- a/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java +++ b/addon/pl3xmap/src/main/java/com/ghostchu/quickshop/addon/pl3xmap/ShopMarkerManager.java @@ -54,11 +54,11 @@ public void addMarker(@NotNull final String key, @NotNull final Shop shop) { removeMarker(shop); - this.shopMarkers.get(shop.getLocation().getWorld().getName()).add(icon); + this.shopMarkers.get(shop.bukkitLocation().getWorld().getName()).add(icon); } public void removeMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); final String key = String.format("%s_%s_%s", Main.PL3X_KEY, worldName, shop.getShopId()); @@ -74,7 +74,7 @@ public void removeMarker(@NotNull final Shop shop) { } public Icon getIcon(@NotNull final String key, @NotNull final Shop shop) { - return Marker.icon(key, point(shop.getLocation()), Main.instance().icon()) + return Marker.icon(key, point(shop.bukkitLocation()), Main.instance().icon()) .setOptions(Options.builder() .tooltipDirection(Tooltip.Direction.TOP) .tooltipContent(fillPlaceholders(Main.instance().markerDetail(), shop)) @@ -89,7 +89,7 @@ public String plain(@NotNull final Component component) { private String fillPlaceholders(String s, final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getX()); final String y = String.valueOf(loc.getY()); final String z = String.valueOf(loc.getZ()); diff --git a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java index bc98a78597..8b80d3046e 100644 --- a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java +++ b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/HikariDataExtension.java @@ -120,7 +120,7 @@ public Table shopsTab() { final String item = dataUtil.getItemName(shop.getItem()) + " x" + shop.getShopStackingAmount(); String price = df.format(shop.getPrice()); if(main.getQuickShop().getEconomyManager().provider() != null) { - price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } final String type = switch(shop.shopType().identifier().toUpperCase(Locale.ROOT)) { @@ -128,7 +128,7 @@ public Table shopsTab() { case "FROZEN" -> "Frozen"; default -> "Selling"; }; - final String location = dataUtil.loc2String(shop.getLocation()); + final String location = dataUtil.loc2String(shop.bukkitLocation()); tableBuilder.addRow(owner, item, price, type, location); } return tableBuilder.build(); @@ -187,7 +187,7 @@ public Table shopsTabPlayer(final UUID playerUUID) { final String item = dataUtil.getItemName(shop.getItem()) + " x" + shop.getShopStackingAmount(); String price = df.format(shop.getPrice()); if(main.getQuickShop().getEconomyManager().provider() != null) { - price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + price = main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(shop.getPrice()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } final String type = switch(shop.shopType().identifier().toUpperCase(Locale.ROOT)) { @@ -195,7 +195,7 @@ public Table shopsTabPlayer(final UUID playerUUID) { case "FROZEN" -> "Frozen"; default -> "Selling"; }; - final String location = dataUtil.loc2String(shop.getLocation()); + final String location = dataUtil.loc2String(shop.bukkitLocation()); tableBuilder.addRow(item, price, type, location); } return tableBuilder.build(); diff --git a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java index 0357a302e2..2afcc57a94 100644 --- a/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java +++ b/addon/plan/src/main/java/com/ghostchu/quickshop/addon/plan/util/DataUtil.java @@ -32,7 +32,7 @@ public String formatEconomy(@NotNull final ShopMetricRecord record) { final DecimalFormat df = new DecimalFormat("#.00"); return df.format(record.getTotal()); } - return main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(record.getTotal()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + return main.getQuickShop().getEconomyManager().provider().format(BigDecimal.valueOf(record.getTotal()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); } @NotNull @@ -73,7 +73,7 @@ public String getShopName(@NotNull final ShopMetricRecord record, @NotNull final nameBuilder.append(ChatColor.stripColor(shopName)); } else { if(shop != null) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String template = "%s %s,%s,%s"; nameBuilder.append(String.format(template, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); } else { diff --git a/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java b/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java index 501fe8b00c..d8a24940e3 100644 --- a/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java +++ b/addon/squaremap/src/main/java/com/ghostchu/quickshop/addon/squaremap/ShopLayerProvider.java @@ -70,7 +70,7 @@ public void updateMarkers() { } public void updateShopMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); if(!registeredWorlds.containsKey(worldName)) { return; } @@ -86,14 +86,14 @@ public void updateShopMarker(@NotNull final Shop shop) { } public void removeShopMarker(@NotNull final Shop shop) { - final String worldName = shop.getLocation().getWorld().getName(); + final String worldName = shop.bukkitLocation().getWorld().getName(); final Key markerKey = Key.of(String.format("%s_%s_%s", Main.SQUAREMAP_KEY, worldName, shop.getShopId())); provider.removeMarker(markerKey); } @NotNull private Icon createShopIcon(@NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final Point point = Point.of(loc.getBlockX(), loc.getBlockZ()); final String tooltip = fillPlaceholders(Main.instance().markerTooltip(), shop); @@ -117,7 +117,7 @@ private String plain(@NotNull final Component component) { @NotNull private String fillPlaceholders(@NotNull final String text, @NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String x = String.valueOf(loc.getBlockX()); final String y = String.valueOf(loc.getBlockY()); final String z = String.valueOf(loc.getBlockZ()); diff --git a/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java b/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java index a6440132b4..5d46acb7a3 100644 --- a/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java +++ b/compatibility/advancedregionmarket/src/main/java/com/ghostchu/quickshop/compatibility/advancedregionmarket/Main.java @@ -87,7 +87,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } @@ -132,7 +132,7 @@ private void handleDeletion(final Region region) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - final Location shopLocation = shop.getLocation(); + final Location shopLocation = shop.bukkitLocation(); if(region.getRegion().contains(shopLocation.getBlockX(), shopLocation.getBlockY(), shopLocation.getBlockZ())) { getApi().getShopManager().deleteShop(shop); diff --git a/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java b/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java index 884ef3ff15..c90109a68a 100644 --- a/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java +++ b/compatibility/bentobox/src/main/java/com/ghostchu/quickshop/compatibility/bentobox/Main.java @@ -96,7 +96,7 @@ public void onIslandResetted(final IslandResettedEvent event) { @EventHandler(ignoreCancelled = true) public void permissionOverride(final ShopPermissionCheckEvent event) { - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); BentoBox.getInstance().getIslandsManager().getIslandAt(shopLoc).ifPresent(island->{ if(event.playerUUID().equals(island.getOwner())) { if(event.pluginNamespace().equals(QuickShop.getInstance().getJavaPlugin().getName()) && event.permissionNode().equals(BuiltInShopPermission.DELETE.getRawNode())) { diff --git a/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java b/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java index ee658060d1..9745874bc7 100644 --- a/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java +++ b/compatibility/common/src/main/java/com/ghostchu/quickshop/compatibility/CompatibilityModule.java @@ -44,7 +44,7 @@ public List getShops(@NotNull final String worldName, final int minX, fina } } final BoundingBox boundingBox = new BoundingBox(minX, Integer.MIN_VALUE, minZ, maxX, Integer.MAX_VALUE, maxZ); - return shopsList.stream().filter(s->boundingBox.contains(s.getLocation().toVector())).toList(); + return shopsList.stream().filter(s->boundingBox.contains(s.bukkitLocation().toVector())).toList(); } public List getShops(@NotNull final String worldName, final int chunkX, final int chunkZ) { diff --git a/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java b/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java index 85b33a0675..4f069edfd6 100644 --- a/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java +++ b/compatibility/dominion/src/main/java/com/ghostchu/quickshop/compatibility/dominion/Main.java @@ -145,7 +145,7 @@ public void onLandsDeleted(final DominionDeleteEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - final Location loc = event.getShop().getLocation(); + final Location loc = event.getShop().bukkitLocation(); if(loc == null) { return; } @@ -163,7 +163,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location loc = event.shop().get().getLocation(); + final Location loc = event.shop().get().bukkitLocation(); if(loc.getWorld() == null) { return; } diff --git a/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java b/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java index 3c8dce5622..dcb2cf8f5a 100644 --- a/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java +++ b/compatibility/fabledskyblock/src/main/java/com/ghostchu/quickshop/compatibility/fabledskyblock/Main.java @@ -119,7 +119,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location loc = event.shop().get().getLocation(); + final Location loc = event.shop().get().bukkitLocation(); final Island island = SkyBlockAPI.getIslandManager().getIslandAtLocation(loc); if(island == null) { @@ -183,6 +183,6 @@ public List getShops(@NotNull final String worldName, final int minX, fina } } final BoundingBox boundingBox = new BoundingBox(minX, Integer.MIN_VALUE, minZ, maxX, Integer.MAX_VALUE, maxZ); - return shopsList.stream().filter(s->boundingBox.contains(s.getLocation().toVector())).toList(); + return shopsList.stream().filter(s->boundingBox.contains(s.bukkitLocation().toVector())).toList(); } } diff --git a/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java b/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java index 04a789c4a4..15bb0d44f6 100644 --- a/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java +++ b/compatibility/griefprevention/src/main/java/com/ghostchu/quickshop/compatibility/griefprevention/Main.java @@ -114,7 +114,7 @@ private void handleMainClaimUnclaimedOrExpired(final Claim claim, final String l final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(claim.contains(shop.getLocation(), false, false)) { + if(claim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [System] Claim/SubClaim Unclaimed/Expired: " + logMessage, this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -128,8 +128,8 @@ private void handleMainClaimResized(final Claim oldClaim, final Claim newClaim) final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(oldClaim.contains(shop.getLocation(), false, false) && - !newClaim.contains(shop.getLocation(), false, false)) { + if(oldClaim.contains(shop.bukkitLocation(), false, false) && + !newClaim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] Claim Resized: ", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -151,8 +151,8 @@ private void handleSubClaimResizedHelper(final Claim claimVerifyChunks, final Cl final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!claimVerifyChunks.getOwnerID().equals(shop.getOwner().getUniqueId()) && - claimVerifyChunks.contains(shop.getLocation(), false, false) && - !claimVerifyShop.contains(shop.getLocation(), false, false)) { + claimVerifyChunks.contains(shop.bukkitLocation(), false, false) && + !claimVerifyShop.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Resized: ", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -194,7 +194,7 @@ public void onTransfer(final ClaimTransferEvent event) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { - if(event.getClaim().contains(shop.getLocation(), false, false)) { + if(event.getClaim().contains(shop.bukkitLocation(), false, false)) { continue; } @@ -271,7 +271,7 @@ private void handleSubClaimUnclaimed(final Claim subClaim) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!subClaim.getOwnerID().equals(shop.getOwner().getUniqueId()) && - subClaim.contains(shop.getLocation(), false, false)) { + subClaim.contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Unclaimed", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -332,7 +332,7 @@ public void onSubClaimCreated(final ClaimCreatedEvent event) { final List shops = getApi().getShopManager().getAllShops(); for(final Shop shop : shops) { if(!event.getClaim().getOwnerID().equals(shop.getOwner().getUniqueId()) && - event.getClaim().contains(shop.getLocation(), false, false)) { + event.getClaim().contains(shop.bukkitLocation(), false, false)) { getApi().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "GriefPrevention", false), String.format("[%s Integration]Shop %s deleted caused by [Single] SubClaim Created", this.getName(), shop), shop.saveToInfoStorage())); getApi().getShopManager().deleteShop(shop); } @@ -348,7 +348,7 @@ public void onTrading(final ShopPurchaseEvent event) { return; } - if(checkPermission(p, event.getShop().getLocation(), tradeLimits)) { + if(checkPermission(p, event.getShop().bukkitLocation(), tradeLimits)) { return; } event.setCancelled(true, getApi().getTextManager().of(event.getPurchaser(), "addon.griefprevention.trade-denied").forLocale()); @@ -363,7 +363,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { } Log.debug("GP-Compat: Starting override permission..."); - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(!griefPrevention.claimsEnabledForWorld(shopLoc.getWorld())) { final String worldName = shopLoc.getWorld() == null? "Null World" : shopLoc.getWorld().getName(); Log.debug("GP-Compat: World " + worldName + " not enabled for claims"); diff --git a/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java b/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java index 47b0148c08..228aee8fe7 100644 --- a/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java +++ b/compatibility/husktowns/src/main/java/com/ghostchu/quickshop/compatibility/husktowns/Main.java @@ -175,8 +175,8 @@ public void onUnClaimAll(final UnClaimAllEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(event.getShop().getLocation().getWorld() == null - || huskAPI.getClaimWorld(huskAPI.getWorld(event.getShop().getLocation().getWorld().getName())).isEmpty()) { + if(event.getShop().bukkitLocation().getWorld() == null + || huskAPI.getClaimWorld(huskAPI.getWorld(event.getShop().bukkitLocation().getWorld().getName())).isEmpty()) { if(ignoreDisabledWorlds) { return; @@ -192,7 +192,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java b/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java index 2aab6e3eb8..b86b8b42e3 100644 --- a/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java +++ b/compatibility/iridiumskyblock/src/main/java/com/ghostchu/quickshop/compatibility/iridiumskyblock/Main.java @@ -101,7 +101,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final Optional island = IridiumSkyblockAPI.getInstance().getIslandViaLocation(shopLoc); if(island.isEmpty()) { return; diff --git a/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java b/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java index 0ce42fcce6..23264dc709 100644 --- a/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java +++ b/compatibility/lands/src/main/java/com/ghostchu/quickshop/compatibility/lands/Main.java @@ -114,7 +114,7 @@ private void deleteShopInLand(final Land land, final UUID target) { if(target.equals(owner)) { recordDeletion(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "Lands", false), shop, "Lands: shop deleted because owner lost permission"); // Use regionThread for Folia/Canvas compatibility - must run on the region thread for this location - Util.regionThread(shop.getLocation(), ()->getApi().getShopManager().deleteShop(shop)); + Util.regionThread(shop.bukkitLocation(), ()->getApi().getShopManager().deleteShop(shop)); } } } @@ -135,8 +135,8 @@ public void onLandsPermissionChanges(final LandUntrustPlayerEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(event.getShop().getLocation().getWorld() == null - || landsIntegration.getWorld(event.getShop().getLocation().getWorld()) == null) { + if(event.getShop().bukkitLocation().getWorld() == null + || landsIntegration.getWorld(event.getShop().bukkitLocation().getWorld()) == null) { if(ignoreDisabledWorlds) { return; } @@ -151,7 +151,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java b/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java index 38c367aba4..4467dedc99 100644 --- a/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java +++ b/compatibility/openinv/src/main/java/com/ghostchu/quickshop/compatibility/openinv/OpenInvCommand.java @@ -42,7 +42,7 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ return; } if(shop.getInventory() instanceof EnderChestWrapper) { - shop.setInventory(new BukkitInventoryWrapper((((InventoryHolder)shop.getLocation().getBlock().getState()).getInventory())), plugin.getApi().getInventoryWrapperRegistry().get("QuickShop-Hikari")); + shop.setInventory(new BukkitInventoryWrapper((((InventoryHolder)shop.bukkitLocation().getBlock().getState()).getInventory())), plugin.getApi().getInventoryWrapperRegistry().get("QuickShop-Hikari")); sender.sendMessage(ChatColor.translateAlternateColorCodes('&', plugin.getConfig().getString("messages.to-chest"))); } else { shop.setInventory(new EnderChestWrapper(shop.getOwner().getUniqueIdIfRealPlayer().orElse(CommonUtil.getNilUniqueId()), plugin.getOpenInv(), plugin), plugin.getManager()); diff --git a/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java b/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java index 1f09b706fd..4b4a7be33d 100644 --- a/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java +++ b/compatibility/plotsquared/src/main/java/com/ghostchu/quickshop/compatibility/plotsquared/Main.java @@ -82,7 +82,7 @@ public void canCreateShopHere(final ShopCreateEvent event) { @EventHandler(ignoreCancelled = true) public void canTradeShopHere(final ShopPurchaseEvent event) { - final Location location = event.getShop().getLocation(); + final Location location = event.getShop().bukkitLocation(); final com.plotsquared.core.location.Location pLocation = com.plotsquared.core.location.Location.at( location.getWorld().getName(), location.getBlockX(), @@ -182,7 +182,7 @@ public void onPlotPlayerUntrusted(final com.plotsquared.core.events.PlayerPlotTr @EventHandler(ignoreCancelled = true) public void onShopTrading(final ShopPurchaseEvent event) { - final Location location = event.getShop().getLocation(); + final Location location = event.getShop().bukkitLocation(); final com.plotsquared.core.location.Location pLocation = com.plotsquared.core.location.Location.at( location.getWorld().getName(), location.getBlockX(), @@ -207,7 +207,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java b/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java index 4445132c0b..9c7cf31c42 100644 --- a/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java +++ b/compatibility/residence/src/main/java/com/ghostchu/quickshop/compatibility/residence/Main.java @@ -83,7 +83,7 @@ private boolean playerHas(final FlagPermissions permissions, final Player player @EventHandler(ignoreCancelled = true) public void onPurchase(final ShopPurchaseEvent event) { - final Location shopLoc = event.getShop().getLocation(); + final Location shopLoc = event.getShop().bukkitLocation(); final ClaimedResidence residence = Residence.getInstance().getResidenceManager().getByLoc(shopLoc); if(residence == null) { return; @@ -103,7 +103,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { if(!getConfig().getBoolean("allow-permission-override") || event.shop().isEmpty()) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final ClaimedResidence residence = ResidenceApi.getResidenceManager().getByLoc(shopLoc); diff --git a/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java b/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java index 6acd3146af..3dc6e1da71 100644 --- a/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java +++ b/compatibility/simpleclaimsystem/src/main/java/com/ghostchu/quickshop/compatibility/simpleclaimsystem/Main.java @@ -159,7 +159,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java b/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java index e20858ee6b..6db7053baa 100644 --- a/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java +++ b/compatibility/superiorskyblock/src/main/java/com/ghostchu/quickshop/compatibility/superiorskyblock/Main.java @@ -161,7 +161,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final Island island = SuperiorSkyblockAPI.getIslandAt(shopLoc); if(island == null) { return; diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java index 2df3e16265..67a7cbc6cf 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/Main.java @@ -273,11 +273,11 @@ private void purgeShops(@NotNull final Set worldCoords, @Nullable fi final QUser actor = QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "Towny", false); //Getting all shop with world-chunk-shop mapping for(final Shop shop : api.getShopManager().getAllShops()) { - if(!worldCoords.contains(WorldCoord.parseWorldCoord(shop.getLocation()))) { + if(!worldCoords.contains(WorldCoord.parseWorldCoord(shop.bukkitLocation()))) { continue; } if(overrideOwner || owner != null && owner.equals(shop.getOwner().getUniqueId())) { - Util.regionThread(shop.getLocation(), ()->{ + Util.regionThread(shop.bukkitLocation(), ()->{ recordDeletion(actor, shop, reason); getApi().getShopManager().deleteShop(shop); }); @@ -349,11 +349,11 @@ public void onPreCreation(final ShopCreateEvent event) { @EventHandler(ignoreCancelled = true) public void onTrading(final ShopPurchaseEvent event) { - if(isWorldIgnored(event.getShop().getLocation().getWorld())) { + if(isWorldIgnored(event.getShop().bukkitLocation().getWorld())) { return; } event.getPurchaser().getBukkitPlayer().ifPresent(player->{ - final Optional component = checkFlags(player, event.getShop().getLocation(), this.tradeFlags); + final Optional component = checkFlags(player, event.getShop().bukkitLocation(), this.tradeFlags); component.ifPresent(value->event.setCancelled(true, value)); }); } @@ -391,7 +391,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(isWorldIgnored(shopLoc.getWorld())) { return; } @@ -507,7 +507,7 @@ public void taxesAccountOverride(final ShopTaxAccountEvent event) { return; } // Modify tax account to town account if they aren't town shop or nation shop but inside town or nation - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town != null) { UUID uuid = QuickShop.getInstance().getPlayerFinder().name2Uuid(town.getAccount().getName()); if(uuid == null) { diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java index f9b05f8f68..143db2ac09 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/TownyShopUtil.java @@ -28,7 +28,7 @@ public static Nation getShopNation(@NotNull final Shop shop) { return null; } final Nation nation = TownyAPI.getInstance().getNation(UUID.fromString(uuid)); - Log.debug("Nation finding for shop " + shop.getLocation() + " => nation uuid: " + uuid + " nation: " + nation); + Log.debug("Nation finding for shop " + shop.bukkitLocation() + " => nation uuid: " + uuid + " nation: " + nation); return nation; } @@ -56,7 +56,7 @@ public static Town getShopTown(@NotNull final Shop shop) { return null; } final Town town = TownyAPI.getInstance().getTown(UUID.fromString(uuid)); - Log.debug("Town finding for shop " + shop.getLocation() + " => town uuid: " + uuid + " town: " + town); + Log.debug("Town finding for shop " + shop.bukkitLocation() + " => town uuid: " + uuid + " town: " + town); return town; } diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java index 4da40ffe47..e08ef02e3c 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/NationCommand.java @@ -66,7 +66,7 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ } // Set as a nation shop - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); return; diff --git a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java index 7979167e2e..4b39783650 100644 --- a/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java +++ b/compatibility/towny/src/main/java/com/ghostchu/quickshop/compatibility/towny/command/TownCommand.java @@ -60,16 +60,16 @@ public void onCommand(final Player sender, @NotNull final String commandLabel, @ } } // Set as a town shop - final Town town = TownyAPI.getInstance().getTown(shop.getLocation()); + final Town town = TownyAPI.getInstance().getTown(shop.bukkitLocation()); if(town == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); return; } if(plugin.getConfig().getBoolean("bank-mode.bank-plot-only", false)) { - final TownBlock townBlock = TownyAPI.getInstance().getTownBlock(shop.getLocation()); + final TownBlock townBlock = TownyAPI.getInstance().getTownBlock(shop.bukkitLocation()); if(townBlock == null) { plugin.getApi().getTextManager().of(sender, "addon.towny.target-shop-not-in-town-region").send(); - plugin.getLogger().warning("Failed to get townBlock at " + shop.getLocation() + " maybe a bug?"); + plugin.getLogger().warning("Failed to get townBlock at " + shop.bukkitLocation() + " maybe a bug?"); return; } if(townBlock.getType() != TownBlockType.BANK) { diff --git a/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java b/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java index 17c12b44b5..7b6367b121 100644 --- a/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java +++ b/compatibility/ultimateclaims/src/main/java/com/ghostchu/quickshop/compatibility/ultimateclaims/Main.java @@ -194,7 +194,7 @@ public void permissionOverride(final ShopPermissionCheckEvent event) { return; } - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); if(shopLoc.getWorld() == null) { return; } diff --git a/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java b/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java index fea0dc0348..cfd8449a16 100644 --- a/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java +++ b/compatibility/worldguard/src/main/java/com/ghostchu/quickshop/compatibility/worldguard/Main.java @@ -89,7 +89,7 @@ public void onPermissionCheck(final ShopPermissionCheckEvent event) { if(event.shop().isEmpty()) return; - final Location shopLoc = event.shop().get().getLocation(); + final Location shopLoc = event.shop().get().bukkitLocation(); final World world = shopLoc.getWorld(); if(world == null) return; @@ -121,7 +121,7 @@ public void onShopCreate(final ShopCreateEvent event) { final RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer(); final RegionQuery query = container.createQuery(); - final Location shopLocation = event.shop().get().getLocation(); + final Location shopLocation = event.shop().get().bukkitLocation(); final ApplicableRegionSet regions = query.getApplicableRegions(BukkitAdapter.adapt(shopLocation)); final RegionManager manager = container.get(BukkitAdapter.adapt(shopLocation.getWorld())); @@ -191,7 +191,7 @@ public void onShopPurchase(final ShopPurchaseEvent event) { event.getPurchaser().getBukkitPlayer().ifPresent(player->{ final RegionContainer container = WorldGuard.getInstance().getPlatform().getRegionContainer(); final RegionQuery query = container.createQuery(); - final Location shopLocation = event.getShop().getLocation(); + final Location shopLocation = event.getShop().bukkitLocation(); final ApplicableRegionSet regions = query.getApplicableRegions(BukkitAdapter.adapt(shopLocation)); final RegionManager manager = container.get(BukkitAdapter.adapt(shopLocation.getWorld())); diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopTypeEvent.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopTypeEvent.java deleted file mode 100644 index 43a28b3bc2..0000000000 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/event/settings/type/ShopTypeEvent.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.ghostchu.quickshop.api.event.settings.type; -/* - * QuickShop-Hikari - * Copyright (C) 2025 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.event.Phase; -import com.ghostchu.quickshop.api.event.settings.ShopSettingEvent; -import com.ghostchu.quickshop.api.shop.Shop; -import com.ghostchu.quickshop.api.shop.ShopType; -import org.jetbrains.annotations.NotNull; - -/** - * ShopTypeEvent represents an event that is tied to actions/retrieval of the ShopType setting for a - * shop. - * - * @author creatorfromhell - * @see ShopType - * @since 6.2.0.9 - * @deprecated Use ShopTypeEnhancedEvent instead - */ -@Deprecated(since = "6.2.0.11", forRemoval = true) -public class ShopTypeEvent extends ShopSettingEvent { - - public ShopTypeEvent(final @NotNull Phase phase, final @NotNull Shop shop, - final @NotNull ShopType old) { - - super(phase, shop, old); - } - - public ShopTypeEvent(final @NotNull Phase phase, final @NotNull Shop shop, - final @NotNull ShopType old, final @NotNull ShopType updated) { - - super(phase, shop, old, updated); - } - - public static ShopTypeEvent PRE(final @NotNull Shop shop, - final ShopType old) { - - return new ShopTypeEvent(Phase.PRE, shop, old); - } - - public static ShopTypeEvent PRE(final @NotNull Shop shop, - final ShopType old, final ShopType updated) { - - return new ShopTypeEvent(Phase.PRE, shop, old, updated); - } - - public static ShopTypeEvent MAIN(final @NotNull Shop shop, - final ShopType old) { - - return new ShopTypeEvent(Phase.MAIN, shop, old); - } - - public static ShopTypeEvent MAIN(final @NotNull Shop shop, - final ShopType old, final ShopType updated) { - - return new ShopTypeEvent(Phase.MAIN, shop, old, updated); - } - - public static ShopTypeEvent POST(final @NotNull Shop shop, - final ShopType old) { - - return new ShopTypeEvent(Phase.POST, shop, old); - } - - public static ShopTypeEvent POST(final @NotNull Shop shop, - final ShopType old, final ShopType updated) { - - return new ShopTypeEvent(Phase.POST, shop, old, updated); - } - - public static ShopTypeEvent RETRIEVE(final @NotNull Shop shop, - final ShopType old) { - - return new ShopTypeEvent(Phase.RETRIEVE, shop, old); - } - - public static ShopTypeEvent RETRIEVE(final @NotNull Shop shop, - final ShopType old, final ShopType updated) { - - return new ShopTypeEvent(Phase.RETRIEVE, shop, old, updated); - } - - /** - * Creates a new instance of PhasedEvent with the specified newPhase. - * - * @param newPhase The new Phase for the cloned PhasedEvent - * - * @return A new instance of PhasedEvent with the specified newPhase - */ - @Override - public ShopTypeEvent clone(final Phase newPhase) { - - if(this.updated != null) { - - return new ShopTypeEvent(newPhase, this.shop, this.old, this.updated); - } - return new ShopTypeEvent(newPhase, this.shop, this.old); - } - - /** - * Creates a clone of the ShopSettingEvent with the provided newPhase, old value, and updated - * value. - * - * @param newPhase The new phase for the cloned ShopSettingEvent - * @param old The old value for the cloned ShopSettingEvent - * @param updated The updated value for the cloned ShopSettingEvent - * - * @return A new instance of ShopSettingEvent with the specified newPhase, old, and updated values - */ - @Override - public ShopTypeEvent clone(final Phase newPhase, final ShopType old, final ShopType updated) { - - return new ShopTypeEvent(newPhase, this.shop, old, updated); - } -} \ No newline at end of file diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Locatable.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Locatable.java index 46ef11c79a..3f25e4e2b3 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Locatable.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/shop/Locatable.java @@ -19,6 +19,8 @@ * along with this program. If not, see . */ +import org.bukkit.Location; + /** * Locatable * @@ -33,4 +35,11 @@ public interface Locatable { * @return the location of type T associated with this object. */ T getLocation(); + + /** + * Converts the location associated with this object to a Bukkit-compatible {@link Location}. + * + * @return the Bukkit {@link Location} representation of this object's location. + */ + Location bukkitLocation(); } \ No newline at end of file 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 a7587f33c2..975abbf83c 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 @@ -35,7 +35,7 @@ /** * A shop */ -public interface Shop extends Locatable, ShopInventory, ShopMeta, ShopTrading, ShopDisplay, ShopPermission { +public interface Shop extends Locatable, ShopInventory, ShopMeta, ShopTrading, ShopDisplay, ShopPermission { NamespacedKey SHOP_NAMESPACED_KEY = new NamespacedKey(QuickShopAPI.getPluginInstance(), "shopsign"); 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/meta/ShopPrice.java index 716036cca3..0a7dc4251e 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/meta/ShopPrice.java @@ -18,6 +18,8 @@ * along with this program. If not, see . */ +import java.util.Comparator; + /** * ShopPrice * @@ -40,6 +42,29 @@ public interface ShopPrice { */ void price(U price); + /** + * 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 + */ + Comparator priceComparator(); + + /** + * Compares the price of the current shop with the price of another shop. + * + * @param other the other {@code ShopPrice} instance to compare with; must not be null + * @param reversed whether the comparison should be reversed (i.e., descending order) + * @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) { + + if(reversed) { + return priceComparator().reversed().compare(this.price(), other); + } + return priceComparator().compare(this.price(), other); + } + /** * 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. diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java index ec79d03a3d..6da4723175 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/QuickShop.java @@ -148,6 +148,7 @@ import net.tnemc.menu.paper.listener.PaperInventoryClickListener; import net.tnemc.menu.paper.listener.PaperInventoryCloseListener; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.PluginCommand; @@ -1034,12 +1035,12 @@ private void registerDisplayItem() { } logger.info("Registering DisplayCheck task...."); folia.getScheduler().runTimerAsync(()->{ - for(final Shop shop : getShopManager().getLoadedShops()) { + for(final Shop shop : getShopManager().getLoadedShops()) { //Shop may be deleted or unloaded when iterating if(!shop.isLoaded()) { continue; } - folia.getScheduler().runAtLocationLater(shop.getLocation(), shop::checkDisplay, 1L); + folia.getScheduler().runAtLocationLater((Location)shop.bukkitLocation(), shop::checkDisplay, 1L); } }, 1L, getDisplayItemCheckTicks()); } else if(getDisplayItemCheckTicks() == 0) { @@ -1231,7 +1232,7 @@ public final void onDisable() { } if(getShopManager() != null) { logger.info("Saving all in-memory changed shops..."); - final List> futures = getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); + final List<@NotNull CompletableFuture> futures = getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); logger.info("Shops needed saved: " + futures.size()); final CompletableFuture[] completableFutures = futures.toArray(new CompletableFuture[0]); @@ -1253,7 +1254,7 @@ public final void onDisable() { shop.updateSync(); } catch(final RuntimeException re) { - logger.warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.getLocation(), re); + logger.warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.bukkitLocation(), re); } } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java index 1fbe91e90a..e1450897ac 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Browse.java @@ -55,12 +55,12 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(world) { shops.addAll(plugin.getShopManager().getAllShops().stream().filter(shop->{ - if(shop.getLocation().getWorld() == null + if(shop.bukkitLocation().getWorld() == null || sender.getLocation().getWorld() == null) { return false; } - return shop.getLocation().getWorld().getUID().equals(sender.getLocation().getWorld().getUID()); + return shop.bukkitLocation().getWorld().getUID().equals(sender.getLocation().getWorld().getUID()); }).toList()); } else { shops.addAll(plugin.getShopManager().getAllShops()); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java index f697c4ffbf..ddd3735d4f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Clean.java @@ -33,7 +33,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String for(final Shop shop : plugin.getShopManager().getAllShops()) { try { - if(Util.isLoaded(shop.getLocation()) + if(Util.isLoaded(shop.bukkitLocation()) && shop.isSelling() && shop.getRemainingStock() == 0) { pendingRemoval.add( @@ -50,7 +50,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } for(final Shop shop : pendingRemoval) { - Util.regionThread(shop.getLocation(), ()->{ + Util.regionThread(shop.bukkitLocation(), ()->{ plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop clean", shop.saveToInfoStorage())); plugin.getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java index 0d87ef227c..1557a1e36b 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_CleanGhost.java @@ -44,7 +44,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final List> pendingTasks = new CopyOnWriteArrayList<>(); for(final Shop shop : plugin.getShopManager().getAllShops()) { - final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc)->{ + final CompletableFuture task = QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (loc)->{ if(shop == null) { return; // WTF } @@ -69,14 +69,14 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); return; } - if(!shop.getLocation().isWorldLoaded()) { + if(!shop.bukkitLocation().isWorldLoaded()) { plugin.text().of(sender, "cleanghost-deleting", shop.getShopId(), "unloaded world").send(); plugin.getShopManager().deleteShop(shop); deletionCounter.incrementAndGet(); plugin.logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "SYSTEM", false), "/quickshop cleanghost command", shop.saveToInfoStorage())); return; } - if(!Util.canBeShop(shop.getLocation().getBlock())) { + if(!Util.canBeShop(shop.bukkitLocation().getBlock())) { plugin.text().of(sender, "cleanghost-deleting", shop.getShopId(), "invalid shop block").send(); plugin.getShopManager().deleteShop(shop); deletionCounter.incrementAndGet(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java index 2881f5fd78..31398cae56 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Currency.java @@ -65,7 +65,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman plugin.text().of(sender, "currency-not-support").send(); return; } - if(!plugin.getEconomyManager().provider().supportsCurrency(Objects.requireNonNull(shop.getLocation().getWorld()).getName(), parser.getArgs().getFirst())) { + if(!plugin.getEconomyManager().provider().supportsCurrency(Objects.requireNonNull(shop.bukkitLocation().getWorld()).getName(), parser.getArgs().getFirst())) { plugin.text().of(sender, "currency-not-exists").send(); return; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java index 13e1f0e1a0..a20702f970 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Database.java @@ -83,7 +83,7 @@ private void handleTrim(@NotNull final CommandSender sender, @NotNull final List private void saveShops(final CommandSender sender, @NotNull final List subParams) { plugin.logger().info("Saving all in-memory changed shops..."); - final List> futures = plugin.getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); + final List futures = plugin.getShopManager().getAllShops().stream().filter(Shop::isDirty).map(Shop::update).toList(); plugin.logger().info("Shops needed saved: " + futures.size()); final CompletableFuture[] completableFutures = futures.toArray(new CompletableFuture[0]); @@ -105,7 +105,7 @@ private void saveShops(final CommandSender sender, @NotNull final List s shop.updateSync(); } catch(final RuntimeException re) { - plugin.logger().warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.getLocation(), re); + plugin.logger().warn("Issue occurred while saving a shop. This may cause data loss. Please check the logs for more information. ID: " + shop.getShopId() + " Location: " + shop.bukkitLocation(), re); } } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java index 1d406820e8..1e6c16dfb5 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Find.java @@ -87,7 +87,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman } //Calc distance between player and shop for(final Shop shop : scanPool) { - if(!Objects.equals(shop.getLocation().getWorld(), loc.getWorld())) { + if(!Objects.equals(shop.bukkitLocation().getWorld(), loc.getWorld())) { continue; } if(aroundShops.size() == shopLimit) { @@ -97,7 +97,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman && !plugin.perm().hasPermission(sender, "quickshop.other.search")) { continue; } - final Vector shopVector = shop.getLocation().toVector(); + final Vector shopVector = shop.bukkitLocation().toVector(); final double distance = shopVector.distance(playerVector); //Check distance if(distance <= maxDistance || global) { @@ -129,7 +129,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(usingOldLogic) { final Map.Entry closest = sortedShops.getFirst(); - final Location lookAt = closest.getKey().getLocation().clone().add(0.5, 0.5, 0.5); + final Location lookAt = closest.getKey().bukkitLocation().clone().add(0.5, 0.5, 0.5); sender.teleportAsync(Util.lookAt(sender.getEyeLocation(), lookAt).add(0, -1.62, 0), PlayerTeleportEvent.TeleportCause.UNKNOWN); plugin.text().of(sender, "nearby-shop-this-way", closest.getValue().intValue()).send(); @@ -138,7 +138,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman for(final Map.Entry shopDoubleEntry : sortedShops) { final Shop shop = shopDoubleEntry.getKey(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); ItemStack previewItemStack = shop.getItem().clone(); final ItemPreviewComponentPrePopulateEvent previewComponentPrePopulateEvent = new ItemPreviewComponentPrePopulateEvent(previewItemStack, sender); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java index 7046855197..562a9b07cf 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Info.java @@ -74,7 +74,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String final AtomicInteger noStockCounter = new AtomicInteger(0); final List> futures = new ArrayList<>(outOfStockCheckQueue.size()); for(final Shop shop : outOfStockCheckQueue) { - final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), task->{ + final CompletableFuture future = QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), task->{ try { if(shop.getRemainingStock() == 0) { noStockCounter.incrementAndGet(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java index 8d84117d8e..49a3d063ba 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Name.java @@ -76,7 +76,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman if(fee > 0) { if(!plugin.perm().hasPermission(sender, "quickshop.bypass.namefee")) { transaction = QSEconomyTransaction.builder() - .world(shop.getLocation().getWorld().getName()) + .world(shop.bukkitLocation().getWorld().getName()) .from(QUserImpl.createFullFilled(sender)) .to(shop.getTaxAccount()) .currency(plugin.getCurrency()) @@ -85,7 +85,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman .amount(BigDecimal.valueOf(fee)) .build(); if(!transaction.completable()) { - plugin.text().of(sender, "you-cant-afford-shop-naming", plugin.getShopManager().format(fee, shop.getLocation().getWorld(), plugin.getCurrency())).send(); + plugin.text().of(sender, "you-cant-afford-shop-naming", plugin.getShopManager().format(fee, shop.bukkitLocation().getWorld(), plugin.getCurrency())).send(); return; } } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java index f3bdcf834d..2b90c428f5 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_RemoveWorld.java @@ -35,7 +35,7 @@ public void onCommand(@NotNull final CommandSender sender, @NotNull final String } int shopsDeleted = 0; for(final Shop shop : plugin.getShopManager().getAllShops()) { - if(Objects.equals(shop.getLocation().getWorld(), world)) { + if(Objects.equals(shop.bukkitLocation().getWorld(), world)) { plugin.getShopManager().deleteShop(shop); shopsDeleted++; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java index 5dce12cbeb..7fadafbc7f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/command/subcommand/SubCommand_Sign.java @@ -49,7 +49,7 @@ public void onCommand(@NotNull final Player sender, @NotNull final String comman return; } for(final Sign sign : shop.getSigns()) { - plugin.getShopManager().makeShopSign(shop.getLocation().getBlock(), sign.getBlock(), material); + plugin.getShopManager().makeShopSign(shop.bukkitLocation().getBlock(), sign.getBlock(), material); shop.claimShopSign(sign); } shop.setSignText(plugin.text().findRelativeLanguages(sender)); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java index daccdb4c6e..9c8dbcb77c 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/database/SimpleDatabaseHelperV2.java @@ -21,7 +21,6 @@ import com.ghostchu.quickshop.database.bean.SimpleDataRecord; import com.ghostchu.quickshop.shop.ContainerShop; import com.ghostchu.quickshop.shop.cache.SimpleShopInventoryCountCache; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.performance.PerfMonitor; import org.apache.commons.lang3.tuple.Triple; @@ -854,7 +853,7 @@ public CompletableFuture updatePlayerProfileInBatch(final List updateShop(@NotNull final Shop shop) { final SimpleDataRecord simpleDataRecord = ((ContainerShop)shop).createDataRecord(); - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); // check if datarecord exists final long shopId = shop.getShopId(); if(shopId < 1) { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java index 19c78cb9b5..d2c3b2e35a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessor.java @@ -85,7 +85,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java index 098be6188a..6ea2523457 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/fworldedit/ShopProcessorLegacy.java @@ -80,7 +80,7 @@ public IChunkSet processSet(final IChunk chunk, final IChunkGet get, final IChun final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java index 8f6d7e3d23..cd49f27ee6 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/hook/worldedit/WorldEditBlockListener.java @@ -54,7 +54,7 @@ public > boolean setBlock(final BlockVector3 posit final Shop shop = QuickShop.getInstance().getShopManager().getShop(location, true); // Because WorldEdit can only remove half of shop, so we can keep another half as shop if it is doublechest shop. if(shop != null) { - Util.regionThread(shop.getLocation(), () -> { + Util.regionThread(shop.bukkitLocation(), () -> { QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(CommonUtil.getNilUniqueId(), "WorldEdit", false), "WorldEdit", shop.saveToInfoStorage())); QuickShop.getInstance().getShopManager().deleteShop(shop); }); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java index 52099e36fa..e7b6518c75 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/InternalListener.java @@ -79,13 +79,13 @@ public void shopCreate(final ShopCreateEvent event) { return; } - if(isForbidden(event.shop().get().getLocation().getBlock().getType(), event.shop().get().getItem().getType())) { + if(isForbidden(event.shop().get().bukkitLocation().getBlock().getType(), event.shop().get().getItem().getType())) { event.setCancelled(true, plugin.text().of(event.user(), "forbidden-vanilla-behavior").forLocale()); return; } if(loggingAction) { - plugin.logEvent(new ShopCreationLog(event.user(), event.shop().get().saveToInfoStorage(), new BlockPos(event.shop().get().getLocation()))); + plugin.logEvent(new ShopCreationLog(event.user(), event.shop().get().saveToInfoStorage(), new BlockPos(event.shop().get().bukkitLocation()))); } } @@ -131,13 +131,13 @@ public void shopInventoryCalc(final ShopInventoryCalculateEvent event) { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void shopPrePurchase(final ShopPurchaseEvent event) { - if(isForbidden(event.getShop().getLocation().getBlock().getType(), event.getShop().getItem().getType())) { + if(isForbidden(event.getShop().bukkitLocation().getBlock().getType(), event.getShop().getItem().getType())) { event.setCancelled(true, plugin.text().of(event.getPurchaser(), "forbidden-vanilla-behavior").forLocale()); return; } if(loggingBalance) { - plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); - plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(true, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); } } @@ -167,8 +167,8 @@ public void shopPurchase(final ShopSuccessPurchaseEvent event) { event.getTax())); } if(loggingBalance) { - plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); - plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().getLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getPurchaser(), plugin.getEconomyManager().provider().balance(event.getPurchaser(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); + plugin.logEvent(new PlayerEconomyPreCheckLog(false, event.getShop().getOwner(), plugin.getEconomyManager().provider().balance(event.getShop().getOwner(), event.getShop().bukkitLocation().getWorld().getName(), event.getShop().getCurrency()))); } if(event.getPurchaser().equals(event.getShop().getOwner())) { plugin.text().of(event.getPurchaser(), "shop-owner-self-trade").send(); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java index 39b7ec39c9..1e0db26d5e 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/listener/WorldListener.java @@ -54,8 +54,8 @@ public void onWorldLoad(final WorldLoadEvent e) { for(final Entry entry : oldInChunk.getValue().entrySet()) { final Shop shop = entry.getValue(); - shop.getLocation().setWorld(world); - inChunk.put(shop.getLocation(), shop); + shop.bukkitLocation().setWorld(world); + inChunk.put(shop.bukkitLocation(), shop); } } // Done - Now we can store the new world dataz! diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java index 49d0591f18..613b3030ff 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MainPage.java @@ -167,8 +167,8 @@ public void handle(final PageOpenCallback callback) { if(i >= (start + items)) break; - final String world = (shop.getLocation().getWorld() != null)? shop.getLocation().getWorld().getName() : "World"; - final String location = world + " " + shop.getLocation().getBlockX() + ", " + shop.getLocation().getBlockY() + ", " + shop.getLocation().getBlockZ(); + final String world = (shop.bukkitLocation().getWorld() != null)? shop.bukkitLocation().getWorld().getName() : "World"; + final String location = world + " " + shop.bukkitLocation().getBlockX() + ", " + shop.bukkitLocation().getBlockY() + ", " + shop.bukkitLocation().getBlockZ(); final QUser owner = shop.getOwner(); SkullProfile ownerProfile = null; if(owner.isRealPlayer() && owner.getUniqueId() != null) { @@ -178,7 +178,7 @@ public void handle(final PageOpenCallback callback) { } final EconomyProvider eco = QuickShop.getInstance().getEconomyManager().provider(); - final String priceFormatted = eco.format(BigDecimal.valueOf(shop.getPrice()), shop.getLocation().getWorld().getName(), shop.getCurrency()); + final String priceFormatted = eco.format(BigDecimal.valueOf(shop.getPrice()), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()); final AbstractItemStack stack = new BukkitItemStack().of(shop.getItem().getType().key().asString(), shop.getShopStackingAmount()) .lore(getConfigLore(id, shopItemConfig, shop.getOwner().getDisplay(), location, diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java index 7c80ca35e3..5972dcd11c 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/MarketUtils.java @@ -30,6 +30,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; /** * MarketUtils - Utility class for market/browse operations Handles grouping shops by item, @@ -208,8 +209,11 @@ public static List sortShops(@NotNull final List shops, final List sorted = new ArrayList<>(shops); switch(sortMode) { - case PRICE_ASC -> sorted.sort(Comparator.comparingDouble(Shop::getPrice)); - case PRICE_DESC -> sorted.sort(Comparator.comparingDouble(Shop::getPrice).reversed()); + case PRICE_ASC -> { + + sorted.sort((a, b) -> a.comparePrice(b.price(), false)); + } + case PRICE_DESC -> sorted.sort((a, b) -> a.comparePrice(b.price(), true)); case STOCK -> sorted.sort(Comparator.comparingInt(MarketUtils::getStockFromCache).reversed()); case NAME -> sorted.sort(Comparator.comparing(shop-> CommonUtil.prettifyText(shop.getItem().getType().name()))); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java index 60303a8d07..fb4394c992 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/browse/ShopListPage.java @@ -269,7 +269,7 @@ public void handle(final PageOpenCallback callback) { // Get teleport location - use shop location + 1 block up // Note: We avoid calling shop.getSigns() here as it requires block access // which can fail on Folia when the shop is in a different region - final Location teleportTarget = shop.getLocation().clone().add(0.5, 1, 0.5); + final Location teleportTarget = shop.bukkitLocation().clone().add(0.5, 1, 0.5); final IconBuilder iconBuilder = new IconBuilder(stack) .withSlot(listStartSlot + (i - start)); @@ -278,7 +278,7 @@ public void handle(final PageOpenCallback callback) { if(canTeleport) { // Capture for lambda final Location finalTeleportTarget = teleportTarget; - final Location shopLoc = shop.getLocation().clone().add(0.5, 0.5, 0.5); + final Location shopLoc = shop.bukkitLocation().clone().add(0.5, 0.5, 0.5); iconBuilder.withActions(new RunnableAction((click)->{ final Player p = Bukkit.getPlayer(click.player().identifier()); @@ -339,11 +339,11 @@ private List buildShopLore(final Shop shop, final double avgPrice, fi } // Location - final String world = shop.getLocation().getWorld() != null? - shop.getLocation().getWorld().getName() : "Unknown"; - final String coords = shop.getLocation().getBlockX() + ", " + - shop.getLocation().getBlockY() + ", " + - shop.getLocation().getBlockZ(); + final String world = shop.bukkitLocation().getWorld() != null? + shop.bukkitLocation().getWorld().getName() : "Unknown"; + final String coords = shop.bukkitLocation().getBlockX() + ", " + + shop.bukkitLocation().getBlockY() + ", " + + shop.bukkitLocation().getBlockZ(); lore.add(mm.deserialize("Location: " + world + "")); lore.add(mm.deserialize("" + coords + "")); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java index a370b8c6e3..0bfca6dea0 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/history/MainPage.java @@ -145,14 +145,14 @@ public void handle(final PageOpenCallback callback) { //header icon final Shop shop = shops.getFirst(); - final String world = (shop.getLocation().getWorld() != null)? shop.getLocation().getWorld().getName() : "World"; + final String world = (shop.bukkitLocation().getWorld() != null)? shop.bukkitLocation().getWorld().getName() : "World"; final Component shopName; if(shop.getShopName() != null) { shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-name", shop.getShopName()).forLocale(); } else { - shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-empty-name", world, shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()).forLocale(); + shopName = QuickShop.getInstance().text().of("history.shop.header-icon-shop-empty-name", world, shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()).forLocale(); } final Component shopType = QuickShop.getInstance().text().of(shop.shopType().translationKey()).forLocale(); @@ -179,10 +179,10 @@ public void handle(final PageOpenCallback callback) { shopType, shop.getOwner().getDisplay(), Util.getItemStackName(shop.getItem()), - shop.getPrice(), shop.getShopStackingAmount(), - shop.getLocation().getWorld().getName() + " " + shop.getLocation().getBlockX() - + ", " + shop.getLocation().getBlockY() + ", " - + shop.getLocation().getBlockZ())) + shop.price(), shop.getShopStackingAmount(), + shop.bukkitLocation().getWorld().getName() + " " + shop.bukkitLocation().getBlockX() + + ", " + shop.bukkitLocation().getBlockY() + ", " + + shop.bukkitLocation().getBlockZ())) .profile(ownerProfile)) .withActions(new SwitchPageAction(returnMenu, returnPage)) .withSlot(shopInfoSlot) diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java index 26003b0c36..521a64b328 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/keeper/MainPage.java @@ -133,7 +133,7 @@ public void open(final PageOpenCallback open) { try { final BigDecimal price = new BigDecimal(message); // Update price and reopen menu in the same region thread to ensure price is updated before GUI shows - Util.regionThread(shop.get().getLocation(), ()->{ + Util.regionThread(shop.get().bukkitLocation(), ()->{ ShopUtil.setPrice(QuickShop.getInstance(), QUserImpl.createFullFilled(player), price.doubleValue(), shop.get()); // Reopen menu after price is set final MenuPlayer menuPlayer = QuickShop.getInstance().createMenuPlayer(player); @@ -181,13 +181,13 @@ public void open(final PageOpenCallback open) { final StateIcon changeIcon = new StateIcon(buyingStack, null, "SHOP_TYPE", modeState, (currentState)->{ if(currentState.toUpperCase(Locale.ROOT).equals("SELLING")) { - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(BUYING_TYPE)); + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(BUYING_TYPE)); return "BUYING"; } else if(currentState.toUpperCase(Locale.ROOT).equals("FROZEN")) { - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(SELLING_TYPE)); + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(SELLING_TYPE)); return "SELLING"; } - Util.regionThread(shop.get().getLocation(), ()->shop.get().shopType(FROZEN_TYPE)); + Util.regionThread(shop.get().bukkitLocation(), ()->shop.get().shopType(FROZEN_TYPE)); return "FROZEN"; }); changeIcon.setSlot(modeToggleSlot); @@ -221,7 +221,7 @@ public void open(final PageOpenCallback open) { } viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); - Util.regionThread(shop.get().getLocation(), ()->{ + Util.regionThread(shop.get().bukkitLocation(), ()->{ player.openInventory(inventory.getHolder().getInventory()); QuickShop.inShop.add(player.getUniqueId()); @@ -318,7 +318,7 @@ public void open(final PageOpenCallback open) { .withActions(new GuiChatAction((message)->{ if(!message.isEmpty()) { if(message.equalsIgnoreCase("confirm")) { - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().deleteShop(shop.get())); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().deleteShop(shop.get())); QuickShop.getInstance().logEvent(new ShopRemoveLog(QUserImpl.createFullFilled(player), "/quickshop remove command", shop.get().saveToInfoStorage())); return true; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java index a629295eac..bb58f7dffc 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/staff/StaffSelectionPage.java @@ -266,7 +266,7 @@ public void handle(final PageOpenCallback callback) { if(!message.isEmpty()) { if(message.equalsIgnoreCase("confirm")) { if(shop.get().playerAuthorize(id, BuiltInShopPermission.OWNERSHIP_TRANSFER)) { - Util.regionThread(shop.get().getLocation(), ()->ShopUtil.transferRequest(id, uuid, name, shop.get())); + Util.regionThread(shop.get().bukkitLocation(), ()->ShopUtil.transferRequest(id, uuid, name, shop.get())); } else { QuickShop.getInstance().text().of(id, "no-permission").send(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java index a622b10aa2..6f55fce332 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/menu/trade/MainPage.java @@ -102,7 +102,7 @@ public void handle(final PageOpenCallback open) { final int stock = (shop.get().isBuying())? -1 : MarketUtils.getStockFromCache(shop.get()); final String stockString = (shop.get().isUnlimited())? "Unlimited" : stock + ""; final String priceFormatted = eco.format(BigDecimal.valueOf(shop.get().getPrice()), - shop.get().getLocation().getWorld().getName(), + shop.get().bukkitLocation().getWorld().getName(), shop.get().getCurrency()); // Shop item display slot from config (centered in row 2) @@ -167,11 +167,11 @@ public void handle(final PageOpenCallback open) { return true; } if(shop.get().isBuying()) { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); } else { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); } return true; } catch(final NumberFormatException ignore) { } @@ -197,7 +197,7 @@ public void handle(final PageOpenCallback open) { final int slot = configSlots.get(i); final int adjustedAmount = (amount * quantity); final String totalPrice = eco.format(BigDecimal.valueOf((quantity * shop.get().getPrice())), - shop.get().getLocation().getWorld().getName(), + shop.get().bukkitLocation().getWorld().getName(), shop.get().getCurrency()); final String displayText = (shop.get().isSelling())? "Buy x" + adjustedAmount + "" : "Sell x" + adjustedAmount + ""; @@ -207,13 +207,13 @@ public void handle(final PageOpenCallback open) { .withActions(new RunnableAction((click->{ if(shop.get().isBuying()) { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionBuying(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); } else { - final Info info = new SimpleInfo(shop.get().getLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); - Util.regionThread(shop.get().getLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); + final Info info = new SimpleInfo(shop.get().bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop.get(), false); + Util.regionThread(shop.get().bukkitLocation(), ()->QuickShop.getInstance().getShopManager().actionSelling(player, new BukkitInventoryWrapper(player.getInventory()), eco, info, shop.get(), quantity)); viewer.get().close(QuickShop.getInstance().createMenuPlayer(player)); } }))) diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java index fa5ed410f4..a6f2a3c45a 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/papi/PAPICache.java @@ -60,8 +60,8 @@ private String compileUniqueKey(@NotNull final UUID player, @NotNull final Strin private long getShopsInWorld(@NotNull final String world, final boolean loadedOnly) { return plugin.getShopManager().getAllShops().stream() - .filter(shop->shop.getLocation().getWorld() != null) - .filter(shop->shop.getLocation().getWorld().getName().equals(world)) + .filter(shop->shop.bukkitLocation().getWorld() != null) + .filter(shop->shop.bukkitLocation().getWorld().getName().equals(world)) .filter(shop->!loadedOnly || shop.isLoaded()) .count(); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java index 0a3475d754..e588f5e8de 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 @@ -96,14 +96,14 @@ public void init() { */ protected void addShopToLookupTable(@NotNull final Shop shop) { - final String world = shop.getLocation().getWorld().getName(); + final String world = shop.bukkitLocation().getWorld().getName(); final Map> inWorld = shops.computeIfAbsent(world, k->new MapMaker().initialCapacity(3).makeMap()); // There's no world storage yet. We need to create that map. // Put it in the data universe // Calculate the chunks coordinates. These are 1,2,3 for each chunk, NOT // location rounded to the nearest 16. - final int x = (int)Math.floor((shop.getLocation().getBlockX()) / 16.0); - final int z = (int)Math.floor((shop.getLocation().getBlockZ()) / 16.0); + final int x = (int)Math.floor((shop.bukkitLocation().getBlockX()) / 16.0); + final int z = (int)Math.floor((shop.bukkitLocation().getBlockZ()) / 16.0); // Get the chunk set from the world info final ShopChunk shopChunk = new SimpleShopChunk(world, x, z); final Map inChunk = @@ -111,8 +111,8 @@ protected void addShopToLookupTable(@NotNull final Shop shop) { // That chunk data hasn't been created yet - Create it! // Put it in the world // Put the shop in its location in the chunk list. - inChunk.put(shop.getLocation(), shop); - shopCache.invalidate(null, shop.getLocation()); + inChunk.put(shop.bukkitLocation(), shop); + shopCache.invalidate(null, shop.bukkitLocation()); } @Override @@ -184,7 +184,7 @@ public void unloadShop(@NotNull final Shop shop, final boolean dontTouchWorld) { */ private void removeShopFromLookupTable(@NotNull final Shop shop) { - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); final String world = Objects.requireNonNull(loc.getWorld()).getName(); final Map> inWorld = this.getShops().get(world); if(inWorld == null) { @@ -198,7 +198,7 @@ private void removeShopFromLookupTable(@NotNull final Shop shop) { return; } inChunk.remove(loc); - shopCache.invalidate(null, shop.getLocation()); + shopCache.invalidate(null, shop.bukkitLocation()); shopRuntimeUUIDCaching.invalidate(shop.getRuntimeRandomUniqueId()); } @@ -258,7 +258,7 @@ public CompletableFuture unregisterShop(@NotNull final Shop shop, final boole removeShopFromLookupTable(shop); if(!persist) return CompletableFuture.completedFuture(null); - final Location loc = shop.getLocation(); + final Location loc = shop.bukkitLocation(); return plugin.getDatabaseHelper().removeShopMap(loc.getWorld().getName(), loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()) .thenCombine(plugin.getDatabaseHelper().removeShop(shop.getShopId()), (a, b)->null) .exceptionally(throwable->{ @@ -475,11 +475,11 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean Log.debug("DEBUG: Setting shop id"); shop.setShopId(id); Log.debug("DEBUG: Creating shop map"); - plugin.getDatabaseHelper().createShopMap(id, shop.getLocation()).join(); + plugin.getDatabaseHelper().createShopMap(id, shop.bukkitLocation()).join(); Log.debug("DEBUG: Creating shop successfully"); shop.setDirty(); - new ShopCreateEvent(Phase.POST, shop, shop.getOwner(), shop.getLocation()).callEvent(); + new ShopCreateEvent(Phase.POST, shop, shop.getOwner(), shop.bukkitLocation()).callEvent(); }) .exceptionally(err->{ processCreationFail(shop, shop.getOwner(), err); @@ -558,7 +558,7 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean final List worldShops = new ArrayList<>(); for(final Shop shop : getAllShops()) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(location.isWorldLoaded() && Objects.equals(location.getWorld(), world)) { worldShops.add(shop); } @@ -571,7 +571,7 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean final List worldShops = new ArrayList<>(); for(final Shop shop : getAllShops()) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(location.isWorldLoaded() && com.ghostchu.quickshop.common.util.CommonUtil.strEquals(worldName, location.getWorld().getName())) { worldShops.add(shop); } 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 183a2d4254..91431ec8ec 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ContainerShop.java @@ -3,6 +3,7 @@ import com.ghostchu.quickshop.QuickShop; import com.ghostchu.quickshop.ServiceInjector; +import com.ghostchu.quickshop.api.economy.EconomyProvider; import com.ghostchu.quickshop.api.economy.benefit.BenefitProvider; import com.ghostchu.quickshop.api.event.Phase; import com.ghostchu.quickshop.api.event.general.ShopSignUpdateEvent; @@ -70,10 +71,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.math.BigDecimal; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; @@ -88,12 +91,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import static com.ghostchu.quickshop.util.Util.waitForFuture; +import static java.math.BigDecimal.ZERO; /** * ChestShop core */ @EqualsAndHashCode -public class ContainerShop implements Shop, Reloadable { +public class ContainerShop implements Shop, Reloadable { // We use deprecated method to create a fake quickshop-reremake namespace to trick bukkit to access legacy data. private static final NamespacedKey LEGACY_SHOP_NAMESPACED_KEY = new NamespacedKey("quickshop", "shopsign"); @@ -561,6 +565,17 @@ public void setItem(@NotNull final ItemStack item) { return this.location; } + /** + * Converts the location associated with this object to a Bukkit-compatible {@link Location}. + * + * @return the Bukkit {@link Location} representation of this object's location. + */ + @Override + public Location bukkitLocation() { + + return this.location; + } + /** * @return The name of the player who owns the shop. */ @@ -662,7 +677,7 @@ public void setPrice(final double price) { @Override public Double price() { - return 0.0; + return price; } /** @@ -672,7 +687,19 @@ public Double price() { */ @Override public void price(final Double price) { + this.price = price; + } + /** + * 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); } /** @@ -683,8 +710,31 @@ public void price(final Double price) { */ @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 0; + return Math.max(0, affordable.intValue()); } /** @@ -698,8 +748,30 @@ public int getMaxAffordable() { */ @Override public boolean canAfford(final int itemAmount) { + if(itemAmount <= 0) { + return false; + } + if(this.isUnlimited() || this.isFreeShop()) { + return true; + } - return false; + 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; } /** @@ -968,7 +1040,7 @@ public List getSignText(@NotNull final ProxiedLocale locale) { Util.ensureThread(false); final List signs = new ArrayList<>(4); - if(this.getLocation().getWorld() == null) { + if(this.bukkitLocation().getWorld() == null) { return Collections.emptyList(); } final Block[] blocks = new Block[4]; @@ -1070,7 +1142,7 @@ public boolean inventoryAvailable() { public boolean isAttached(@NotNull final Block b) { Util.ensureThread(false); - return this.getLocation().getBlock().equals(Util.getAttached(b)); + return this.bukkitLocation().getBlock().equals(Util.getAttached(b)); } @Override @@ -1233,7 +1305,7 @@ public boolean isShopSign(@NotNull final Sign sign) { } } if(shopSignStorage != null) { - return shopSignStorage.equals(getLocation().getWorld().getName(), getLocation().getBlockX(), getLocation().getBlockY(), getLocation().getBlockZ()); + return shopSignStorage.equals(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); } return false; } @@ -1279,7 +1351,7 @@ public boolean isValid() { if(this.isDeleted) { return false; } - return Util.canBeShop(this.getLocation().getBlock()); + return Util.canBeShop(this.bukkitLocation().getBlock()); } /** @@ -1549,8 +1621,8 @@ public void remove(@NotNull ItemStack item, final int amount) { @Override public ShopInfoStorage saveToInfoStorage() { - return new ShopInfoStorage(getLocation().getWorld().getName(), - new BlockPos(getLocation()), this.owner, this.price, + return new ShopInfoStorage(this.bukkitLocation().getWorld().getName(), + new BlockPos(this.bukkitLocation()), this.owner, this.price, QuickShop.getInstance().platform().encodeStack(this.originalItem), isUnlimited()? 1 : 0 , shopType().id(), saveExtraToYaml(), this.currency, this.disableDisplay, @@ -1767,7 +1839,7 @@ public void setSignText(@NotNull final List lines) { } if(plugin.getSignHooker() != null) { Log.debug("Start sign broadcast..."); - plugin.getSignHooker().updatePerPlayerShopSignBroadcast(getLocation(), this); + plugin.getSignHooker().updatePerPlayerShopSignBroadcast(this.bukkitLocation(), this); Log.debug("Sign broadcast completed."); } } @@ -1944,7 +2016,7 @@ public ReloadResult reloadModule() throws Exception { private ShopSignStorage saveToShopSignStorage() { - return new ShopSignStorage(getLocation().getWorld().getName(), getLocation().getBlockX(), getLocation().getBlockY(), getLocation().getBlockZ()); + return new ShopSignStorage(this.bukkitLocation().getWorld().getName(), this.bukkitLocation().getBlockX(), this.bukkitLocation().getBlockY(), this.bukkitLocation().getBlockZ()); } @Override diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java index 9a37f04372..d49959cfbd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/ShopLoader.java @@ -13,7 +13,6 @@ import com.ghostchu.quickshop.common.util.JsonUtil; import com.ghostchu.quickshop.common.util.Timer; import com.ghostchu.quickshop.economy.QSBenefitProvider; -import com.ghostchu.quickshop.util.PackageUtil; import com.ghostchu.quickshop.util.Util; import com.ghostchu.quickshop.util.logger.Log; import com.ghostchu.quickshop.util.paste.item.SubPasteItem; @@ -267,7 +266,7 @@ private boolean shopNullCheck(@Nullable final Shop shop) { Log.debug("Shop itemStack amount can't be 0"); return true; } - if(shop.getLocation() == null) { + if(shop.bukkitLocation() == null) { Log.debug("Shop location is null"); return true; } 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 7d46feebba..2c76021d25 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 @@ -394,7 +394,7 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor } BigDecimal fromTax = BigDecimal.ZERO; final QSEconomyTransaction transaction; - final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().amount(BigDecimal.valueOf(total)).toTax(new BigDecimal(taxEvent.getTax().interactorRate())).taxer(taxAccount).currency(shop.getCurrency()).world(shop.getLocation().getWorld().getName()).to(buyerQUser); + final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().amount(BigDecimal.valueOf(total)).toTax(new BigDecimal(taxEvent.getTax().interactorRate())).taxer(taxAccount).currency(shop.getCurrency()).world(shop.bukkitLocation().getWorld().getName()).to(buyerQUser); if(!shop.isUnlimited() || (plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners") && shop.isUnlimited())) { fromTax = new BigDecimal(taxEvent.getTax().shopRate()); @@ -404,7 +404,7 @@ public boolean actionBuying(@NotNull final Player buyer, @NotNull final Inventor } if(!transaction.completable()) { - plugin.text().of(buyer, "the-owner-cant-afford-to-buy-from-you", format((total + fromTax.doubleValue()), shop.getLocation().getWorld(), shop.getCurrency()), format(eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(buyer, "the-owner-cant-afford-to-buy-from-you", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return false; } if(!transaction.safeCommit()) { @@ -440,7 +440,7 @@ private void notifySold(@NotNull final QUser buyerQUser, @NotNull final Shop sho if(space == amount) { Component spaceWarn; if(shop.getShopName() == null) { - spaceWarn = plugin.text().of("shop-out-of-space", shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ()).forLocale(langCode); + spaceWarn = plugin.text().of("shop-out-of-space", shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ()).forLocale(langCode); } else { spaceWarn = plugin.text().of("shop-out-of-space-name", shop.getShopName(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } @@ -625,7 +625,7 @@ public boolean actionSelling(@NotNull final Player seller, @NotNull final Invent } } final BigDecimal fromTax = new BigDecimal(taxEvent.getTax().interactorRate()); - final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().from(sellerQUser).amount(BigDecimal.valueOf(total)).fromTax(fromTax).taxer(taxAccount).benefitManager(shop.getShopBenefit()).world(shop.getLocation().getWorld().getName()).currency(shop.getCurrency()); + final QSEconomyTransactionBuilder builder = QSEconomyTransaction.builder().from(sellerQUser).amount(BigDecimal.valueOf(total)).fromTax(fromTax).taxer(taxAccount).benefitManager(shop.getShopBenefit()).world(shop.bukkitLocation().getWorld().getName()).currency(shop.getCurrency()); if(!shop.isUnlimited() || (plugin.getConfig().getBoolean("shop.pay-unlimited-shop-owners") && shop.isUnlimited())) { transaction = builder.to(shop.getOwner()).toTax(new BigDecimal(taxEvent.getTax().shopRate())).build(); @@ -634,7 +634,7 @@ public boolean actionSelling(@NotNull final Player seller, @NotNull final Invent } if(!transaction.completable()) { - plugin.text().of(seller, "you-cant-afford-to-buy", format((total + fromTax.doubleValue()), shop.getLocation().getWorld(), shop.getCurrency()), format(eco.balance(sellerQUser, shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(seller, "you-cant-afford-to-buy", format((total + fromTax.doubleValue()), shop.bukkitLocation().getWorld(), shop.getCurrency()), format(eco.balance(sellerQUser, shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(), shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return false; } if(!transaction.safeCommit()) { @@ -716,7 +716,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock return; } // Check if target block is allowed shop-block - if(!Util.canBeShop(shop.getLocation().getBlock())) { + if(!Util.canBeShop(shop.bukkitLocation().getBlock())) { plugin.text().of(p, "chest-was-removed").send(); return; } @@ -736,7 +736,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock // Protection check if(!bypassProtectionCheck) { - final Result result = plugin.getPermissionChecker().canBuild(p, shop.getLocation()); + final Result result = plugin.getPermissionChecker().canBuild(p, shop.bukkitLocation()); if(!result.isSuccess()) { plugin.text().of(p, "3rd-plugin-build-check-failed", result.getMessage()).send(); if(plugin.perm().hasPermission(p, "quickshop.alerts")) { @@ -748,13 +748,13 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock } // Check if the shop is already created - if(plugin.getShopManager().getShop(shop.getLocation()) != null) { + if(plugin.getShopManager().getShop(shop.bukkitLocation()) != null) { plugin.text().of(p, "shop-already-owned").send(); return; } // Check if player and server allow double chest shop - if(Util.isDoubleChest(shop.getLocation().getBlock().getBlockData()) && !plugin.perm().hasPermission(p, "quickshop.create.double")) { + if(Util.isDoubleChest(shop.bukkitLocation().getBlock().getBlockData()) && !plugin.perm().hasPermission(p, "quickshop.create.double")) { plugin.text().of(p, "no-double-chests").send(); return; } @@ -789,7 +789,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock case PASS -> { // Calling ShopCreateEvent - ShopCreateEvent event = new ShopCreateEvent(Phase.PRE_CANCELLABLE, shop, shop.getOwner(), shop.getLocation()); + ShopCreateEvent event = new ShopCreateEvent(Phase.PRE_CANCELLABLE, shop, shop.getOwner(), shop.bukkitLocation()); if(event.callCancellableEvent()) { @@ -805,9 +805,9 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock createCost = 0; } if(createCost > 0) { - final QSEconomyTransaction economyTransaction = QSEconomyTransaction.builder().taxer(cacheTaxAccount).tax(BigDecimal.ZERO).from(QUserImpl.createFullFilled(p)).to(null).amount(BigDecimal.valueOf(createCost)).currency(plugin.getCurrency()).world(shop.getLocation().getWorld().getName()).build(); + final QSEconomyTransaction economyTransaction = QSEconomyTransaction.builder().taxer(cacheTaxAccount).tax(BigDecimal.ZERO).from(QUserImpl.createFullFilled(p)).to(null).amount(BigDecimal.valueOf(createCost)).currency(plugin.getCurrency()).world(shop.bukkitLocation().getWorld().getName()).build(); if(!economyTransaction.completable()) { - plugin.text().of(p, "you-cant-afford-a-new-shop", format(createCost, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "you-cant-afford-a-new-shop", format(createCost, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return; } if(!economyTransaction.safeCommit()) { @@ -826,7 +826,7 @@ public void createShop(@NotNull final Shop shop, @Nullable final Block signBlock // Shop info sign check if(signBlock != null && autoSign) { if(signBlock.getType().isAir() || signBlock.getType() == Material.WATER) { - final BlockState signState = this.makeShopSign(shop.getLocation().getBlock(), signBlock, null); + final BlockState signState = this.makeShopSign(shop.bukkitLocation().getBlock(), signBlock, null); if(signState instanceof final Sign puttedSign) { try { @@ -934,7 +934,7 @@ public void handleChat(@NotNull final Player p, @NotNull final String msg) { private void refundShop(final Shop shop) { - final World world = shop.getLocation().getWorld(); + final World world = shop.bukkitLocation().getWorld(); if(plugin.getConfig().getBoolean("shop.refund")) { double cost = plugin.getConfig().getDouble("shop.cost"); final QSEconomyTransaction transaction; @@ -1174,7 +1174,7 @@ private void notifyBought(@NotNull final QUser seller, @NotNull final Shop shop, if(stock == amount) { Component stockWarn; if(shop.getShopName() == null) { - stockWarn = plugin.text().of("shop-out-of-stock", shop.getLocation().getBlockX(), shop.getLocation().getBlockY(), shop.getLocation().getBlockZ(), Util.getItemStackName(shop.getItem())).forLocale(langCode); + stockWarn = plugin.text().of("shop-out-of-stock", shop.bukkitLocation().getBlockX(), shop.bukkitLocation().getBlockY(), shop.bukkitLocation().getBlockZ(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } else { stockWarn = plugin.text().of("shop-out-of-stock-name", shop.getShopName(), Util.getItemStackName(shop.getItem())).forLocale(langCode); } @@ -1226,7 +1226,7 @@ private int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull final final int shopHaveSpaces = Util.countSpace(shop.getInventory(), shop); final int invHaveItems = Util.countItems(new BukkitInventoryWrapper(p.getInventory()), shop); // Check if shop owner has enough money - final double ownerBalance = eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double ownerBalance = eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int ownerCanAfford; if(shop.getPrice() != 0) { ownerCanAfford = (int)(ownerBalance / shop.getPrice()); @@ -1254,7 +1254,7 @@ private int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull final if(ownerCanAfford == 0 && (!shop.isUnlimited() || payUnlimitedShopOwner)) { // when typed 'all' but the shop owner doesn't have enough money to buy at least 1 // item (and shop isn't unlimited or pay-unlimited is true) - plugin.text().of(p, "the-owner-cant-afford-to-buy-from-you", plugin.getShopManager().format(shop.getPrice(), shop.getLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(ownerBalance, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "the-owner-cant-afford-to-buy-from-you", plugin.getShopManager().format(shop.getPrice(), shop.bukkitLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(ownerBalance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return 0; } // when typed 'all' but player doesn't have any items to sell @@ -1403,7 +1403,7 @@ private void actionTrade(@NotNull final Player p, final Info info, @NotNull fina actionSelling(p, new BukkitInventoryWrapper(p.getInventory()), eco, info, shop, amount); } else { plugin.text().of(p, "shop-purchase-cancelled").send(); - plugin.logger().warn("Shop data broken? Loc: {}", shop.getLocation()); + plugin.logger().warn("Shop data broken? Loc: {}", shop.bukkitLocation()); } } @@ -1429,7 +1429,7 @@ private int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull fina } // typed 'all', check if player has enough money than price * amount final double price = shop.getPrice(); - final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); amount = Math.min(amount, (int)Math.floor(balance / price)); if(amount < 1) { // typed 'all' but the auto set amount is 0 // when typed 'all' but player can't buy any items @@ -1443,7 +1443,7 @@ private int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNull fina plugin.text().of(p, "not-enough-space", Component.text(invHaveSpaces)).send(); return 0; } - plugin.text().of(p, "you-cant-afford-to-buy", plugin.getShopManager().format(price, shop.getLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(balance, shop.getLocation().getWorld(), shop.getCurrency())).send(); + plugin.text().of(p, "you-cant-afford-to-buy", plugin.getShopManager().format(price, shop.bukkitLocation().getWorld(), shop.getCurrency()), plugin.getShopManager().format(balance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); } return 0; } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java index 93e29d7777..7f4ecbb7cd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/AbstractDisplayItem.java @@ -149,7 +149,7 @@ public static ItemStack createGuardItemStack(@NotNull ItemStack itemStack, @NotN public static ShopProtectionFlag createShopProtectionFlag( @NotNull final ItemStack itemStack, @NotNull final Shop shop) { - return new ShopProtectionFlag(shop.getLocation().toString(), Util.serialize(itemStack)); + return new ShopProtectionFlag(shop.bukkitLocation().toString(), Util.serialize(itemStack)); } public static boolean isVirtualDisplayDoesntWork() { @@ -226,7 +226,7 @@ protected void init() { final double y = PLUGIN.getConfig().getDouble("shop.display-coords.y", 0.8); final double z = PLUGIN.getConfig().getDouble("shop.display-coords.z", 0.5); - return this.shop.getLocation().clone().add(x, y, z); + return this.shop.bukkitLocation().clone().add(x, y, z); } /** diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java index 2a4674c68d..5a5a03a568 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/display/virtual/VirtualDisplayItem.java @@ -231,15 +231,15 @@ private void load() { Util.ensureThread(false); //some time shop can be loaded when world isn't loaded - chunkLocation = SimpleShopChunk.fromLocation(shop.getLocation()); + chunkLocation = SimpleShopChunk.fromLocation(shop.bukkitLocation()); manager.put(chunkLocation, this); //Let nearby player can saw fake item final List onlinePlayers = new ArrayList<>(Bukkit.getOnlinePlayers()); - onlinePlayers.removeIf(p->!p.getWorld().equals(shop.getLocation().getWorld())); + onlinePlayers.removeIf(p->!p.getWorld().equals(shop.bukkitLocation().getWorld())); for(final Player onlinePlayer : onlinePlayers) { - final double distance = onlinePlayer.getLocation().distance(shop.getLocation()); + final double distance = onlinePlayer.getLocation().distance(shop.bukkitLocation()); if(Math.abs(distance) > Bukkit.getViewDistance() * 16) { continue; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java index bbef4d6a00..ffe1965c3e 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/sign/SignHooker.java @@ -105,12 +105,12 @@ public void unload() { public void updatePerPlayerShopSignBroadcast(final Location location, final Shop shop) { - final World world = shop.getLocation().getWorld(); + final World world = shop.bukkitLocation().getWorld(); if(world == null) { return; } - QuickShop.folia().getScheduler().runAtLocation(shop.getLocation(), (loc)->{ - final Collection nearbyPlayers = world.getNearbyEntities(shop.getLocation(), Bukkit.getViewDistance() * 16, shop.getLocation().getWorld().getMaxHeight(), Bukkit.getViewDistance() * 16); + QuickShop.folia().getScheduler().runAtLocation(shop.bukkitLocation(), (loc)->{ + final Collection nearbyPlayers = world.getNearbyEntities(shop.bukkitLocation(), Bukkit.getViewDistance() * 16, shop.bukkitLocation().getWorld().getMaxHeight(), Bukkit.getViewDistance() * 16); for(final Entity nearbyPlayer : nearbyPlayers) { if(nearbyPlayer instanceof final Player player) { updatePerPlayerShopSign(player, location, shop); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java index a6d14371b9..d019638cbe 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/tax/ProgressiveProvider.java @@ -75,9 +75,9 @@ public String identifier() { public TaxRates calculateTax(final Shop shop, final QUser player) { final double interactorRate = (appliesTo.equalsIgnoreCase("player") - || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, player, getRate(player, shop.getLocation().getWorld().getName(), shop.getCurrency())) : 0.0; + || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, player, getRate(player, shop.bukkitLocation().getWorld().getName(), shop.getCurrency())) : 0.0; final double ownerRate = (appliesTo.equalsIgnoreCase("shop") - || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, shop.getOwner(), getRate(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency())) : 0.0; + || appliesTo.equalsIgnoreCase("both"))? normalizeRate(shop, shop.getOwner(), getRate(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency())) : 0.0; return new TaxRates(interactorRate, ownerRate); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java index 1fd2589d69..2d534e4f05 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/MetricDataUtil.java @@ -77,7 +77,7 @@ public String getShopName(@NotNull final ShopMetricRecord record, @NotNull final nameBuilder.append(ChatColor.stripColor(shopName)); } else { if(shop != null) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); final String template = "%s %s,%s,%s"; nameBuilder.append(String.format(template, location.getWorld().getName(), location.getBlockX(), location.getBlockY(), location.getBlockZ())); } else { diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java index 6eae2e1c6a..93de0b0a22 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/ShopUtil.java @@ -65,9 +65,9 @@ public class ShopUtil { public static boolean allowed(final Shop shop, final ItemStack itemStack) { - if(shop.getLocation().getWorld() != null) { + if(shop.bukkitLocation().getWorld() != null) { - final Block block = shop.getLocation().getBlock(); + final Block block = shop.bukkitLocation().getBlock(); return allowed(block, itemStack); } @@ -187,7 +187,7 @@ public static void setPrice(final QuickShop plugin, @NotNull final QUser user, f final QSEconomyTransaction transaction = QSEconomyTransaction.builder() .from(QUserImpl.createFullFilled(user.getBukkitPlayer().get())) .amount(BigDecimal.valueOf(fee)) - .world(Objects.requireNonNull(shop.getLocation().getWorld()).getName()) + .world(Objects.requireNonNull(shop.bukkitLocation().getWorld()).getName()) .currency(plugin.getCurrency()) .build(); if(!transaction.completable()) { @@ -237,12 +237,12 @@ public static boolean sellToShop(@NotNull final Player p, @Nullable final Shop s final double price = shop.getPrice(); final Inventory playerInventory = p.getInventory(); final String tradeAllWord = QuickShop.getInstance().getConfig().getString("shop.word-for-trade-all-items", "all"); - final double ownerBalance = eco.balance(shop.getOwner(), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double ownerBalance = eco.balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int items = getPlayerCanSell(shop, ownerBalance, price, new BukkitInventoryWrapper(playerInventory)); final ShopManager.InteractiveManager actions = QuickShop.getInstance().getShopManager().getInteractiveManager(); if(shop.playerAuthorize(p.getUniqueId(), BuiltInShopPermission.PURCHASE) || QuickShop.getInstance().perm().hasPermission(p, "quickshop.other.use")) { - final Info info = new SimpleInfo(shop.getLocation(), ShopAction.PURCHASE_SELL, null, null, shop, false); + final Info info = new SimpleInfo(shop.bukkitLocation(), ShopAction.PURCHASE_SELL, null, null, shop, false); actions.put(p.getUniqueId(), info); if(!direct) { if(shop.isStackingShop()) { @@ -315,11 +315,11 @@ public static boolean buyFromShop(@NotNull final Player p, @Nullable final Shop final Inventory playerInventory = p.getInventory(); final String tradeAllWord = QuickShop.getInstance().getConfig().getString("shop.word-for-trade-all-items", "all"); final ShopManager.InteractiveManager actions = QuickShop.getInstance().getShopManager().getInteractiveManager(); - final double traderBalance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); + final double traderBalance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int itemAmount = getPlayerCanBuy(shop, traderBalance, price, new BukkitInventoryWrapper(playerInventory)); if(shop.playerAuthorize(p.getUniqueId(), BuiltInShopPermission.PURCHASE) || QuickShop.getInstance().perm().hasPermission(p, "quickshop.other.use")) { - final Info info = new SimpleInfo(shop.getLocation(), ShopAction.PURCHASE_BUY, null, null, shop, false); + final Info info = new SimpleInfo(shop.bukkitLocation(), ShopAction.PURCHASE_BUY, null, null, shop, false); actions.put(p.getUniqueId(), info); if(!direct) { if(shop.isStackingShop()) { @@ -368,7 +368,7 @@ private static int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNul final int invHaveItems = Util.countItems(new BukkitInventoryWrapper(p.getInventory()), shop); // Check if shop owner has enough money final double ownerBalance = eco - .balance(shop.getOwner(), shop.getLocation().getWorld().getName(), + .balance(shop.getOwner(), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); final int ownerCanAfford; if(shop.getPrice() != 0) { @@ -401,9 +401,9 @@ private static int buyingShopAllCalc(@NotNull final EconomyProvider eco, @NotNul // when typed 'all' but the shop owner doesn't have enough money to buy at least 1 // item (and shop isn't unlimited or pay-unlimited is true) QuickShop.getInstance().text().of(p, "the-owner-cant-afford-to-buy-from-you", - QuickShop.getInstance().getShopManager().format(shop.getPrice(), shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(shop.getPrice(), shop.bukkitLocation().getWorld(), shop.getCurrency()), - QuickShop.getInstance().getShopManager().format(ownerBalance, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(ownerBalance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); return 0; } @@ -444,7 +444,7 @@ private static int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNu } // typed 'all', check if player has enough money than price * amount final double price = shop.getPrice(); - final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.getLocation().getWorld().getName(), + final double balance = eco.balance(QUserImpl.createFullFilled(p), shop.bukkitLocation().getWorld().getName(), shop.getCurrency()).doubleValue(); amount = Math.min(amount, (int)Math.floor(balance / price)); if(amount < 1) { // typed 'all' but the auto set amount is 0 @@ -463,9 +463,9 @@ private static int sellingShopAllCalc(@NotNull final EconomyProvider eco, @NotNu return 0; } QuickShop.getInstance().text().of(p, "you-cant-afford-to-buy", - QuickShop.getInstance().getShopManager().format(price, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(price, shop.bukkitLocation().getWorld(), shop.getCurrency()), - QuickShop.getInstance().getShopManager().format(balance, shop.getLocation().getWorld(), + QuickShop.getInstance().getShopManager().format(balance, shop.bukkitLocation().getWorld(), shop.getCurrency())).send(); } return 0; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java index 689785afb3..1870aa62fd 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/economyformatter/EconomyFormatter.java @@ -113,7 +113,7 @@ private String getInternalFormat(final double amount, @Nullable final String cur @NotNull public String format(final double n, @NotNull final Shop shop) { - return format(n, disableVaultFormat, shop.getLocation().getWorld(), shop); + return format(n, disableVaultFormat, shop.bukkitLocation().getWorld(), shop); } @NotNull diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java index 6606c98464..d247bcefc9 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/matcher/item/ModernCustomMatcher.java @@ -25,6 +25,7 @@ import com.ghostchu.simplereloadlib.Reloadable; import dev.dejvokep.boostedyaml.block.implementation.Section; import io.papermc.paper.datacomponent.DataComponentType; +import io.papermc.paper.datacomponent.DataComponentTypes; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.registry.RegistryKey; import org.bukkit.NamespacedKey; @@ -303,6 +304,7 @@ private void add(final Set> out, try { //System.out.println("ModernCustomMatcher.resolveTypeKeyFromDataComponentTypeKeys: " + name); final DataComponentType.Valued dataType = (DataComponentType.Valued)Registry.DATA_COMPONENT_TYPE.get(NamespacedKey.minecraft(name.toLowerCase(Locale.ROOT))); + return dataType; } catch(final ClassCastException ex) { //System.out.println("ModernCustomMatcher.resolveTypeKeyFromDataComponentTypeKeys: " + name + " is not a valued type!"); diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java index c444d7b8d2..9ff3a6bf4f 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/util/paste/item/ShopsInfoItem.java @@ -19,15 +19,15 @@ public ShopsInfoItem() { final QuickShop plugin = QuickShop.getInstance(); this.totalShops = String.valueOf(plugin.getShopManager().getAllShops().size()); plugin.getShopManager().getAllShops().stream() - .filter(shop->shop.getLocation().getWorld() != null) + .filter(shop->shop.bukkitLocation().getWorld() != null) .forEach(shop->{ - List worldShops = shopsMapping.get(shop.getLocation().getWorld().getName()); + List worldShops = shopsMapping.get(shop.bukkitLocation().getWorld().getName()); if(worldShops == null) { worldShops = new ArrayList<>(); } worldShops.add(shop); - shopsMapping.put(shop.getLocation().getWorld().getName(), worldShops); + shopsMapping.put(shop.bukkitLocation().getWorld().getName(), worldShops); }); } diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java index 5ddfcea839..f4734ef158 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/DisplayAutoDespawnWatcher.java @@ -65,8 +65,8 @@ public void run() { if(shop.isDisableDisplay()) { continue; } - final Location location = shop.getLocation(); - final World world = shop.getLocation().getWorld(); //Cache this, because it will took some time. + final Location location = shop.bukkitLocation(); + final World world = shop.bukkitLocation().getWorld(); //Cache this, because it will took some time. final AbstractDisplayItem displayItem = ((ContainerShop)shop).getDisplayItem(); if(displayItem != null) { // Check the range has player? diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java index 18f3f4b389..05eb4ae8f7 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/watcher/OngoingFeeWatcher.java @@ -50,7 +50,7 @@ public void run() { for(final Shop shop : plugin.getShopManager().getAllShops()) { if(!shop.isUnlimited() || !ignoreUnlimited) { final QUser shopOwner = shop.getOwner(); - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); if(!location.isWorldLoaded()) { //ignore unloaded world continue; @@ -121,7 +121,7 @@ public void stop() { */ public void removeShop(@NotNull final Shop shop) { - final Location location = shop.getLocation(); + final Location location = shop.bukkitLocation(); Util.regionThread(location, ()->plugin.getShopManager().deleteShop(shop)); MsgUtil.send(shop, shop.getOwner(), plugin.text().of("shop-removed-cause-ongoing-fee", LegacyComponentSerializer.legacySection().deserialize("World:" + Objects.requireNonNull(location.getWorld()).getName() From e71bc01b8fe05cd03f34d6728840fc60a6b8d805 Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Wed, 15 Apr 2026 14:49:52 -0400 Subject: [PATCH 13/29] Add TradeService to ShopManager, and integrate it into AbstractShopManager. Removed unused tag parser class in AbstractShopManager. --- .../quickshop/api/economy/EconomyManager.java | 1 + .../quickshop/api/shop/ShopManager.java | 10 +++ .../quickshop/economy/QSEconomyManager.java | 1 + .../quickshop/shop/AbstractShopManager.java | 69 ++++--------------- 4 files changed, 24 insertions(+), 57 deletions(-) diff --git a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/EconomyManager.java b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/EconomyManager.java index dcf300b915..c0ade25c14 100644 --- a/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/EconomyManager.java +++ b/quickshop-api/src/main/java/com/ghostchu/quickshop/api/economy/EconomyManager.java @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import com.ghostchu.quickshop.api.shop.trading.TradeService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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 07a3adcf53..8487a09aa0 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 @@ -6,6 +6,7 @@ import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; import com.ghostchu.quickshop.api.shop.state.ShopState; import com.ghostchu.quickshop.api.shop.tax.TaxManager; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Material; @@ -46,6 +47,15 @@ public interface ShopManager { */ TaxManager taxManager(); + /** + * Retrieves the TradeService associated with the EconomyManager. + * + * @return A non-null instance of TradeService, which provides functionality for executing + * and previewing trade operations such as buying from and selling to shops. + */ + @NotNull + TradeService tradeService(); + /** * Sets the shop layout provider to customize the layout of the shop. * diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java index 89900705f2..165b2d13d9 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/economy/QSEconomyManager.java @@ -19,6 +19,7 @@ import com.ghostchu.quickshop.api.economy.EconomyManager; import com.ghostchu.quickshop.api.economy.EconomyProvider; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java index e588f5e8de..a1766e1dd8 100644 --- a/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java +++ b/quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/AbstractShopManager.java @@ -10,6 +10,7 @@ import com.ghostchu.quickshop.api.shop.cache.ShopCache; import com.ghostchu.quickshop.api.shop.cache.ShopCacheNamespacedKey; import com.ghostchu.quickshop.api.shop.cache.ShopInventoryCountCache; +import com.ghostchu.quickshop.api.shop.trading.TradeService; import com.ghostchu.quickshop.common.util.QuickExecutor; import com.ghostchu.quickshop.shop.cache.BoxedShop; import com.ghostchu.quickshop.shop.cache.SimpleShopCache; @@ -65,6 +66,7 @@ public abstract class AbstractShopManager implements ShopManager { .initialCapacity(50) .build(); protected final QuickShop plugin; + protected final TradeService tradeService; protected final EconomyFormatter formatter; protected final Map>> shops = Maps.newConcurrentMap(); protected final Set loadedShops = Sets.newConcurrentHashSet(); // Handle it by collection to reduce @@ -77,6 +79,7 @@ public AbstractShopManager(@NotNull final QuickShop plugin) { Util.ensureThread(false); this.plugin = plugin; this.formatter = new EconomyFormatter(plugin); + this.tradeService = null; } public void init() { @@ -588,63 +591,15 @@ public CompletableFuture registerShop(@NotNull final Shop shop, final boolean return plugin.getDatabaseHelper().queryInventoryCache(shop.getShopId()); } + /** + * Retrieves the TradeService associated with the EconomyManager. + * + * @return A non-null instance of TradeService, which provides functionality for executing and + * previewing trade operations such as buying from and selling to shops. + */ + @Override + public @NotNull TradeService tradeService() { - static class TagParser { - - private final List tags; - private final ShopManager shopManager; - private final UUID tagger; - private final Map> singleCaching = new HashMap<>(); - - public TagParser(final UUID tagger, final ShopManager shopManager, final List tags) { - - Util.ensureThread(true); - this.shopManager = shopManager; - this.tags = tags; - this.tagger = tagger; - } - - public List parseTags() { - - final List finalShop = new ArrayList<>(); - for(final String tag : tags) { - final ParseResult result = parseSingleTag(tag); - if(result.getBehavior() == Behavior.INCLUDE) { - finalShop.addAll(result.getShops()); - } else if(result.getBehavior() == Behavior.EXCLUDE) { - finalShop.removeAll(result.getShops()); - } - } - return finalShop; - } - - public ParseResult parseSingleTag(final String tag) throws IllegalArgumentException { - - Util.ensureThread(true); - Behavior behavior = Behavior.INCLUDE; - if(tag.startsWith("-")) { - behavior = Behavior.EXCLUDE; - } - final String tagName = tag.substring(1); - if(tagName.isEmpty()) { - throw new IllegalArgumentException("Tag name can't be empty"); - } - final List shops = singleCaching.computeIfAbsent(tag, (t)->shopManager.queryTaggedShops(tagger, t).join()); - return new ParseResult(behavior, shops); - } - - enum Behavior { - INCLUDE, - EXCLUDE - } - - @AllArgsConstructor - @Data - static class ParseResult { - - private final Behavior behavior; - private final List shops; - } + return tradeService; } - } From 2b5ab08b0590c55e8ac3dc5e478d09396904a86a Mon Sep 17 00:00:00 2001 From: "Daniel V." Date: Wed, 15 Apr 2026 16:01:24 -0400 Subject: [PATCH 14/29] Refactor trade logic with `SimpleTradeService`, integrate into `Shop` classes, and ensure case-insensitive handling for shop states. --- pom.xml | 3 +- .../quickshop/api/shop/ShopManager.java | 5 +- .../quickshop/shop/AbstractShopManager.java | 4 +- .../quickshop/shop/ContainerShop.java | 107 +--- .../quickshop/shop/SimpleShopManager.java | 4 + .../quickshop/shop/SimpleTradeService.java | 457 ++++++++++++++++++ .../util/metric/faststats/FastStats.java | 4 +- 7 files changed, 488 insertions(+), 96 deletions(-) create mode 100644 quickshop-bukkit/src/main/java/com/ghostchu/quickshop/shop/SimpleTradeService.java diff --git a/pom.xml b/pom.xml index 05b4cdb27a..d2b3fd4b74 100644 --- a/pom.xml +++ b/pom.xml @@ -220,6 +220,7 @@ org.slf4j:slf4j-jdk14:* cc.carm.lib:* com.rollbar:* + dev.faststats.metrics:* net.kyori:adventure-api @@ -282,7 +283,7 @@ ${qs.relocation}.com.rollbar. - dev.faststats + dev.faststats. ${qs.relocation}.faststats