/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.royaldev.royalcommands.rcommands.trade;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.royaldev.royalcommands.MessageColor;
import org.royaldev.royalcommands.gui.inventory.ClickHandler;
import org.royaldev.royalcommands.gui.inventory.GUIItem;
import org.royaldev.royalcommands.gui.inventory.InventoryGUI;
import org.royaldev.royalcommands.rcommands.trade.clickhandlers.AddPartyCommand;
import org.royaldev.royalcommands.rcommands.trade.clickhandlers.GiveHelpBook;
import org.royaldev.royalcommands.rcommands.trade.clickhandlers.RemindOtherParty;
import org.royaldev.royalcommands.rcommands.trade.clickhandlers.RemoveItem;
import org.royaldev.royalcommands.rcommands.trade.clickhandlers.ToggleTradeAcceptance;
import org.royaldev.royalcommands.rcommands.trade.guiitems.AddCommandItem;
import org.royaldev.royalcommands.rcommands.trade.guiitems.HelpItem;
import org.royaldev.royalcommands.rcommands.trade.guiitems.RemindItem;
import org.royaldev.royalcommands.rcommands.trade.guiitems.ToggleAcceptanceItem;
import org.royaldev.royalcommands.rcommands.trade.tradables.Tradable;
import org.royaldev.royalcommands.rcommands.trade.tradables.TradableCommand;
import org.royaldev.royalcommands.rcommands.trade.tradables.TradableItem;
import org.royaldev.royalcommands.tools.Pair;
import org.royaldev.royalcommands.tools.Vector2D;
import org.royaldev.royalcommands.wrappers.player.MemoryRPlayer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/*
* FIXME:
* Trade accept button sometimes doesn't update until reopen (?)
*/
/**
* A trade between two {@link Party Parties} involving a mix of items and commands.
*/
public class Trade {
private static final Map<Pair<UUID, UUID>, Trade> trades = new HashMap<>();
private final Map<Party, UUID> parties = new HashMap<>();
private final Map<Party, Boolean> acceptances = new HashMap<>();
private final InventoryGUI inventoryGUI;
private final UUID acceptButtonUUID = UUID.randomUUID();
public Trade(final UUID trader, final UUID tradee) {
this.parties.put(Party.TRADER, trader);
this.parties.put(Party.TRADEE, tradee);
this.acceptances.put(Party.TRADER, false);
this.acceptances.put(Party.TRADEE, false);
this.inventoryGUI = this.makeInventoryGUI();
Trade.trades.put(new Pair<>(trader, tradee), this);
}
/**
* Gets the trade that matches both UUIDs. The order they are provided in does not matter.
*
* @param first First UUID to match
* @param second Second UUID to match
* @return A trade or null if none has been initiated
*/
public static Trade getTradeFor(final UUID first, final UUID second) {
for (final Map.Entry<Pair<UUID, UUID>, Trade> entry : Trade.trades.entrySet()) {
final Pair<UUID, UUID> pair = entry.getKey();
if ((pair.getFirst().equals(first) || pair.getSecond().equals(first)) && (pair.getFirst().equals(second) || pair.getSecond().equals(second))) {
return entry.getValue();
}
}
return null;
}
/**
* Gets all the trades involving the given UUID.
*
* @param uuid UUID to search for
* @return A trade or null if none has been initiated
*/
public static List<Trade> getTradesFor(final UUID uuid) {
final List<Trade> trades = new ArrayList<>();
for (final Map.Entry<Pair<UUID, UUID>, Trade> entry : Trade.trades.entrySet()) {
final Pair<UUID, UUID> pair = entry.getKey();
if (pair.getFirst().equals(uuid) || pair.getSecond().equals(uuid)) {
trades.add(entry.getValue());
}
}
return trades;
}
/**
* Removes this trade from the map of open trades. It should be picked up by garbage collection.
*/
private void destroy() {
for (final Map.Entry<Pair<UUID, UUID>, Trade> entry : Trade.trades.entrySet()) {
if (!entry.getValue().equals(this)) continue;
Trade.trades.remove(entry.getKey());
break;
}
}
/**
* Gets the name of this trade, formulated as "trader name/tradee name"
*
* @return Name of trade
*/
private String getTradeName() {
final String traderName = MemoryRPlayer.getRPlayer(this.get(Party.TRADER)).getName();
final String tradeeName = MemoryRPlayer.getRPlayer(this.get(Party.TRADEE)).getName();
final String tradeName = traderName + "/" + tradeeName;
return tradeName.length() > 32 ? tradeName.substring(0, 32) : tradeName;
}
/**
* Creates an {@link org.royaldev.royalcommands.gui.inventory.InventoryGUI} for this trade. If this has already been
* called, it will return null. Otherwise, it will return a new InventoryGUI. To get it after it has already been
* created, use {@link #getInventoryGUI()}.
*
* @return InventoryGUI or null
*/
private InventoryGUI makeInventoryGUI() {
if (this.getInventoryGUI() != null) return null;
final InventoryGUI inventoryGUI = new InventoryGUI(this.getTradeName());
inventoryGUI.addItem(
new AddPartyCommand(this, Party.TRADER),
5, 1,
new AddCommandItem(this, Party.TRADER)
);
inventoryGUI.addItem(
new AddPartyCommand(this, Party.TRADEE),
5, 2,
new AddCommandItem(this, Party.TRADEE)
);
inventoryGUI.addItem(
this.acceptButtonUUID,
new ToggleTradeAcceptance(this),
5, 3,
new ToggleAcceptanceItem(this)
);
inventoryGUI.addItem(
new RemindOtherParty(this),
5, 4,
new RemindItem()
);
inventoryGUI.addItem(
new GiveHelpBook(),
5, 5,
new HelpItem()
);
return inventoryGUI;
}
/**
* Processes this ItemStack before adding to the inventory. If it is a special, it will be processed into whatever
* it should be and added to the returned list.
*
* @param is ItemStack to process
* @return List, never null
*/
private List<Tradable> processSpecialItem(final ItemStack is) {
final List<Tradable> trades = new ArrayList<>();
if (this.getInventoryGUI().getClickHandler(is) instanceof RemoveItem) { // TODO: Do this better
// TODO: Extract logic
final ItemMeta im = is.getItemMeta();
for (final String lore : im.getLore()) {
if (!lore.startsWith(MessageColor.NEUTRAL + "/")) continue;
trades.add(new TradableCommand(this, lore.substring((MessageColor.NEUTRAL + "/").length())));
}
return trades;
}
return null;
}
/**
* Sends a message to all parties in this trade, if they are online.
*
* @param message Message to send
*/
private void sendMessageToAll(final String message) {
final Player trader = this.getPlayer(Party.TRADER);
final Player tradee = this.getPlayer(Party.TRADEE);
if (trader != null) trader.sendMessage(message);
if (tradee != null) tradee.sendMessage(message);
}
/**
* Processes the trade. This will run through the list of tradables provided by {@link #getTradablesFor(Party)}. It
* will first run through TRADER tradables, then TRADEE tradables. The
* {@link org.royaldev.royalcommands.rcommands.trade.tradables.Tradable#trade(Party, Party)} method will be called
* for each one. If all was successful, the trade will be destroyed. Otherwise, a message will be sent and the trade
* will be left open. The trade inventory will then close.
*
* @return If all was successful
*/
private boolean trade() {
final List<Tradable> tradeeTradables = this.getTradablesFor(Party.TRADEE); // tradee has offered
final List<Tradable> traderTradables = this.getTradablesFor(Party.TRADER); // trader has offered
if (!this.areBothPartiesOnline()) return false;
boolean allSuccessful = true;
for (final Tradable trade : tradeeTradables) {
final boolean success = trade.trade(Party.TRADEE, Party.TRADER);
if (success) trade.destroy();
if (allSuccessful) allSuccessful = success;
}
for (final Tradable trade : traderTradables) {
final boolean success = trade.trade(Party.TRADER, Party.TRADEE);
if (success) trade.destroy();
if (allSuccessful) allSuccessful = success;
}
if (allSuccessful) {
this.destroy();
} else {
this.sendMessageToAll(MessageColor.NEGATIVE + "The trade did not successfully complete. Please check the trade to see any remaining items.");
this.setAcceptance(Party.TRADER, false);
this.setAcceptance(Party.TRADEE, false);
}
Party.TRADER.closeTrade(this);
Party.TRADEE.closeTrade(this);
return allSuccessful;
}
/**
* Adds an item to the InventoryGUI for this trade.
*
* @param clickHandler ClickHandler of the item
* @param party Side of the inventory to add the item to
* @param guiItem Item to add
*/
public void addItem(final ClickHandler clickHandler, final Party party, final GUIItem guiItem) {
final int freeSlot = party.getNextFreeSlot(this.getInventoryGUI().getBase());
if (freeSlot == -1) return; // TODO: Throw?
final Vector2D xy = this.getInventoryGUI().getXYFromSlot(freeSlot);
this.getInventoryGUI().addItem(clickHandler, xy.getX(), xy.getY(), guiItem);
}
/**
* Returns if both parties are currently online.
*
* @return true if online, false if otherwise
*/
public boolean areBothPartiesOnline() {
return this.getPlayer(Party.TRADEE) != null && this.getPlayer(Party.TRADER) != null;
}
@Override
public boolean equals(final Object obj) {
if (obj == null || !(obj instanceof Trade)) return false;
final Trade t = (Trade) obj;
return this.getInventoryGUI().equals(t.getInventoryGUI()) && this.parties.equals(t.parties);
}
/**
* Gets the UUID for the given party.
*
* @param party Party to get UUID of
* @return UUID or null
*/
public UUID get(final Party party) {
return this.parties.get(party);
}
/**
* Gets the Party of the given UUID.
*
* @param uuid UUID to get Party of.
* @return Party or null
*/
public Party get(final UUID uuid) {
for (final Map.Entry<Party, UUID> entry : this.parties.entrySet()) {
if (!entry.getValue().equals(uuid)) continue;
return entry.getKey();
}
return null;
}
/**
* Gets the InventoryGUI for this trade. This may be null if {@link #makeInventoryGUI()} was never called.
*
* @return InventoryGUI or null
*/
public InventoryGUI getInventoryGUI() {
return this.inventoryGUI;
}
/**
* Gets the name of the Player that corresponds to the given Party. This is a convenience method.
*
* @param party Party to get name of
* @return Name
*/
public String getName(final Party party) {
return MemoryRPlayer.getRPlayer(this.get(party)).getName();
}
/**
* Gets the Player that corresponds to the given Party. This is a convenience method. This may return null if the
* Player is not online.
*
* @param party Party to get Player of
* @return Player or null
*/
public Player getPlayer(final Party party) {
return MemoryRPlayer.getRPlayer(this.get(party)).getPlayer();
}
/**
* Gets all Tradables on the given Party's side of the trade.
*
* @param party Party to get Tradables for
* @return List of Tradables, never null
*/
public List<Tradable> getTradablesFor(final Party party) {
final List<Tradable> items = new ArrayList<>();
final Inventory base = this.getInventoryGUI().getBase();
for (int i = 0; i < base.getSize(); i++) {
if (!party.canAccessSlot(i)) continue;
final ItemStack item = base.getItem(i);
if (item == null || item.getType() == Material.AIR) continue;
final List<Tradable> special = this.processSpecialItem(item);
if (special == null) items.add(new TradableItem(this, item));
else items.addAll(special);
}
return items;
}
/**
* Gets the lore for the trade status item for the given party.
*
* @param party Party to get lore for
* @return String
*/
public String getTradeStatusLore(final Party party) {
final boolean accepted = this.hasAccepted(party);
final StringBuilder sb = new StringBuilder().append(MessageColor.NEUTRAL).append(this.getName(party)).append(": ");
sb.append(accepted ? MessageColor.POSITIVE : MessageColor.NEGATIVE).append(accepted ? "Accepted" : "Declined");
return sb.toString();
}
/**
* Checks to see if the given party has accepted the trade.
*
* @param party Party to check
* @return true if the trade has been accepted, false if otherwise
*/
public boolean hasAccepted(final Party party) {
return this.acceptances.get(party);
}
/**
* Checks to see if both parties have accepted the trade.
*
* @return true if both have accepted, false if not
*/
public boolean haveBothAccepted() {
return this.hasAccepted(Party.TRADER) && this.hasAccepted(Party.TRADEE);
}
/**
* Sets the acceptance of the trade for the given party.
*
* @param party Party to set acceptance of
* @param acceptance Acceptance to set
* @return acceptance
*/
public boolean setAcceptance(final Party party, final boolean acceptance) {
this.acceptances.put(party, acceptance);
this.updateAcceptButton();
if (this.haveBothAccepted()) this.trade();
return acceptance;
}
/**
* Shows the InventoryGUI for this trade to the given UUID.
*
* @param uuid UUID to show to
*/
public void showInventoryGUI(final UUID uuid) {
final Party party = this.get(uuid);
if (party == null) throw new IllegalArgumentException("No such UUID");
final Player p = MemoryRPlayer.getRPlayer(uuid).getPlayer();
if (p == null) return;
p.openInventory(this.getInventoryGUI().getBase());
}
/**
* Toggles the acceptance for the given party.
*
* @param party Party to toggle acceptance for
* @return New acceptance status
*/
public boolean toggleAcceptance(final Party party) {
return this.setAcceptance(party, !this.acceptances.get(party));
}
/**
* Toggles acceptance for the given UUID.
*
* @param uuid UUID to toggle acceptance for
* @return New acceptance status
*/
public boolean toggleAcceptance(final UUID uuid) {
final Party party = this.get(uuid);
if (party == null) throw new IllegalArgumentException("No such UUID");
return this.toggleAcceptance(party);
}
/**
* Updates the accept trade button with new lore to reflect the acceptance status of both parties.
*/
public void updateAcceptButton() {
final ItemStack updateButton = this.getInventoryGUI().getItemStack(this.acceptButtonUUID);
final List<String> lore = new ToggleAcceptanceItem(this).getLore();
this.getInventoryGUI().updateItemStack(this.getInventoryGUI().setItemMeta(
updateButton,
null,
lore.toArray(new String[lore.size()])
));
}
}