Skip to content

Development

Mose edited this page Oct 28, 2022 · 8 revisions

Development

Dependency

When it comes to depending on AccountInterface, you will first need to add AccountInterface into your dependencies. If your project uses maven, the following can be used

     <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>com.github.EcoToolBox</groupId>
            <artifactId>AccountInterface</artifactId>
            <version>main-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

After adding the dependency into your development environment, you will then need to add AccountInterface dependency into your plugin.yml

depend:
  - AccountInterface

AccountInterfaceManager

The AccountInterfaceManager is the one stop shop for all AccountInterface generic handling (aka, you will use this a lot). This is provided by the currency implementation, however you are able to receive it in two different ways depending on your situation / what you prefer.

By AccountInterface

The simplest way is to simply do one of the following:

By hard depends

AccountInterfaceManager manager = AccountInterface.getManager();

By soft depends

public Optional<AccountInterfaceManager> getAccountInterfaceManager(){
  Plugin accountInterfacePlugin = Bukkit
      .getPluginManager()
      .getPlugin("AccountInterface");
  if(
      accountInterfacePlugin == null || 
      (!(accountInterfacePlugin instanceOf AccountInterface plugin))){
    return Optional.empty();
  }
  return Optional.of(plugin.getManager());
}

This has the benefit of just being a one-liner, however if you prefer to do the old vault method, then you can too (The above does this way under the hood, but caches the result). This method allows you to also get the Plugin of the implementation if you so prefer

RegisteredServiceProvider<AccountInterfaceManager> reg = Bukkit
                                .getServicesManager()
                                .getRegistration(AccountInterfaceManager.class);
AccountInterfaceManager manager = reg.getProvider();

Currency

AccountInterface supports multiple currencies, this being directly supported by AccountInterface means that other plugins can interact with currencies without guessing the default, with that being said, if the admin only wants a single currency then that is a possibility.

Getting default currency

The default currency is the currency that should be used if you don't need to specify a currency. This is also the currency that the vault emulation will use by default (as vault doesn't support multiple currencies)

AccountInterfaceManager accountInterfaceManager;
Currency currency = accountInterfaceManager.getDefaultCurrency();

Getting all registered currencies

AccountInterfaceManager accountInterfaceManager;
Collection<Currency> currencies = accountInterfaceManager.getCurrencies();

Creating a new currency

When creating a new currency, we will use the CurrencyBuilder to develop the new currency, here are the details of using a currency builder

Name Type Optional Description
name String false The registered name of the currency, this typically will not be shown to the user in transactions
plugin Plugin false The implementation plugin of the currency. This cannot be specified in the builder
symbol String false The currencies sign
worth BigDecimal true The worth of the currency compared to 1 (value must be greater then 0)
name single String true The name of the currency if the amount is 1
name multiple String true The name of the currency if the amount is more then 1
name short String true The initials of the currency
default boolean false If the currency is the default currency, please note that setting this in the builder may result in a error where two or more default currencies are active, so best practice would be to remove the default status from the currency default currency before registering yours

When it comes to the specified names (single, multiple and short) these are optional, however if not specified, it will use the next best thing based upon the implementation, so it is best to specify these if known.

Plugin plugin;
Currency currency = new CurrencyBuilder()
  .setWorth(1)
  .setDisplayNameSingle("pound")
  .setDisplayNameMultiple("pounds")
  .setDisplayNameShort("gbp")
  .setSymbol("£")
  .setName("British Pounds")
  .setDefault(true);

Payment

When handling any transaction, a payment object will be needed. This will give the context for the transaction before the transaction has even started.

A payment includes the following:

Name Type Optional
amount BigDecimal false
currency Currency false
reason String true
from Account true

As you can see, the amount is not a double but instead a BigDecimal. This is due to the precision of BigDecimal compared to double. With that being said, the PaymentBuilder does accept double values for the convenience, this then gets converted to a BigDecimal.

The reason and from are optional as they are only there to provide context for the payment.

Account

A account is the holder of money, typically this will be a player, however could be a bank or something else (depending on the currency plugin).

A account class is prefered to be connected directly onto the object its representing, its not always possible (such as Player whereby the player object is part of the Bukkit API), so we may use the account as a wrapper

Player Account

The player account is a wrapper for the Bukkit's OfflinePlayer. A Player account can have banks attached to it too (see below) and therefore you gain extra transaction methods (see below) for handling transactions between what your wanting and the PlayerAccount as well as all banks that are loaded that the PlayerAccount has access to.

Bank Account

Bank accounts may not be attached to a player, therefore there is a basic BankAccount with a further PlayerBankAccount to represent the holder being a player.

A Bank Account allows multiple accounts to access the money inside the account. Each account will have BankPermissions attached to them. While you can bypass these permissions with code, the permissions should be respected unless for a specific reason (such as a direct debit)

Transaction

There are two main types of transaction objects in AccountInterface, the first being Transaction with the other being TransactionResult.

Transaction class

The transaction class is typically only accessible while the transaction is happening. It is designed in a way to give you even more context to the transaction happening then the Payment, so much so that the payment object is part of the transaction. You are also able to receive the account target of the transaction, the type of transaction (withdraw, deposit, set, etc) and you can even modify the transaction amount

TransactionResult class

The transaction result class is created when the transaction has been completed (if that be though success or failure). The result class is designed to give you context on if it was a success or a failure, if the latter then why it failed.

TransactionResult result;
if(result instanceof SuccessfulTransactionResult){
  getLogger().log("Successful");
}
if(result instanceof FailedTransactionResult failed){
  getLogger().warning("Failed due to " + failed.getReason());
}

Start a transaction

Its all well and good knowing how to manipulate the transaction, but how we start one is the fun part.

Transactions in AccountInterface can be asynced (meaning off main thread). This maybe different to what you are used to as Bukkit was written when Minecraft was heavily single threaded, however thats a side story. The main result of being asynced is that you will be using CompleteableFuture if you care about the result of the transaction.

When it comes to actual transactions, there are 3 to choose from as of writing.

type code description
Set account.set(Payment) Sets the account's balance in the currency specified to the specified amount
deposit account.deposit(Payment) Adds the specified amount in the specified currency to the account
withdraw acount.withdraw(Payment) Removes the specified amount in the specified currency from the account

Please note that account balances cannot be below 0, so attempting to set the value lower or withdrawing more then what the account has will result in a fail

Isolated Transactions

If you require to do multiple transactions whereby if one fails, all should fail and not make any changes (example such as paying someone else, whereby a withdraw needs to occur to one account and a deposit of the same amount needs to occur to the other) then isolated transactions are the way to do it.

The accounts being used in the transaction need to be of AccountType

In the following example, we will be paying an account £1.

final AccountType payee;
final AccountType payer;
final BigDecimal amount = BigDecimal.ONE;
final Currency currency;

CompletableFuture<TransactionResult> result = new IsolatedTransaction(
  accounts -> {
    IsolatedAccount isolatedPayee = accounts.get(payee);
    IsolatedAccount isolatedPayer = accounts.get(payer);

    Payment withdrawPayment = new PaymentBuilder().setAmount(amount).setCurrency(currency).build();
    CompletableFuture<SingleTransactionResult> withdraw = isolatedPayer.withdraw(withdrawPayment);

    Payment depositPayment = new PaymentBuilder().setAmount(amount).setCurrency(currency).from(payer).build();
    CompletableFuture<SingleTransactionResult> deposit = isolatedPayee.deposit(depositPayment);

    return Arrays.asList(withdrawPayment, depositPayment);
  },
  payee, 
  payer).start();

The magic behind the IsolatedTransaction comes mainly from the lamda. All accounts (2nd parameter and beyond) are converted into a IsolatedAccount. These are then stored in a Map that is sent with the lamda (hence the get(payee)). The transactions you wish to commit are then done on the isolated accounts so not to interact with the balance of the regular account. These are then all awaited for with the results then checked for any failures. If all passed, then the isolated accounts are then applied to the respected original account.

The isolated transaction needs to know what transactions to await for, therefore we return a collection that holds all of the transactions (hence the Arrays.asList().