pmmp日本界隈の皆さんお元気ですか?
早速ですが、既存のフォームライブラリは、フォームシーケンスが作りづらくて仕方ない!
なので、AwaitGeneratorとAwaitFormを悪魔合体したフォームライブラリ書きました。
詳しくはGithubに行ってReadmeを見てほしいですが、ここでは特徴を述べます。
ただ、書いた私も悩むぐらい難しいので、pmmp中級者以上の方向けです。
特徴1
フォームの部品一つ一つがオプションという単位に分割されており、使いまわせる。
フォームキーの管理?必要ありません。
AwaitFormOptionsのフォーム要素の最小単位はオプションであり、各オプションにはそれぞれ処理を組み込めます。
言葉で言ってもわかりずらいので、例を見てみましょう
まず、Await::f2c(function() use ($player){でAwaitGeneratorの並行処理に突入し、
sendFormAsyncという処理をyield fromして応答を待機します。
エラーは例外という形で受け取ります。
オプションクラスは、getOptions、userDisposeで構成されるシンプルな処理であり、 getOptions()でジェネレーターを返せばそのオプションが実行され、yield from $this->request()で指定したフォームがgetOptionsで渡した順番上通りにフォームに追加されます。
各オプションは、個別に指定したオプションの応答をyield fromから受け取り、必要に応じてreturnすることで親に値を返すことができます。
エラーが発生した場合は、親ジェネレーターも子ジェネレーターももれなく例外を受け取ります。もうぬるぽにおびえる必要はないぞ
で、このオプションクラス、複数の親フォームで使いまわせます。既存のフォームは、使いまわせる最小単位がフォーム単位でしたか、AwaitFormOptionsではこれを「オプション」という単位に分割化することとしました。
特徴2
フォームのキーは重複セーフ
フォームを使いまわすときに、同じキーが重複することに怯えてる?心配ありません。
AwaitFormOptionsはキー重複セーフです。
例えばこんなわけわからんフォームでも、AwaitFormOptionsは正しく処理します
特徴3
オプションは、値を返すことができる
例を見てみましょう
親オプションの配列でキーを指定すると、その通りにマッピングされた返り値を受け取ります。
例外が発生したときは、返り値は受け取れません。
子ジェネレーターはyield from $this->requestから応答を受け取りますが、それを自由に加工してreturnすることができます。今回は配列の最初を返します。
すると、ジェネレーターからのすべてのreturnが収集され、親ジェネレーターはoptionsとgetOptionsで指定した通りのキーを持つ配列を受け取ります。
特徴4
フォームはネスト可能
1階層までに制限されてますが、getOptionsはフォームオプションを指定しても大丈夫です。
この場合、returnは親が受け取ります。
なんでもありですね
特徴6
便利なシステム関数
フォームを送信する前に非同期のデータベースクエリをしたい場合はどうすればいいですか?
他のフォームはこれはできず、クロージャー地獄に頼る必要があります。
AwaitFormOptions?心配ありません。$this->schedule();がここにあります!
子ジェネレーターで$this->schedule();を呼ぶと、ご丁寧に予約した子ジェネレーターが$this->requestするまでフォームの送信を延期してくれます。
フォームの結果をフラットなオブジェクト指向ですべて収集して、最終処理を子ジェネレーターでしたい場合どうすればいいですか?
AwaitFormOptionsでは心配ありません。yield from $this->finalize(10000);があります!
yield from $this->finalize(0)を実行すると、すべての子ジェネレーターに結果がいきわたり、処理が終了した後にコルーチンが再開されます!
特徴7
条件に応じてメニューのボタンや、オプションが消えるフォーム?
心配ありません。
getOptionsで条件分岐するだけです。
フォームで利用可能なエレメント
メニューで利用可能なエレメント
特徴8
複雑なフォームシーケンス?怯える必要はありません。
エラーがあれば例外で中断するため、応答がnullであるかどうかを確認する必要は一切ありません
GithubのReadmeで詳しく解説してるので、よかったら見てね
早速ですが、既存のフォームライブラリは、フォームシーケンスが作りづらくて仕方ない!
なので、AwaitGeneratorとAwaitFormを悪魔合体したフォームライブラリ書きました。
詳しくはGithubに行ってReadmeを見てほしいですが、ここでは特徴を述べます。
ただ、書いた私も悩むぐらい難しいので、pmmp中級者以上の方向けです。
特徴1
フォームの部品一つ一つがオプションという単位に分割されており、使いまわせる。
フォームキーの管理?必要ありません。
AwaitFormOptionsのフォーム要素の最小単位はオプションであり、各オプションにはそれぞれ処理を組み込めます。
言葉で言ってもわかりずらいので、例を見てみましょう
PHP:
use pocketmine\plugin\PluginBase;
use pocketmine\event\Listener;
use pocketmine\event\player\PlayerItemUseEvent;
use SOFe\AwaitGenerator\Await;
use DaisukeDaisuke\AwaitFormOptions\AwaitFormOptions;
use DaisukeDaisuke\AwaitFormOptions\exception\AwaitFormOptionsParentException;
use pocketmine\form\FormValidationException;
use cosmicpe\awaitform\AwaitForm;
//plugin関連をここに挿入
public function a(PlayerItemUseEvent $event) : void{
$player = $event->getPlayer();
Await::f2c(function() use ($player){
try{
yield from AwaitFormOptions::sendFormAsync(
player: $player,
title: "test",
options: [
new HPFormOptions($player),
]
);
}catch(FormValidationException|AwaitFormOptionsParentException){
// Form failed validation
}
});
}
sendFormAsyncという処理をyield fromして応答を待機します。
エラーは例外という形で受け取ります。
PHP:
<?php
declare(strict_types=1);
namespace test\test;
use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;
use pocketmine\player\Player;
use DaisukeDaisuke\AwaitFormOptions\exception\AwaitFormOptionsChildException;
class HPFormOptions extends FormOptions{
public function __construct(private Player $player){
}
public function maxHP() : \Generator{
try{
$form = [
FormControl::input("Max HP:", "20", (string) $this->player->getMaxHealth()),
];
[$maxHP] = yield from $this->request($form); // awaiting response
$this->player->setMaxHealth((int) $maxHP);
$this->player->sendMessage("Max HP: {$maxHP}");
}catch(AwaitFormOptionsChildException $e){
var_dump($e->getCode());
}
}
public function currentHP() : \Generator{
try{
$form = [
FormControl::input("Current HP:", "20", (string) $this->player->getHealth()),
];
[$currentHP] = yield from $this->request($form); // awaiting response
$this->player->setHealth((float) $currentHP);
$this->player->sendMessage("Current HP: {$currentHP}");
}catch(AwaitFormOptionsChildException $e){
var_dump($e->getCode());
}
}
public function getOptions() : array{
return [
$this->maxHP(),
$this->currentHP(),
];
}
public function userDispose() : void{
unset($this->player);
}
}
各オプションは、個別に指定したオプションの応答をyield fromから受け取り、必要に応じてreturnすることで親に値を返すことができます。
エラーが発生した場合は、親ジェネレーターも子ジェネレーターももれなく例外を受け取ります。もうぬるぽにおびえる必要はないぞ
で、このオプションクラス、複数の親フォームで使いまわせます。既存のフォームは、使いまわせる最小単位がフォーム単位でしたか、AwaitFormOptionsではこれを「オプション」という単位に分割化することとしました。
特徴2
フォームのキーは重複セーフ
フォームを使いまわすときに、同じキーが重複することに怯えてる?心配ありません。
AwaitFormOptionsはキー重複セーフです。
例えばこんなわけわからんフォームでも、AwaitFormOptionsは正しく処理します
PHP:
public function a(PlayerItemUseEvent $event): void {
$player = $event->getPlayer();
Await::f2c(function () use ($player) : \Generator{
try {
yield from AwaitFormOptions::sendMenuAsync(
player: $player,
title: "test",
content: "a",
buttons: [
new NameMenuOptions($player, ["a", "b"]),
new NameMenuOptions($player, ["c", "d"]),
new NameMenuOptions($player, ["e", "f"]),
new NameMenuOptions($player, ["g", "h"]),
new NameMenuOptions($player, ["i", "j"]),
]
);
} catch (FormValidationException) {
}
});
}
オプションは、値を返すことができる
例を見てみましょう
親オプションの配列でキーを指定すると、その通りにマッピングされた返り値を受け取ります。
例外が発生したときは、返り値は受け取れません。
PHP:
public function onUse(PlayerItemUseEvent $event): void{
$player = $event->getPlayer();
if(!$player->isSneaking()){
return;
}
Await::f2c(function() use ($player) {
try {
$selected = yield from AwaitFormOptions::sendFormAsync(
player: $player,
title: "test",
options: [
"input1" => new SimpleInput("test1", "test", "test", 0),
"input2" => new SimpleInput("test2", "test2", "test2", 0),
]
);
var_dump($selected);
} catch (FormValidationException|AwaitFormOptionsParentException) {
// The form was cancelled or failed
}
});
}
PHP:
<?php
declare(strict_types=1);
namespace test\test;
use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;
class SimpleInput extends FormOptions{
public function __construct(private string $text, private string $default, private string $placeholder, private int $id){
}
/**
* @throws AwaitFormOptionsChildException
*/
public function input(int $offset) : \Generator{
$output = yield from $this->request([FormControl::input($this->text, $this->default, $this->placeholder), $this->id + $offset]);
return $output[array_key_first($output)];
}
/**
* @throws AwaitFormOptionsChildException
*/
public function getOptions() : array{
return [
$this->input(0),
$this->input(1),
];
}
public function userDispose() : void{
unset($this->text, $this->default, $this->placeholder, $this->id);
}
}
Code:
array(2) {
["input1"]=>
array(2) {
[0]=>
string(4) "test"
[1]=>
string(4) "test"
}
["input2"]=>
array(2) {
[0]=>
string(5) "test2"
[1]=>
string(5) "test2"
}
}
フォームはネスト可能
1階層までに制限されてますが、getOptionsはフォームオプションを指定しても大丈夫です。
この場合、returnは親が受け取ります。
なんでもありですね
PHP:
public function onUse(PlayerItemUseEvent $event) : void{
$player = $event->getPlayer();
if(!$player->isSneaking()){
return;
}
Await::f2c(function() use ($player){
while(true){
try{
$result = yield from AwaitFormOptions::sendFormAsync(
player: $player,
title: "Confirmation",
options: ["output" => new ConfirmInputForm()]
);
var_dump($result);
//generator returns
$typed = $result["output"]["confirm"];
if(strtolower(trim($typed)) === "yes"){
$player->sendToastNotification("Confirmed", "Thanks for typing!");
break;
}
}catch(AwaitFormOptionsParentException $exception){
if($exception->getCode() !== AwaitFormException::ERR_PLAYER_REJECTED){
break;
}
}
$player->sendToastNotification("You must type 'yes'.", "please Type 'Yes'");
}
});
}
PHP:
<?php
declare(strict_types=1);
namespace test\test;
use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;
use DaisukeDaisuke\AwaitFormOptions\exception\AwaitFormOptionsChildException;
class ConfirmInputForm extends FormOptions{
/**
* @throws AwaitFormOptionsChildException
*/
public function confirmOnce(): \Generator {
[$input] = yield from $this->request([
FormControl::input("Type 'yes' to confirm", "yes", ""),
]);
return $input;
}
/**
* @throws AwaitFormOptionsChildException
*/
public function getOptions(): array {
return [
"entity" => new SimpleInput("nested!", "nested", "nested", 0),
"confirm" => $this->confirmOnce(),
];
}
public function userDispose() : void{
}
}
Code:
array(1) {
["output"]=>
array(2) {
["entity"]=>
array(2) {
["a"]=>
string(6) "a"
[0]=>
string(6) "nested"
}
["confirm"]=>
string(3) "yes"
}
}
便利なシステム関数
フォームを送信する前に非同期のデータベースクエリをしたい場合はどうすればいいですか?
他のフォームはこれはできず、クロージャー地獄に頼る必要があります。
AwaitFormOptions?心配ありません。$this->schedule();がここにあります!
子ジェネレーターで$this->schedule();を呼ぶと、ご丁寧に予約した子ジェネレーターが$this->requestするまでフォームの送信を延期してくれます。
PHP:
<?php
namespace daisukedaisuke\test;
use cosmicpe\awaitform\Button;
use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use SOFe\AwaitGenerator\RaceLostException;
class HpBasedFoodOptions extends MenuOptions{
/**
* @throws AwaitFormOptionsChildException
*/
public function giveRawFish1() : \Generator{
$this->schedule(); // This ensures that the awaitformoptions coroutine is temporarily suspended
//A few awaits
try{
yield from $this->request([Button::simple("a"), 0]); // Here, the suspension is lifted
}catch(RaceLostException){
var_dump("??");
}
}
/**
* @throws AwaitFormOptionsChildException
*/
public function getOptions() : array{
return [
$this->giveRawFish1(),
];
}
public function userDispose() : void{
}
}
フォームの結果をフラットなオブジェクト指向ですべて収集して、最終処理を子ジェネレーターでしたい場合どうすればいいですか?
AwaitFormOptionsでは心配ありません。yield from $this->finalize(10000);があります!
PHP:
<?php
namespace test\test;
use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;
use DaisukeDaisuke\AwaitFormOptions\exception\AwaitFormOptionsChildException;
class ConfirmInputForm extends FormOptions{
/**
* @throws AwaitFormOptionsChildException
*/
public function confirmOnce() : \Generator{
[$input] = yield from $this->request([
FormControl::input("Type 'yes' to confirm", "yes", ""),
]);
yield from $this->finalize(10000);//Awaiting other generators with priority 10000
return $input;
}
/**
* @throws AwaitFormOptionsChildException
*/
public function getOptions() : array{
return [
"entity" => new SimpleInput("nested!", "nested", "nested", 0),
"confirm" => $this->confirmOnce(),
];
}
public function userDispose() : void{
}
}
特徴7
条件に応じてメニューのボタンや、オプションが消えるフォーム?
心配ありません。
getOptionsで条件分岐するだけです。
PHP:
<?php
declare(strict_types=1);
namespace test\test;
use pocketmine\player\Player;
use pocketmine\item\VanillaItems;
use cosmicpe\awaitform\Button;
use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use DaisukeDaisuke\AwaitFormOptions\exception\AwaitFormOptionsChildException;
class HpBasedFoodOptions extends MenuOptions{
public function __construct(private Player $player){
}
/**
* @throws AwaitFormOptionsChildException
*/
public function giveRawFish() : \Generator{
yield from $this->request([
Button::simple("§2You are full of strength! Enjoy this raw fish.§r"),
]);
$this->player->getInventory()->addItem(VanillaItems::RAW_FISH()->setCount(1));
$this->player->sendToastNotification("Food Given", "Raw Fish");
}
/**
* @throws AwaitFormOptionsChildException
*/
public function giveCookedFish() : \Generator{
yield from $this->request([
Button::simple("§6You're moderately hurt. Take this cooked fish.§r"),
]);
$this->player->getInventory()->addItem(VanillaItems::COOKED_FISH()->setCount(1));
$this->player->sendToastNotification("Food Given", "Cooked Fish");
}
/**
* @throws AwaitFormOptionsChildException
*/
public function giveSteak() : \Generator{
yield from $this->request([
Button::simple("§4You're starving! Here's a juicy steak.§r"),
]);
$this->player->getInventory()->addItem(VanillaItems::STEAK()->setCount(1));
$this->player->sendToastNotification("Food Given", "Steak");
}
/**
* @throws AwaitFormOptionsChildException
*/
public function getOptions() : array{
$hp = $this->player->getHealth();
$result = [];
if($hp <= 20){
$result[] = $this->giveRawFish();
}
if($hp <= 10){
$result[] = $this->giveCookedFish();
}
if($hp <= 5){
$result[] = $this->giveSteak();
}
return $result;
}
public function userDispose() : void{
unset($this->player);
}
}
PHP:
FormControl::divider() // Adds a horizontal divider to visually separate form sections.
FormControl::dropdown(string $label, array $options, ?string $default = null) // Select from a list of options, returns the selected value.
FormControl::dropdownIndex(string $label, array $options, int $default = 0) // Select from a list of options, returns the selected index.
FormControl::dropdownMap(string $label, array $options, array $mapping, mixed $default = null) // Select from a list of options, returns a mapped value.
FormControl::header(string $label) // Adds a bold header text to highlight sections.
FormControl::input(string $label, string $placeholder = "", string $default = "") // Text input field. Returns user input as a string.
FormControl::label(string $label) // Static text label, for descriptions or instructions.
FormControl::slider(string $label, float $min, float $max, float $step = 0.0, float $default = 0.0) // A numeric slider. Returns a float value.
FormControl::stepSlider(string $label, array $steps, ?string $default = null) // A discrete slider with string options. Returns a selected step.
FormControl::toggle(string $label, bool $default = false) // A boolean toggle (checkbox). Returns true/false.
メニューで利用可能なエレメント
PHP:
Button::simple(string $text) // One user selectable button with text
複雑なフォームシーケンス?怯える必要はありません。
エラーがあれば例外で中断するため、応答がnullであるかどうかを確認する必要は一切ありません
PHP:
Await::f2c(static function() use($array, $owner){
try{
$array[$owner->getId()] = $owner;
[$array] = yield from AwaitFormOptions::sendFormAsync(
player: $owner,
title: "デバック棒",
options: [
new PlayerOREntitySelectionOptions($array),
],
);
/** @var ?Entity $entity */
$entity = $array["Entity"];
$name = $entity instanceof Player ? $entity->getName() : substr(strrchr($entity::class, "\\"), 1);
$menus = [
new DebugKillFrom(),
//mobをぶっ飛ばすやつ
//ゲームから強制退室
//ゲーム参加からタイムアウト
];
$result = [];
foreach($menus as $menu){
$result[] = new SimpleMappedButtonOptions($menu->getExplanation(), $menu);
}
/** @var DebugStickMenuInterface|FormOptions $form */
$form = yield from AwaitFormOptions::sendMenuAsync(
player: $owner,
title: $name . "さんに対してあんなことやこんなことができます!",
content: "",
buttons: $result
);
if(!$entity->isAlive() || ($entity instanceof Player && !$entity->isOnline())){
if($owner->isOnline()){
$owner->sendToastNotification("おーっと、プレーヤーがオフラインになってしまったようです。", $name);
}
}
$form->setTarget($entity);
yield from AwaitFormOptions::sendFormAsync(
player: $owner,
title: $name,
options: [$form],
);
}catch(FormValidationException | AwaitFormOptionsParentException){
return;
}
});
GithubのReadmeで詳しく解説してるので、よかったら見てね