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 BoundCommand
s 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
. Guard
s 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 Guard
s 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 Arg
s 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.
MenuForm
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.
- Install
Remark.phar
from this project's Github Releases. - Place
Remark.phar
into your server'svirions
folder, which is next to theplugins
folder. - 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
MenuFormButton
s
MenuFormButton
A button for a menu form.
string $text,
?MenuFormImage $image = null,
- text - The text of the button
- image - An optional image to display
MenuFormImage
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
CustomFormElement
s
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.
Dropdown
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,