MilkBowl / VaultAPI

API Component of Vault
GNU Lesser General Public License v3.0
274 stars 112 forks source link

Multicurrency Support #145

Open creatorfromhell opened 2 years ago

creatorfromhell commented 2 years ago

This is mainly a placeholder issue to request that multicurrency support be added to Vault. I plan to open a PR for this after PR #138 gets merged, but I wanted to open this as an advance notice so nothing is released as "vault2" when this PR would ultimately cause some incompatibility issues.

Now to the actual tidbits for the why. Most servers during the modern age of Minecraft utilize multiple currencies in one form or another, be it a mini-game and normal currency or other cases. While Vault was originally designed before all of this became the "norm" it's time to add support for multicurrency to Vault and let the modern economy ecosystem branch off.

cerealcable commented 2 years ago

Since we're currently going through a redesign, it might be worthwhile to do a little bit of broad strokes here as far as proposed methods (names, args, return types) that would satisfy this feature. That would give us a head start on what this would look like but also what kind of work would be involved.

creatorfromhell commented 2 years ago

Since we're currently going through a redesign, it might be worthwhile to do a little bit of broad strokes here as far as proposed methods (names, args, return types) that would satisfy this feature. That would give us a head start on what this would look like but also what kind of work would be involved.

Sure, I could provide some information as it relates to the methods. I could do this as the aforementioned PR or provide it here as a online paste reference.

cerealcable commented 2 years ago

Pasting it into here is just fine. It would give us a chance to at least have a direction while we sort out #138.

creatorfromhell commented 2 years ago

Pasting it into here is just fine. It would give us a chance to at least have a direction while we sort out #138.

   /**
   * Checks to see if a name of the currency exists with this name.
   *
   * @param name The name of the name of the currency to search for.
   *
   * @return True if the currency exists, else false.
   */
  boolean hasCurrency(String name);

  /**
   * Checks to see if a name of the currency exists with this name.
   *
   * @param name  The name of the name of the currency to search for.
   * @param world The name of the {@link World} to check for this name of the currency in.
   *
   * @return True if the currency exists, else false.
   */
  boolean hasCurrency(String name, String world);

/**
   * Used to get the default currency. This could be the default currency for the server globally or
   * for the default world if the implementation supports multi-world.
   * @return The currency that is the default for the server if multi-world support is not available
   * otherwise the default for the default world.
   *
   */
  @NotNull
  String getDefaultCurrency();

  /**
   * Used to get the default currency for the specified world if this implementation has multi-world
   * support, otherwise the default currency for the server.
   * @param world The world to get the default currency for.
   * @return The default currency for the specified world if this implementation has multi-world
   * support, otherwise the default currency for the server.
   *
   */
  @NotNull
  String getDefaultCurrency(@NotNull String world);

  /**
   * Used to get a set of every  currency identifier for the server.
   * @return A set of every currency identifier that is available for the server.
   *
   */
  Set<String> getCurrencies();

  /**
   * Used to get a set of every {@link Currency} object that is available in the specified world if
   * this implementation has multi-world support, otherwise all {@link Currency} objects for the server.
   * @param world The world we want to get the {@link Currency} objects for.
   * @return A set of every currency identifier that is available in the specified world if
   * this implementation has multi-world support, otherwise all currency identifiers for the server.
   *
   */
  Set<String> getCurrencies(@NotNull String world);

  //The holdings-related methods

    /**
     * Used to get all the holdings that are associated with the specified account. This includes every currency that has a
     * balance within this account.
     */
    Map<String, BigDecimal> getMultiHoldings(String player);

    /**
     * Used to get all the holdings that are associated with the specified account for the specified world. This includes every currency that has a
     * balance within this account for the associated world.
     */
    Map<String, BigDecimal> getMultiHoldings(String player, @NotNull String world);

    /**
     * Used to get all the holdings that are associated with the specified account. This includes every currency that has a
     * balance within this account.
     */
    Map<String, BigDecimal> getMultiHoldings(OfflinePlayer player);

    /**
     * Used to get all the holdings that are associated with the specified account for the specified world. This includes every currency that has a
     * balance within this account for the associated world.
     */
    Map<String, BigDecimal> getMultiHoldings(OfflinePlayer player, @NotNull String world);

    /**
     * @deprecated As of VaultAPI 1.4 use {@link #getBalance(OfflinePlayer, String)} instead.
     */
    @Deprecated
    public BigDecimal getBalance(String playerName, String world, String currency);

    /**
     * Gets balance of a player on the specified world.
     * IMPLEMENTATION SPECIFIC - if an economy plugin does not support this the global balance will be returned.
     * @param player to check
     * @param world name of the world
     * @return Amount currently held in players account
     */
    public BigDecimal getBalance(OfflinePlayer player, String world, String currency);

    /**
     * @deprecated As of VaultAPI 1.4 use @{link {@link #has(OfflinePlayer, String, double)} instead.
     */
    @Deprecated
    public boolean has(String playerName, String worldName, BigDecimal amount, String currency);

    /**
     * Checks if the player account has the amount in a given world - DO NOT USE NEGATIVE AMOUNTS
     * IMPLEMENTATION SPECIFIC - if an economy plugin does not support this the global balance will be returned.
     * 
     * @param player to check
     * @param worldName to check with
     * @param amount to check for
     * @return True if <b>player</b> has <b>amount</b>, False else wise
     */
    public boolean has(OfflinePlayer player, String worldName, BigDecimal amount, String currency);

  /**
     * @deprecated As of VaultAPI 1.4 use {@link #withdrawPlayer(OfflinePlayer, String, double)} instead.
     */
    @Deprecated
    public EconomyResponse withdrawPlayer(String playerName, String worldName, BigDecimal amount, String currency);

  /**
     * @deprecated As of VaultAPI 1.4 use {@link #withdrawPlayer(OfflinePlayer, String, double)} instead.
     */
    @Deprecated
    public EconomyResponse withdrawPlayer(String playerName, String worldName, BigDecimal amount, String currency, String source);

    /**
     * Withdraw an amount from a player on a given world - DO NOT USE NEGATIVE AMOUNTS
     * IMPLEMENTATION SPECIFIC - if an economy plugin does not support this the global balance will be returned.
     * @param player to withdraw from
     * @param worldName - name of the world
     * @param amount Amount to withdraw
     * @return Detailed response of transaction
     */
    public EconomyResponse withdrawPlayer(OfflinePlayer player, String worldName, BigDecimal amount, String currency);

    /**
     * Withdraw an amount from a player on a given world - DO NOT USE NEGATIVE AMOUNTS
     * IMPLEMENTATION SPECIFIC - if an economy plugin does not support this the global balance will be returned.
     * @param player to withdraw from
     * @param worldName - name of the world
     * @param amount Amount to withdraw
     * @return Detailed response of transaction
     */
    public EconomyResponse withdrawPlayer(OfflinePlayer player, String worldName, BigDecimal amount, String currency, String source);

    /**
     * Deposit an amount to a player - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param player to deposit to
     * @param amount Amount to deposit
     * @return Detailed response of transaction
     */
    public EconomyResponse depositPlayer(OfflinePlayer player, BigDecimal amount, String currency);

    /**
     * Deposit an amount to a player - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param player to deposit to
     * @param amount Amount to deposit
     * @return Detailed response of transaction
     */
    public EconomyResponse depositPlayer(OfflinePlayer player, BigDecimal amount, String currency, String source);

    /**
     * @deprecated As of VaultAPI 1.4 use {@link #depositPlayer(OfflinePlayer, String, double)} instead.
     */
    @Deprecated
    public EconomyResponse depositPlayer(String playerName, String worldName, BigDecimal amount, String currency);

    /**
     * @deprecated As of VaultAPI 1.4 use {@link #depositPlayer(OfflinePlayer, String, double)} instead.
     */
    @Deprecated
    public EconomyResponse depositPlayer(String playerName, String worldName, BigDecimal amount, String currency, String source);

   /**
     * Returns true or false whether the bank has the amount specified - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param name of the account
     * @param amount to check for
     * @return EconomyResponse Object
     */
    public EconomyResponse bankHas(String name, BigDecimal amount, String currency);

    /**
     * Withdraw an amount from a bank account - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param name of the account
     * @param amount to withdraw
     * @return EconomyResponse Object
     */
    public EconomyResponse bankWithdraw(String name, BigDecimal amount, String currency);

    /**
     * Withdraw an amount from a bank account - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param name of the account
     * @param amount to withdraw
     * @return EconomyResponse Object
     */
    public EconomyResponse bankWithdraw(String name, BigDecimal amount, String currency, String source);

    /**
     * Deposit an amount into a bank account - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param name of the account
     * @param amount to deposit
     * @return EconomyResponse Object
     */
    public EconomyResponse bankDeposit(String name, BigDecimal amount, String currency);

    /**
     * Deposit an amount into a bank account - DO NOT USE NEGATIVE AMOUNTS
     * 
     * @param name of the account
     * @param amount to deposit
     * @return EconomyResponse Object
     */
    public EconomyResponse bankDeposit(String name, BigDecimal amount, String currency, String source);

Here's a quick start to the methods based on the current Vault methods. I also threw in a source parameter, which could even be formed into an ActionSource object, which would represent whatever is performing that action, in this case the deposit/withdraw. This could be, for example, a PluginSource, which would mean the plugin specifically called this method, or however detailed each plugin wants to log it. Like I said this is a quick writeup, but it at least gets the conversation rolling.

anjoismysign commented 1 year ago

I like the idea. I just disagree how it's achieved. I think we should think about allowing compatibility with current Economy interface, it would just require updating two of its methods documentation.

Here's my proposal. In case of using the plugin in a multicurrency environment, Economy#isEnabled would return if the currency(the Economy) is enabled. Economy#getName would return the name of the currency. This 'name' would be used inside MultiCurrency#existsImplementation and such. It also allows MultiCurrency#getDefault to be registered as Economy provider. The only issue is that it forces a default economy to be global in legacy plugins (I am calling legacy plugins the one that are not updated to this new changes). But it would still allow server admin to implement legacy plugins in their server without worrying for their authors to update! Plugin developers will need to explain their playerbase about this, even though we've been in the verge while attempting to do multicurrency 😪

    /**
     * Checks if economy method/implementation is enabled.
     *
     * @return Success or Failure
     */
    public boolean isEnabled();

    /**
     * Gets name of economy method or implementation
     * in case of being used in multi-currency environment.
     *
     * @return Name of Economy Method
     */
    public String getName();

Now regarding the MultiEconomy interface:

import org.bukkit.World;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;

public interface MultiEconomy {

    /**
     * Checks to see if a name of the implementation exists with this name.
     *
     * @param name The name of the name of the implementation to search for.
     * @return True if the implementation exists, else false.
     */
    public boolean existsImplementation(String name);

    /**
     * Checks to see if a name of the implementation exists with this name.
     *
     * @param name  The name of the name of the implementation to search for.
     * @param world The name of the {@link World} to check for this name of the implementation in.
     * @return True if the implementation exists, else false.
     */
    public boolean existsImplementation(String name, String world);

    /**
     * Used to get the implementation with the specified name.
     *
     * @param name The name of the implementation to get.
     * @return The implementation with the specified name.
     */
    public Economy getImplementation(String name);

    /**
     * Used to get the default implementation. This could be the default implementation for the server globally or
     * for the default world if the implementation supports multi-world.
     *
     * @return The implementation that is the default for the server if multi-world support is not available
     * otherwise the default for the default world.
     */
    @NotNull
    public Economy getDefault();

    /**
     * Used to get the default implementation for the specified world if this implementation has multi-world
     * support, otherwise the default implementation for the server.
     *
     * @param world The world to get the default implementation for.
     * @return The default implementation for the specified world if this implementation has multi-world
     * support, otherwise the default implementation for the server.
     */
    @NotNull
    public Economy getDefault(@NotNull String world);

    /**
     * Used to get a collection of every implementation identifier for the server.
     *
     * @return A collection of every implementation identifier that is available for the server.
     */
    public Collection<Economy> getAllImplementations();

    /**
     * Used to get a collection of every {@link Economy} object that is available in the specified world if
     * this implementation has multi-world support, otherwise all {@link Economy} objects for the server.
     *
     * @param world The world we want to get the {@link Economy} objects for.
     * @return A collection of every implementation identifier that is available in the specified world if
     * this implementation has multi-world support, otherwise all implementation identifiers for the server.
     */
    public Collection<Economy> getAllImplementations(@NotNull String world);
}

Most of it was based on creatorfromhell proposal. Credits to them!

Regarding how it would be implemented, plugin developers would need to check if there's a provider for its class. If so, they would use it. If not provided, they would still check if Economy has a provider, if so, they decide if give "legacy" support.

Here my tip of how stuff can be implemented:

public class EconomyHelper {

    /**
     * Checks if the economy provider is available.
     * If using MultiEconomy, it will return the default economy provider.
     *
     * @return The economy provider, or null if not available.
     */
    @Nullable
    public static Economy hasEconomyProvider() {
        if (Bukkit.getServer().getPluginManager().getPlugin("Vault") == null) {
            return null;
        }
        RegisteredServiceProvider<Economy> economyServiceProvider = Bukkit.getServer().getServicesManager().getRegistration(Economy.class);
        if (economyServiceProvider != null)
            return economyServiceProvider.getProvider();
        RegisteredServiceProvider<MultiEconomy> multiEconomyServiceProvider =
                Bukkit.getServer().getServicesManager().getRegistration(MultiEconomy.class);
        if (multiEconomyServiceProvider == null)
            return null;
        return multiEconomyServiceProvider.getProvider().getDefault();
    }

    /**
     * @return True if the there's no economy provider registered and no multi-economy provider registered.
     */
    public static boolean canRegisterEconomy() {
        if (Bukkit.getServer().getPluginManager().getPlugin("Vault") == null) {
            return false;
        }
        RegisteredServiceProvider<Economy> economyServiceProvider = Bukkit.getServer().getServicesManager().getRegistration(Economy.class);
        if (economyServiceProvider != null)
            return false;
        RegisteredServiceProvider<MultiEconomy> multiEconomyServiceProvider =
                Bukkit.getServer().getServicesManager().getRegistration(MultiEconomy.class);
        return multiEconomyServiceProvider == null;
    }
}
lightPlugins commented 8 months ago

You don't need Vault to utilize the VaultAPI. Checking for Vault's presence is unnecessary.