By continuing to use this site, you agree to the Terms of Service of this website, including usage of cookies.

OK, Don't show this again

An extensible economy API for PocketMine-MP.
version 0.1.2
Approved
Direct Download How to install?
Switch version
2155 Downloads / 2316 Total
15 Reviews
Plugin Description §

Capital

CI

An extensible economy plugin for PocketMine-MP.

Important: To use this plugin, you have to install InfoAPI too.

How is Capital different from other economy plugins?

As a core API for economy, Capital supports different styles of account management:

  • You can have the old, simple one-account-per-player mechanism.
  • Or do you like currencies? You can add new currencies to config.yml and other plugins will let you configure which currency to use in each case.
  • Or are currencies too complicated for you? What about just having one account per world? You don't need any special configuration in other plugins!
  • Are commands and form UI boring for you? Maybe use banknote/wallet items so that players lose money when they drop the item? (Capital itself does not support banknote/wallet items, but it is the only economy API where both simple accounts and item payment can be used from other plugins without writing code twice)
  • Or maybe sometimes the money goes to the faction bank account instead of player account?
  • Capital is extensible for other plugins to include new account management styles, And it will work automatically with all plugins!

Other cool features include:

  • Powerful analytics commands. How much active capital is there? How is wealth distributed on the server? Which industries are the most active? What are the biggest transactions in the server yesterday? Capital can help you answer these questions with label-based analytics.
  • Is editing the config file too confusing for you? Capital supports self-healing configuration. Your config file will be automatically regenerated if something is wrong, and Capital will try its best to guess what you really wanted.
  • Supports migration from other economy plugins, including:
    • EconomyAPI
  • Uses async database access, supporting both SQLite and MySQL. Capital will not lag your server.
  • Safe for multiple servers. Transactions are strictly atomic. Players cannot duplicate money by joining multiple servers.

Setting up

After running the server with Capital the first time, Capital generates config.yml and db.yml, which you can edit to configure Capital.

db.yml is used for configuring the database used by Capital. You can use sqlite or mysql here. The configuration is same as most other plugins.

config.yml is a large config that allows you to change almost everything in Capital. Read the comments in config.yml for more information. Text after '# xxx: are comments. If you edit config.yml incorrectly, Capital will try to fix the config.yml and save the old one as config.yml.old so that you can refer to it if Capital fixed it incorrectly.

Default commands

All commands in Capital can be configured in config.yml. Try searching them in the config file to find out the correct place. The following commands come from the default config:

Player commands:

  • /pay <player> <amount> [account...]: Pay money to another player with your own account.
  • /checkmoney: Check your own wealth.
  • /richest: View the richest players on the server.

Admin commands:

  • addmoney <player> <amount> [account...]: Add money to a player's account.
  • takemoney <player> <amount> [account...]: Remove money from a player's account.
  • /checkmoney <player>: Check the wealth of another player.
  • /capital-migrate: Import database from EconomyAPI.

[account...] can be used to select the account (e.g. currency) if you change the schema in config.yml. (You can still disable these arguments by setting up selector in config.yml)

You can create many other useful commands by editing config.yml, e.g. check how much money was paid by /pay command! Check out the comments in config.yml for more information.

Community, Contact & Contributing

If you want to get help, share your awesome config setup or show off your cool plugin that uses Capital, create a discussion on GitHub.

To report bugs, create an isuse on GitHub.

If you want to help with developing Capital, see the API tab.

Simple API

Summary

Capital supports different account classifications (known as schemas). All operations involving player accounts require a config entry to select which account, e.g. if the user selects the Currency schema, you need a config that selects which currency to use. The good news is, you don't have to consider each schema, because Capital will figure it out. For each use case, just create an empty config entry to select the account:

selector:

Users can fill this selector with options like allowed-currencies etc., depending on the schema they chose.

In onEnable, store this in a class property:

use SOFe\Capital\{Capital, CapitalException, LabelSet};

class Main extends PluginBase {
  private $selector;

  protected function onEnable() : void {
    Capital::api("0.1.0", function(Capital $api) {
      $this->selector = $api->completeConfig($this->getConfig()->get("selector"));
    });
  }
}

Then you can use the stored selector in the code where you want to manipulate player money.

Take money from a player

Let's make a plugin called "ChatFee" which charges the player $5 for every chat message:

public function onChat(PlayerChatEvent $event) : void {
  $player = $event->getPlayer();

  Capital::api("0.1.0", function(Capital $api) use($player) {
      try {
        yield from $api->takeMoney(
          "ChatFee",
          $player,
          $this->selector,
          5, 
          new LabelSet(["reason" => "chatting"]),
        );

        $player->sendMessage("You lost $5 for chatting");
      } catch(CapitalException $e) {
        $player->kick("You don't have money to chat");
      }
  });
}

The first "ChatFee" is your plugin name so that server admins can track which plugin gave the money. Capital will create a system account for your plugin, and the money will actually go into the ChatFee system account.

The second parameter $player tells Capital which player to take money from.

The third parameter $this->selector tells Capital which account to take money from, as we have explained in the previous section. Note: if you have multiple different scenarios of giving/taking money, consider using different selectors.

The fourth parameter 5 is the amount of money to take. This value must be an integer.

The last parameter new LabelSet(["reason" => "chatting"]) provides the labels for the transaction. Server admins can use these labels to perform analytics. You may want to let users configure these labels in the config too.

The try-catch block lets you handle the scenario where player does not have enough money to be taken. However, remember that you cannot cancel $event after the first yield, because transactions are asynchronous, which means that the event already happened by that time and it is too late to cancel.

Giving money to a player

Giving money is similar to taking money, except takeMoney becomes addMoney. Let's make a plugin called "HitReward" that gives the player money when they attack someone:

public function onDamage(EntityDamageByEntityEvent $event) : void {
  $player = $event->getDamager();
  if(!$player instanceof Player) {
    return;
  }

  Capital::api("0.1.0", function(Capital $api) use($player) {
    try {
      yield from $api->addMoney(
        "HitReward",
        $player,
        $this->selector,
        5, 
        new LabelSet(["reason" => "attacking"]),
      );

      $player->sendMessage("You got $5");
    } catch(CapitalException $e) {
      $player->sendMessage("You have too much money!");
    }
  });
}

Paying money from a player to another

Paying money is like taking money from one player and giving to another, but it only happens when both players have enough money and don't exceed limits. Neither player will lose or receive money if any limits are violated.

public function pay(Player $player1, Player $player2) : void {
  Capital::api("0.1.0", function(Capital $api) use($player1, $player2) {
    try {
      yield from $api->pay(
        $player1,
        $player2,
        $this->selector,
        5, 
        new LabelSet(["reason" => "payment"]),
      );

      $player1->sendMessage("You paid $5 to " . $player2->getName());
    } catch(CapitalException $e) {
      $player1->sendMessage("Failed!");
    }
  });
}

All arguments are same as before, except you don't need to pass your plugin name because the money came from a player and you don't need a plugin account.

If the payer and receiver sides have different amounts (e.g. service fee), you have to use payUnequal instead. The following code makes $player1 pay $player2 $5, plus giving $3 service fee to the "ServiceFee" system account:

public function pay(Player $player1, Player $player2) : void {
  Capital::api("0.1.0", function(Capital $api) use($player1, $player2) {
    try {
      yield from $api->payUnequal(
        "ServiceFee",
        $player1,
        $player2,
        $this->selector,
        5 + 3, // this is the total amount that $player1 has to lose
        5, // this is the total amount that $player2 gets
        new LabelSet(["reason" => "payment"]),
        new LabelSet(["reason" => "service-fee"]), // this label set is applied on the transaction from $player1 to the system account
      );

      $player1->sendMessage("You paid $5 to " . $player2->getName() . " and paid $3 service fee");
    } catch(CapitalException $e) {
      $player1->sendMessage("Failed!");
    }
  });
}

It is also possible for player1 to pay less and player2 to pay more. In that case, player1 only pays the amount to player2, then the system account will pay the rest to player2.

Getting money for a player

If you want to check whether the player has enough money for something, use takeMoney as explained above and handle the error case.

If you just want to display player money, use InfoAPI. The default config registered the {money} info on players, but users can change this based on their config setup. Consider using InfoAPI to compute the messages and let the user set their own messages. See InfoAPI readme for usage guide.

Advanced API

The advanced API is for advanced developers who want to use the specific features in Capital in addition to the basic money manipulation operations.

Async functions

Capital uses await-generator, which enables asynchronous programming using generators. Functions that return Generator<mixed, mixed, mixed, T> are async functions that return values of type T. There is a special phpstan-level type alias VoidPromise, which is just shorthand for Generator<mixed, mixed, mixed, void>.

Generator functions must always be called with yield from instead of yield. Functions that delegate to another generator function MUST always return yield from delegate_function();, even though return delegate_function(); and return yield delegate_function(); have similar semantics. This is to ensure consistent behavior where async functions only start executing when passed to the await-generator runtime.

Module system and dependency injection

Capital is module-based. Every module containing a Mod class is a module. Each module has its independent semantic versioning line, as indicated by the API_VERSION constant. The Mod class is responsible for starting and stopping components that cannot wait to be initialized only on-demand, such as commands, event handlers and InfoAPI infos/fallbacks.

Classes that only have one useful instance in the runtime are called "singletons". To facilitate unit testing in the future, singletons do not use the traditional getInstance() style. Instead, all singletons are managed by the Di namespace. If a (singleton or non-singleton) class requires an instance of a singleton class, it can implement the Di\FromContext interface and use the Di\SingletonArgs trait, then declare all required classes in the constructor, for example:

use SOFe\Capital\Di;

class Foo implements Di\FromContext {
  use Di\SingletonArgs;

  public function __construct(
    private Bar $bar,
    private Qux $qux,
  )
}

Alternatively, if initialization of the object is async (e.g. Database\Database initialization requires waiting for table creation queries), or the developer does not wish to mix initialization logic in the constructor (it is a bad practice to do anything other than field initialization and validation in the constructor), declare public static function fromSingletonArgs in a similar style. fromSingletonArgs can return either self or Generator<mixed, mixed, mixed, self>.

The class can be instantiated with Foo::instantiateFromContext(Di\Context $context). Alternatively, if Foo itself is a singleton class, it can additionally implement Di\Singleton and use the trait Di\SingletonTrait, then it can be requested from another FromContext class with the same style.

All Mod classes are singleton and use Di\SingletonArgs. They are required in the Loader\Loader (this is not the main class) singleton, which is explicitly created from the main class onEnable.

The main class (Plugin\MainClass) is a singleton, although it is not initialized lazily like other FromContext classes (since it is the same instance that started loading everything). Note that, unlike many other plugins, the main class does not have any functionality by itself. It serves only to implement the Plugin interface as required by some PocketMine APIs, and is generally useless except for registering commands and event handlers.

The Di\Context is also a singleton, but similar to the main class, it is not initialized lazily. Other classes can use it to flexibly require new objects that were not requested in the constructor under special circumstances.

The await-std instance (\SOFe\AwaitStd\AwaitStd) does not implement Di\Singleton, but it is also special-cased to allow singleton-like usage.

The \Logger interface is not a singleton. However, FromContext classes can declare a parameter of type \Logger, then the DI framework will create a new logger for the class. (This logger is derived from the plugin logger, but is not equal to the plugin logger itself)

Config

Capital implements a self-healing config manager. Each module has its own Config class to manage module-specific configuration. In addition to the singleton and FromContext interfaces, each Config class also implements Config\ConfigInterface and uses Config\ConfigTrait, implementing a parse method that reads values from a Config\Parser object into itself.

The first time parse is called, Config\Parser is in non-fail-safe mode, which means methods like expectString would throw a Config\ConfigException if the parsed config contains invalid types or data. Upon catching a Config\ConfigException, the config framework calls parse on all Config classes again, this time providing a Config\Parser in fail-safe mode, which would no longer throw Config\ConfigException. Instead, the parser will add missing fields (along with documentation) or replace incorrect fields in the config, which are saved to the config after all module configs have been parsed. Config classes can also use the failSafe method in the Config\Parser to either return a value or throw a Config\ConfigException depending on the parser mode. This strategy allows automatic config refactor when the user changes critical settings like the schema, which cascades changes to many other parts in the config.

Due to difficulties with cyclic dependencies, all Config classes must be separated listed in the Config\Raw::ALL_CONFIG constant.

Database

Capital uses libasynql for database connection. Note that the libasynql DataConnector is exposed in the Database API, whcih means the SQL definition is part of semantic versioning. All structural changes are considered as backward-incompatible changes. The Database class also provides some low-level (although higher level than raw SQL queries) APIs to access the database. Other modules should consider using the APIs in the SOFe\Capital\Capital singleton, which provides more user-friendly and type-safe APIs than the Database class.

Raw queries are written in resources/mysql and resources/sqlite. There is a slight diversion in MySQL and SQLite queries due to different requirements; SQLite does not require any synchronization and assumes FIFO query execution. MySQL assumes there may be multiple servers using the database, plus external services (such as web servers) that may modify the data arbitrarily.

Labels

The Capital database is gameplay-agnostic. This means the database design is independent of how accounts are created. The database module does not know anything about players or currencies or worlds. Instead, each account is attached with labels (a string-to-string map), which provide information about the account and enable account searching.

Each player may have zero or more accounts, determined by the schema configured. Generally speaking, player accounts are identified by the capital/playerUuid label (or capital/playerName if username display is required); analytics modules can use this label to identify accounts associated to a player.

Capital aims to provide reproducible transactions. All account balance changes other than initial account creation should be performed through a transaction. In the case of balance change as initiated by an operator or automatic reward provided by certain gameplay (e.g. kill reward), a system account (known as an "oracle") should be used as the payer/payee. Oracles do not have player identification labels like capital/playerUuid, but they use the capital/oracle to identify themselves.

Other modules/plugins can also define other account types. As long as they have their own labels that do not collide with existing labels, they are expected to work smoothly along with other components.

Transactions can also have their own labels. At the current state, the exact usage of transaction labels is not confirmed yet. It is expected that transaction labels can be used to analyze economic activity, such as the amount of capital flow in certain industries.

The database can search accounts and transactions matching a "label selector", which is an array of label names to values. Accounts/transactions are matched by a selector if they have all the labels specified in the selector. An empty value in a label selector matches all accounts/transactions with that label name regardless of the value.

Schema

A schema abstracts the concept of labels by expressing them in more user-friendly terminology. A schema is responsible for defining the accounts for a player and parsing config into a specific account selector.

There are currently two builtin schema types:

  • basic: Each player uses the same account for everything.
  • currency: Currencies are defined in schema config, where each player has one account for each currency.

There are other planned schema types, which impose speical challenges:

  • world: Each player has one account for each world. This means accounts must be created lazily and dynamically, because new worlds may be loaded over time.
  • wallet: Accounts are bound to inventory items instead of players. Players can spend money in an account when the item associated with the account is in the player's inventory. This means the player label is mutable and requires real-time updating.

Let's explain how schemas work with a payment command and a currency schema. The default schema is configured as:

schema:
  type: currency
  currencies:
    coins: {...}
    gems: {...}
    tokens: {...}

The payment command is configured with a section like this:

accounts:
  allowed-currencies: [coins, tokens]

This config section is passed to the default schema, which returns a new Schema object that only contains the currency subset [coins, tokens].

When a player runs the payment command (e.g. /pay SOFe 100 coins), the remaining command arguments (["coins"]) are passed to the subset schema, which decides to parse the first argument as the currency name. Since we only use the subset schema, only coins and tokens are accepted. The subset schema returns a final Schema object that knows coins have been selected. The sender and recipient are passed to the final Schema, which returns a label selector for the sender and recipient accounts. If no eligible accounts are found, the plugin tries to migrate the accounts from imported sources as specified by the schema. If no migration is eligible, it creates new accounts based on initial setup specified by the schema.

Analytics

The Analytics module consists of two parts: Single and Top.

Single metrics

Analytics\Single computes single-value metrics.

The Analytics\Single\Query interface abstracts different metric types parameterized by a generic parameter P. Use CachedValue::loop to spawn a refresh loop that fetches the latest metric value. If P is Player, use PlayerInfoUpdater::registerListener() to automatically spawn refresh loops for online players.

Top metrics

Analytics\Top reports server-wide top metrics.

Due to the label-oriented mechanism, it is not possible to efficiently fetch the top accounts directly because the SQL database cannot be indexed by a specific label. To allow efficient top metric queries, the metric is first computed for each grouping label value (usually the player UUID) and cached in the capital_analytics_top_cache table.

A top metric query is defined by the following:

  • The aggregator to use. This also defines whether the query operates on accounts or transactions. Currently all aggregators are accounts-only or transactions-only, but there will be aggregators on transactions for each account in the future.
  • The label selector that filters rows. For example, if the aggregator is about number of transactions of each player, the label selector filters away non-player accounts (it is not a transaction label selector).
  • The grouping label name, where its values will be used to group rows. For queries on top players, this is AccountLabels::PLAYER_UUID.

These three values uniquely identify a top query for computation cache. These values are md5-hashed into the capital_analytics_top_cache.query column, which are reused on multiple servers. The computation takes place in batches, updating a subset of label values each time. Call Analytics\Top\Mod::runRefreshLoop() to start a refreshing loop.

Analytics\Top\DatabaseUtils::fetchTopAnalytics fetches the cached data for display. For each displayLabels label, a random label value for matching rows with the name equal to the displayLabels label is returned in the output for display.

Transfer

Migration

What's new §
  • Implemented EconomyAPI YAML (first-class support) and MySQL (untested) migration using the /capital-migrate command.

MCPEAbdu77
using v0.1.2
18 Jun 22
DavidGlitch04
using v0.1.2
23 May 22
Great
Tobias-2006
using v0.1.2
22 May 22
Good job
MintoD
using v0.1.2
22 May 22
Nice plugin!, I'll use it for all my plugins
RevandDev
using v0.1.2
14 Apr 22
Best Economy Plugin
KuaterCraft
using v0.1.2
13 Apr 22
multiworld???
SOF3
Staff
13 Apr 22
coming in next update, code is already written, you can look at the PR
BeeAZ-pm-pl
using v0.1.2
07 Apr 22
XzAlan
using v0.1.2
05 Mar 22
Scorehud?
SOF3
Staff
06 Mar 22
maybe through InfoAPI
jasonwynn10
Staff
using v0.1.2
05 Mar 22
I'll be using this in all my projects
MulqiGaming64
Outdated
using v0.1.1-beta1
04 Mar 22
devgocri
Outdated
using v0.1.1-beta1
04 Mar 22
IhsanNugroho
Outdated
using v0.1.1-beta1
28 Feb 22
PushkarOP
Outdated
using v0.1.1-beta1
28 Feb 22
Nice, SOF3 op
ItzLuckyL
Outdated
using v0.1.1-beta1
28 Feb 22
best icon, also good plugin
NhanAZ
Outdated
using v0.1.1-beta1
28 Feb 22

Reply to review by :

/ 5
Supported API versions
4.0.0
->
4.9.0
Dependencies
InfoAPI 1.2.0
Required
View Plugin
Requirements & Enhancements
MySQL server Enhancement If you use MySQL database, it requires full control of a MySQL schema, but it only creates tables and procedures starting with `capital_`.
Producers §
  • Collaborators:
    • @SOF3
    • @Swift-Strider
  • Contributors:
    • @AIPTU
    • @Endermanbugzjfc
    • @KygekDev
    • @SmallkingDev
License §
Categories:
Economy
API plugins
Permissions
Database
Other files
Permissions
Commands
External Internet clients
Custom threading

You can leave one review per plugin release, and delete or update your review at any time

/ 5
Loading...