Introduction

For onlooking developers, Remark simplifies Command Handling and Forms through a declarative and asynchronous syntax. It is a virion that makes heavy uses of PHP 8's new attributes to provide simple, elegant solutions for usually boilerplate-heavy command handling. It tackles the async nature of PocketMine through await-generator, making complex forms easy.

For getting started check out the Quick Guide!

Quick Guide

Get familiar with Remark, fast!

Start with learning Command Handling by building a plugin step-by-step.

Or skip straight to Forms if that's what you're looking for.

Command Handling

Prerequisites:

  • Basic knowledge of PHP

About Attributes

Skip to Your First Command if you already know PHP 8's attributes.

PHP 8 added attributes, markers that can be placed on classes or methods and later found by Remark through reflection.

#[Test(debug: false)]
public function myMethod(): void {}

PHP 8.1 attributes look a lot like comments. They start with #[ and end with ] and inside the brackets the name of attributes are placed. Attributes are really just classes, and inside you put parameters for their constructor. You have to make sure to import the attribute just like you would for a class.

You can have multiple attributes inside the same #[] structure.

#[Test(debug: false), Logging(level: 2)]
public function myMethod(): void {}

You can also omit the parameter names, to make the attributes more concise.

#[Test(false), Logging(2)]
public function myMethod(): void {}

Your First Command

In Remark, you mark methods using attributes to describe how you want to take arguments from the command line.

use DiamondStrider1\Remark\Command\Cmd;
use DiamondStrider1\Remark\Command\CmdConfig;
use DiamondStrider1\Remark\Command\Arg\sender;
use DiamondStrider1\Remark\Command\Arg\text;
use DiamondStrider1\Remark\Command\Guard\permission;
use pocketmine\command\CommandSender;

#[CmdConfig(
    name: 'helloworld',
    description: 'Run my hello world command!',
    aliases: ['hw'],
    permission: 'plugin.helloworld.use'
)]
class MyCommand {
    #[Cmd('helloworld'), permission('plugin.helloworld.use')]
    #[sender(), text()]
    public function myFirstCommand(CommandSender $sender, string $text): void
    {
        $sender->sendMessage("§aHello Command Handling! - $text");
    }
}

This is a lot of code to tackle, but before going over it let's register our command so it can be used.

Registering Your First Command

public function onEnable(): void
{
    Remark::command($this, new MyCommand());
    Remark::activate($this);
}

Remark::command() adds one or more BoundCommands that implement PluginOwned. Remark::activate() registers a listener that will add TAB-completion for players.

The Breakdown

Let's go over everything in MyCommand.php.

CmdConfig Attribute

#[CmdConfig( /* ... */ )]

CmdConfig customizes the underlying command that will ultimately be registered to PocketMine's CommandMap. Everything is pretty self-explanatory. name is the name of the command (i. e. /command_name).permission is set as the permission of the command, hiding the command from those who have insufficient permissions. IT DOES NOT, however, stop people from running the command by itself. We will get into that soon.

Cmd and permission

#[Cmd('helloworld'), permission('plugin.helloworld.use')]

This line uses two attributes at once. The method these attributes are placed on are called a HandlerMethod. A method simply has to be marked by Cmd to become a HandlerMethod.

Cmd Attribute

The Cmd is passed the name of the command. You can change the command to a subcommand by adding a comma followed by another string. For example, #[Cmd('cmd', 'subcmd')] would bind the method to the subcommand at /cmd subcmd. The command or subcommand chosen must be unique, meaning there is one HandlerMethod per command/subcommand.

The permission Guard

The permission is something Remark calls a Guard. Guards prevent unauthorized access to commands by ensuring that certain requirements are met. If the permission guard fails, it will send a generic not-enough-permissions error to the command sender. The HandlerMethod only runs if all Guards attached to it succeed. Unlike the permission property of CmdConfig, this guard actually enforces that the command sender has permission to using the command. More permissions can be added by adding a comma and another string, ex: permission('perm1', 'perm2').

Args

#[sender(), text()]

Ranked's Args have many responsibilities.

  • Extracting data from command line arguments
  • Validation
  • Converting from string to a useful type (i. e. int)

It's required that for every Arg of a HandlerMethod that there is a corresponding parameter of the correct type. That parameter will be given the value extracted by the Arg whenever the command is run.

The sender() Arg

The sender() Arg requires that it's parameter is of type CommandSender or Player. It doesn't actually take any arguments from the command line, but simply supplies the command sender performing an instanceof check if needed.

The text() Arg

The text() Arg requires that it's parameter is of type string, ?string or array depending on the arguments passed to it. In this case the type of the parameter must be string. By default text() takes a single string from the command line arguments, and errors if none was given.

Asynchronous Commands

A key aspect of Remark is it's asynchronous approach to UI. This is important because of how complex asynchronous programming will become if not given care. AwaitGenerator is a virion that allows async/await style programming through Generators and their pause/resume functions. It's not required you use AwaitGenerator, but it is recommended and supported by Remark.

Here is the example extended to use Remark's first-class support of AwaitGenerator.

#[Cmd('helloworld'), permission('plugin.helloworld.use')]
#[sender(), text()]
public function myFirstCommand(CommandSender $sender, string $text): Generator
{
    $sender->sendMessage("§aHello Command Handling! - $text");
    $response = yield from AsyncApi::fetch($text);
    if ($sender instanceof Player && !$sender->isConnected()) {
        return;
    }
    $sender->sendMessage("Got Response: $response");
}

The return type of the function is now Generator, and yield from is used to call other AwaitGenerator functions. Remember to check that the player is still connected after doing any asynchronous logic.

If you got all of that, let's move on to forms!

Forms

Prerequisites:

  • Basic knowledge of PHP
  • Basic knowledge of PHP 8's attributes

The main way to send forms to players is through Remark's DiamondStrider1\Remark\Form\Forms class. It contains the methods modal2then(), modal2gen(), menu2then(), menu2gen(), custom2then(), and custom2gen(). Methods ending in gen return an AwaitGenerator compatible generator that sends the form of its type and returns it's result. Methods ending in then exist in case AwaitGenerator isn't available, and return Thenable's that are resolved with the form's result.

Let's continue the example from Command Handling.

#[Cmd('helloworld'), permission('plugin.helloworld.use')]
#[sender(), text()]
public function myFirstCommand(CommandSender $sender, string $text): Generator
{
    $sender->sendMessage("§aHello Command Handling! - $text");
    $response = yield from AsyncApi::fetch($text);
    if ($sender instanceof Player && !$sender->isConnected()) {
        return;
    }
    $sender->sendMessage("Got Response: $response");
}

For those not familiar with Remark's command handling, the method myFirstCommand() will be ran whenever a player (or the console) runs /helloworld and has the permission plugin.helloworld.use.

ModalForm

Starting off simple, we will send the player a yes/no modal form. Modal forms cannot be closed out of, if you try to hit ESC the game doesn't acts as if you had pressed the no button.

/** @var bool $choice */
$choice = yield from Forms::modal2gen(
    $sender,
    'What is your favorite ice cream?',
    'Pick from these ice cream flavors.',
);
$choice = match ($choice) {
    true => '§aYes',
    false => '§cNo',
};
$sender->sendMessage("You said {$choice}§r.");

Notice we don't have to check if the player's online because when a PocketMine guarantees the player is still connected when a form is submitted.

Let's now give the player a menu form with options to choose from.

/** @var ?int $choice */
$choice = yield from Forms::menu2gen(
    $sender,
    'What is your favorite ice cream?',
    'Pick from these ice cream flavors.',
    [
        new MenuFormButton('Vanilla'),
        new MenuFormButton('Blueberry'),
        new MenuFormButton('Lime'),
    ]
);
if (null !== $choice) {
    $choice = ['vanilla', 'blueberry', 'lime'][$choice];
    $sender->sendMessage("You chose §g{$choice}§r.");
}

A MenuFormButton can also have an attached MenuFormImage like so.

new MenuFormButton('My Button', new MenuFormImage(type: 'url', location: 'https://my.image.com/image'))
new MenuFormButton('My Button', new MenuFormImage(type: 'path', location: 'textures/blocks/dirt.png'))

CustomForm

Now for the most complex type of form. A custom form sends a list of elements for the player to fill out and a submit button to press when the player is finished. Remark gives you two ways to create a custom form. First we will start with the custom2gen()/custom2then() method.

/** @var array<int, mixed> $response */
$response = yield from Forms::custom2gen(
    $sender,
    'What is your favorite ice cream?',
    [
        new Label('Type a valid ice cream flavor!'),
        new Input('Ice Cream Flavor', placeholder: 'Vanilla'),
    ]
);
$sender->sendMessage("You said {$response[1]}§r.");

Using this method of sending custom forms, you have to index the response data with the index of the element. Remark already handles the validation for you.

Another way to make custom forms is through attributes.

use DiamondStrider1\Remark\Form\CustomFormElement\Label;
use DiamondStrider1\Remark\Form\CustomFormElement\Input;
use DiamondStrider1\Remark\Form\CustomFormResultTrait;

final class MySurveyForm
{
    use CustomFormResultTrait;

    #[Label('Type a valid ice cream flavor!')]
    #[Input('Ice Cream Flavor', placeholder: 'Vanilla')]
    public string $name;
}
/** @var MySurveyForm $formResult */
$formResult = yield from MySurveyForm::custom2gen(
    $sender, 'What is your favorite ice cream?'
);
$sender->sendMessage("You said {$formResult->name}§r.");

The CustomFormResultTrait adds the static functions custom2gen() and custom2then() that take a player and a title for the form. It's recommended that you mark form fields as public so they are easily accessible.

In Depth

For installing for development and production check Installing.

Documentation can be found for Command Args, Command Guards, and Custom Form Elements.

Installing

Setting up your plugin to use Remark is quite easy!

It's advised that during development DEVirion is used and when the plugin is ready production that this library is either installed by Poggit or installed by hand.

Using DEVirion

DEVirion allows your plugin to use Remark.

  1. Install Remark.phar from this project's Github Releases.
  2. Place Remark.phar into your server's virions folder, which is next to the plugins folder.
  3. If not already downloaded, get DEVirion from Poggit.

Installing with Poggit

Plugins that are built on poggit and use virions should declare there dependencies in .poggit.yml. To use Remark add an entry like this.

projects:
  my-pugin-project:
    path: ""
    libs:
      - src: Swift-Strider/Remark/Remark
        version: ^3.3.0
        epitope: .random

Manually Installing into a Plugin Phar

Install Remark.phar from this project's Github Releases.

If you haven't already, build your plugin into a phar file. This example script assumes you're in your plugin's directory and that the files/directories plugin.yml, src, and resources exist. The following works on both Windows 10 (Powershell) and Ubuntu.

wget https://raw.githubusercontent.com/pmmp/DevTools/master/src/ConsoleScript.php -O ConsoleScript.php
php -dphar.readonly=0 ConsoleScript.php --make src,resources,plugin.yml --relative . --out plugin.phar

Next you will "infect" your plugin's .phar file, embedding the Remark library inside of your plugin.

php -dphar.readonly=0 Remark.phar plugin.phar

In Depth | Commands

Remark::command() is used to bind the HandlerMethods of an object to a CommandMap. By default, Remark::command() uses the CommandMap attached to the running PocketMine Server. Under-the-hood, a new BoundCommand is made for every CmdConfig, and they implement PluginOwned returning the plugin passed to Remark::command().

Remark::activate() registers a listener which will add TAB-completion for players.

CmdConfig

Configures the commands of a handler object.

string $name,
string $description,
array $aliases = [],
?string $permission = null,
  • name - The name of the underlying command
  • description - The description of the command
  • aliases - The aliases of the command
  • permission - If set, one or more permissions separated by ;

Cmd

Marks a method as a handler for a command. You may bind a HandlerMethod to multiple commands by repeating this attribute.

string $name,
string ...$subNames,
  • name - The name of the command to attach this HandlerMethod to
  • subNames - Zero or more subcommand names

In Depth | Forms

The DiamondStrider1\Remark\Form\Forms class holds static methods for creating forms. They are modal2then(), modal2gen(), menu2then(), menu2gen(), custom2then(), and custom2gen(). The *2gen() methods return generators to be used with await-generator, and the *2then() methods return Thenable a type defined by Remark.

Thenable

A Thenable is much like a promise. One may call $thenable->then($onResolve, $onReject). If you omit $onReject Remark will throw an UnhandledAsyncException if the Thenable is rejected.

Modal Form

Use either Forms::modal2then() or Forms::modal2gen(). Resolves with a boolean, true if the yes button was hit, false if the no button was.

Player $player,
string $title,
string $content,
string $yesText = 'gui.yes',
string $noText = 'gui.no',

Menu Form

Use either Forms::menu2then() or Forms::menu2gen(). Resolves with an integer, the index of the button the player chose.

Player $player,
string $title,
string $content,
array $buttons,
  • buttons - A list of MenuFormButtons

A button for a menu form.

string $text,
?MenuFormImage $image = null,
  • text - The text of the button
  • image - An optional image to display

An image that can be to present on a menu form.

string $type,
string $location,
  • type - Either 'url' or 'path'.
  • location - The location of the image

If the image's type is url, Minecraft will fetch the image from online, and it may take some time to load. If the type is path, Minecraft will instantly load the image from the resource pack.

On Minecraft Windows 10, url images may not show until ALT-TAB'ing out of then back into Minecraft.

An example location of a path type image is "textures/block/dirt.png" without the leading slash.

Custom Form

You may use Forms::custom2then()/Forms::custom2gen() or CustomFormResultTrait.

Custom Form, static functions

Returns an array with the indexes of elements mapped to the values of the player's response.

Player $player,
string $title,
array $elements,
  • elements - A list of CustomFormElements

Custom Form, CustomFormResultTrait

A class that uses this trait will have the static methods custom2gen() and custom2then() added which return a new instance of the class instead of an array.

A class using CustomFormResultTrait must meet the following requirements:

  • Must not be abstract
  • Every property to be filled in...
    • May be marked with any number of Label attributes
    • Must be marked with at most one CustomFormElement that isn't Label

Properties are filled in according to the non-Label attribute attached to them, or ignored if only Labels are attached to them.

All properties without CustomFormElement attributes are ignored.

Command Args

An Arg provides a value to its corresponding parameter of a HandlerMethod. The number of Args in a HandlerMethod must be equal to its number of parameters. An Arg may extract its value from the arguments given when the command is run, or it may get its value from another source.

sender

Extracts the CommandSender. If its corresponding parameter has the type Player it will do an instance-of check automatically. Otherwise the type of the parameter must be CommandSender.

sender() does not take any arguments.

player_arg

Extracts a Player using the name provided by a command argument.

bool $exact = false,
  • exact - whether to not match by prefix

text

Extracts one or more strings from the command arguments. Depending on the parameters given to this Arg, the corresponding parameter must have a type of string, ?string, or array.

int $count = 1,
bool $require = true,
  • count - number of arguments to take
  • require - wether to fail if the number of arguments remaining is less than count

remaining

Extracts the remaining strings from the command arguments.

remaining() does not take any arguments.

enum

Extracts a string that must be in an immutable set of predefined strings.

string $name,
string $choice,
string ...$otherChoices,
  • name - the enum's name, in-game it will show as a command hint (i. e. <paramName: name>)
  • choice - A possible choice
  • otherChoices - Other possible choices

bool_arg

Extracts a true / false boolean. Valid command arguments for both choices are:

  • true: "true", "on", and "yes"
  • false: "false", "off", and "no"

bool_arg() does not take any arguments.

int_arg

Extracts an integer.

int_arg() does not take any arguments.

float_arg

Extracts a float.

float_arg() does not take any arguments.

vector_arg

Extracts either a Vector3 or RelativeVector3 depending on the type of it's corresponding parameter.

If your parameter has the type RelativeVector3, you can then call $relativeVector->relativeTo($vector) to get a real Vector3.

vector_arg() does not take any arguments.

json_arg

Extracts a string WITHOUT validating that it's proper json. This is more of a marker, telling the player's client that JSON is needed. You MUST verify that the JSON is valid, yourself.

json_arg() does not take any arguments.

command_arg

Extracts a string WITHOUT validating that it's the name of a command. This is more of a marker, telling the player's client that a command name is needed. You MUST verify that the command name is valid, yourself.

command_arg() does not take any arguments.

Command Guards

A Guard prevents a HandlerMethod from being ran when a requirement isn't satisfied. I. e. the command sender not having permission.

permission

Requires that the CommandSender has all of the permissions passed in.

string $permission,
string ...$otherPermissions,
  • permission - One required permission
  • otherPermissions - Other required permissions

Custom Form Elements

Dropdown, Input, Label, Slider, StepSlider, and Toggle are found in the DiamondStrider1\Remark\Forms\CustomFormElement namespace and all implement CustomFormElement. They may be used as normal classes (new Label('Some Text')) or as attributes (#[Label('Some Text')]).

Information on creating custom forms can be found here.

Returns an integer which is the index of the choice the player selected.

string $text,
array $options,
int $defaultOption = 0,
bool $allowDefault = true,
  • allowDefault - whether the player may skip filling out a dropdown when the Dropdown's default value is -1

Input

Returns a string that the player entered.

string $text,
string $placeholder = '',
string $default = '',

Label

Does not return anything, but it does place text at its location.

string $text

Slider

Returns a float in within the range [min, max]. It DOES NOT validate the step, however, so that responsibility is left to the developer.

string $text,
float $min,
float $max,
float $step = 1.0,
?float $default = null,

StepSlider

Returns an integer, the index of the step the player chose. Visually, looks like a Slider but the player chooses one of the steps.

string $text,
array $steps,
int $defaultOption = 0,
  • steps - list of strings to choose from

Toggle

Returns a boolean. Creates a switch that the player can toggle.

string $text,
bool $default = false,