diff --git a/build.gradle.kts b/build.gradle.kts index ce49e28..18995f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,7 +97,10 @@ bukkit { children = listOf( "playershops.shop.create", "playershops.shop.break.other", - "playershops.shop.use" + "playershops.shop.use", + "playershops.shop.use.buy", + "playershops.shop.use.sell", + "playershops.shop.use.gamble" ) } register("playershops.shoplimit") { diff --git a/src/main/java/com/alttd/playershops/PlayerShops.java b/src/main/java/com/alttd/playershops/PlayerShops.java index 680232f..3b1363b 100644 --- a/src/main/java/com/alttd/playershops/PlayerShops.java +++ b/src/main/java/com/alttd/playershops/PlayerShops.java @@ -6,12 +6,10 @@ 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.BlockListener; -import com.alttd.playershops.listener.PlayerListener; -import com.alttd.playershops.listener.ShopListener; -import com.alttd.playershops.listener.TransactionListener; +import com.alttd.playershops.listener.*; import com.alttd.playershops.shop.ShopType; -import com.alttd.playershops.storage.DatabaseManager; +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; @@ -28,11 +26,13 @@ public class PlayerShops extends JavaPlugin { private ShopHandler shopHandler; @Getter private DatabaseManager databaseManager; - + @Getter + private DatabaseHelper databaseHelper; private ShopListener shopListener; private PlayerListener playerListener; private BlockListener blockListener; private TransactionListener transactionListener; + private InventoryListener inventoryListener; public void onEnable() { instance = this; @@ -43,7 +43,12 @@ public class PlayerShops extends JavaPlugin { } Bukkit.getLogger().info("Hooked into Vault economy provided by " + econ.getName()); reloadConfigs(); - databaseManager = new DatabaseManager(instance); + if(!setupDatabase()) { + Bukkit.getLogger().warning("Error setting up database connection.\n Disabling plugin"); + this.setEnabled(false); + return; + } + shopHandler = new ShopHandler(instance); registerListeners(); @@ -51,6 +56,7 @@ public class PlayerShops extends JavaPlugin { } public void onDisable() { + shopHandler.unloadShops(); unRegisterListeners(); Bukkit.getScheduler().cancelTasks(this); @@ -69,6 +75,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(); @@ -80,6 +93,7 @@ public class PlayerShops extends JavaPlugin { playerListener = new PlayerListener(this); blockListener = new BlockListener(this); transactionListener = new TransactionListener(this); + inventoryListener = new InventoryListener(this); } private void unRegisterListeners() { @@ -87,6 +101,7 @@ public class PlayerShops extends JavaPlugin { playerListener.unregister(); blockListener.unregister(); transactionListener.unregister(); + inventoryListener.unregister(); } private void registerCommands() { diff --git a/src/main/java/com/alttd/playershops/commands/ShopCommand.java b/src/main/java/com/alttd/playershops/commands/ShopCommand.java index cf960c6..2e6c1d2 100644 --- a/src/main/java/com/alttd/playershops/commands/ShopCommand.java +++ b/src/main/java/com/alttd/playershops/commands/ShopCommand.java @@ -1,5 +1,6 @@ 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; @@ -25,7 +26,7 @@ public class ShopCommand implements CommandExecutor, TabCompleter { case "reload": break; case "open": - ShopManagementGui gui = new ShopManagementGui(player.getUniqueId(), new PlayerShop(player.getLocation(), player.getLocation(), player)); + HomeGui gui = new HomeGui(player.getUniqueId()); gui.open(); break; default: diff --git a/src/main/java/com/alttd/playershops/config/Config.java b/src/main/java/com/alttd/playershops/config/Config.java index 51d0717..67520f0 100644 --- a/src/main/java/com/alttd/playershops/config/Config.java +++ b/src/main/java/com/alttd/playershops/config/Config.java @@ -27,11 +27,13 @@ public class Config extends AbstractConfiguration { 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 + "default-shop-limit", shopLimit); usePermissionShopLimit = config.getBoolean(path + "use-permission-based-shop-limit", usePermissionShopLimit); shopCreationWord = config.getString(path + "creation-word", shopCreationWord); + shopCreationBalance = config.getDouble(path + "creation-balance", shopCreationBalance); } } diff --git a/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java b/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java index 6634811..0691c63 100644 --- a/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java +++ b/src/main/java/com/alttd/playershops/config/ShopTypeConfig.java @@ -45,10 +45,12 @@ public class ShopTypeConfig { 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."; 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..015c2b9 --- /dev/null +++ b/src/main/java/com/alttd/playershops/conversation/ConversationManager.java @@ -0,0 +1,214 @@ +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) { + this.player = player; + this.playerShop = playerShop; + ConversationFactory conversationFactory = new ConversationFactory(plugin) + .withModality(true) + .withFirstPrompt(getPrompt(conversationType)) + .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/events/ShopBalanceChangeEvent.java b/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java index 31c5703..d9f04d8 100644 --- a/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java +++ b/src/main/java/com/alttd/playershops/events/ShopBalanceChangeEvent.java @@ -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/gui/AbstractGui.java b/src/main/java/com/alttd/playershops/gui/AbstractGui.java index ffe372f..b649364 100644 --- a/src/main/java/com/alttd/playershops/gui/AbstractGui.java +++ b/src/main/java/com/alttd/playershops/gui/AbstractGui.java @@ -1,6 +1,5 @@ package com.alttd.playershops.gui; -import com.alttd.playershops.config.GuiIconConfig; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; @@ -17,10 +16,13 @@ public abstract class AbstractGui implements InventoryHolder { int currentSlot; UUID uuid; int pageIndex; + AbstractGui lastGui; + AbstractGui(UUID uuid) { this.uuid = uuid; this.currentSlot = 0; + this.lastGui = null; } public void open() { @@ -60,6 +62,11 @@ public abstract class AbstractGui implements InventoryHolder { } } + protected boolean addItem(int slot, ItemStack icon) { + currentSlot = slot; + return addItem(icon); + } + protected boolean addItem(ItemStack icon) { if (currentSlot == inventory.getSize()) return false; @@ -77,7 +84,8 @@ public abstract class AbstractGui implements InventoryHolder { protected void clearInvBody() { for (int i = 0; i < inventory.getSize() - 9; ++i) { - inventory.setItem(i, null); +// inventory.setItem(i, null); + inventory.setItem(i, getDivider()); } } @@ -113,4 +121,38 @@ public abstract class AbstractGui implements InventoryHolder { 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 index 2a2aa8f..5fd33b3 100644 --- a/src/main/java/com/alttd/playershops/gui/GuiIcon.java +++ b/src/main/java/com/alttd/playershops/gui/GuiIcon.java @@ -27,13 +27,16 @@ public enum GuiIcon { 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; + LIST_PLAYER_ADMIN, + EMPTY_SHOP; @Getter private final int slot; 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 index 94ae024..dadfc36 100644 --- a/src/main/java/com/alttd/playershops/gui/ShopManagementGui.java +++ b/src/main/java/com/alttd/playershops/gui/ShopManagementGui.java @@ -1,9 +1,18 @@ 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.Util; +import net.kyori.adventure.text.Component; 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 { @@ -15,30 +24,70 @@ public class ShopManagementGui extends AbstractGui { this.inventory = Bukkit.createInventory(this, INV_SIZE, Util.parseMiniMessage("Config.gui.management-title", null)); this.shop = shop; initInvContents(); + makeMenuBar(); } @Override void initInvContents() { -// super.initInvContents(); - makeMenuBar(); - for (int i = 0; i < inventory.getSize() - 9; ++i) { - inventory.setItem(i, getDivider()); - } - inventory.setItem(GuiIcon.MANAGE_SHOP.getSlot(), GuiIcon.MANAGE_SHOP.getItemStack()); + super.initInvContents(); + + ItemStack shopIcon = GuiIcon.MANAGE_SHOP.getItemStack(); + ItemMeta meta = shopIcon.getItemMeta(); + List lore = new ArrayList<>(); + lore.add(Util.parseMiniMessage("Balance: " + shop.getBalance(), null)); + lore.add(Util.parseMiniMessage("item: " + shop.getItemStack(), null)); + lore.add(Util.parseMiniMessage("amount: " + shop.getAmount(), null)); + lore.add(Util.parseMiniMessage("Type: " + shop.getType(), null)); + lore.add(Util.parseMiniMessage("Price: " + shop.getPrice(), null)); + + 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(); - inventory.setItem(GuiIcon.MENUBAR_BACK.getSlot(), GuiIcon.MENUBAR_BACK.getItemStack()); - inventory.setItem(GuiIcon.MENUBAR_PREV_PAGE.getSlot(), GuiIcon.MENUBAR_PREV_PAGE.getItemStack()); -// inventory.setItem(49, new ItemStack(Material.NETHER_STAR)); - inventory.setItem(GuiIcon.MENUBAR_NEXT_PAGE.getSlot(), GuiIcon.MENUBAR_NEXT_PAGE.getItemStack()); + } + + @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)) { + openChangePrompt(ConversationType.ADD_BALANCE); + } 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 8f08ee0..682cb96 100644 --- a/src/main/java/com/alttd/playershops/handler/ShopHandler.java +++ b/src/main/java/com/alttd/playershops/handler/ShopHandler.java @@ -3,6 +3,8 @@ package com.alttd.playershops.handler; import com.alttd.playershops.PlayerShops; import com.alttd.playershops.config.Config; 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; @@ -12,6 +14,8 @@ import org.bukkit.Tag; import org.bukkit.block.Block; import org.bukkit.entity.Player; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -34,6 +38,25 @@ public class ShopHandler { shopBuildLimits.defaultReturnValue(Config.shopLimit); shopMaterials = new ArrayList<>(); // TODO move into parent method where materials are loaded in. shopMaterials.add(Material.BARREL); + + loadShops(); + } + + 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) { @@ -50,6 +73,10 @@ public class ShopHandler { 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,7 +85,11 @@ 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(); } @@ -93,6 +124,7 @@ public class ShopHandler { public void removeShop(PlayerShop shop) { shopLocation.remove(shop.getShopLocation()); shopSignLocation.remove(shop.getSignLocation()); + plugin.getDatabaseHelper().removeShop(shop); } public boolean canPlayerBreakShop(Player player, PlayerShop shop) { // TODO move to util? diff --git a/src/main/java/com/alttd/playershops/listener/BlockListener.java b/src/main/java/com/alttd/playershops/listener/BlockListener.java index 53d0fa8..d149c86 100644 --- a/src/main/java/com/alttd/playershops/listener/BlockListener.java +++ b/src/main/java/com/alttd/playershops/listener/BlockListener.java @@ -51,8 +51,12 @@ public class BlockListener extends EventListener { if (shop == null) return; - event.setCancelled(!shopHandler.canPlayerBreakShop(event.getPlayer(), shop)); + if (!shopHandler.canPlayerBreakShop(event.getPlayer(), shop)) { + event.setCancelled(true); + return; + } + shopHandler.removeShop(shop); } @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @@ -78,6 +82,11 @@ public class BlockListener extends EventListener { if (shop == null) return; - event.setCancelled(!shopHandler.canPlayerBreakShop(event.getPlayer(), shop)); + if (!shopHandler.canPlayerBreakShop(event.getPlayer(), shop)) { + event.setCancelled(true); + return; + } + + shopHandler.removeShop(shop); } } 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 f72ad08..5a5db7a 100644 --- a/src/main/java/com/alttd/playershops/listener/PlayerListener.java +++ b/src/main/java/com/alttd/playershops/listener/PlayerListener.java @@ -5,6 +5,7 @@ import com.alttd.playershops.config.Config; import com.alttd.playershops.config.MessageConfig; import com.alttd.playershops.handler.ShopHandler; 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; @@ -132,9 +133,16 @@ public class PlayerListener extends EventListener { return; } + if (!EconomyUtils.hasSufficientFunds(player, Config.shopCreationBalance)) { + event.setCancelled(true); + return; + } + PlayerShop playerShop = new PlayerShop(bRelative.getLocation(), signBlock.getLocation(), player); - // TODO instance shopCreationManagement + playerShop.addBalance(Config.shopCreationBalance); + EconomyUtils.removeFunds(player, Config.shopCreationBalance); shopHandler.addShop(playerShop); + PlayerShops.getInstance().getDatabaseHelper().createShop(playerShop); } } diff --git a/src/main/java/com/alttd/playershops/listener/TransactionListener.java b/src/main/java/com/alttd/playershops/listener/TransactionListener.java index 7056dc0..40e88a2 100644 --- a/src/main/java/com/alttd/playershops/listener/TransactionListener.java +++ b/src/main/java/com/alttd/playershops/listener/TransactionListener.java @@ -6,7 +6,11 @@ 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.OfflinePlayer; import org.bukkit.Tag; @@ -35,20 +39,19 @@ public class TransactionListener extends EventListener { if(!this.isRegistered) return; - if (event.getHand() == EquipmentSlot.OFF_HAND) { + if (event.getHand() == EquipmentSlot.OFF_HAND) return; - } - Player player = event.getPlayer(); + 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())) + if (block == null || !Tag.WALL_SIGNS.isTagged(block.getType())) return; PlayerShop playerShop = plugin.getShopHandler().getShopBySignLocation(block.getLocation()); - if (playerShop == null || !playerShop.isInitialized()) + if (playerShop == null) return; // if we ever need worldguard support add it to the hook @@ -59,23 +62,29 @@ public class TransactionListener extends EventListener { 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; } - // TODO upgrade this to an util method check if owner/trusted and open management interface - if (playerShop.getOwnerUUID().equals(player.getUniqueId())) { + if (ShopUtil.canManageShop(player, playerShop)) { ShopManagementGui gui = new ShopManagementGui(player.getUniqueId(), playerShop); gui.open(); return; } + + if (!playerShop.isInitialized()) + return; + executeTransaction(player, playerShop); } @@ -90,7 +99,11 @@ public class TransactionListener extends EventListener { 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() + "") + ); if (transactionError != TransactionError.NONE) { switch (transactionError) { case INSUFFICIENT_FUNDS_SHOP -> { @@ -99,25 +112,25 @@ public class TransactionListener extends EventListener { // 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().shopSold, null)); } - player.sendMiniMessage(shop.getType().getShopTypeConfig().shopNoStock, null); + player.sendMiniMessage(shop.getType().getShopTypeConfig().shopNoStock, placeholders); } case INSUFFICIENT_FUNDS_PLAYER -> { - player.sendMiniMessage(shop.getType().getShopTypeConfig().playerNoFunds, null); + player.sendMiniMessage(shop.getType().getShopTypeConfig().playerNoFunds, placeholders); } case INVENTORY_FULL_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 } - player.sendMiniMessage(shop.getType().getShopTypeConfig().shopInventoryFull, null); + player.sendMiniMessage(shop.getType().getShopTypeConfig().shopInventoryFull, placeholders); } case INVENTORY_FULL_PLAYER -> { - player.sendMiniMessage(shop.getType().getShopTypeConfig().playerInventoryFull, null); + player.sendMiniMessage(shop.getType().getShopTypeConfig().playerInventoryFull, placeholders); } } return; } - player.sendActionBar(Util.parseMiniMessage(shop.getType().getShopTypeConfig().playerBought, null)); + player.sendActionBar(Util.parseMiniMessage(shop.getType().getShopTypeConfig().playerBought, placeholders)); } private boolean notifyOwner(PlayerShop playerShop) { diff --git a/src/main/java/com/alttd/playershops/shop/PlayerShop.java b/src/main/java/com/alttd/playershops/shop/PlayerShop.java index 932d0d2..b18f0a5 100644 --- a/src/main/java/com/alttd/playershops/shop/PlayerShop.java +++ b/src/main/java/com/alttd/playershops/shop/PlayerShop.java @@ -11,6 +11,7 @@ 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; @@ -59,11 +60,11 @@ public class PlayerShop { public PlayerShop(Location shopLocation, Location signLocation, UUID uuid, String playerName) { this.shopID = UUID.randomUUID(); - this.shopLocation = shopLocation; - this.signLocation = signLocation; + 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; - updateSign(); + this.server = Bukkit.getServerName(); } public static PlayerShop load(UUID shopID, String ownerName, UUID ownerUUID, ShopType shopType, String server, @@ -133,23 +134,29 @@ public class PlayerShop { } 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() { Sign signBlock = (Sign) signLocation.getBlock().getState(); MiniMessage miniMessage = MiniMessage.miniMessage(); - List signLines; TagResolver tagResolver = TagResolver.resolver( Placeholder.unparsed("ownername", getOwnerName()), Placeholder.unparsed("price", String.valueOf(getPrice())), - Placeholder.unparsed("amount", String.valueOf(getAmount())) + Placeholder.unparsed("amount", String.valueOf(getAmount())), + Placeholder.unparsed("item", ShopUtil.getItemName(getItemStack())) ); - if (!isInitialized()) { - signLines = type.getShopTypeConfig().inActiveSignLines; - } else { - signLines = type.getShopTypeConfig().activeSignLines; - } for (int i = 0; i < 4; i++) { signBlock.line(i, miniMessage.deserialize(signLines.get(i), tagResolver)); } @@ -159,18 +166,18 @@ public class PlayerShop { } public double getPricePerItem() { - double pricePer = this.getPrice() / this.getAmount(); return this.getPrice() / this.getAmount(); } public boolean removeBalance(double amount) { - ShopBalanceChangeEvent shopBalanceChangeEvent = new ShopBalanceChangeEvent(this, ShopBalanceChangeEvent.ChangeReason.WIDRAW); + 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; } @@ -182,6 +189,7 @@ public class PlayerShop { setDirty(true); update(); this.balance += amount; + this.setLastTransaction(System.currentTimeMillis()); return true; } @@ -280,13 +288,14 @@ public class PlayerShop { } public void setItemStack(ItemStack itemStack) { - if (this.itemStack.equals(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(); } @@ -311,6 +320,8 @@ public class PlayerShop { return; // cancelled by another plugin, does this need logging? this.amount = amount; + if (this.itemStack != null) + this.itemStack.setAmount(this.amount); setDirty(true); update(); } @@ -331,7 +342,11 @@ public class PlayerShop { * 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/ShopType.java b/src/main/java/com/alttd/playershops/shop/ShopType.java index 1b23021..0c789de 100644 --- a/src/main/java/com/alttd/playershops/shop/ShopType.java +++ b/src/main/java/com/alttd/playershops/shop/ShopType.java @@ -18,6 +18,18 @@ public enum ShopType { 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() { return name().toLowerCase(); diff --git a/src/main/java/com/alttd/playershops/storage/DatabaseManager.java b/src/main/java/com/alttd/playershops/storage/DatabaseManager.java deleted file mode 100644 index f51d6af..0000000 --- a/src/main/java/com/alttd/playershops/storage/DatabaseManager.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.alttd.playershops.storage; - -import com.alttd.playershops.PlayerShops; -import com.alttd.playershops.config.DatabaseConfig; -import com.alttd.playershops.storage.database.DatabaseQuery; -import lombok.Getter; -import org.bukkit.scheduler.BukkitRunnable; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; - -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); - } - - private DatabaseConnection getConnection() { - for (int i = 0; i < DatabaseConfig.maxDatabaseConnections; i++) { - DatabaseConnection connection = CONNECTIONPOOL.get(i); - if (connection == null) { - return genConnection(i); - } else if (!connection.isActive()) { - if (connection.isValid()) { - return connection; - } else { - connection.close(); - return genConnection(i); - } - } - } - - // This will cause an infinite running loop, throw an exception or wait for a connection to be available? - return getConnection(); - } - - private DatabaseConnection genConnection(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(); - } - - class DatabaseConnection implements AutoCloseable { - private Connection connection; - private volatile boolean isActive; - - 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&allowPublicKeyRetrieval=true", - DatabaseConfig.USERNAME, DatabaseConfig.PASSWORD); - } - } - - public synchronized Connection getConnection() { - 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(); - } - } - } - - class DatabaseQueue extends BukkitRunnable { - - private final DatabaseManager databaseManager; - - DatabaseQueue(DatabaseManager databaseManager) { - this.databaseManager = databaseManager; - } - @Getter - private static final Queue databaseQueryQueue = new LinkedBlockingQueue<>(); - - @Override - public void run() { - runTaskQueue(); - } - - public synchronized void runTaskQueue() { - if (databaseQueryQueue.isEmpty()) - return; - - DatabaseConnection databaseConnection = databaseManager.getConnection(); - Connection connection = databaseConnection.getConnection(); - - try { - 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/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..4975c91 --- /dev/null +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseHelper.java @@ -0,0 +1,178 @@ +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.inventory.ItemStack; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +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"); + } + + 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 + ); + } +} 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 index 1b1af1a..804c9fa 100644 --- a/src/main/java/com/alttd/playershops/storage/database/DatabaseQuery.java +++ b/src/main/java/com/alttd/playershops/storage/database/DatabaseQuery.java @@ -30,7 +30,7 @@ public class DatabaseQuery { public interface DatabaseTask { - void edit(PreparedStatement preparedStatement); + void edit(PreparedStatement preparedStatement) throws SQLException; default void onSuccess() {}; 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/utils/ShopUtil.java b/src/main/java/com/alttd/playershops/utils/ShopUtil.java index 8600428..ac6af14 100644 --- a/src/main/java/com/alttd/playershops/utils/ShopUtil.java +++ b/src/main/java/com/alttd/playershops/utils/ShopUtil.java @@ -1,16 +1,19 @@ package com.alttd.playershops.utils; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.World; +import com.alttd.playershops.shop.PlayerShop; +import com.destroystokyo.paper.profile.PlayerProfile; +import org.bukkit.*; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; 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 { @@ -106,18 +109,89 @@ public class ShopUtil { return space; } - public static String serialize(ItemStack itemStack) { - YamlConfiguration cfg = new YamlConfiguration(); - cfg.set("item", itemStack); - return cfg.saveToString(); + 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 ItemStack deserialize(String config) throws InvalidConfigurationException { - YamlConfiguration cfg = new YamlConfiguration(); - cfg.loadFromString(config); - ItemStack stack = cfg.getItemStack("item"); - return stack; + 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 String getItemName(ItemStack item) { + if (item == null || !item.getType().equals(Material.AIR)) + return "Nothing"; + + String NAME = "{item}"; + boolean dname = item.hasItemMeta() && item.getItemMeta().hasDisplayName(); + String replacer = ChatColor.translateAlternateColorCodes('&',NAME+"&r"); + if (dname) { + String trp = item.getItemMeta().getDisplayName(); + replacer = replacer.replace(NAME, trp); + } else { + replacer = replacer.replace(NAME, materialToName(item.getType())); + } + return replacer; + } + + private static String materialToName(Material m) { + if (m.equals(Material.TNT)) { + return "TNT"; + } + String orig = m.toString().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(); + } }