diff --git a/.gitignore b/.gitignore index fd4ec93..cc8dd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,7 @@ out/ # Linux temp files *~ -!gradle/wrapper/gradle-wrapper.jar \ No newline at end of file +!gradle/wrapper/gradle-wrapper.jar +/run1/ +/lib/ +/run/ diff --git a/build.gradle.kts b/build.gradle.kts index 1fa9fdb..ccd13ef 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,37 +1,53 @@ import java.io.ByteArrayOutputStream +import java.io.FileOutputStream +import java.net.URL +import net.minecrell.pluginyml.bukkit.BukkitPluginDescription plugins { id("java") id("net.minecrell.plugin-yml.bukkit") version "0.5.1" - id("com.github.johnrengelman.shadow") version "7.1.0" +// id("com.github.johnrengelman.shadow") version "7.1.0" + id("xyz.jpenilla.run-paper") version "1.0.6" } -allprojects { - group = "com.alttd.playershops" - version = "1.0-SNAPSHOT" - description = "Player Shop plugin for Altitude." +group = "com.alttd.playershops" +version = "1.0-SNAPSHOT" +description = "Player Shop plugin for Altitude." - apply() +apply() - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) - } - } - - tasks { - withType { - options.encoding = Charsets.UTF_8.name() - } - - withType { - options.encoding = Charsets.UTF_8.name() - } +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) } } +tasks { + withType { + options.encoding = Charsets.UTF_8.name() + } + + withType { + options.encoding = Charsets.UTF_8.name() + } + + runServer { + val fileName = "./run/galaxy.jar" + var file = File(fileName) + if (!file.parentFile.exists()) { + file.parentFile.mkdirs() + } + if (!file.exists()) { + download("https://repo.destro.xyz/snapshots/com/alttd/Galaxy-Server/Galaxy-paperclip-1.19.2-R0.1-SNAPSHOT-reobf.jar", fileName) + } + serverJar(file) + minecraftVersion("1.19.2") + } +} + + dependencies { - compileOnly("com.alttd:Galaxy-API:1.18.2-R0.1-SNAPSHOT") + compileOnly("com.alttd:Galaxy-API:1.19.2-R0.1-SNAPSHOT") compileOnly("com.github.milkbowl:VaultAPI:1.7") { exclude("org.bukkit","bukkit") } @@ -50,11 +66,82 @@ fun gitCommit(): String { return String(os.toByteArray()).trim() } +fun download(link: String, path: String) { + URL(link).openStream().use { input -> + FileOutputStream(File(path)).use { output -> + input.copyTo(output) + } + } +} + bukkit { name = rootProject.name main = "$group.${rootProject.name}" version = "${rootProject.version}-${gitCommit()}" - apiVersion = "1.18" + apiVersion = "1.19" authors = listOf("destro174") depend = listOf("Vault") + + commands { + register("playershop") { + description = "This is a test command!" + aliases = listOf("shop") + permission = "playershops.command.playershop" + } + } + + permissions { + register("playershops.admin") { + description = "Admin permission for the ${rootProject.name} plugin." + default = BukkitPluginDescription.Permission.Default.FALSE + children = listOf( + "playershops.shop.create", + "playershops.shop.break.other", + "playershops.shop.use", + "playershops.shop.use.buy", + "playershops.shop.use.sell", + "playershops.shop.use.gamble" + ) + } + register("playershops.shoplimit") { + description = "Base permission to allow per player shop limits." + default = BukkitPluginDescription.Permission.Default.FALSE + } + register("playershops.shop.create") { + description = "Allows players to create shops." + default = BukkitPluginDescription.Permission.Default.FALSE + } + register("playershops.shop.break") { + description = "Allows players to break shops." + default = BukkitPluginDescription.Permission.Default.FALSE + } + register("playershops.shop.break.other") { + description = "Allows players to break other players shops." + default = BukkitPluginDescription.Permission.Default.FALSE + children = listOf( + "playershops.shop.break", + ) + } + register("playershops.shop.use") { + description = "Allows players to use all playershops." + default = BukkitPluginDescription.Permission.Default.FALSE + children = listOf( + "playershops.shop.use.buy", + "playershops.shop.use.sell", + "playershops.shop.use.gamble" + ) + } + register("playershops.shop.use.buy") { + description = "Allows players to use buying playershops." + default = BukkitPluginDescription.Permission.Default.FALSE + } + register("playershops.shop.use.sell") { + description = "Allows players to use selling playershops." + default = BukkitPluginDescription.Permission.Default.FALSE + } + register("playershops.shop.use.gamble") { + description = "Allows players to use gamble playershops." + default = BukkitPluginDescription.Permission.Default.FALSE + } + } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f8050ed..71eb4e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,8 +1,5 @@ rootProject.name = "PlayerShops" -include(":api") -include(":plugin") - dependencyResolutionManagement { repositories { mavenCentral() diff --git a/src/main/java/com/alttd/playershops/PlayerShops.java b/src/main/java/com/alttd/playershops/PlayerShops.java index 7e73b5c..f571cef 100644 --- a/src/main/java/com/alttd/playershops/PlayerShops.java +++ b/src/main/java/com/alttd/playershops/PlayerShops.java @@ -1,10 +1,15 @@ package com.alttd.playershops; +import com.alttd.playershops.commands.ShopCommand; import com.alttd.playershops.config.Config; +import com.alttd.playershops.config.DatabaseConfig; import com.alttd.playershops.config.MessageConfig; +import com.alttd.playershops.gui.GuiIcon; import com.alttd.playershops.handler.ShopHandler; -import com.alttd.playershops.listener.PlayerListener; -import com.alttd.playershops.listener.ShopListener; +import com.alttd.playershops.listener.*; +import com.alttd.playershops.shop.ShopType; +import com.alttd.playershops.storage.database.DatabaseManager; +import com.alttd.playershops.storage.database.DatabaseHelper; import lombok.Getter; import net.milkbowl.vault.economy.Economy; import org.bukkit.Bukkit; @@ -19,9 +24,14 @@ public class PlayerShops extends JavaPlugin { private Economy econ = null; @Getter private ShopHandler shopHandler; - + @Getter + private DatabaseManager databaseManager; + @Getter + private DatabaseHelper databaseHelper; private ShopListener shopListener; private PlayerListener playerListener; + private TransactionListener transactionListener; + private InventoryListener inventoryListener; public void onEnable() { instance = this; @@ -32,11 +42,24 @@ public class PlayerShops extends JavaPlugin { } Bukkit.getLogger().info("Hooked into Vault economy provided by " + econ.getName()); reloadConfigs(); + if(!setupDatabase()) { + Bukkit.getLogger().warning("Error setting up database connection.\n Disabling plugin"); + this.setEnabled(false); + return; + } + + shopHandler = new ShopHandler(instance); registerListeners(); registerCommands(); + } - shopHandler = new ShopHandler(instance); + public void onDisable() { + shopHandler.unloadShops(); + unRegisterListeners(); + Bukkit.getScheduler().cancelTasks(this); + + databaseManager.unload(); } private boolean setupEconomy() { @@ -51,6 +74,13 @@ public class PlayerShops extends JavaPlugin { return true; } + private boolean setupDatabase() { + this.databaseManager = new DatabaseManager(this); + this.databaseHelper = new DatabaseHelper(this, this.databaseManager); + this.databaseHelper.init(); + return true; + } + public Economy getEconomy() { if(econ == null) setupEconomy(); @@ -60,22 +90,32 @@ public class PlayerShops extends JavaPlugin { private void registerListeners() { shopListener = new ShopListener(this); playerListener = new PlayerListener(this); + transactionListener = new TransactionListener(this); + inventoryListener = new InventoryListener(this); } - private void UnregisterListeners() { + private void unRegisterListeners() { shopListener.unregister(); playerListener.unregister(); + transactionListener.unregister(); + inventoryListener.unregister(); } private void registerCommands() { -// for(String command : this.getDescription().getCommands().keySet()) { -// getCommand(command).setExecutor(); -// } + getCommand("playershop").setExecutor(new ShopCommand()); } public void reloadConfigs() { Config.reload(); MessageConfig.reload(); + DatabaseConfig.reload(); + + for (ShopType shopType : ShopType.values()) { + // preload ShopType to get the configs active + } + for (GuiIcon guiIcon : GuiIcon.values()) { + // preload to get config values generated + } } } diff --git a/src/main/java/com/alttd/playershops/commands/ShopCommand.java b/src/main/java/com/alttd/playershops/commands/ShopCommand.java new file mode 100644 index 0000000..2e6c1d2 --- /dev/null +++ b/src/main/java/com/alttd/playershops/commands/ShopCommand.java @@ -0,0 +1,48 @@ +package com.alttd.playershops.commands; + +import com.alttd.playershops.gui.HomeGui; +import com.alttd.playershops.gui.ShopManagementGui; +import com.alttd.playershops.shop.PlayerShop; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +// expose brig in Galaxy? +public class ShopCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (!(sender instanceof Player player)) return true; + if (args.length == 0) { + return true; + } + switch (args[0].toLowerCase()) { + case "reload": + break; + case "open": + HomeGui gui = new HomeGui(player.getUniqueId()); + gui.open(); + break; + default: + sender.sendMessage("invalid command useage"); + break; + } + return false; + } + + @Override + public List onTabComplete(CommandSender sender, Command cmd, String label, String[] args) { + if (args.length == 1) { + return Stream.of("reload", "open") + .filter(arg -> arg.startsWith(args[0].toLowerCase())) + .collect(Collectors.toList()); + } + return null; + } +} diff --git a/src/main/java/com/alttd/playershops/config/Config.java b/src/main/java/com/alttd/playershops/config/Config.java index 08f65bf..67520f0 100644 --- a/src/main/java/com/alttd/playershops/config/Config.java +++ b/src/main/java/com/alttd/playershops/config/Config.java @@ -1,22 +1,19 @@ package com.alttd.playershops.config; import com.alttd.galaxy.configuration.AbstractConfiguration; -import com.alttd.playershops.shop.ShopType; import java.io.File; -import java.util.HashMap; @SuppressWarnings("unused") public class Config extends AbstractConfiguration { - public static File configPath = new File(System.getProperty("user.home") + File.separator + "share" + File.separator + "configs" + File.separator + "com/alttd/playershops"); + public static File configPath = new File(System.getProperty("user.home") + File.separator + "share" + File.separator + "configs" + File.separator + "playershops"); private Config() { super(Config.configPath, "config"); } static Config config; static int version; - static HashMap shopTypeConfigs; public static void reload() { config = new Config(); @@ -25,27 +22,18 @@ public class Config extends AbstractConfiguration { config.set("config-version", 1); config.readConfig(Config.class, null); - - shopTypeConfigs = new HashMap<>(); - for (ShopType shopType : ShopType.values()) { - shopTypeConfigs.put(shopType, new ShopTypeConfig(shopType.toString())); - } } public static int shopLimit = 100; public static boolean usePermissionShopLimit = false; public static String shopCreationWord = "[SHOP]"; + public static double shopCreationBalance = 2500; // minimum amount of balance to create a shop, this is to cover the cost to manage shops and upkeep private static void shopSettings() { String path = "shop-settings."; - shopLimit = config.getInt(path + "player-shop-limit", shopLimit); + shopLimit = config.getInt(path + "default-shop-limit", shopLimit); usePermissionShopLimit = config.getBoolean(path + "use-permission-based-shop-limit", usePermissionShopLimit); shopCreationWord = config.getString(path + "creation-word", shopCreationWord); - } - - public static String shopLimitPermission = "shop.buildlimit"; - private static void permissionSettings() { - String path = "permission."; - shopLimitPermission = config.getString(path + "build-limit", shopLimitPermission); + shopCreationBalance = config.getDouble(path + "creation-balance", shopCreationBalance); } } diff --git a/src/main/java/com/alttd/playershops/config/DatabaseConfig.java b/src/main/java/com/alttd/playershops/config/DatabaseConfig.java index 5e31be9..5777aa7 100644 --- a/src/main/java/com/alttd/playershops/config/DatabaseConfig.java +++ b/src/main/java/com/alttd/playershops/config/DatabaseConfig.java @@ -26,7 +26,7 @@ public class DatabaseConfig extends AbstractConfiguration { public static String DRIVER = "mysql"; public static String IP = "localhost"; public static String PORT = "3306"; - public static String DATABASE_NAME = "AltitudeQuests"; + public static String DATABASE_NAME = "PlayerShops"; public static String USERNAME = "root"; public static String PASSWORD = "root"; @@ -38,4 +38,12 @@ public class DatabaseConfig extends AbstractConfiguration { USERNAME = config.getString("database.username", USERNAME); PASSWORD = config.getString("database.password", PASSWORD); } + + // Time in seconds between database tasks + public static int queueDelay = 5; + public static int maxDatabaseConnections = 10; + private static void databaseSettings() { + queueDelay = config.getInt("database.queue.delay" , queueDelay); + maxDatabaseConnections = config.getInt("database.maximum-connections" , maxDatabaseConnections); + } } diff --git a/src/main/java/com/alttd/playershops/config/GuiIconConfig.java b/src/main/java/com/alttd/playershops/config/GuiIconConfig.java new file mode 100644 index 0000000..592776c --- /dev/null +++ b/src/main/java/com/alttd/playershops/config/GuiIconConfig.java @@ -0,0 +1,65 @@ +package com.alttd.playershops.config; + +import io.leangen.geantyref.TypeToken; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class GuiIconConfig { + + + private final String icon; + private final String configPath; + private final String defaultPath; + public GuiIconConfig(String icon) { + this.icon = icon; + this.configPath = "gui-icon." + this.icon + "."; + this.defaultPath = "gui-icon.divider."; + init(); + } + + static int version; + + public void init() { + Config.config.readConfig(GuiIconConfig.class, this); + } + + private static void set(String path, Object def) { + Config.config.set(path, def); + } + + private String getString(String path, String def) { + set(defaultPath + path, def); + return Config.config.getNode(configPath + path).getString( + Config.config.getNode(defaultPath + path).getString(def)); + } + + private int getInt(String path, int def) { + set(defaultPath + path, def); + return Config.config.getNode(configPath + path).getInt( + Config.config.getNode(defaultPath + path).getInt(def)); + } + + private List getStringList(String path, List def) { + try { + set(defaultPath + path, def); + return Config.config.getNode(configPath + path).getList(TypeToken.get(String.class), + Config.config.getNode(defaultPath + path).getList(TypeToken.get(String.class), def)); + } catch(SerializationException ignore) {} + return new ArrayList<>(); + } + + public int slot = 0; + public String displayName = "Default"; + public String material = "dirt"; + public List lore = new ArrayList<>(); + private void guiItems() { + slot = getInt("slot", slot); + material = getString("material", material); + displayName = getString("display-name", displayName); + lore = getStringList("lore", lore); + } + +} diff --git a/src/main/java/com/alttd/playershops/config/MessageConfig.java b/src/main/java/com/alttd/playershops/config/MessageConfig.java index e65e161..dfcb5f3 100644 --- a/src/main/java/com/alttd/playershops/config/MessageConfig.java +++ b/src/main/java/com/alttd/playershops/config/MessageConfig.java @@ -1,9 +1,6 @@ package com.alttd.playershops.config; import com.alttd.galaxy.configuration.AbstractConfiguration; -import com.alttd.playershops.shop.ShopType; - -import java.util.HashMap; public class MessageConfig extends AbstractConfiguration { @@ -13,7 +10,6 @@ public class MessageConfig extends AbstractConfiguration { static MessageConfig config; static int version; - static HashMap shopTypeConfigs; public static void reload() { config = new MessageConfig(); @@ -22,11 +18,6 @@ public class MessageConfig extends AbstractConfiguration { config.set("config-version", 1); config.readConfig(Config.class, null); - - shopTypeConfigs = new HashMap<>(); - for (ShopType shopType : ShopType.values()) { - shopTypeConfigs.put(shopType, new ShopTypeConfig(shopType.toString())); - } } public static String SHOP_ALREADY_EXISTS = "This block is already a Shop"; diff --git a/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java b/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java index e86fd7b..f5a4855 100644 --- a/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java +++ b/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java @@ -1,5 +1,11 @@ package com.alttd.playershops.config; +import io.leangen.geantyref.TypeToken; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.util.ArrayList; +import java.util.List; + public class ShopTypeConfig { private final String shopType; @@ -27,4 +33,41 @@ public class ShopTypeConfig { Config.config.getNode(defaultPath + path).getString(def)); } + private List getStringList(String path, List def) { + try { + set(defaultPath + path, def); + return Config.config.getNode(configPath + path).getList(TypeToken.get(String.class), + Config.config.getNode(defaultPath + path).getList(TypeToken.get(String.class), def)); + } catch(SerializationException ignore) {} + return new ArrayList<>(); + } + + public List activeSignLines = List.of("[Shop]", "", "", ""); + public List inActiveSignLines = List.of("[Shop]", "right click", "to manage", ""); + public List expiredSignLines = List.of("[Shop]", "expired", "", ""); + public List deletedSignLines = List.of("[Shop]", "deleted", "", ""); + private void signSettings() { + activeSignLines = getStringList("sign.active-lines", activeSignLines); + inActiveSignLines = getStringList("sign.inactive-lines", inActiveSignLines); + expiredSignLines = getStringList("sign.expired-lines", expiredSignLines); + deletedSignLines = getStringList("sign.deleted-lines", deletedSignLines); + } + + public String playerInventoryFull = "You do not have enough space in your inventory to buy from this shop."; + public String playerNoFunds = "You do not have sufficient funds to trade with this shop."; + public String playerBought = "You bought (s) from for ."; + public String shopSold = " bought (s) from you for ."; + public String shopInventoryFull = "This shop does not have enough space in its inventory."; + public String shopNoStock = "This shop is out of stock."; + public String yourShopNoFunds = "Your shop at is out of funds"; + public String yourShopNoStock = "Your shop at is out of stock"; + private void transactionMessages() { + playerInventoryFull = getString("transaction.player-inventory-full", playerInventoryFull); + playerNoFunds = getString("transaction.player-no-funds", playerNoFunds); + shopInventoryFull = getString("transaction.shop-inventory-full", shopInventoryFull); + shopNoStock = getString("transaction.shop-no-stock", shopNoStock); + yourShopNoFunds = getString("transaction.your-shop-no-funds", yourShopNoFunds); + yourShopNoStock = getString("transaction.your-shop-no-stock", yourShopNoStock); + } + } diff --git a/src/main/java/com/alttd/playershops/conversation/ConversationManager.java b/src/main/java/com/alttd/playershops/conversation/ConversationManager.java new file mode 100644 index 0000000..166669c --- /dev/null +++ b/src/main/java/com/alttd/playershops/conversation/ConversationManager.java @@ -0,0 +1,216 @@ +package com.alttd.playershops.conversation; + +import com.alttd.playershops.gui.ShopManagementGui; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.shop.ShopType; +import com.alttd.playershops.utils.EconomyUtils; +import com.google.common.base.Joiner; +import org.bukkit.conversations.*; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +public class ConversationManager implements ConversationAbandonedListener { + + PlayerShop playerShop; + Player player; + Conversation conversation; + + public ConversationManager(JavaPlugin plugin, Player player, ConversationType conversationType, PlayerShop playerShop) { + if (player.isConversing()) return; + this.player = player; + this.playerShop = playerShop; + ConversationFactory conversationFactory = new ConversationFactory(plugin) + .withModality(true) + .withFirstPrompt(getPrompt(conversationType)) + .addConversationAbandonedListener(this) + .withEscapeSequence("cancel"); + conversation = conversationFactory.buildConversation(player); + conversation.begin(); + } + + Prompt getPrompt(ConversationType conversationType) { + switch (conversationType) { + case CHANGE_ITEM -> { + return new ChangeItemPrompt(); + } + case CHANGE_AMOUNT -> { + return new ChangeAmountPrompt(); + } + case CHANGE_TYPE -> { + return new ChangeTypePrompt(); + } + case WITHDRAW_BALANCE -> { + return new WithdrawBalancePrompt(); + } + case ADD_BALANCE -> { + return new AddBalancePrompt(); + } + case CHANGE_PRICE -> { + return new ChangePricePrompt(); + } + }; + return null; + } + + private void openGui() { + player.abandonConversation(conversation); + ShopManagementGui shopManagementGui = new ShopManagementGui(player.getUniqueId(), playerShop); + shopManagementGui.open(); + } + + @Override + public void conversationAbandoned(@NotNull ConversationAbandonedEvent abandonedEvent) { +// abandonedEvent.getContext().getForWhom().sendRawMessage("Conversation ended."); + } + + private class ChangeTypePrompt extends FixedSetPrompt { + + public ChangeTypePrompt() { + super("buy", "sell", "none"); // todo can this be automated shoptype.values() + } + + public @NotNull String getPromptText(ConversationContext context) { + return "What shoptype would you like to use: " + Joiner.on(", ").join(fixedSet) + " Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, String input) { + ShopType newType = ShopType.fromString(input); + if (playerShop.getType() == newType) { + context.getForWhom().sendRawMessage("Shoptype was already set to " + newType.toString() + " Type cancel to cancel this action."); + return new ChangeTypePrompt(); + } + playerShop.setShopType(newType); + openGui(); + return END_OF_CONVERSATION; + } + + } + + private class ChangePricePrompt extends NumericPrompt { + + public @NotNull String getPromptText(ConversationContext context) { + return "What should the price be? Type cancel to cancel this action."; + } + + @Override + protected boolean isNumberValid(ConversationContext context, Number input) { + return input.doubleValue() >= 0; + } + + @Override + protected String getFailedValidationText(ConversationContext context, Number invalidInput) { + return "Input must 0 or higher. Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, Number number) { + playerShop.setPrice(number.doubleValue()); + openGui(); + return END_OF_CONVERSATION; + } + + } + + private class ChangeAmountPrompt extends NumericPrompt { + + public @NotNull String getPromptText(ConversationContext context) { + return "How many items would you like to sell? Type cancel to cancel this action."; + } + + @Override + protected boolean isNumberValid(ConversationContext context, Number input) { + return input.intValue() > 0 && input.intValue() <= 3456; + } + + @Override + protected String getFailedValidationText(ConversationContext context, Number invalidInput) { + return "Input must be between 1 and 3456. Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, Number number) { + playerShop.setAmount(number.intValue()); + openGui(); + return END_OF_CONVERSATION; + } + + } + + private class AddBalancePrompt extends NumericPrompt { + + public @NotNull String getPromptText(ConversationContext context) { + return "How much money would you like to add? Type cancel to cancel this action."; + } + + @Override + protected boolean isNumberValid(ConversationContext context, Number input) { + if (input.doubleValue() < 0) + return false; + return EconomyUtils.hasSufficientFunds(player, input.doubleValue()); + } + + @Override + protected String getFailedValidationText(ConversationContext context, Number invalidInput) { + return "You do not have enough balance to deposit this amount. Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, Number number) { + playerShop.addBalance(number.doubleValue()); + EconomyUtils.removeFunds(player, number.doubleValue()); + openGui(); + return END_OF_CONVERSATION; + } + + } + + private class WithdrawBalancePrompt extends NumericPrompt { + + public @NotNull String getPromptText(ConversationContext context) { + return "How much money would you like to withdraw? Type cancel to cancel this action."; + } + + @Override + protected boolean isNumberValid(ConversationContext context, Number input) { + if (input.doubleValue() < 0) + return false; + return EconomyUtils.hasSufficientFunds(playerShop, input.doubleValue()); + } + + @Override + protected String getFailedValidationText(ConversationContext context, Number invalidInput) { + return "Your shop does not have enough balance to withdraw this amount. Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, Number number) { + playerShop.removeBalance(number.doubleValue()); + EconomyUtils.addFunds(player, number.doubleValue()); + openGui(); + return END_OF_CONVERSATION; + } + + } + + private class ChangeItemPrompt extends FixedSetPrompt { + + ChangeItemPrompt() { + super("continue"); + } + + public @NotNull String getPromptText(ConversationContext context) { + return "Hold the item you would like to sell and type continue? Type cancel to cancel this action."; + } + + @Override + protected Prompt acceptValidatedInput(ConversationContext context, String input) { + playerShop.setItemStack(player.getInventory().getItemInMainHand().clone()); + openGui(); + return END_OF_CONVERSATION; + } + + } + +} diff --git a/src/main/java/com/alttd/playershops/conversation/ConversationType.java b/src/main/java/com/alttd/playershops/conversation/ConversationType.java new file mode 100644 index 0000000..6c6d6cb --- /dev/null +++ b/src/main/java/com/alttd/playershops/conversation/ConversationType.java @@ -0,0 +1,10 @@ +package com.alttd.playershops.conversation; + +public enum ConversationType { + CHANGE_ITEM, + CHANGE_AMOUNT, + CHANGE_TYPE, + CHANGE_PRICE, + WITHDRAW_BALANCE, + ADD_BALANCE; +} diff --git a/src/main/java/com/alttd/playershops/database/Database.java b/src/main/java/com/alttd/playershops/database/Database.java deleted file mode 100644 index ebebe39..0000000 --- a/src/main/java/com/alttd/playershops/database/Database.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.alttd.playershops.database; - -import com.alttd.playershops.PlayerShops; -import com.alttd.playershops.config.DatabaseConfig; -import com.alttd.playershops.utils.Logger; -import org.bukkit.Bukkit; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; - -public class Database { - - private static Database instance = null; - private Connection connection = null; - - private Database() {} - - public static Database getDatabase(){ - if (instance == null) - { - instance = new Database(); - instance.init(); - } - return (instance); - } - - protected void init() { - try { - openConnection(); - } catch (SQLException e) { - e.printStackTrace(); - } - - //Run all create table functions - for (Method method : Database.class.getDeclaredMethods()) { - if (Modifier.isPrivate(method.getModifiers())) { - if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { - try { - method.setAccessible(true); - method.invoke(instance); - } catch (InvocationTargetException ex) { - throw new RuntimeException(ex.getCause()); - } catch (Exception ex) { - Logger.severe("Error invoking " + method + "."); - ex.printStackTrace(); - } - } - } - } - } - - /** - * Opens the connection if it's not already open. - * @throws SQLException If it can't create the connection. - */ - private void openConnection() throws SQLException { - if (connection != null && !connection.isClosed()) { - return; - } - - synchronized (this) { - if (connection != null && !connection.isClosed()) { - return; - } - try { - Class.forName("com.mysql.cj.jdbc.Driver"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - - connection = DriverManager.getConnection( - "jdbc:mysql://" + DatabaseConfig.IP + ":" + DatabaseConfig.PORT + "/" + DatabaseConfig.DATABASE_NAME + - "?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true", - DatabaseConfig.USERNAME, DatabaseConfig.PASSWORD); - } - } - - public Connection getConnection() { - try { - openConnection(); - } catch (SQLException e) { - e.printStackTrace(); - } - return connection; - } - - private static void createShopTable() { - try { - String sql = "CREATE TABLE IF NOT EXISTS shops(" + - "id INT NOT NULL AUTO_INCREMENT, " + - "owner_name VARCHAR(16) NOT NULL, " + - "owner_uuid VARCHAR(36) NOT NULL, " + - "shop_type VARCHAR(36) NOT NULL, " + - "server VARCHAR(16) NOT NULL, " + - "container_location VARCHAR(256), " + - "sign_location VARCHAR(256), " + - "price DOUBLE NOT NULL, " + - "amount INT NOT NULL, " + - "balance DOUBLE NOT NULL, " + - "item_one TEXT, " + - "item_two TEXT, " + - "last_transaction BIGINT, " + - "PRIMARY KEY (id)" + - ")"; - getDatabase().getConnection().prepareStatement(sql).executeUpdate(); - } catch (SQLException e) { - e.printStackTrace(); - Logger.severe("Error while trying to create shop table"); - Logger.severe("Shutting down PlayerShops"); - Bukkit.getPluginManager().disablePlugin(PlayerShops.getInstance()); - } - } - -} diff --git a/src/main/java/com/alttd/playershops/database/ShopQueries.java b/src/main/java/com/alttd/playershops/database/ShopQueries.java deleted file mode 100644 index 26dd6eb..0000000 --- a/src/main/java/com/alttd/playershops/database/ShopQueries.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.alttd.playershops.database; - -import com.alttd.playershops.shop.AbstractShop; -import com.alttd.playershops.shop.ShopType; -import com.alttd.playershops.utils.AMath; -import com.alttd.playershops.utils.Logger; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -public class ShopQueries { - - public static boolean saveShop(AbstractShop shop) { - String sql = "INSERT INTO shops " + - "(id, owner_name, owner_uuid, shop_type, server, container_location, sign_location, " + - "price, amount, balance, item_one, item_two, last_transaction)" + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + - "ON DUPLICATE KEY UPDATE owner_name = ?, owner_uuid = ?, shop_type = ?, server = ?, " + - "container_location = ?, sign_location = ?, price = ?, amount = ?, balance = ?, " + - "item_one = ?, item_two = ?, last_transaction = ?"; - try { - PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); - - statement.setInt(1, shop.getId()); - statement.setString(2, shop.getOwnerName()); - statement.setString(3, shop.getOwnerUUID().toString()); - statement.setString(4, shop.getServer()); - statement.setString(5, shop.getType().toString()); - statement.setString(6, locationToString(shop.getContainerLocation())); - statement.setString(7, locationToString(shop.getSignLocation())); - statement.setDouble(8, shop.getPrice()); - statement.setInt(9, shop.getAmount()); - statement.setDouble(10, shop.getBalance()); - statement.setBytes(11, shop.getItemStack().serializeAsBytes()); - statement.setBytes(12, shop.getSecondaryItem().serializeAsBytes()); - statement.setLong(13, shop.getLastTransaction()); - //repeat everything except id for update - statement.setString(14, shop.getOwnerName()); - statement.setString(15, shop.getOwnerUUID().toString()); - statement.setString(16, shop.getServer()); - statement.setString(17, shop.getType().toString()); - statement.setString(18, locationToString(shop.getContainerLocation())); - statement.setString(19, locationToString(shop.getSignLocation())); - statement.setDouble(20, shop.getPrice()); - statement.setInt(21, shop.getAmount()); - statement.setDouble(22, shop.getBalance()); - statement.setBytes(23, shop.getItemStack().serializeAsBytes()); - statement.setBytes(24, shop.getSecondaryItem().serializeAsBytes()); - statement.setLong(25, shop.getLastTransaction()); - - return statement.executeUpdate() == 1; - } catch (SQLException e) { - e.printStackTrace(); - } - return false; - } - - public static AbstractShop loadShop(int id) { - String sql = "SELECT * FROM shops WHERE id = ?"; - try { - PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); - - statement.setInt(1, id); - ResultSet resultSet = statement.executeQuery(); - if (resultSet.next()) - return shopFromResultSet(resultSet); - } catch (SQLException e) { - e.printStackTrace(); - } - return null; - } - - public static List loadShops() { - String sql = "SELECT * FROM shops"; - ArrayList shops = new ArrayList<>(); - try { - PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); - - ResultSet resultSet = statement.executeQuery(); - while (resultSet.next()) { - AbstractShop shop = shopFromResultSet(resultSet); - if (shop == null) { - Logger.warn("Tried to load a shop but failed [" + resultSet + "]"); - continue; - } - shops.add(shop); - } - } catch (SQLException e) { - e.printStackTrace(); - } - return shops; - } - - /** - * Loads a shop from a result set, does not iterate - * @param resultSet Result set to load from - * @return A shop - * @throws SQLException if data is missing or formatted incorrectly - */ - private static AbstractShop shopFromResultSet(ResultSet resultSet) throws SQLException { - int id = resultSet.getInt("id"); - String ownerName = resultSet.getString("owner_name"); - UUID ownerUuid = UUID.fromString(resultSet.getString("owner_uuid")); - ShopType shopType = ShopType.valueOf(resultSet.getString("shop_type")); - String server = resultSet.getString("server"); - Location containerLocation = stringToLocation(resultSet.getString("container_location")); - Location signLocation = stringToLocation(resultSet.getString("sign_location")); - double price = resultSet.getDouble("price"); - int amount = resultSet.getInt("amount"); - double balance = resultSet.getDouble("balance"); - ItemStack itemOne = ItemStack.deserializeBytes(resultSet.getBytes("item_one")); - ItemStack itemTwo = ItemStack.deserializeBytes(resultSet.getBytes("item_two")); - long lastTransaction = resultSet.getLong("last_transaction"); - - if (containerLocation == null || signLocation == null) - return null; - - return AbstractShop.create(id, ownerName, ownerUuid, shopType, server, containerLocation, signLocation, - price, amount, balance, itemOne, itemTwo, lastTransaction); - } - - private static String locationToString(Location location) { - return location.getWorld() + ":" + - AMath.round(location.getX(), 1) + ":" + - AMath.round(location.getY(), 1) + ":" + - AMath.round(location.getZ(), 1); - } - - private static Location stringToLocation(String string) { - String[] split = string.split(":"); - if (split.length != 4) { - Logger.warn("Unable to load location [" + string + "] due to invalid format"); - return null; - } - - try { - return new Location(Bukkit.getWorld(split[0]), - Double.parseDouble(split[1]), Double.parseDouble(split[2]), Double.parseDouble(split[3])); - } catch (NumberFormatException e) { - Logger.warn("Unable to load location [" + string + "] due to invalid format"); - return null; - } - } -} diff --git a/src/main/java/com/alttd/playershops/events/PlayerCreateShopEvent.java b/src/main/java/com/alttd/playershops/events/PlayerCreateShopEvent.java index 827bdde..a446d32 100644 --- a/src/main/java/com/alttd/playershops/events/PlayerCreateShopEvent.java +++ b/src/main/java/com/alttd/playershops/events/PlayerCreateShopEvent.java @@ -1,6 +1,6 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -9,7 +9,7 @@ public class PlayerCreateShopEvent extends ShopEvent { private static final HandlerList handlers = new HandlerList(); private final Player player; - public PlayerCreateShopEvent(Player player, AbstractShop shop) { + public PlayerCreateShopEvent(Player player, PlayerShop shop) { super(shop); this.player = player; } diff --git a/src/main/java/com/alttd/playershops/events/PlayerDestroyShopEvent.java b/src/main/java/com/alttd/playershops/events/PlayerDestroyShopEvent.java index 9f3f53a..e1b7d25 100644 --- a/src/main/java/com/alttd/playershops/events/PlayerDestroyShopEvent.java +++ b/src/main/java/com/alttd/playershops/events/PlayerDestroyShopEvent.java @@ -1,6 +1,6 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -10,7 +10,7 @@ public class PlayerDestroyShopEvent extends ShopEvent { private static final HandlerList handlers = new HandlerList(); private final Player player; - public PlayerDestroyShopEvent(Player player, AbstractShop shop) { + public PlayerDestroyShopEvent(Player player, PlayerShop shop) { super(shop); this.player = player; } diff --git a/src/main/java/com/alttd/playershops/events/PlayerExchangeShopEvent.java b/src/main/java/com/alttd/playershops/events/PlayerExchangeShopEvent.java index 588b24d..7c9e1e6 100644 --- a/src/main/java/com/alttd/playershops/events/PlayerExchangeShopEvent.java +++ b/src/main/java/com/alttd/playershops/events/PlayerExchangeShopEvent.java @@ -1,6 +1,6 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -10,7 +10,7 @@ public class PlayerExchangeShopEvent extends ShopEvent { private static final HandlerList handlers = new HandlerList(); private final Player player; - public PlayerExchangeShopEvent(Player player, AbstractShop shop) { + public PlayerExchangeShopEvent(Player player, PlayerShop shop) { super(shop); this.player = player; } diff --git a/src/main/java/com/alttd/playershops/events/PlayerInitializeShopEvent.java b/src/main/java/com/alttd/playershops/events/PlayerInitializeShopEvent.java index 1cadb05..9fca108 100644 --- a/src/main/java/com/alttd/playershops/events/PlayerInitializeShopEvent.java +++ b/src/main/java/com/alttd/playershops/events/PlayerInitializeShopEvent.java @@ -1,6 +1,6 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -10,7 +10,7 @@ public class PlayerInitializeShopEvent extends ShopEvent { private static final HandlerList handlers = new HandlerList(); private final Player player; - public PlayerInitializeShopEvent(Player player, AbstractShop shop) { + public PlayerInitializeShopEvent(Player player, PlayerShop shop) { super(shop); this.player = player; } diff --git a/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java index 10fad83..d9f04d8 100644 --- a/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java +++ b/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java @@ -1,6 +1,6 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; @@ -8,7 +8,7 @@ public class ShopBalanceChangeEvent extends ShopEvent { private static final HandlerList handlers = new HandlerList(); private final ChangeReason changeReason; - public ShopBalanceChangeEvent(AbstractShop shop, ChangeReason reason) { + public ShopBalanceChangeEvent(PlayerShop shop, ChangeReason reason) { super(shop); this.changeReason = reason; } @@ -31,7 +31,7 @@ public class ShopBalanceChangeEvent extends ShopEvent { public enum ChangeReason { DEPOSIT, - WIDRAW, + WITHDRAW, SELL, BUY, UPKEEP diff --git a/src/main/java/com/alttd/playershops/events/ShopEvent.java b/src/main/java/com/alttd/playershops/events/ShopEvent.java index 2da3c18..7504607 100644 --- a/src/main/java/com/alttd/playershops/events/ShopEvent.java +++ b/src/main/java/com/alttd/playershops/events/ShopEvent.java @@ -1,19 +1,19 @@ package com.alttd.playershops.events; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; public abstract class ShopEvent extends Event implements Cancellable { private boolean cancelled; - private final AbstractShop shop; + private final PlayerShop shop; - public ShopEvent(AbstractShop shop) { + public ShopEvent(PlayerShop shop) { this.shop = shop; } - public AbstractShop getShop() { + public PlayerShop getShop() { return shop; } diff --git a/src/main/java/com/alttd/playershops/events/ShopItemAmountChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopItemAmountChangeEvent.java new file mode 100644 index 0000000..18bb5aa --- /dev/null +++ b/src/main/java/com/alttd/playershops/events/ShopItemAmountChangeEvent.java @@ -0,0 +1,31 @@ +package com.alttd.playershops.events; + +import com.alttd.playershops.shop.PlayerShop; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class ShopItemAmountChangeEvent extends ShopEvent { + private static final HandlerList handlers = new HandlerList(); + private final int newAmount; + + public ShopItemAmountChangeEvent(PlayerShop shop, int newAmount) { + super(shop); + this.newAmount = newAmount; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return handlers; + } + + @NotNull + public static HandlerList getHandlerList() { + return handlers; + } + + public int getNewAmount() { + return newAmount; + } +} diff --git a/src/main/java/com/alttd/playershops/events/ShopItemChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopItemChangeEvent.java new file mode 100644 index 0000000..269554f --- /dev/null +++ b/src/main/java/com/alttd/playershops/events/ShopItemChangeEvent.java @@ -0,0 +1,31 @@ +package com.alttd.playershops.events; + +import com.alttd.playershops.shop.PlayerShop; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class ShopItemChangeEvent extends ShopEvent { + private static final HandlerList handlers = new HandlerList(); + private final ItemStack newItem; + + public ShopItemChangeEvent(PlayerShop shop, ItemStack newItem) { + super(shop); + this.newItem = newItem; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return handlers; + } + + @NotNull + public static HandlerList getHandlerList() { + return handlers; + } + + public ItemStack getNewItem() { + return newItem; + } +} diff --git a/src/main/java/com/alttd/playershops/events/ShopPriceChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopPriceChangeEvent.java new file mode 100644 index 0000000..f4e32ae --- /dev/null +++ b/src/main/java/com/alttd/playershops/events/ShopPriceChangeEvent.java @@ -0,0 +1,31 @@ +package com.alttd.playershops.events; + +import com.alttd.playershops.shop.PlayerShop; +import org.bukkit.event.HandlerList; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class ShopPriceChangeEvent extends ShopEvent { + private static final HandlerList handlers = new HandlerList(); + private final double newPrice; + + public ShopPriceChangeEvent(PlayerShop shop, double newPrice) { + super(shop); + this.newPrice = newPrice; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return handlers; + } + + @NotNull + public static HandlerList getHandlerList() { + return handlers; + } + + public double getNewPrice() { + return newPrice; + } +} diff --git a/src/main/java/com/alttd/playershops/events/ShopTypeChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopTypeChangeEvent.java new file mode 100644 index 0000000..2776cf9 --- /dev/null +++ b/src/main/java/com/alttd/playershops/events/ShopTypeChangeEvent.java @@ -0,0 +1,31 @@ +package com.alttd.playershops.events; + +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.shop.ShopType; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class ShopTypeChangeEvent extends ShopEvent { + private static final HandlerList handlers = new HandlerList(); + private final ShopType shopType; + + public ShopTypeChangeEvent(PlayerShop shop, ShopType shopType) { + super(shop); + this.shopType = shopType; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return handlers; + } + + @NotNull + public static HandlerList getHandlerList() { + return handlers; + } + + public ShopType getShopType() { + return shopType; + } +} diff --git a/src/main/java/com/alttd/playershops/gui/AbstractGui.java b/src/main/java/com/alttd/playershops/gui/AbstractGui.java new file mode 100644 index 0000000..b649364 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/AbstractGui.java @@ -0,0 +1,158 @@ +package com.alttd.playershops.gui; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public abstract class AbstractGui implements InventoryHolder { + + Inventory inventory; + protected final int INV_SIZE = 54; + int currentSlot; + UUID uuid; + int pageIndex; + AbstractGui lastGui; + + + AbstractGui(UUID uuid) { + this.uuid = uuid; + this.currentSlot = 0; + this.lastGui = null; + } + + public void open() { + Player player = getPlayer(); + if (player != null) { + player.openInventory(inventory); + } + } + + boolean close() { + Player player = getPlayer(); + if (player != null) { + player.closeInventory(); + return true; + } + return false; + } + + void scrollPageNext() { + ItemStack nextPageIcon = inventory.getItem(GuiIcon.MENUBAR_NEXT_PAGE.getSlot()); + if (nextPageIcon != null && nextPageIcon.equals(getNextPageIcon())) { + inventory.setItem(GuiIcon.MENUBAR_NEXT_PAGE.getSlot(), getPrevPageIcon()); + ++pageIndex; + initInvContents(); + } + } + + void scrollPagePrev() { + ItemStack prevPageIcon = inventory.getItem(GuiIcon.MENUBAR_PREV_PAGE.getSlot()); + if (prevPageIcon != null && prevPageIcon.equals(getPrevPageIcon())) { + inventory.setItem(GuiIcon.MENUBAR_PREV_PAGE.getSlot(), getNextPageIcon()); + --pageIndex; + if (pageIndex == 0) { + inventory.setItem(GuiIcon.MENUBAR_PREV_PAGE.getSlot(), null); + } + initInvContents(); + } + } + + protected boolean addItem(int slot, ItemStack icon) { + currentSlot = slot; + return addItem(icon); + } + + protected boolean addItem(ItemStack icon) { + if (currentSlot == inventory.getSize()) + return false; + + inventory.setItem(currentSlot, icon); + currentSlot++; + + return true; + } + + void initInvContents() { + clearInvBody(); + currentSlot = 0; + } + + protected void clearInvBody() { + for (int i = 0; i < inventory.getSize() - 9; ++i) { +// inventory.setItem(i, null); + inventory.setItem(i, getDivider()); + } + } + + void makeMenuBar() { + for (int i = inventory.getSize() - 9; i < inventory.getSize(); ++i) { + inventory.setItem(i, getDivider()); + } + } + + Player getPlayer() { + return Bukkit.getPlayer(uuid); + } + + @NotNull + @Override + public Inventory getInventory() { + return inventory; + } + + ItemStack getPrevPageIcon() { + return GuiIcon.MENUBAR_PREV_PAGE.getItemStack(); + } + + ItemStack getNextPageIcon() { + return GuiIcon.MENUBAR_NEXT_PAGE.getItemStack(); + } + + ItemStack getExitIcon() { + return null; + } + + ItemStack getDivider() { + return GuiIcon.DIVIDER.getItemStack(); + } + + public void onClick(int slot, ItemStack item) { + // TODO a better way to do this. + // check all menu actions here + if (slot == GuiIcon.MENUBAR_BACK.getSlot() && GuiIcon.MENUBAR_BACK.getItemStack().equals(item)) { + if (hasLastGui()) + lastGui.open(); + } else if (slot == GuiIcon.MENUBAR_EXIT.getSlot() && GuiIcon.MENUBAR_EXIT.getItemStack().equals(item)) { + getPlayer().closeInventory(); + } else if (slot == GuiIcon.MENUBAR_SEARCH.getSlot() && GuiIcon.MENUBAR_SEARCH.getItemStack().equals(item)) { + // TODO + } else if (slot == GuiIcon.MENUBAR_PREV_PAGE.getSlot() && GuiIcon.MENUBAR_PREV_PAGE.getItemStack().equals(item)) { + scrollPagePrev(); + } else if (slot == GuiIcon.MENUBAR_NEXT_PAGE.getSlot() && GuiIcon.MENUBAR_NEXT_PAGE.getItemStack().equals(item)) { + scrollPageNext(); + } + } + + public void setLastGui(AbstractGui lastGui) { + this.lastGui = lastGui; + inventory.setItem(GuiIcon.MENUBAR_BACK.getSlot(), GuiIcon.MENUBAR_BACK.getItemStack()); + } + + public boolean hasLastGui() { + return lastGui != null; + } + + void addPrevPageItem() { + inventory.setItem(GuiIcon.MENUBAR_PREV_PAGE.getSlot(), GuiIcon.MENUBAR_PREV_PAGE.getItemStack()); + } + + void addNextPageItem() { + inventory.setItem(GuiIcon.MENUBAR_NEXT_PAGE.getSlot(), GuiIcon.MENUBAR_NEXT_PAGE.getItemStack()); + } + +} diff --git a/src/main/java/com/alttd/playershops/gui/GuiIcon.java b/src/main/java/com/alttd/playershops/gui/GuiIcon.java new file mode 100644 index 0000000..5fd33b3 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/GuiIcon.java @@ -0,0 +1,79 @@ +package com.alttd.playershops.gui; + +import com.alttd.playershops.config.GuiIconConfig; +import com.alttd.playershops.config.ShopTypeConfig; +import com.alttd.playershops.utils.Util; +import lombok.Getter; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +public enum GuiIcon { + DIVIDER, + MENUBAR_BACK, + MENUBAR_EXIT, + MENUBAR_SEARCH, + MENUBAR_PREV_PAGE, + MENUBAR_NEXT_PAGE, + + MANAGE_SHOP, + MANAGE_SHOP_BALANCE_ADD, + MANAGE_SHOP_BALANCE_REMOVE, + MANAGE_SHOP_SALES, + MANAGE_SHOP_ITEM, + MANAGE_SHOP_TYPE, + MANAGE_SHOP_AMOUNT, + MANAGE_SHOP_PRICE, + + HOME_LIST_OWN_SHOPS, + HOME_LIST_PLAYERS, + HOME_SETTINGS, + LIST_SHOP, + LIST_PLAYER, + LIST_PLAYER_ADMIN, + EMPTY_SHOP; + + @Getter + private final int slot; + @Getter + private final ItemStack itemStack; + + GuiIcon() { + GuiIconConfig config = new GuiIconConfig(this.toString()); + this.slot = config.slot; + this.itemStack = createItem(config.material, config.displayName, config.lore); + } + + private ItemStack createItem(String materialString, String displayName, List itemlore) { + Material material = Material.getMaterial(materialString); + if (material == null) + return null; + + ItemStack item = new ItemStack(material); + ItemMeta itemMeta = item.getItemMeta(); + itemMeta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES); + itemMeta.displayName(format(displayName)); + List lore = new ArrayList<>(); + for (String line : itemlore) { + lore.add(format(line)); + } + itemMeta.lore(lore); + item.setItemMeta(itemMeta); + + return item; + } + + private Component format(String s) { + return Util.parseMiniMessage(s, null); + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/alttd/playershops/gui/HomeGui.java b/src/main/java/com/alttd/playershops/gui/HomeGui.java new file mode 100644 index 0000000..579bf63 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/HomeGui.java @@ -0,0 +1,41 @@ +package com.alttd.playershops.gui; + +import com.alttd.playershops.utils.Util; +import org.bukkit.Bukkit; +import org.bukkit.inventory.ItemStack; + +import java.util.UUID; + +public class HomeGui extends AbstractGui { + + public HomeGui(UUID uuid) { + super(uuid); + this.inventory = Bukkit.createInventory(this, INV_SIZE, Util.parseMiniMessage("Config.gui.home-title", null)); + initInvContents(); + makeMenuBar(); + } + + @Override + void initInvContents() { + super.initInvContents(); + inventory.setItem(GuiIcon.HOME_LIST_OWN_SHOPS.getSlot(), GuiIcon.HOME_LIST_OWN_SHOPS.getItemStack()); + inventory.setItem(GuiIcon.HOME_LIST_PLAYERS.getSlot(), GuiIcon.HOME_LIST_PLAYERS.getItemStack()); + inventory.setItem(GuiIcon.HOME_SETTINGS.getSlot(), GuiIcon.HOME_SETTINGS.getItemStack()); + } + + @Override + public void onClick(int slot, ItemStack item) { + super.onClick(slot, item); + if (slot == GuiIcon.HOME_LIST_OWN_SHOPS.getSlot() && GuiIcon.HOME_LIST_OWN_SHOPS.getItemStack().equals(item)) { + ListShopsGui listShopsGui = new ListShopsGui(uuid, uuid); + listShopsGui.setLastGui(this); + listShopsGui.open(); + } else if (slot == GuiIcon.HOME_LIST_PLAYERS.getSlot() && GuiIcon.HOME_LIST_PLAYERS.getItemStack().equals(item)) { + ListPlayersGui listPlayersGui = new ListPlayersGui(uuid); + listPlayersGui.setLastGui(this); + listPlayersGui.open(); + } else if (slot == GuiIcon.HOME_SETTINGS.getSlot() && GuiIcon.HOME_SETTINGS.getItemStack().equals(item)) { + + } + } +} diff --git a/src/main/java/com/alttd/playershops/gui/ListPlayersGui.java b/src/main/java/com/alttd/playershops/gui/ListPlayersGui.java new file mode 100644 index 0000000..b1f3b24 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/ListPlayersGui.java @@ -0,0 +1,56 @@ +package com.alttd.playershops.gui; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.utils.ShopUtil; +import com.alttd.playershops.utils.Util; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.UUID; + +public class ListPlayersGui extends AbstractGui { + + private List owners; + public ListPlayersGui(UUID uuid) { + super(uuid); + this.inventory = Bukkit.createInventory(this, INV_SIZE, Util.parseMiniMessage("Config.gui.list-players-title", null)); + initInvContents(); + makeMenuBar(); + } + + @Override + void initInvContents() { + super.initInvContents(); + + owners = PlayerShops.getInstance().getShopHandler().getShopOwners(); + int startIndex = pageIndex * 45; + ItemStack item; + for (int i = startIndex; i < owners.size(); i++) { + item = ShopUtil.getPlayerHead(owners.get(i)); + + if (!addItem(item)) { + addNextPageItem(); + break; + } + } + } + + @Override + public void onClick(int slot, ItemStack item) { + super.onClick(slot, item); + + if (item.getType() != Material.PLAYER_HEAD) + return; + + int index = pageIndex * 45 + slot; + if (index > owners.size()) + return; + UUID playerToList = owners.get(index); + + ListShopsGui listShopGUI = new ListShopsGui(uuid, playerToList); + listShopGUI.setLastGui(this); + listShopGUI.open(); + } +} diff --git a/src/main/java/com/alttd/playershops/gui/ListShopsGui.java b/src/main/java/com/alttd/playershops/gui/ListShopsGui.java new file mode 100644 index 0000000..5acd328 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/ListShopsGui.java @@ -0,0 +1,80 @@ +package com.alttd.playershops.gui; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.utils.ShopUtil; +import com.alttd.playershops.utils.Util; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.List; +import java.util.UUID; + +public class ListShopsGui extends AbstractGui { + + private UUID playerToList; + List shops; + + public ListShopsGui(UUID uuid, UUID playerToList) { + super(uuid); + this.playerToList = playerToList; + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerToList); + + TagResolver placeholders = TagResolver.resolver( + Placeholder.unparsed("name", offlinePlayer.hasPlayedBefore() ? offlinePlayer.getName() : "error") + ); + this.inventory = Bukkit.createInventory(this, INV_SIZE, Util.parseMiniMessage("Config.gui.list-shops-title", placeholders)); + initInvContents(); + makeMenuBar(); + } + + @Override + void initInvContents() { + super.initInvContents(); + + shops = PlayerShops.getInstance().getShopHandler().getShops(playerToList); + // Todo add option to sort shops? + + int startIndex = pageIndex * 45; + for (int i = startIndex; i < shops.size(); i++) { + PlayerShop shop = shops.get(i); + ItemStack item = shop.getItemStack(); + if (!shop.isInitialized()) + item = GuiIcon.EMPTY_SHOP.getItemStack(); + + if (!this.addItem(item)) { + addNextPageItem(); + break; + } + } + + } + + @Override + public void onClick(int slot, ItemStack item) { + super.onClick(slot, item); + + Player player = getPlayer(); + if (player == null) + return; + + int index = pageIndex * 45 + slot; + if (index > shops.size()) + return; + + PlayerShop playerShop = shops.get(index); + if (playerShop == null) return; + + if (!ShopUtil.canManageShop(player, playerShop)) + return; + + ShopManagementGui shopManagementGui = new ShopManagementGui(player.getUniqueId(), playerShop); + shopManagementGui.setLastGui(this); + shopManagementGui.open(); + } +} diff --git a/src/main/java/com/alttd/playershops/gui/ShopManagementGui.java b/src/main/java/com/alttd/playershops/gui/ShopManagementGui.java new file mode 100644 index 0000000..8211270 --- /dev/null +++ b/src/main/java/com/alttd/playershops/gui/ShopManagementGui.java @@ -0,0 +1,108 @@ +package com.alttd.playershops.gui; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.conversation.ConversationManager; +import com.alttd.playershops.conversation.ConversationType; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.utils.EconomyUtils; +import com.alttd.playershops.utils.ShopUtil; +import com.alttd.playershops.utils.Util; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ShopManagementGui extends AbstractGui { + + PlayerShop shop; + + public ShopManagementGui(UUID uuid, PlayerShop shop) { + super(uuid); + this.inventory = Bukkit.createInventory(this, INV_SIZE, Util.parseMiniMessage("Config.gui.management-title", null)); + this.shop = shop; + initInvContents(); + makeMenuBar(); + } + + @Override + void initInvContents() { + super.initInvContents(); + + ItemStack shopIcon = GuiIcon.MANAGE_SHOP.getItemStack(); + ItemMeta meta = shopIcon.getItemMeta(); + List lore = new ArrayList<>(); + TagResolver placeholders = TagResolver.resolver( + Placeholder.unparsed("balance", shop.getBalance() + ""), + Placeholder.unparsed("price", shop.getPrice() + ""), + Placeholder.unparsed("amount", shop.getAmount() + ""), + Placeholder.unparsed("shoptype", shop.getType().toString()), + Placeholder.component("itemname", ShopUtil.itemNameComponent(shop.getItemStack())) + ); + lore.add(Util.parseMiniMessage("Balance: ", placeholders)); + lore.add(Util.parseMiniMessage("item: ", placeholders)); + lore.add(Util.parseMiniMessage("amount: ", placeholders)); + lore.add(Util.parseMiniMessage("Type: ", placeholders)); + lore.add(Util.parseMiniMessage("Price: ", placeholders)); + + meta.lore(lore); + shopIcon.setItemMeta(meta); + + inventory.setItem(GuiIcon.MANAGE_SHOP.getSlot(), shopIcon); + inventory.setItem(GuiIcon.MANAGE_SHOP_BALANCE_ADD.getSlot(), GuiIcon.MANAGE_SHOP_BALANCE_ADD.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_BALANCE_REMOVE.getSlot(), GuiIcon.MANAGE_SHOP_BALANCE_REMOVE.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_SALES.getSlot(), GuiIcon.MANAGE_SHOP_SALES.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_ITEM.getSlot(), GuiIcon.MANAGE_SHOP_ITEM.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_TYPE.getSlot(), GuiIcon.MANAGE_SHOP_TYPE.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_AMOUNT.getSlot(), GuiIcon.MANAGE_SHOP_AMOUNT.getItemStack()); + inventory.setItem(GuiIcon.MANAGE_SHOP_PRICE.getSlot(), GuiIcon.MANAGE_SHOP_PRICE.getItemStack()); + } + + @Override + void makeMenuBar() { + super.makeMenuBar(); + } + + @Override + public void onClick(int slot, ItemStack item) { + super.onClick(slot, item); + if (slot == GuiIcon.MANAGE_SHOP.getSlot() && GuiIcon.MANAGE_SHOP.getItemStack().equals(item)) { + + } else if (slot == GuiIcon.MANAGE_SHOP_BALANCE_ADD.getSlot() && GuiIcon.MANAGE_SHOP_BALANCE_ADD.getItemStack().equals(item)) { + if (EconomyUtils.getFunds(getPlayer()) > 0) { + openChangePrompt(ConversationType.ADD_BALANCE); + } else { + getPlayer().sendMiniMessage("You do not have money to add to this shop", null); + } + } else if (slot == GuiIcon.MANAGE_SHOP_BALANCE_REMOVE.getSlot() && GuiIcon.MANAGE_SHOP_BALANCE_REMOVE.getItemStack().equals(item)) { + if (shop.getBalance() > 0) { + openChangePrompt(ConversationType.WITHDRAW_BALANCE); + } else { + getPlayer().sendMiniMessage("You can't widraw money from this shop", null); + } + } else if (slot == GuiIcon.MANAGE_SHOP_SALES.getSlot() && GuiIcon.MANAGE_SHOP_SALES.getItemStack().equals(item)) { + + } else if (slot == GuiIcon.MANAGE_SHOP_ITEM.getSlot() && GuiIcon.MANAGE_SHOP_ITEM.getItemStack().equals(item)) { + openChangePrompt(ConversationType.CHANGE_ITEM); + } else if (slot == GuiIcon.MANAGE_SHOP_TYPE.getSlot() && GuiIcon.MANAGE_SHOP_TYPE.getItemStack().equals(item)) { + openChangePrompt(ConversationType.CHANGE_TYPE); + } else if (slot == GuiIcon.MANAGE_SHOP_AMOUNT.getSlot() && GuiIcon.MANAGE_SHOP_AMOUNT.getItemStack().equals(item)) { + openChangePrompt(ConversationType.CHANGE_AMOUNT); + } else if (slot == GuiIcon.MANAGE_SHOP_PRICE.getSlot() && GuiIcon.MANAGE_SHOP_PRICE.getItemStack().equals(item)) { + openChangePrompt(ConversationType.CHANGE_PRICE); + } + } + + private void openChangePrompt(ConversationType conversationType) { + Player player = getPlayer(); + player.closeInventory(); + new ConversationManager(PlayerShops.getInstance(), player, conversationType, shop); + } + +} diff --git a/src/main/java/com/alttd/playershops/handler/ShopHandler.java b/src/main/java/com/alttd/playershops/handler/ShopHandler.java index 53a93bc..db2b7c1 100644 --- a/src/main/java/com/alttd/playershops/handler/ShopHandler.java +++ b/src/main/java/com/alttd/playershops/handler/ShopHandler.java @@ -2,9 +2,9 @@ package com.alttd.playershops.handler; import com.alttd.playershops.PlayerShops; import com.alttd.playershops.config.Config; -import com.alttd.playershops.events.PlayerCreateShopEvent; -import com.alttd.playershops.shop.AbstractShop; -import com.alttd.playershops.shop.ShopType; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.storage.database.DatabaseHelper; +import com.alttd.playershops.utils.Logger; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.Getter; @@ -13,8 +13,12 @@ import org.bukkit.Material; import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.block.DoubleChest; import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryHolder; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -24,19 +28,41 @@ public class ShopHandler { @Getter private final Object2IntMap shopBuildLimits; @Getter - private final Map shopLocation; + private final Map shopLocation; + private final Map shopSignLocation; @Getter private final ArrayList shopMaterials; public ShopHandler(PlayerShops instance) { plugin = instance; shopLocation = new ConcurrentHashMap<>(); + shopSignLocation = new ConcurrentHashMap<>(); shopBuildLimits = new Object2IntOpenHashMap<>(); shopBuildLimits.defaultReturnValue(Config.shopLimit); shopMaterials = new ArrayList<>(); // TODO move into parent method where materials are loaded in. + shopMaterials.add(Material.BARREL); + + loadShops(); } - public AbstractShop getShop(Location location) { + void loadShops() { + Logger.info("Loading all shops from database..."); + // TODO add a timer to test performance + DatabaseHelper databaseHelper = plugin.getDatabaseHelper(); + try (ResultSet resultSet = databaseHelper.selectAllShops()) { + while (resultSet.next()) { + PlayerShop playerShop = databaseHelper.shopFromResultSet(resultSet); + if (playerShop == null) continue; + + addShop(playerShop); + } + } catch (Exception e) { + Logger.error("Error loading shops\n" + e); + e.printStackTrace(); + } + } + + public PlayerShop getShop(Location location) { Location newLocation = new Location(location.getWorld(), location.getBlockX(), location.getBlockY(), location.getBlockZ()); return shopLocation.get(newLocation); @@ -46,10 +72,14 @@ public class ShopHandler { return getShop(location) != null; } - public Collection getShops() { + public Collection getShops() { return Collections.unmodifiableCollection(shopLocation.values()); } + public List getShopOwners() { + return shopLocation.values().stream().map(PlayerShop::getOwnerUUID).distinct().toList(); + } + public void addPlayerLimit(UUID uuid, int limit) { shopBuildLimits.put(uuid, limit); } @@ -58,55 +88,70 @@ public class ShopHandler { return shopBuildLimits.getInt(uuid); } - public void removeShops() { + public void unloadShops() { + for (PlayerShop shop : shopLocation.values()) { + if (shop.isDirty()) + plugin.getDatabaseHelper().updateShop(shop, true); + } shopLocation.clear(); + shopSignLocation.clear(); } public boolean isShopMaterial(Block block) { - if (Tag.SHULKER_BOXES.isTagged(block.getType())) { - return true; - } - return shopMaterials.contains(block.getType()); +// if (Tag.SHULKER_BOXES.isTagged(block.getType())) { +// return true; +// } +// return shopMaterials.contains(block.getType()); + // TODO make a cache of this somehow? + return block.getState() instanceof InventoryHolder inventoryHolder && !(inventoryHolder.getInventory().getHolder() instanceof DoubleChest); } - public List getShops(UUID uuid) { - List shops = new ArrayList<>(); - for (AbstractShop shop : shopLocation.values()) { + public List getShops(UUID uuid) { + List shops = new ArrayList<>(); + for (PlayerShop shop : shopLocation.values()) { if (shop.getOwnerUUID().equals(uuid)) shops.add(shop); } return shops; } - public AbstractShop getShopBySignLocation(Location signLocation) { - for (AbstractShop shop : shopLocation.values()) { - if (shop.getSignLocation().equals(signLocation)) - return shop; - } - return null; + public PlayerShop getShopBySignLocation(Location signLocation) { + Location newLocation = new Location(signLocation.getWorld(), signLocation.getBlockX(), signLocation.getBlockY(), signLocation.getBlockZ()); + + return shopSignLocation.get(newLocation); } - public AbstractShop getShopNearBlock(Block block) { + public PlayerShop getShopsNear(Block block) { BlockFace[] faces = {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST}; for (BlockFace face : faces) { if (this.isShopMaterial(block.getRelative(face))) { - Block blockRelative = block.getRelative(face); - if (isShop(blockRelative.getLocation())) - return getShop(blockRelative.getLocation()); + Block shopChest = block.getRelative(face); + PlayerShop shop = getShop(shopChest.getLocation()); + if (shop != null) + return shop; } } return null; } - public AbstractShop createShop(Location signLocation, Player player, double price, int amount, ShopType shopType) { - AbstractShop shop = AbstractShop.create(signLocation, player.getUniqueId(), price, amount, shopType); + public void addShop(PlayerShop shop) { + shopLocation.put(shop.getShopLocation(), shop); + shopSignLocation.put(shop.getSignLocation(), shop); + } - PlayerCreateShopEvent playerCreateShopEvent = new PlayerCreateShopEvent(player, shop); - plugin.getServer().getPluginManager().callEvent(playerCreateShopEvent); + public void removeShop(PlayerShop shop) { + shopLocation.remove(shop.getShopLocation()); + shopSignLocation.remove(shop.getSignLocation()); + plugin.getDatabaseHelper().removeShop(shop); + } - if(playerCreateShopEvent.isCancelled()) - return null; + public boolean canPlayerBreakShop(Player player, PlayerShop shop) { // TODO move to util? + if (player.getUniqueId().equals(shop.getOwnerUUID()) && player.hasPermission("playershops.shop.break")) + return true; - return shop; + if (player.hasPermission("playershops.shop.break.other")) + return true; + + return false; } } diff --git a/src/main/java/com/alttd/playershops/hook/WorldGuardHook.java b/src/main/java/com/alttd/playershops/hook/WorldGuardHook.java new file mode 100644 index 0000000..310e1f7 --- /dev/null +++ b/src/main/java/com/alttd/playershops/hook/WorldGuardHook.java @@ -0,0 +1,11 @@ +package com.alttd.playershops.hook; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +public class WorldGuardHook { + + public static boolean canUseShopInRegion(Player player, Location location) { + return true; + } +} diff --git a/src/main/java/com/alttd/playershops/listener/InventoryListener.java b/src/main/java/com/alttd/playershops/listener/InventoryListener.java index daca892..111241d 100644 --- a/src/main/java/com/alttd/playershops/listener/InventoryListener.java +++ b/src/main/java/com/alttd/playershops/listener/InventoryListener.java @@ -1,39 +1,45 @@ package com.alttd.playershops.listener; -import org.bukkit.OfflinePlayer; +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.gui.AbstractGui; +import com.alttd.playershops.gui.ShopManagementGui; +import org.bukkit.Material; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.Inventory; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; public class InventoryListener extends EventListener { - protected final Inventory inventory; - protected boolean cancelCloseUnregister = false; - - public InventoryListener(Inventory inv) { - this.inventory = inv; + private final PlayerShops plugin; + public InventoryListener(PlayerShops plugin) { + this.plugin = plugin; + this.register(this.plugin); } - @EventHandler(ignoreCancelled = true) - public void unregisterOnClose(InventoryCloseEvent event) { - if (event.getView().getTopInventory().equals(inventory) && !cancelCloseUnregister) unregister(); - } + @EventHandler + public void onInventoryClickEvent(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) + return; - @EventHandler(ignoreCancelled = true) - public void unregisterOnLeaveEvent(PlayerQuitEvent event) { - if ((inventory.getHolder() instanceof OfflinePlayer) && event.getPlayer().getUniqueId().equals(((OfflinePlayer) inventory.getHolder()).getUniqueId())) - unregister(); - } + if (!(event.getView().getTopInventory().getHolder() instanceof AbstractGui gui)) + return; - @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) - public void onInventoryDrag(InventoryDragEvent event) { - if (event.getView().getTopInventory().equals(inventory)) for (int slot : event.getRawSlots()) if (slot < inventory.getSize()) { + if ((event.getView().getBottomInventory().equals(event.getClickedInventory()))) + return; + + if (event.getClick() == ClickType.NUMBER_KEY) { event.setCancelled(true); return; } + + ItemStack clicked = event.getCurrentItem(); + if (!(clicked != null && clicked.getType() != Material.AIR)) + return; + + event.setCancelled(true); + gui.onClick(event.getRawSlot(), clicked); } } diff --git a/src/main/java/com/alttd/playershops/listener/PlayerListener.java b/src/main/java/com/alttd/playershops/listener/PlayerListener.java index 5da955f..2bad04a 100644 --- a/src/main/java/com/alttd/playershops/listener/PlayerListener.java +++ b/src/main/java/com/alttd/playershops/listener/PlayerListener.java @@ -4,12 +4,16 @@ import com.alttd.playershops.PlayerShops; import com.alttd.playershops.config.Config; import com.alttd.playershops.config.MessageConfig; import com.alttd.playershops.handler.ShopHandler; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.utils.EconomyUtils; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; +import org.bukkit.block.Chest; import org.bukkit.block.Sign; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Directional; @@ -18,29 +22,37 @@ import org.bukkit.block.data.type.WallSign; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.SignChangeEvent; +import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.permissions.PermissionAttachmentInfo; import java.util.UUID; +/** + * Dedicated class to listen to player events + */ public class PlayerListener extends EventListener { private final PlayerShops plugin; + ShopHandler shopHandler; public PlayerListener(PlayerShops plugin) { this.plugin = plugin; this.register(this.plugin); + shopHandler = plugin.getShopHandler(); } @EventHandler(ignoreCancelled = true) - public void onPlayerJoin(PlayerJoinEvent event) { + public void PlayerJoinEvent(PlayerJoinEvent event) { if(!this.isRegistered || !Config.usePermissionShopLimit) return; Player player = event.getPlayer(); - if (!player.hasPermission(Config.shopLimitPermission)) return; + if (!player.hasPermission("playershops.shoplimit")) return; - ShopHandler shopHandler = plugin.getShopHandler(); UUID uuid = player.getUniqueId(); // early return to not check this all the time, if this changes by rankup etc handle it in another event @@ -62,22 +74,13 @@ public class PlayerListener extends EventListener { shopHandler.addPlayerLimit(player.getUniqueId(), buildPermissionNumber); } - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) - public void onSignChange(SignChangeEvent event) { - Block block = event.getBlock(); - - if (!(block.getState() instanceof Sign)) return; - - AbstractShop shop = plugin.getShopHandler().getShop(block.getLocation()); - if(shop == null) return; - - if(shop.isInitialized()) event.setCancelled(true); - } - @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onShopCreation(SignChangeEvent event) { - Block b = event.getBlock(); + if(!this.isRegistered) + return; + Block b = event.getBlock(); + Player player = event.getPlayer(); if (!(b.getState() instanceof Sign)) return; @@ -90,19 +93,19 @@ public class PlayerListener extends EventListener { } Block bRelative = b.getRelative(facing.getOppositeFace()); - ShopHandler shopHandler = plugin.getShopHandler(); if (shopHandler.isShopMaterial(bRelative)) { Sign signBlock = (Sign) b.getState(); Component signLine = event.line(0); if (signLine == null) return; + String signLineString = PlainTextComponentSerializer.plainText().serialize(signLine); if (!signLineString.equalsIgnoreCase(Config.shopCreationWord)) return; - AbstractShop shop = shopHandler.getShop(bRelative.getLocation()); - Player player = event.getPlayer(); + PlayerShop shop = shopHandler.getShop(bRelative.getLocation()); +// Player player = event.getPlayer(); if(shop != null) { event.setCancelled(true); player.sendMiniMessage(MessageConfig.SHOP_ALREADY_EXISTS, null); @@ -110,7 +113,7 @@ public class PlayerListener extends EventListener { } UUID playerUUID = player.getUniqueId(); - if (!player.hasPermission("shop.create")) { + if (!player.hasPermission("playershops.shop.create")) { event.setCancelled(true); player.sendMiniMessage(MessageConfig.NO_SHOP_CREATE_PERMISSION, null); return; @@ -125,9 +128,61 @@ public class PlayerListener extends EventListener { return; } - // TODO instance shopCreationManagement + if (!EconomyUtils.hasSufficientFunds(player, Config.shopCreationBalance)) { + event.setCancelled(true); + return; + } + + PlayerShop playerShop = new PlayerShop(bRelative.getLocation(), signBlock.getLocation(), player); + playerShop.addBalance(Config.shopCreationBalance); + EconomyUtils.removeFunds(player, Config.shopCreationBalance); + shopHandler.addShop(playerShop); + PlayerShops.getInstance().getDatabaseHelper().createShop(playerShop); } } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onDoubleChestPlace(BlockPlaceEvent event) { + if(!this.isRegistered) + return; + Block block = event.getBlock(); + if (!(block.getState() instanceof Chest)) + return; + + PlayerShop playerShop = shopHandler.getShopsNear(block); + if (playerShop == null) + return; + + // As net.minecraft.world.level.block.state.properties.ChestType + // is not exposed in the api this is the shit we need to do to ensure these chests do not connect. + BlockData blockData = block.getState().getBlockData(); + Material material = blockData.getMaterial(); + BlockData newBlockData = material.createBlockData(); + BlockFace facing = ((Directional) blockData).getFacing(); + ((Directional) newBlockData).setFacing(facing); + block.getLocation().getBlock().setBlockData(newBlockData, true); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onShopBlockInteraction(PlayerInteractEvent event) { + if(!this.isRegistered) + return; + + Block block = event.getClickedBlock(); + if (block == null) + return; + + PlayerShop playerShop = shopHandler.getShop(block.getLocation()); + if (playerShop == null) + return; + + // todo add bypass permission to open shop containers? + Player player = event.getPlayer(); + if (player.getUniqueId().equals(playerShop.getOwnerUUID())) + return; + + event.setCancelled(true); + // We could send some info here eg remaining inventory, item details + } } diff --git a/src/main/java/com/alttd/playershops/listener/ShopListener.java b/src/main/java/com/alttd/playershops/listener/ShopListener.java index 46151e9..ae3d766 100644 --- a/src/main/java/com/alttd/playershops/listener/ShopListener.java +++ b/src/main/java/com/alttd/playershops/listener/ShopListener.java @@ -1,39 +1,62 @@ package com.alttd.playershops.listener; import com.alttd.playershops.PlayerShops; -import com.alttd.playershops.shop.AbstractShop; +import com.alttd.playershops.handler.ShopHandler; +import com.alttd.playershops.shop.PlayerShop; import org.bukkit.Location; import org.bukkit.Tag; +import org.bukkit.TreeType; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Rotatable; +import org.bukkit.block.data.type.WallSign; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockPhysicsEvent; +import org.bukkit.event.block.SignChangeEvent; import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.world.StructureGrowEvent; +import org.bukkit.inventory.InventoryHolder; import java.util.Iterator; +/** + * Dedicated class to listen to events related to shops. + * + */ public class ShopListener extends EventListener { private final PlayerShops plugin; + ShopHandler shopHandler; public ShopListener(PlayerShops plugin) { this.plugin = plugin; this.register(this.plugin); + shopHandler = plugin.getShopHandler(); } - @EventHandler(ignoreCancelled = true) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onEntityExplosion(EntityExplodeEvent event) { - if(!this.isRegistered) return; - // This might be heavy when tnt is chained, would it be better to expand the unbreakable block api in galaxy and use that? - // No need for slow bukkit events eating up cpu and memory + if(!this.isRegistered) + return; + + // This might be heavy when tnt is chained Iterator blockIterator = event.blockList().iterator(); - AbstractShop shop = null; + PlayerShop shop = null; while (blockIterator.hasNext()) { Block block = blockIterator.next(); Location location = block.getLocation(); if (Tag.WALL_SIGNS.isTagged(block.getType())) { - shop = plugin.getShopHandler().getShopBySignLocation(location); - } else if (plugin.getShopHandler().isShopMaterial(block)) { - shop = plugin.getShopHandler().getShop(location); + shop = shopHandler.getShopBySignLocation(location); + } else if (shopHandler.isShopMaterial(block)) { + shop = shopHandler.getShop(location); } if (shop != null) { @@ -41,4 +64,138 @@ public class ShopListener extends EventListener { } } } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockPhysics(BlockPhysicsEvent event) { + if(!this.isRegistered) + return; + + Block block = event.getBlock(); + if (!Tag.WALL_SIGNS.isTagged(block.getType())) return; + + + if (shopHandler.getShopBySignLocation(block.getLocation()) != null) + event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInventoryHolderBlockBreak(BlockBreakEvent event) { + if(!this.isRegistered) + return; + + Block block = event.getBlock(); + + if (!(block.getState() instanceof InventoryHolder)) + return; + + PlayerShop shop = shopHandler.getShop(block.getLocation()); + if (shop == null) + return; + + // Shop sign must be broken in order to remove the shop? + event.setCancelled(true); + } + + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onSignBlockBreak(BlockBreakEvent event) { + if(!this.isRegistered) + return; + // this is going to be heavy, add a cache shopsignlocation <-> Shop? + Block block = event.getBlock(); + + if (!(block.getState() instanceof Sign)) + return; + + BlockFace facing; + BlockData data = block.getState().getBlockData(); + if (data instanceof WallSign) { + facing = ((Directional) data).getFacing(); + } else { + facing = ((Rotatable) data).getRotation(); + } + + Block relativeBlock = block.getRelative(facing.getOppositeFace()); + PlayerShop shop = shopHandler.getShop(relativeBlock.getLocation()); + if (shop == null) + return; + + if (!shopHandler.canPlayerBreakShop(event.getPlayer(), shop)) { + event.setCancelled(true); + return; + } + + shopHandler.removeShop(shop); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBucketEmptyAtSign(PlayerBucketEmptyEvent event) { + if (!this.isRegistered) + return; + + Block block = event.getBlock(); + if (!Tag.WALL_SIGNS.isTagged(block.getType())) + return; + + PlayerShop playerShop = shopHandler.getShopBySignLocation(block.getLocation()); + if (playerShop != null) + event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBucketEmptyOnBlock(PlayerBucketEmptyEvent event) { + if (!this.isRegistered) + return; + + Block block = event.getBlock(); + + PlayerShop playerShop = shopHandler.getShop(block.getLocation()); + if (playerShop != null) + event.setCancelled(true); + } + + // mushrooms can replace blocks anywhere in the world w/o a check, this needs to be fixed in Galaxy or upstreams but is a pita + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onTreeGrow(StructureGrowEvent event) { + if (!this.isRegistered) + return; + + // as mushrooms only grow from bonemeal we can cut off early + if (!event.isFromBonemeal()) + return; + + // Only continue with mushrooms + TreeType treeType = event.getSpecies(); + if (treeType != TreeType.RED_MUSHROOM && treeType != TreeType.BROWN_MUSHROOM) + return; + + // we can either cancel the event or remove the blocks from the event. + // as canceling would mean we get back here on the next attempt I opt to remove the blocks + for (int i = 0; i < event.getBlocks().size(); i++) { + BlockState block = event.getBlocks().get(i); + PlayerShop playerShop; + if (block instanceof Sign sign) { // does not work? + playerShop = shopHandler.getShopBySignLocation(sign.getLocation()); + System.out.println(block.getBlock()); + } else { + playerShop = shopHandler.getShop(block.getLocation()); + } + if (playerShop != null) { + event.getBlocks().remove(i--); + } + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onSignChange(SignChangeEvent event) { + if(!this.isRegistered) + return; + + if (!(event.getBlock().getState() instanceof Sign sign)) return; + + PlayerShop shop = shopHandler.getShopBySignLocation(sign.getLocation()); + if(shop == null) return; + + event.setCancelled(true); + } } diff --git a/src/main/java/com/alttd/playershops/listener/TransactionListener.java b/src/main/java/com/alttd/playershops/listener/TransactionListener.java new file mode 100644 index 0000000..a53a94d --- /dev/null +++ b/src/main/java/com/alttd/playershops/listener/TransactionListener.java @@ -0,0 +1,154 @@ +package com.alttd.playershops.listener; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.gui.ShopManagementGui; +import com.alttd.playershops.handler.ShopHandler; +import com.alttd.playershops.hook.WorldGuardHook; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.shop.TransactionError; +import com.alttd.playershops.utils.Logger; +import com.alttd.playershops.utils.ShopUtil; +import com.alttd.playershops.utils.Util; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; + +/** + * Dedicated class to listen to transactions for shops. + * + */ +public class TransactionListener extends EventListener { + + private final PlayerShops plugin; + + public TransactionListener(PlayerShops plugin) { + this.plugin = plugin; + this.register(this.plugin); + } + + @EventHandler(ignoreCancelled = true) + public void onShopSignClick(PlayerInteractEvent event) { + if(!this.isRegistered) + return; + + if (event.getHand() == EquipmentSlot.OFF_HAND) + return; + + Player player = event.getPlayer(); + if (!(event.getAction() == Action.RIGHT_CLICK_BLOCK)) + return; + + Block block = event.getClickedBlock(); + if (block == null || !Tag.WALL_SIGNS.isTagged(block.getType())) + return; + + PlayerShop playerShop = plugin.getShopHandler().getShopBySignLocation(block.getLocation()); + if (playerShop == null) + return; + + // if we ever need worldguard support add it to the hook + if (!WorldGuardHook.canUseShopInRegion(player, block.getLocation())) { + event.setCancelled(true); + return; + } + + if (!player.hasPermission("playershops.shop.use." + playerShop.getType().toString())) { + player.sendMiniMessage("You do not have permission to use " + playerShop.getType().toString() + " shops.", null); // TODO config + return; + } + + ShopHandler shopHandler = plugin.getShopHandler(); + + // Failsafe. If we have a shopsign but no block cancel the event, log error save and unload the shop + if (!shopHandler.isShopMaterial(playerShop.getShopLocation().getBlock())) { + Logger.error("We have a shop here but no connected container"); + event.setCancelled(true); + // TODO LOG THIS ERROR + shopHandler.removeShop(playerShop); + return; + } + + if (ShopUtil.canManageShop(player, playerShop)) { + if (player.isSneaking()) + return; + + ShopManagementGui gui = new ShopManagementGui(player.getUniqueId(), playerShop); + gui.open(); + return; + } + + if (!playerShop.isInitialized()) + return; + + executeTransaction(player, playerShop); + } + + private void executeTransaction(Player player, PlayerShop shop) { + if (shop == null || shop.getItemStack() == null) + return; + + int orders = 1; + + if (player.isSneaking()) + orders = shop.getItemStack().getMaxStackSize(); + TransactionError transactionError = shop.executeTransaction(orders, player); + + // TODO minimessage placeholders + TagResolver placeholders = TagResolver.resolver( + Placeholder.unparsed("ownername", shop.getOwnerName()), + Placeholder.unparsed("price", shop.getPrice() + ""), + Placeholder.unparsed("amount", shop.getAmount() + ""), + Placeholder.component("item", ShopUtil.itemNameComponent(shop.getItemStack())), + Placeholder.unparsed("location", formatLocation(shop.getShopLocation())) + ); + if (transactionError != TransactionError.NONE) { + switch (transactionError) { + case INSUFFICIENT_FUNDS_SHOP -> { + Player shopOwner = Bukkit.getPlayer(shop.getOwnerUUID()); + if (shopOwner != null && notifyOwner(shop)) { + // TODO notify shopowner in game if not on cooldown and once per day on discord if linked and enabled + shopOwner.sendActionBar(Util.parseMiniMessage(shop.getType().getShopTypeConfig().yourShopNoFunds, placeholders)); + } + player.sendMiniMessage(shop.getType().getShopTypeConfig().shopNoStock, placeholders); + } + case INSUFFICIENT_FUNDS_PLAYER -> { + player.sendMiniMessage(shop.getType().getShopTypeConfig().playerNoFunds, placeholders); + } + case INVENTORY_FULL_SHOP -> { + Player shopOwner = Bukkit.getPlayer(shop.getOwnerUUID()); + if (shopOwner != null && notifyOwner(shop)) { + shopOwner.sendActionBar(Util.parseMiniMessage(shop.getType().getShopTypeConfig().yourShopNoStock, placeholders)); + // TODO notify shopowner in game if not on cooldown and once per day on discord if linked and enabled + } + player.sendMiniMessage(shop.getType().getShopTypeConfig().shopInventoryFull, placeholders); + } + case INVENTORY_FULL_PLAYER -> { + player.sendMiniMessage(shop.getType().getShopTypeConfig().playerInventoryFull, placeholders); + } + } + return; + } + player.sendActionBar(Util.parseMiniMessage(shop.getType().getShopTypeConfig().playerBought, placeholders)); + plugin.getDatabaseHelper().logTransaction(player, shop, orders); + } + + private String formatLocation(Location location) { + return Util.capitalize(location.getWorld().getName()) + ": " + + " " + (int) location.getX() + + ", " + (int) location.getY() + + ", " + (int) location.getZ(); + } + + private boolean notifyOwner(PlayerShop playerShop) { + // TODO notify shopowner in game if not on cooldown and once per day on discord if linked and enabled + return playerShop.isNotifiedOwner(); + } +} diff --git a/src/main/java/com/alttd/playershops/shop/AbstractShop.java b/src/main/java/com/alttd/playershops/shop/AbstractShop.java deleted file mode 100644 index ea91e28..0000000 --- a/src/main/java/com/alttd/playershops/shop/AbstractShop.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.alttd.playershops.shop; - -import lombok.Getter; -import lombok.Setter; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Location; -import org.bukkit.block.data.Directional; -import org.bukkit.inventory.ItemStack; - -import java.util.UUID; - -public abstract class AbstractShop { - - @Getter - private int id; - private String ownerName; - @Getter - private UUID ownerUUID; - @Getter @Setter - private ShopType type; - @Getter - private Location signLocation; - @Getter - private Location containerLocation; - @Getter - private String server; - @Getter @Setter - private double price; - @Getter @Setter - private int amount; - @Getter @Setter - private double balance; - @Getter @Setter - private ItemStack itemStack; - @Getter @Setter - private ItemStack secondaryItem; - @Getter @Setter - private long lastTransaction; - - protected boolean initialized; - - AbstractShop(Location signLocation, UUID uuid, double price, int amount) { - this.signLocation = signLocation; - if (signLocation != null) { - Directional sign = (Directional) signLocation.getBlock().getState().getBlockData(); - this.containerLocation = signLocation.getBlock().getRelative(sign.getFacing().getOppositeFace()).getLocation(); - } - this.ownerUUID = uuid; - ownerName = getOwnerName(); - this.price = price; - this.amount = amount; - this.server = Bukkit.getServerName(); - } - - AbstractShop(int id, String ownerName, UUID ownerUUID, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - this.id = id; - this.ownerName = ownerName; - this.ownerUUID = ownerUUID; - this.server = server; - this.containerLocation = containerLocation; - this.signLocation = signLocation; - this.price = price; - this.amount = amount; - this.balance = balance; - this.itemStack = itemOne; - this.secondaryItem = itemTwo; - this.lastTransaction = lastTransaction; - } - - public static AbstractShop create(Location signLocation, UUID player, double price, int amount, ShopType shopType) { - return switch (shopType) { - case SELL -> new SellShop(signLocation, player, price, amount); - case BUY -> new BuyShop(signLocation, player, price, amount); - case GAMBLE -> new GambleShop(signLocation, player, price, amount); - case BARTER -> new BarterShop(signLocation, player, price, amount); - }; - } - - public static AbstractShop create(int id, String ownerName, UUID ownerUUID, ShopType shopType, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - return switch (shopType) { - case SELL -> new SellShop(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, balance, itemOne, itemTwo, lastTransaction); - case BUY -> new BuyShop(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, balance, itemOne, itemTwo, lastTransaction); - case GAMBLE -> new GambleShop(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, balance, itemOne, itemTwo, lastTransaction); - case BARTER -> new BarterShop(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, balance, itemOne, itemTwo, lastTransaction); - }; - } - - public String getOwnerName() { - if(this.ownerName != null) return ownerName; - if (this.getOwnerUUID() != null) { - ownerName = Bukkit.getOfflinePlayer(this.getOwnerUUID()).getName(); - return ownerName; - } - return ChatColor.RED + "[CLOSED]"; - } - - public boolean isInitialized() { - return initialized; - } -} diff --git a/src/main/java/com/alttd/playershops/shop/BarterShop.java b/src/main/java/com/alttd/playershops/shop/BarterShop.java deleted file mode 100644 index 97c972c..0000000 --- a/src/main/java/com/alttd/playershops/shop/BarterShop.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.alttd.playershops.shop; - -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; - -import java.util.UUID; - -public class BarterShop extends AbstractShop { - - public BarterShop(Location location, UUID player, double price, int amount) { - super(location, player, price, amount); - - this.setType(ShopType.BARTER); - } - - public BarterShop(int id, String ownerName, UUID ownerUUID, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - super(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, - balance, itemOne, itemTwo, lastTransaction); - this.setType(ShopType.BARTER); - } - -} diff --git a/src/main/java/com/alttd/playershops/shop/BuyShop.java b/src/main/java/com/alttd/playershops/shop/BuyShop.java deleted file mode 100644 index a324410..0000000 --- a/src/main/java/com/alttd/playershops/shop/BuyShop.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.alttd.playershops.shop; - -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; - -import java.util.UUID; - -public class BuyShop extends AbstractShop { - - public BuyShop(Location location, UUID player, double price, int amount) { - super(location, player, price, amount); - - this.setType(ShopType.BUY); - } - - public BuyShop(int id, String ownerName, UUID ownerUUID, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - super(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, - balance, itemOne, itemTwo, lastTransaction); - this.setType(ShopType.BUY); - } -} diff --git a/src/main/java/com/alttd/playershops/shop/GambleShop.java b/src/main/java/com/alttd/playershops/shop/GambleShop.java deleted file mode 100644 index 8968a11..0000000 --- a/src/main/java/com/alttd/playershops/shop/GambleShop.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.alttd.playershops.shop; - -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; - -import java.util.UUID; - -public class GambleShop extends AbstractShop { - - private ItemStack gambleItem; - - public GambleShop(Location location, UUID player, double price, int amount) { - super(location, player, price, amount); - - this.setType(ShopType.GAMBLE); - this.gambleItem = this.getItemStack(); - } - - public GambleShop(int id, String ownerName, UUID ownerUUID, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - super(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, - balance, itemOne, itemTwo, lastTransaction); - this.setType(ShopType.GAMBLE); - } - -} diff --git a/src/main/java/com/alttd/playershops/shop/PlayerShop.java b/src/main/java/com/alttd/playershops/shop/PlayerShop.java new file mode 100644 index 0000000..6e4e0f1 --- /dev/null +++ b/src/main/java/com/alttd/playershops/shop/PlayerShop.java @@ -0,0 +1,351 @@ +package com.alttd.playershops.shop; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.events.*; +import com.alttd.playershops.utils.EconomyUtils; +import com.alttd.playershops.utils.InventoryUtils; +import com.alttd.playershops.utils.ShopUtil; +import com.alttd.playershops.utils.Util; +import lombok.Getter; +import lombok.Setter; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Sign; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scheduler.BukkitRunnable; + +import java.util.List; +import java.util.UUID; + +public class PlayerShop { + + @Getter + private UUID shopID; + @Getter @Setter // TODO a way to check if the player changed name and update if needed + private String ownerName; + @Getter + private UUID ownerUUID; + @Getter + private ShopType type = ShopType.NONE; + @Getter + private final Location signLocation; + @Getter + private final Location shopLocation; + @Getter + private String server; + @Getter + private double price; + @Getter + private int amount; + @Getter + private double balance; + @Getter + private ItemStack itemStack; + @Getter @Setter + private long lastTransaction; + @Getter @Setter + private boolean notifiedOwner = false; + @Getter @Setter + private boolean dirty; + + public PlayerShop(Location shopLocation, Location signLocation, Player player) { + this(shopLocation, signLocation, player.getUniqueId(), player.getName()); + } + + public PlayerShop(Location shopLocation, Location signLocation, UUID uuid, String playerName) { + this.shopID = UUID.randomUUID(); + this.shopLocation = new Location(shopLocation.getWorld(), shopLocation.getBlockX(), shopLocation.getBlockY(), shopLocation.getBlockZ()); + this.signLocation = new Location(signLocation.getWorld(), signLocation.getBlockX(), signLocation.getBlockY(), signLocation.getBlockZ()); + this.ownerUUID = uuid; + this.ownerName = playerName; + this.server = Bukkit.getServerName(); + } + + public static PlayerShop load(UUID shopID, String ownerName, UUID ownerUUID, ShopType shopType, String server, + Location shopLocation, Location signLocation, double price, int amount, + double balance, ItemStack item, long lastTransaction) { + PlayerShop playerShop = new PlayerShop(shopLocation, signLocation, ownerUUID, ownerName); + + playerShop.shopID = shopID; + playerShop.type = shopType; + playerShop.server = server; + playerShop.price = price; + playerShop.amount = amount; + playerShop.balance = balance; + playerShop.itemStack = item; + playerShop.lastTransaction = lastTransaction; + + return playerShop; + } + + public int getRemainingStock() { + return InventoryUtils.countItems(getInventory(), getItemStack()); + } + + public int getRemainingSpace() { + return ShopUtil.countSpace(getInventory(), getItemStack()); + } + + public boolean matches(ItemStack item) { + return ShopUtil.matches(getItemStack(), item); + } + + public void remove(ItemStack item, int amount) { + Inventory inv = getInventory(); + int remains = amount; + while (remains > 0) { + int stackSize = Math.min(remains, item.getMaxStackSize()); + item.setAmount(stackSize); + inv.removeItem(item); + remains = remains - stackSize; + } + } + + public void add(ItemStack item, int amount) { + Inventory inv = getInventory(); + int remains = amount; + while (remains > 0) { + int stackSize = Math.min(remains, item.getMaxStackSize()); + item.setAmount(stackSize); + inv.addItem(item); + remains = remains - stackSize; + } + } + + public void setOwner(Player player) { + ownerUUID = player.getUniqueId(); + ownerName = player.getName(); + } + + public Inventory getInventory() { + // Could use a check if the block is still an InventoryHolder. + InventoryHolder container = (InventoryHolder) shopLocation.getBlock().getState(); + return container.getInventory(); + } + + public boolean isInitialized() { + return type != ShopType.NONE; + } + + public void updateSign() { + if (!isInitialized()) { + setSignLines(type.getShopTypeConfig().inActiveSignLines); + } else { + setSignLines(type.getShopTypeConfig().activeSignLines); + } + } + + public void removeSignLines() { + setSignLines(type.getShopTypeConfig().activeSignLines); + } + + void setSignLines(List signLines) { + new BukkitRunnable() { + public void run() { + if (!(signLocation.getBlock().getState() instanceof Sign signBlock)) return; + MiniMessage miniMessage = MiniMessage.miniMessage(); + TagResolver tagResolver = TagResolver.resolver( + Placeholder.unparsed("ownername", getOwnerName()), + Placeholder.unparsed("price", String.valueOf(getPrice())), + Placeholder.unparsed("amount", String.valueOf(getAmount())), + Placeholder.component("itemname", ShopUtil.itemNameComponent(getItemStack())) + ); + for (int i = 0; i < 4; i++) { + signBlock.line(i, miniMessage.deserialize(signLines.get(i), tagResolver)); + } + signBlock.update(true); + } + }.runTaskLater(PlayerShops.getInstance(), 2L); + } + + public double getPricePerItem() { + return this.getPrice() / this.getAmount(); + } + + public boolean removeBalance(double amount) { + ShopBalanceChangeEvent shopBalanceChangeEvent = new ShopBalanceChangeEvent(this, ShopBalanceChangeEvent.ChangeReason.WITHDRAW); + if (Util.callCancellableEvent(shopBalanceChangeEvent)) + return false; // cancelled by another plugin, does this need logging? + + setDirty(true); + update(); + this.balance -= amount; + this.setLastTransaction(System.currentTimeMillis()); + return true; + } + + public boolean addBalance(double amount) { + ShopBalanceChangeEvent shopBalanceChangeEvent = new ShopBalanceChangeEvent(this, ShopBalanceChangeEvent.ChangeReason.DEPOSIT); + if (Util.callCancellableEvent(shopBalanceChangeEvent)) + return false; // cancelled by another plugin, does this need logging? + + setDirty(true); + update(); + this.balance += amount; + this.setLastTransaction(System.currentTimeMillis()); + return true; + } + + + public TransactionError executeTransaction(int orders, Player player) { + return switch (getType()) { + case SELL -> executeSellTransaction(orders, player); + case BUY -> executeBuyTransaction(orders, player); + case GAMBLE -> executeGambleTransaction(orders, player); + default -> TransactionError.NONE; // This should not happen + }; + } + + private TransactionError executeSellTransaction(int orders, Player player) { + ItemStack itemStack = getItemStack().clone(); + double price = getPrice(); + if (orders == itemStack.getMaxStackSize()) { + itemStack.setAmount(orders); + price = getPricePerItem() * orders; + } + + int shopItems = InventoryUtils.countItems(getInventory(), itemStack); + if (shopItems < itemStack.getAmount()) + return TransactionError.INSUFFICIENT_FUNDS_SHOP; + + boolean hasFunds = EconomyUtils.hasSufficientFunds(player, price); + if (!hasFunds) + return TransactionError.INSUFFICIENT_FUNDS_PLAYER; + + boolean hasRoom = EconomyUtils.canAcceptFunds(this, price); + if (!hasRoom) + return TransactionError.INVENTORY_FULL_SHOP; + + boolean playerHasRoom = InventoryUtils.hasRoom(player.getInventory(), itemStack); + if (!playerHasRoom) + return TransactionError.INVENTORY_FULL_PLAYER; + + PlayerExchangeShopEvent playerExchangeShopEvent = new PlayerExchangeShopEvent(player, this); + if (Util.callCancellableEvent(playerExchangeShopEvent)) { + return TransactionError.CANCELLED; + } + + if (!addBalance(price)) + return TransactionError.CANCELLED; + + InventoryUtils.removeItem(getInventory(), itemStack); + EconomyUtils.removeFunds(player, price); + InventoryUtils.addItem(player.getInventory(), itemStack); + player.updateInventory(); + + return TransactionError.NONE; + } + + private TransactionError executeBuyTransaction(int orders, Player player) { + ItemStack itemStack = getItemStack().clone(); + double price = getPrice(); + if (orders == itemStack.getMaxStackSize()) { + itemStack.setAmount(orders); + price = getPricePerItem() * orders; + } + + int playerItems = InventoryUtils.countItems(player.getInventory(), itemStack); + if (playerItems < itemStack.getAmount()) + return TransactionError.INSUFFICIENT_FUNDS_PLAYER; + + boolean hasFunds = EconomyUtils.hasSufficientFunds(this, price); + if (!hasFunds) + return TransactionError.INSUFFICIENT_FUNDS_SHOP; + + boolean hasRoom = EconomyUtils.canAcceptFunds(player, price); + if (!hasRoom) + return TransactionError.INVENTORY_FULL_PLAYER; + + boolean shopHasRoom = InventoryUtils.hasRoom(getInventory(), itemStack); + if (!shopHasRoom) + return TransactionError.INVENTORY_FULL_SHOP; + + PlayerExchangeShopEvent playerExchangeShopEvent = new PlayerExchangeShopEvent(player, this); + if (Util.callCancellableEvent(playerExchangeShopEvent)) { + return TransactionError.CANCELLED; + } + + if (!removeBalance(price)) + return TransactionError.CANCELLED; + + InventoryUtils.removeItem(player.getInventory(), itemStack); + EconomyUtils.addFunds(player, price); + InventoryUtils.addItem(getInventory(), itemStack); + player.updateInventory(); + + return TransactionError.NONE; + } + + private TransactionError executeGambleTransaction(int orders, Player player) { + return TransactionError.NONE; + } + + public void setItemStack(ItemStack itemStack) { + if (this.itemStack != null && this.itemStack.equals(itemStack)) + return; // no changes have been made. + ShopItemChangeEvent shopItemChangeEvent = new ShopItemChangeEvent(this, itemStack); + if (Util.callCancellableEvent(shopItemChangeEvent)) + return; // cancelled by another plugin, does this need logging? + + this.itemStack = itemStack; + this.itemStack.setAmount(this.amount != 0 ? this.amount : 1); + setDirty(true); + update(); + } + + public void setPrice(double price) { + if (this.price == price) + return; // no changes have been made. + ShopPriceChangeEvent shopPriceChangeEvent = new ShopPriceChangeEvent(this, price); + if (Util.callCancellableEvent(shopPriceChangeEvent)) + return; // cancelled by another plugin, does this need logging? + + this.price = price; + setDirty(true); + update(); + } + + public void setAmount(int amount) { + if (this.amount == amount) + return; // no changes have been made. + ShopItemAmountChangeEvent ShopItemAmountChangeEvent = new ShopItemAmountChangeEvent(this, amount); + if (Util.callCancellableEvent(ShopItemAmountChangeEvent)) + return; // cancelled by another plugin, does this need logging? + + this.amount = amount; + if (this.itemStack != null) + this.itemStack.setAmount(this.amount); + setDirty(true); + update(); + } + + public void setShopType(ShopType shopType) { + if (this.type == shopType) + return; // no changes have been made. + ShopTypeChangeEvent shopTypeChangeEvent = new ShopTypeChangeEvent(this, shopType); + if (Util.callCancellableEvent(shopTypeChangeEvent)) + return; // cancelled by another plugin, does this need logging? + + this.type = shopType; + setDirty(true); + update(); + } + + /** + * Updates and saves the PlayerShop in the database + */ + private void update() { + PlayerShops.getInstance().getDatabaseHelper().updateShop(this, false); + if (!ShopUtil.isLoaded(signLocation)) + return; + + updateSign(); + } + +} diff --git a/src/main/java/com/alttd/playershops/shop/SellShop.java b/src/main/java/com/alttd/playershops/shop/SellShop.java deleted file mode 100644 index 38e1ad3..0000000 --- a/src/main/java/com/alttd/playershops/shop/SellShop.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.alttd.playershops.shop; - -import org.bukkit.Location; -import org.bukkit.inventory.ItemStack; - -import java.util.UUID; - -public class SellShop extends AbstractShop { - - public SellShop(Location location, UUID player, double price, int amount) { - super(location, player, price, amount); - - this.setType(ShopType.SELL); - } - - public SellShop(int id, String ownerName, UUID ownerUUID, String server, - Location containerLocation, Location signLocation, double price, int amount, - double balance, ItemStack itemOne, ItemStack itemTwo, long lastTransaction) { - super(id, ownerName, ownerUUID, server, containerLocation, signLocation, price, amount, - balance, itemOne, itemTwo, lastTransaction); - this.setType(ShopType.SELL); - } - -} diff --git a/src/main/java/com/alttd/playershops/shop/ShopAction.java b/src/main/java/com/alttd/playershops/shop/ShopAction.java new file mode 100644 index 0000000..9b4fa4e --- /dev/null +++ b/src/main/java/com/alttd/playershops/shop/ShopAction.java @@ -0,0 +1,8 @@ +package com.alttd.playershops.shop; + +public enum ShopAction { + BUY, + SELL, + CREATE, + CANCELLED; +} diff --git a/src/main/java/com/alttd/playershops/shop/ShopInfo.java b/src/main/java/com/alttd/playershops/shop/ShopInfo.java new file mode 100644 index 0000000..38e4f24 --- /dev/null +++ b/src/main/java/com/alttd/playershops/shop/ShopInfo.java @@ -0,0 +1,4 @@ +package com.alttd.playershops.shop; + +public class ShopInfo { +} diff --git a/src/main/java/com/alttd/playershops/shop/ShopTransaction.java b/src/main/java/com/alttd/playershops/shop/ShopTransaction.java deleted file mode 100755 index cfa5ab9..0000000 --- a/src/main/java/com/alttd/playershops/shop/ShopTransaction.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.alttd.playershops.shop; - -public class ShopTransaction { - - public enum ShopTransactionError { - CANCELLED, - INSUFFICIENT_FUNDS_SHOP, - INSUFFICIENT_FUNDS_PLAYER, - INVENTORY_FULL_SHOP, - INVENTORY_FULL_PLAYER, - NONE; - } -} diff --git a/src/main/java/com/alttd/playershops/shop/ShopType.java b/src/main/java/com/alttd/playershops/shop/ShopType.java index 1916207..0c789de 100644 --- a/src/main/java/com/alttd/playershops/shop/ShopType.java +++ b/src/main/java/com/alttd/playershops/shop/ShopType.java @@ -1,10 +1,34 @@ package com.alttd.playershops.shop; +import com.alttd.playershops.config.ShopTypeConfig; + public enum ShopType { - SELL, - BUY, - GAMBLE, - BARTER; + + NONE(), + SELL(), + BUY(), + GAMBLE(); + + private ShopTypeConfig shopTypeConfig; + ShopType() { + this.shopTypeConfig = new ShopTypeConfig(this.toString()); + } + + public ShopTypeConfig getShopTypeConfig() { + return shopTypeConfig; + } + + public static ShopType fromString(String name) { + if (name == null) + return ShopType.NONE; + + for (ShopType shopType : ShopType.values()) { + if (name.equalsIgnoreCase(shopType.toString())) + return shopType; + } + + return ShopType.NONE; + } @Override public String toString() { diff --git a/src/main/java/com/alttd/playershops/shop/ShopTransactionError.java b/src/main/java/com/alttd/playershops/shop/TransactionError.java old mode 100755 new mode 100644 similarity index 77% rename from src/main/java/com/alttd/playershops/shop/ShopTransactionError.java rename to src/main/java/com/alttd/playershops/shop/TransactionError.java index 5affe5a..235258a --- a/src/main/java/com/alttd/playershops/shop/ShopTransactionError.java +++ b/src/main/java/com/alttd/playershops/shop/TransactionError.java @@ -1,10 +1,11 @@ package com.alttd.playershops.shop; -public enum ShopTransactionError { +public enum TransactionError { CANCELLED, INSUFFICIENT_FUNDS_SHOP, INSUFFICIENT_FUNDS_PLAYER, INVENTORY_FULL_SHOP, INVENTORY_FULL_PLAYER, + PLAYER_OFFLINE, NONE; } diff --git a/src/main/java/com/alttd/playershops/storage/database/Database.java b/src/main/java/com/alttd/playershops/storage/database/Database.java new file mode 100644 index 0000000..8c41356 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/Database.java @@ -0,0 +1,117 @@ +package com.alttd.playershops.storage.database; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.config.DatabaseConfig; +import com.alttd.playershops.utils.Logger; +import org.bukkit.Bukkit; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class Database { +// +// private static Database instance = null; +// private Connection connection = null; +// +// private Database() {} +// +// public static Database getDatabase(){ +// if (instance == null) +// { +// instance = new Database(); +// instance.init(); +// } +// return (instance); +// } +// +// protected void init() { +// try { +// openConnection(); +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// +// //Run all create table functions +// for (Method method : Database.class.getDeclaredMethods()) { +// if (Modifier.isPrivate(method.getModifiers())) { +// if (method.getParameterTypes().length == 0 && method.getReturnType() == Void.TYPE) { +// try { +// method.setAccessible(true); +// method.invoke(instance); +// } catch (InvocationTargetException ex) { +// throw new RuntimeException(ex.getCause()); +// } catch (Exception ex) { +// Logger.severe("Error invoking " + method + "."); +// ex.printStackTrace(); +// } +// } +// } +// } +// } +// +// /** +// * Opens the connection if it's not already open. +// * @throws SQLException If it can't create the connection. +// */ +// private void openConnection() throws SQLException { +// if (connection != null && !connection.isClosed()) { +// return; +// } +// +// synchronized (this) { +// if (connection != null && !connection.isClosed()) { +// return; +// } +// try { +// Class.forName("com.mysql.cj.jdbc.Driver"); +// } catch (ClassNotFoundException e) { +// e.printStackTrace(); +// } +// +// connection = DriverManager.getConnection( +// "jdbc:mysql://" + DatabaseConfig.IP + ":" + DatabaseConfig.PORT + "/" + DatabaseConfig.DATABASE_NAME + +// "?autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true", +// DatabaseConfig.USERNAME, DatabaseConfig.PASSWORD); +// } +// } +// +// public Connection getConnection() { +// try { +// openConnection(); +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return connection; +// } +// +// private static void createShopTable() { +// try { +// String sql = "CREATE TABLE IF NOT EXISTS shops(" + +// "id INT NOT NULL AUTO_INCREMENT, " + +// "owner_name VARCHAR(16) NOT NULL, " + +// "owner_uuid VARCHAR(36) NOT NULL, " + +// "shop_type VARCHAR(36) NOT NULL, " + +// "server VARCHAR(16) NOT NULL, " + +// "container_location VARCHAR(256), " + +// "sign_location VARCHAR(256), " + +// "price DOUBLE NOT NULL, " + +// "amount INT NOT NULL, " + +// "balance DOUBLE NOT NULL, " + +// "item_one TEXT, " + +// "last_transaction BIGINT, " + +// "PRIMARY KEY (id)" + +// ")"; +// getDatabase().getConnection().prepareStatement(sql).executeUpdate(); +// } catch (SQLException e) { +// e.printStackTrace(); +// Logger.severe("Error while trying to create shop table"); +// Logger.severe("Shutting down PlayerShops"); +// Bukkit.getPluginManager().disablePlugin(PlayerShops.getInstance()); +// } +// } + +} diff --git a/src/main/java/com/alttd/playershops/storage/database/DatabaseConnection.java b/src/main/java/com/alttd/playershops/storage/database/DatabaseConnection.java new file mode 100644 index 0000000..747eb30 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseConnection.java @@ -0,0 +1,83 @@ +package com.alttd.playershops.storage.database; + +import com.alttd.playershops.config.DatabaseConfig; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DatabaseConnection implements AutoCloseable { + private Connection connection; + private volatile boolean isActive; + + public DatabaseConnection() { + try { + openConnection(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + private synchronized void openConnection() throws SQLException { + if (connection != null && !connection.isClosed()) { + return; + } + + synchronized (this) { + if (connection != null && !connection.isClosed()) { + return; + } + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + connection = DriverManager.getConnection( + "jdbc:mysql://" + DatabaseConfig.IP + ":" + DatabaseConfig.PORT + "/" + DatabaseConfig.DATABASE_NAME + + "?autoReconnect=true&useSSL=false", + DatabaseConfig.USERNAME, DatabaseConfig.PASSWORD); + } + } + + public synchronized Connection get() { + try { + openConnection(); + } catch (SQLException e) { + e.printStackTrace(); + } + + return connection; + } + + public synchronized boolean isValid() { + try { + return !connection.isClosed() && connection.isValid(8000); + } catch (SQLException e) { + e.printStackTrace(); + return false; + } + } + + synchronized void setActive(boolean active) { + isActive = active; + } + + public synchronized boolean isActive() { + return isActive; + } + + @Override + public synchronized void close() { + try { + if (!connection.isClosed()) { + if (!connection.getAutoCommit()) { + connection.commit(); + } + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/alttd/playershops/storage/database/DatabaseHelper.java b/src/main/java/com/alttd/playershops/storage/database/DatabaseHelper.java new file mode 100644 index 0000000..24e5f70 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseHelper.java @@ -0,0 +1,228 @@ +package com.alttd.playershops.storage.database; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.shop.ShopType; +import com.alttd.playershops.utils.Logger; +import com.alttd.playershops.utils.ShopUtil; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.UUID; + +/** + * Util class to predefine sql queries that need to run. + */ +public record DatabaseHelper(PlayerShops plugin, DatabaseManager databaseManager) { + + /** + * Checks if all the tables are present, if not create them. + */ + public void init() { + Logger.info("Checking required tables"); + if (!databaseManager().hasTable("shops")) + createShopTable(); + + if (databaseManager().hasTable("transactions")) + createTransactionsTable(); + } + + void createShopTable() { + Logger.info("Creating shops table"); + String sql = "CREATE TABLE IF NOT EXISTS shops(" + + "id VARCHAR(36) NOT NULL, " + + "owner_name VARCHAR(16) NOT NULL, " + + "owner_uuid VARCHAR(36) NOT NULL, " + + "shop_type VARCHAR(36), " + + "server VARCHAR(16) NOT NULL, " + + "container_location VARCHAR(256), " + + "sign_location VARCHAR(256), " + + "price DOUBLE, " + + "amount INT, " + + "balance DOUBLE, " + + "item BLOB, " + + "last_transaction BIGINT, " + + "PRIMARY KEY (id)" + + ")"; + databaseManager().addDatabaseQuery(new DatabaseQuery(sql), false); + } + + void createTransactionsTable() { + Logger.info("Creating transactions table"); + String sql = "CREATE TABLE IF NOT EXISTS transactions(" + + "id INT NOT NULL AUTO INCREMENT, " + + "shop_id VARCHAR(36) NOT NULL, " + + "actor_name VARCHAR(16) NOT NULL, " + + "actor_uuid VARCHAR(36) NOT NULL, " + + "server VARCHAR(16) NOT NULL, " + + "action VARCHAR(36) NOT NULL, " + + "time BIGINT NOT NULL, " + + "price DOUBLE, " + + "amount INT, " + + "item BLOB, " + + "PRIMARY KEY (id)" + + ")"; + databaseManager().addDatabaseQuery(new DatabaseQuery(sql), false); + } + + ResultSet selectTable(String tableName) throws SQLException { + String sql = "SELECT * FROM " + tableName; + return databaseManager().getDatabaseConnection().get().prepareStatement(sql).executeQuery(); + } + + public ResultSet selectAllShops() throws SQLException { + return selectTable("shops"); + } + + public void createShop(PlayerShop shop) { + String sql = "INSERT INTO shops " + + "(id, owner_name, owner_uuid, server, container_location, sign_location)" + + "VALUES (?, ?, ?, ?, ?, ?)"; + databaseManager().addDatabaseQuery( + new DatabaseQuery(sql, new DatabaseQuery.DatabaseTask() { + @Override + public void edit(PreparedStatement ps) throws SQLException { + ps.setString(1, shop.getShopID().toString()); + ps.setString(2, shop.getOwnerName()); + ps.setString(3, shop.getOwnerUUID().toString()); + ps.setString(4, Bukkit.getServerName()); + ps.setString(5, ShopUtil.locationToString(shop.getShopLocation())); + ps.setString(6, ShopUtil.locationToString(shop.getSignLocation())); + } + + @Override + public void onSuccess() { + shop.updateSign(); + } + + @Override + public void onFailure(SQLException e) { + Logger.error("Could not save shop for " + shop.getOwnerName() + " at " + shop.getShopLocation() + " to the database.\n" + e); + } + }), false + ); + } + + public void removeShop(PlayerShop shop) { + String sql = "DELETE FROM shops WHERE id = ?"; + databaseManager().addDatabaseQuery( + new DatabaseQuery(sql, new DatabaseQuery.DatabaseTask() { + @Override + public void edit(PreparedStatement ps) throws SQLException { + ps.setString(1, shop.getShopID().toString()); + } + + @Override + public void onSuccess() { + shop.removeSignLines(); + } + + @Override + public void onFailure(SQLException e) { + Logger.error("Could not remove shop for " + shop.getOwnerName() + " at " + shop.getShopLocation() + " to the database.\n" + e); + } + }), true); + } + + /** + * Loads a shop from a result set, does not iterate + * @param resultSet Result set to load from + * @return A shop + * @throws SQLException if data is missing or formatted incorrectly + */ + public PlayerShop shopFromResultSet(ResultSet resultSet) throws SQLException { + UUID id = UUID.fromString(resultSet.getString("id")); + String ownerName = resultSet.getString("owner_name"); + UUID ownerUuid = UUID.fromString(resultSet.getString("owner_uuid")); + ShopType shopType = ShopType.fromString(resultSet.getString("shop_type")); + String server = resultSet.getString("server"); + Location containerLocation = ShopUtil.stringToLocation(resultSet.getString("container_location")); + Location signLocation = ShopUtil.stringToLocation(resultSet.getString("sign_location")); + double price = resultSet.getDouble("price"); + int amount = resultSet.getInt("amount"); + double balance = resultSet.getDouble("balance"); + byte[] itemstackbytes = resultSet.getBytes("item"); + ItemStack itemStack = null; + if (itemstackbytes != null) { + itemStack = ItemStack.deserializeBytes(resultSet.getBytes("item")); + } + long lastTransaction = resultSet.getLong("last_transaction"); + + if (containerLocation == null || signLocation == null) + return null; + + return PlayerShop.load(id, ownerName, ownerUuid, shopType, server, containerLocation, signLocation, + price, amount, balance, itemStack, lastTransaction); + } + + /** + * Updates and saves the PlayerShop in the database + */ + public void updateShop(PlayerShop shop, boolean queue) { + String query = "UPDATE shops SET owner_name = ?, owner_uuid = ?, shop_type = ?, server = ?, " + + "container_location = ?, sign_location = ?, price = ?, amount = ?, balance = ?, " + + "item = ?, last_transaction = ? WHERE id = ?"; + databaseManager().addDatabaseQuery( + new DatabaseQuery(query, ps -> { + ps.setString(1, shop.getOwnerName()); + ps.setString(2, shop.getOwnerUUID().toString()); + ps.setString(3, shop.getType().toString()); + ps.setString(4, shop.getServer()); + ps.setString(5, ShopUtil.locationToString(shop.getShopLocation())); + ps.setString(6, ShopUtil.locationToString(shop.getSignLocation())); + ps.setDouble(7, shop.getPrice()); + ps.setInt(8, shop.getAmount()); + ps.setDouble(9, shop.getBalance()); + ItemStack itemStack = shop.getItemStack(); + if (itemStack != null && !itemStack.getType().equals(Material.AIR)) { + ps.setBytes(10, shop.getItemStack().serializeAsBytes()); + } else { + ps.setBytes(10, null); + } + ps.setLong(11, shop.getLastTransaction()); + ps.setString(12, shop.getShopID().toString()); + }), queue + ); + } + + /** + * Log a transaction with a shop + */ + public void logTransaction(Player player, PlayerShop shop, int orders) { + String query = "INSERT INTO transaction (shop_id, actor_name, actor_uuid, server, action, time, price, amount, item) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + databaseManager().addDatabaseQuery( + new DatabaseQuery(query, new DatabaseQuery.DatabaseTask() { + @Override + public void edit(PreparedStatement ps) throws SQLException { + ps.setString(1, shop.getShopID().toString()); + ps.setString(2, player.getName()); + ps.setString(3, player.getUniqueId().toString()); + ps.setString(4, shop.getServer()); + ps.setString(5, shop.getType().toString()); + ps.setLong(6, new Date().getTime()); + ps.setDouble(7, shop.getPrice()); + ps.setInt(8, orders); + ItemStack itemStack = shop.getItemStack(); + if (itemStack != null && !itemStack.getType().equals(Material.AIR)) { + ps.setBytes(9, shop.getItemStack().serializeAsBytes()); + } else { + ps.setBytes(9, null); + } + } + + @Override + public void onFailure(SQLException e) { + Logger.error("Could not log transaction by " + player.getName() + " at " + shop.getShopLocation() + " to the database.\n" + e); + } + }), true + ); + } +} diff --git a/src/main/java/com/alttd/playershops/storage/database/DatabaseManager.java b/src/main/java/com/alttd/playershops/storage/database/DatabaseManager.java new file mode 100644 index 0000000..4a2cd81 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseManager.java @@ -0,0 +1,109 @@ +package com.alttd.playershops.storage.database; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.config.DatabaseConfig; +import com.alttd.playershops.storage.database.DatabaseConnection; +import com.alttd.playershops.storage.database.DatabaseQuery; +import com.alttd.playershops.storage.database.DatabaseQueue; +import org.jetbrains.annotations.NotNull; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class DatabaseManager { + + DatabaseQueue databaseQueue; + private final List CONNECTIONPOOL = new ArrayList<>(); + + public DatabaseManager(PlayerShops playerShops) { + databaseQueue = new DatabaseQueue(this); + int delay = DatabaseConfig.queueDelay * 20; + databaseQueue.runTaskTimerAsynchronously(playerShops, delay, delay); + // preload out database connections, TODO FIND A BETTER WAY TO LIMIT THIS + for (int i = 1; i < DatabaseConfig.maxDatabaseConnections; i++) { + CONNECTIONPOOL.add(null); + } + } + + public DatabaseConnection getDatabaseConnection() { + for (int i = 0; i < DatabaseConfig.maxDatabaseConnections; i++) { + DatabaseConnection connection = CONNECTIONPOOL.get(i); + if (connection == null) { + return generateDatabaseConnection(i); + } else if (!connection.isActive()) { + if (connection.isValid()) { + return connection; + } else { + connection.close(); + return generateDatabaseConnection(i); + } + } + } + + // This will cause an infinite running loop, throw an exception or wait for a connection to be available? + return getDatabaseConnection(); + } + + private DatabaseConnection generateDatabaseConnection(int index) { + DatabaseConnection connection = new DatabaseConnection(); + CONNECTIONPOOL.set(index, connection); + + return connection; + } + + private void closeDatabaseConnections() { + for (DatabaseConnection connection : CONNECTIONPOOL) { + if (connection == null || connection.isValid()) + continue; + + if (!connection.isActive()) { + connection.close(); + } else { + while (connection.isActive()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // This should not be interrupted as this is saving all the shops in the background for us. + e.printStackTrace(); + } + } + connection.close(); + } + } + } + + public void unload() { + if (databaseQueue != null && !databaseQueue.isCancelled()) { + databaseQueue.cancel(); + databaseQueue.runTaskQueue(); + } + closeDatabaseConnections(); + } + + public void addDatabaseQuery(DatabaseQuery databaseQuery, boolean queue) { + if (queue) { + databaseQueue.getDatabaseQueryQueue().offer(databaseQuery); + } else { + databaseQuery.execute(getDatabaseConnection().get()); + } + } + + boolean hasTable(String table){ + DatabaseConnection connection = getDatabaseConnection(); + boolean match = false; + try (ResultSet rs = connection.get().getMetaData().getTables(null, null, table, null)) { + while (rs.next()) { + if (table.equalsIgnoreCase(rs.getString("TABLE_NAME"))) { + match = true; + break; + } + } + } catch (SQLException e) { + return match; + } + return match; + } + +} diff --git a/src/main/java/com/alttd/playershops/storage/database/DatabaseQuery.java b/src/main/java/com/alttd/playershops/storage/database/DatabaseQuery.java new file mode 100644 index 0000000..804c9fa --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseQuery.java @@ -0,0 +1,41 @@ +package com.alttd.playershops.storage.database; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class DatabaseQuery { + + private final String statement; + private final DatabaseTask databaseTask; + + public DatabaseQuery(String statement, DatabaseTask databaseTask) { + this.statement = statement; + this.databaseTask = databaseTask; + } + + public DatabaseQuery(String statement) { + this(statement, ps -> {}); + } + + public void execute(Connection connection) { + try (PreparedStatement preparedStatement = connection.prepareStatement(statement)) { + databaseTask.edit(preparedStatement); + preparedStatement.execute(); + databaseTask.onSuccess(); + } catch (SQLException e) { + databaseTask.onFailure(e); + } + } + + public interface DatabaseTask { + + void edit(PreparedStatement preparedStatement) throws SQLException; + + default void onSuccess() {}; + + default void onFailure(SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/alttd/playershops/storage/database/DatabaseQueue.java b/src/main/java/com/alttd/playershops/storage/database/DatabaseQueue.java new file mode 100644 index 0000000..5d4d4a2 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseQueue.java @@ -0,0 +1,58 @@ +package com.alttd.playershops.storage.database; + +import lombok.Getter; +import org.bukkit.scheduler.BukkitRunnable; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +public class DatabaseQueue extends BukkitRunnable { + + private final DatabaseManager databaseManager; + + public DatabaseQueue(DatabaseManager databaseManager) { + this.databaseManager = databaseManager; + } + + @Getter + public final Queue databaseQueryQueue = new LinkedBlockingQueue<>(); + + @Override + public void run() { + runTaskQueue(); + } + + public synchronized void runTaskQueue() { + if (databaseQueryQueue.isEmpty()) + return; + + DatabaseConnection databaseConnection = databaseManager.getDatabaseConnection(); + Connection connection = databaseConnection.get(); + + try { + databaseConnection.setActive(true); + connection.setAutoCommit(false); + while (!databaseQueryQueue.isEmpty()) { + if (!databaseConnection.isValid()) + return; + + DatabaseQuery databaseQuery = databaseQueryQueue.poll(); + if (databaseQuery == null) + return; + + databaseQuery.execute(connection); + } + if (!connection.getAutoCommit()) { + connection.commit(); + connection.setAutoCommit(true); + } + } catch (SQLException e) { + e.printStackTrace(); + } finally { + databaseConnection.setActive(false); + } + + } +} diff --git a/src/main/java/com/alttd/playershops/storage/database/ShopQueries.java b/src/main/java/com/alttd/playershops/storage/database/ShopQueries.java new file mode 100644 index 0000000..d87d059 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/ShopQueries.java @@ -0,0 +1,149 @@ +package com.alttd.playershops.storage.database; + +import com.alttd.playershops.shop.PlayerShop; +import com.alttd.playershops.shop.ShopType; +import com.alttd.playershops.utils.AMath; +import com.alttd.playershops.utils.Logger; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.inventory.ItemStack; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class ShopQueries { +// +// public static boolean saveShop(PlayerShop shop) { +// String sql = "INSERT INTO shops " + +// "(id, owner_name, owner_uuid, shop_type, server, container_location, sign_location, " + +// "price, amount, balance, item_one, item_two, last_transaction)" + +// "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + +// "ON DUPLICATE KEY UPDATE owner_name = ?, owner_uuid = ?, shop_type = ?, server = ?, " + +// "container_location = ?, sign_location = ?, price = ?, amount = ?, balance = ?, " + +// "item_one = ?, last_transaction = ?"; +// try { +// PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); +// +// statement.setInt(1, shop.getId()); +// statement.setString(2, shop.getOwnerName()); +// statement.setString(3, shop.getOwnerUUID().toString()); +// statement.setString(4, shop.getServer()); +// statement.setString(5, shop.getType().toString()); +// statement.setString(6, locationToString(shop.getShopLocation())); +// statement.setString(7, locationToString(shop.getSignLocation())); +// statement.setDouble(8, shop.getPrice()); +// statement.setInt(9, shop.getAmount()); +// statement.setDouble(10, shop.getBalance()); +// statement.setBytes(11, shop.getItemStack().serializeAsBytes()); +// statement.setLong(12, shop.getLastTransaction()); +// //repeat everything except id for update +// statement.setString(13, shop.getOwnerName()); +// statement.setString(14, shop.getOwnerUUID().toString()); +// statement.setString(15, shop.getServer()); +// statement.setString(16, shop.getType().toString()); +// statement.setString(17, locationToString(shop.getShopLocation())); +// statement.setString(18, locationToString(shop.getSignLocation())); +// statement.setDouble(19, shop.getPrice()); +// statement.setInt(20, shop.getAmount()); +// statement.setDouble(21, shop.getBalance()); +// statement.setBytes(22, shop.getItemStack().serializeAsBytes()); +// statement.setLong(23, shop.getLastTransaction()); +// +// return statement.executeUpdate() == 1; +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return false; +// } +// +// public static PlayerShop loadShop(int id) { +// String sql = "SELECT * FROM shops WHERE id = ?"; +// try { +// PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); +// +// statement.setInt(1, id); +// ResultSet resultSet = statement.executeQuery(); +// if (resultSet.next()) +// return shopFromResultSet(resultSet); +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return null; +// } +// +// public static List loadShops() { +// String sql = "SELECT * FROM shops"; +// ArrayList shops = new ArrayList<>(); +// try { +// PreparedStatement statement = Database.getDatabase().getConnection().prepareStatement(sql); +// +// ResultSet resultSet = statement.executeQuery(); +// while (resultSet.next()) { +// PlayerShop shop = shopFromResultSet(resultSet); +// if (shop == null) { +// Logger.warn("Tried to load a shop but failed [" + resultSet + "]"); +// continue; +// } +// shops.add(shop); +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// } +// return shops; +// } +// +// /** +// * Loads a shop from a result set, does not iterate +// * @param resultSet Result set to load from +// * @return A shop +// * @throws SQLException if data is missing or formatted incorrectly +// */ +// private static PlayerShop shopFromResultSet(ResultSet resultSet) throws SQLException { +// int id = resultSet.getInt("id"); +// String ownerName = resultSet.getString("owner_name"); +// UUID ownerUuid = UUID.fromString(resultSet.getString("owner_uuid")); +// ShopType shopType = ShopType.valueOf(resultSet.getString("shop_type")); +// String server = resultSet.getString("server"); +// Location containerLocation = stringToLocation(resultSet.getString("container_location")); +// Location signLocation = stringToLocation(resultSet.getString("sign_location")); +// double price = resultSet.getDouble("price"); +// int amount = resultSet.getInt("amount"); +// double balance = resultSet.getDouble("balance"); +// ItemStack itemOne = ItemStack.deserializeBytes(resultSet.getBytes("item_one")); +//// ItemStack itemTwo = ItemStack.deserializeBytes(resultSet.getBytes("item_two")); +// long lastTransaction = resultSet.getLong("last_transaction"); +// +// if (containerLocation == null || signLocation == null) +// return null; +// +// return PlayerShop.create(id, ownerName, ownerUuid, shopType, server, containerLocation, signLocation, +// price, amount, balance, itemOne, lastTransaction); +// } +// +// private static String locationToString(Location location) { +// return location.getWorld() + ":" + +// AMath.round(location.getX(), 1) + ":" + +// AMath.round(location.getY(), 1) + ":" + +// AMath.round(location.getZ(), 1); +// } +// +// private static Location stringToLocation(String string) { +// String[] split = string.split(":"); +// if (split.length != 4) { +// Logger.warn("Unable to load location [" + string + "] due to invalid format"); +// return null; +// } +// +// try { +// return new Location(Bukkit.getWorld(split[0]), +// Double.parseDouble(split[1]), Double.parseDouble(split[2]), Double.parseDouble(split[3])); +// } catch (NumberFormatException e) { +// Logger.warn("Unable to load location [" + string + "] due to invalid format"); +// return null; +// } +// } +} diff --git a/src/main/java/com/alttd/playershops/utils/EconomyUtils.java b/src/main/java/com/alttd/playershops/utils/EconomyUtils.java new file mode 100644 index 0000000..93dfdeb --- /dev/null +++ b/src/main/java/com/alttd/playershops/utils/EconomyUtils.java @@ -0,0 +1,58 @@ +package com.alttd.playershops.utils; + +import com.alttd.playershops.PlayerShops; +import com.alttd.playershops.shop.PlayerShop; +import net.milkbowl.vault.economy.EconomyResponse; +import org.bukkit.entity.Player; + +// TODO document +public class EconomyUtils { + + //check to see if the player has enough funds to take out [amount] + //return false if they do not + public static boolean hasSufficientFunds(Player player, double amount) { + double balance = PlayerShops.getInstance().getEconomy().getBalance(player); + return (balance >= amount); + } + + public static boolean hasSufficientFunds(PlayerShop shop, double amount) { + double balance = shop.getBalance(); + return (balance >= amount); + } + + //gets the current funds of the player + public static double getFunds(Player player) { + return PlayerShops.getInstance().getEconomy().getBalance(player); + } + + //removes [amount] of funds from the player + //return false if the player did not have sufficient funds or if something went wrong + public static boolean removeFunds(Player player, double amount) { + EconomyResponse response = PlayerShops.getInstance().getEconomy().withdrawPlayer(player, amount); + return response.transactionSuccess(); + } + + //adds [amount] of funds to the player + //return false if the player is offline + public static boolean addFunds(Player player, double amount) { + if(player.isOnline()) { + EconomyResponse response = PlayerShops.getInstance().getEconomy().depositPlayer(player, amount); + return response.transactionSuccess(); + } else { + return false; +// int temp = PlayerShops.getInstance().Earnings.getInt(player.getUniqueId() + ".earnings"); +// PlayerShops.getInstance().Earnings.set(player.getUniqueId() + ".earnings", temp + amount); +// PlayerShops.getInstance().Earnings.save(); + } + } + + public static boolean canAcceptFunds(Player player, double price) { + // if we ever need to limit the maximum balance a player can have this is the place + return true; + } + + public static boolean canAcceptFunds(PlayerShop shop, double price) { + // if we ever need to limit the maximum balance a shop can have this is the place + return true; + } +} diff --git a/src/main/java/com/alttd/playershops/utils/InventoryUtils.java b/src/main/java/com/alttd/playershops/utils/InventoryUtils.java new file mode 100644 index 0000000..106fdcf --- /dev/null +++ b/src/main/java/com/alttd/playershops/utils/InventoryUtils.java @@ -0,0 +1,119 @@ +package com.alttd.playershops.utils; + +import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.HashMap; + +public class InventoryUtils { + + /** + * Counts the number of items in the given inventory where + * Util.matches(inventory item, item) is true. + * + * @param inv + * The inventory to search + * @param item + * The ItemStack to search for + * @return The number of items that match in this inventory. + */ + public static int countItems(Inventory inv, ItemStack item) { + int items = 0; + for (ItemStack iStack : inv.getContents()) { + if (iStack == null) + continue; + if (ShopUtil.matches(item, iStack)) { + items += iStack.getAmount(); + } + } + return items; + } + + // TODO DOCS + //removes itemstack from inventory + //returns the amount of items it could not remove + public static int removeItem(Inventory inventory, ItemStack itemStack) { + if (inventory == null) + return itemStack.getAmount(); + if (itemStack == null || itemStack.getAmount() <= 0) + return 0; + ItemStack[] contents = inventory.getContents(); + int amount = itemStack.getAmount(); + for (ItemStack stack : contents) { + if (stack != null) { + if (ShopUtil.matches(stack, itemStack)) { + if (stack.getAmount() > amount) { + stack.setAmount(stack.getAmount() - amount); + inventory.setContents(contents); + return 0; + } else if (stack.getAmount() == amount) { + stack.setType(Material.AIR); + inventory.setContents(contents); + return 0; + } else { + amount -= stack.getAmount(); + stack.setType(Material.AIR); + } + } + } + } + inventory.setContents(contents); + return amount; + } + + //takes an ItemStack and splits it up into multiple ItemStacks with correct stack sizes + //then adds those items to the given inventory + public static int addItem(Inventory inventory, ItemStack itemStack) { + if (inventory == null) + return itemStack.getAmount(); + if (itemStack.getAmount() <= 0) + return 0; + ArrayList itemStacksAdding = new ArrayList<>(); + + //break up the itemstack into multiple ItemStacks with correct stack size + int fullStacks = itemStack.getAmount() / itemStack.getMaxStackSize(); + int partialStack = itemStack.getAmount() % itemStack.getMaxStackSize(); + for (int i = 0; i < fullStacks; i++) { + ItemStack is = itemStack.clone(); + is.setAmount(is.getMaxStackSize()); + itemStacksAdding.add(is); + } + ItemStack is = itemStack.clone(); + is.setAmount(partialStack); + if (partialStack > 0) + itemStacksAdding.add(is); + + //try adding all items from itemStacksAdding and return number of ones you couldn't add + int amount = 0; + for (ItemStack addItem : itemStacksAdding) { + HashMap noAdd = inventory.addItem(addItem); + for (ItemStack noAddItemstack : noAdd.values()) { + amount += noAddItemstack.getAmount(); + } + } + return amount; + } + + public static boolean hasRoom(Inventory inventory, ItemStack itemStack) { + if (inventory == null) + return false; + if (itemStack.getAmount() <= 0) + return true; + + int overflow = addItem(inventory, itemStack); + + //revert back if inventory cannot hold all of the items + if (overflow > 0) { + ItemStack revert = itemStack.clone(); + revert.setAmount(revert.getAmount() - overflow); + InventoryUtils.removeItem(inventory, revert); + return false; + } + removeItem(inventory, itemStack); + return true; + } + +} diff --git a/src/main/java/com/alttd/playershops/utils/Logger.java b/src/main/java/com/alttd/playershops/utils/Logger.java index 1d9bb77..f59b12a 100644 --- a/src/main/java/com/alttd/playershops/utils/Logger.java +++ b/src/main/java/com/alttd/playershops/utils/Logger.java @@ -1,29 +1,43 @@ package com.alttd.playershops.utils; -import net.md_5.bungee.api.ChatColor; -import org.bukkit.Bukkit; +import com.alttd.playershops.PlayerShops; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; -import java.util.logging.Level; +public class Logger { -public class Logger -{ + private static ComponentLogger logger; + /** + * Logs a message on the INFO level + * + * @param str Message to be logged. + */ public static void info(String str) { - log(Level.INFO,"&e" + str); + getLogger().info(Util.parseMiniMessage(str, null)); } + /** + * Logs a message on the WARN level + * + * @param str Message to be logged. + */ public static void warn(String str) { - log(Level.SEVERE,"&6" + str); + getLogger().warn(Util.parseMiniMessage(str, null)); } - public static void severe(String str) { - log(Level.SEVERE,"&c" + str); + /** + * Logs a message on the ERROR level + * + * @param str Message to be logged. + */ + public static void error(String str) { + getLogger().error(Util.parseMiniMessage("" + str, null)); } - public static void log(Level level, String str) { - Bukkit.getLogger().log(level, - ChatColor.translateAlternateColorCodes('&', - "&r " + str)); - } + static ComponentLogger getLogger() { + if (logger == null) + logger = PlayerShops.getInstance().getComponentLogger(); + return logger; + } } diff --git a/src/main/java/com/alttd/playershops/utils/ShopUtil.java b/src/main/java/com/alttd/playershops/utils/ShopUtil.java new file mode 100644 index 0000000..5ccb5ae --- /dev/null +++ b/src/main/java/com/alttd/playershops/utils/ShopUtil.java @@ -0,0 +1,196 @@ +package com.alttd.playershops.utils; + +import com.alttd.playershops.shop.PlayerShop; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.Map; +import java.util.UUID; + +public class ShopUtil { + + /** + * Returns true if the given location is loaded or not. + * + * @param loc + * The location + * @return true if the given location is loaded or not. + */ + public static boolean isLoaded(Location loc) { + if (!loc.isWorldLoaded()) { + return false; + } + int x = (int) Math.floor((loc.getBlockX()) / 16.0); + int z = (int) Math.floor((loc.getBlockZ()) / 16.0); + return (loc.getWorld().isChunkLoaded(x, z)); + } + + /** + * Compares two items to each other. Returns true if they match. + * + * @param stack1 + * The first item stack + * @param stack2 + * The second item stack + * @return true if the itemstacks match. (Material, durability, enchants, name) + */ + public static boolean matches(ItemStack stack1, ItemStack stack2) { + if (stack1 == stack2) + return true; // Referring to the same thing, or both are null. + if (stack1 == null || stack2 == null) + return false; // One of them is null (Can't be both, see above) + if (stack1.getType() != stack2.getType()) + return false; // Not the same material + if (stack1.getDurability() != stack2.getDurability()) + return false; // Not the same durability + if (!stack1.getEnchantments().equals(stack2.getEnchantments())) + return false; // They have the same enchants + if (stack1.getItemMeta().hasDisplayName() || stack2.getItemMeta().hasDisplayName()) { + if (stack1.getItemMeta().hasDisplayName() && stack2.getItemMeta().hasDisplayName()) { + if (!stack1.getItemMeta().getDisplayName().equals(stack2.getItemMeta().getDisplayName())) { + return false; // items have different display name + } + } else { + return false; // one of the item stacks have a display name + } + } + try { + Class.forName("org.bukkit.inventory.meta.EnchantmentStorageMeta"); + boolean book1 = stack1.getItemMeta() instanceof EnchantmentStorageMeta; + boolean book2 = stack2.getItemMeta() instanceof EnchantmentStorageMeta; + if (book1 != book2) + return false;// One has enchantment meta, the other does not. + if (book1 == true) { // They are the same here (both true or both + // false). So if one is true, the other is + // true. + Map ench1 = ((EnchantmentStorageMeta) stack1.getItemMeta()).getStoredEnchants(); + Map ench2 = ((EnchantmentStorageMeta) stack2.getItemMeta()).getStoredEnchants(); + if (!ench1.equals(ench2)) + return false; // Enchants aren't the same. + } + } catch (ClassNotFoundException e) { + } + return true; + } + + /** + * Returns the number of items that can be given to the inventory safely. + * + * @param inv + * The inventory to count + * @param item + * The item. Material, durabiltiy and enchants must + * match for 'stackability' to occur. + * @return The number of items that can be given to the inventory safely. + */ + public static int countSpace(Inventory inv, ItemStack item) { + int space = 0; + + try { + ItemStack[] contents = inv.getContents(); + for (ItemStack iStack : contents) { + if (iStack == null || iStack.getType() == Material.AIR) { + space += item.getMaxStackSize(); + } else if (matches(item, iStack)) { + space += item.getMaxStackSize() - iStack.getAmount(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return space; + } + + public static String locationToString(Location location) { + return location.getWorld().getName() + ":" + + AMath.round(location.getX(), 1) + ":" + + AMath.round(location.getY(), 1) + ":" + + AMath.round(location.getZ(), 1); + } + + public static Location stringToLocation(String string) { + String[] split = string.split(":"); + if (split.length != 4) { + Logger.warn("Unable to load location [" + string + "] due to invalid format"); + return null; + } + + try { + return new Location(Bukkit.getWorld(split[0]), + Double.parseDouble(split[1]), Double.parseDouble(split[2]), Double.parseDouble(split[3])); + } catch (NumberFormatException e) { + Logger.warn("Unable to load location [" + string + "] due to invalid format"); + return null; + } + } + + public static ItemStack getPlayerHead(UUID uuid) { + ItemStack skull = new ItemStack(Material.PLAYER_HEAD); + + Player player = Bukkit.getPlayer(uuid); + if (player == null) return skull; + + // TODO add skins to skulls and name them. + SkullMeta meta = (SkullMeta) skull.getItemMeta(); + meta.setPlayerProfile(player.getPlayerProfile()); + skull.setItemMeta(meta); + + return skull; + } + + // TODO upgrade this to an util method check if owner/trusted and open management interface + public static boolean canManageShop(Player player, PlayerShop playerShop) { + if (playerShop.getOwnerUUID().equals(player.getUniqueId())) { + return true; + } + + return false; + } + + public static Component itemNameComponent(ItemStack item) { + Component component = Component.empty(); + if(item == null || item.getType().equals(Material.AIR)) + return Component.text("Nothing"); + boolean dname = item.hasItemMeta() && item.getItemMeta().hasDisplayName(); + if(dname) { + component = component.append(item.getItemMeta().displayName()); + } else { + component = component.append(Component.text(materialToName(item.getType()), NamedTextColor.WHITE)); + } + + return component; + } + + private static String materialToName(Material m) { + if (m.equals(Material.TNT)) { + return "TNT"; + } + String orig = m.name().toLowerCase(); + String[] splits = orig.split("_"); + StringBuilder sb = new StringBuilder(orig.length()); + int pos = 0; + for (String split : splits) { + sb.append(split); + int loc = sb.lastIndexOf(split); + char charLoc = sb.charAt(loc); + if (!(split.equalsIgnoreCase("of") || split.equalsIgnoreCase("and") || + split.equalsIgnoreCase("with") || split.equalsIgnoreCase("on"))) + sb.setCharAt(loc, Character.toUpperCase(charLoc)); + if (pos != splits.length - 1) + sb.append(' '); + ++pos; + } + + return sb.toString(); + } + +} diff --git a/src/main/java/com/alttd/playershops/utils/Util.java b/src/main/java/com/alttd/playershops/utils/Util.java new file mode 100644 index 0000000..ec9309c --- /dev/null +++ b/src/main/java/com/alttd/playershops/utils/Util.java @@ -0,0 +1,82 @@ +package com.alttd.playershops.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.Bukkit; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; + +import java.util.HashMap; + +public class Util { + + private static MiniMessage miniMessage = null; + + public static HashMap colors; + static { // this might be in minimessage already? + colors = new HashMap<>(); + colors.put("&0", ""); + colors.put("&1", ""); + colors.put("&2", ""); + colors.put("&3", ""); + colors.put("&4", ""); + colors.put("&5", ""); + colors.put("&6", ""); + colors.put("&7", ""); + colors.put("&8", ""); + colors.put("&9", ""); + colors.put("&a", ""); + colors.put("&b", ""); + colors.put("&c", ""); + colors.put("&d", ""); + colors.put("&e", ""); + colors.put("&f", ""); + colors.put("&l", ""); + colors.put("&o", ""); + } + + public static String parseColors(String message) { + for (String key : colors.keySet()) { + if (message.contains(key)) { + message = message.replace(key, colors.get(key)); + } + } + return message; + } + + private static MiniMessage miniMessage() { + if (miniMessage == null) + miniMessage = MiniMessage.miniMessage(); + return miniMessage; + } + + public static Component parseMiniMessage(String message, TagResolver tagResolver) { + message = parseColors(message); + if (tagResolver == null) { + return miniMessage().deserialize(message); + } else { + return miniMessage().deserialize(message, tagResolver); + } + } + + public static String capitalize(String string) { + if (string.length() <= 1) + return string.toUpperCase(); + return string.substring(0, 1).toUpperCase() + string.toLowerCase().substring(1); + } + + /** + * Call an event and check it is cancelled. + * + * @param event The event implement the Cancellable interface. + * @return The event is cancelled. + */ + public static boolean callCancellableEvent(Cancellable event) { + if (!(event instanceof Event)) { + throw new IllegalArgumentException("Cancellable must is event implement"); + } + Bukkit.getPluginManager().callEvent((Event) event); + return event.isCancelled(); + } +}