daisukedaisuke/awaitformoptions

v1.0.4 2025-06-30 05:58 UTC

This package is not auto-updated.

Last update: 2025-06-30 09:10:24 UTC


README

Overview

An option-driven form handler framework built on AwaitForm for pmmp plugins.
Designed to modularize complex user interactions and support clean, reusable, async code.

Requirements

Why?

Using AwaitForm directly is simple for small forms:

public function a(PlayerItemUseEvent $event): void {
	$player = $event->getPlayer();
	try {
		await::f2c(function() use ($player) {
			$form = AwaitForm::form("form", [
				FormControl::input("Current HP:", "20", (string) $player->getHealth()),
				FormControl::input("Max HP:", "20", (string) $player->getMaxHealth()),
			]);
			[$current, $max] = yield from $form->request($player);
			$player->setHealth((float) $current);
			$player->setMaxHealth((int) $max);
			$player->sendMessage("HP: {$current}/{$max}");
		});
	} catch (AwaitFormException | FormValidationException) {
		// Cancelled or invalid input
	}
}

But when handling multiple related form steps in one screen, things get messy fast.
Too many responsibilities are packed into one place.

See demo
m1.mp4

Solution: AwaitFormOptions

Split your form logic into reusable, testable option classes:

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),
                ],
                neverRejects: false, // If false, the awaitFormOption propagates the AwaitFormException to the generator.
                throwExceptionInCaller: false, // If true, awaitFormOption will throw an exception on the caller
            );
        }catch(FormValidationException){
            // Form failed validation
        }
    });
}

Example Option Class

Each option will yield from $this->request($form); and wait for the response. No more losing context!

<?php

namespace daisukedaisuke\test;

use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;
use pocketmine\player\Player;
use cosmicpe\awaitform\AwaitFormException;

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 (AwaitFormException $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 (AwaitFormException $e) {
			var_dump($e->getCode());
		}
	}

	public function getOptions(): array {
		return [
			$this->maxHP(),
			$this->currentHP(),
		];
	}
}

Image

Reusability

Yes, option classes are reusable!
Try passing the same class multiple times:

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),
                    new HPFormOptions($player),
                    new HPFormOptions($player),
                    new HPFormOptions($player),
                    new HPFormOptions($player),
                    new HPFormOptions($player),
                ],
                neverRejects: false,
                throwExceptionInCaller: true,
            );
        } catch (FormValidationException|AwaitFormException) {
        }
    });
}

Image

Each instance is handled independently.

neverRejects and throwExceptionInCaller

If neverRejects is false, the child generator must handle the AwaitFormException

If throwExceptionInCaller is true, the parent generator will receive an AwaitFormException

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),
                ],
                neverRejects: false, // If false, the awaitFormOption propagates the AwaitFormException to the generator.
                throwExceptionInCaller: true, // If true, awaitFormOption will throw an exception on the caller
            );
        }catch(FormValidationException|AwaitFormException){
            // Form failed validation
        }
    });
}

Standalone

sendForm and sendMenu can also be called completely standalone, without receiving exceptions

public function a(PlayerItemUseEvent $event): void {
    $player = $event->getPlayer();
    AwaitFormOptions::sendForm(
        player: $player,
        title: "test",
        options: [
            new HPFormOptions($player),
        ],
        neverRejects: true,
    );
}

Menu Support

AwaitFormOptions also supports menu interactions.
Unselected menu options are discarded and not executed.

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, ["f", "a"]),
                ],
                neverRejects: false,
                throwExceptionInCaller: false,
            );
        }catch(FormValidationException){

        }
    });
}

Example: MenuOptions

Even if multiple buttons share the same label or value, AwaitFormOptions resolves conflicts automatically.

<?php

namespace daisukedaisuke\test;

use cosmicpe\awaitform\Button;
use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use pocketmine\player\Player;
use cosmicpe\awaitform\AwaitFormException;

class NameMenuOptions extends MenuOptions{
	public function __construct(private Player $player, private array $options){
	}

	public function optionsA() : \Generator{
		try{
			$test = [];
			foreach($this->options as $item){
				$test[$item] = Button::simple($item);
			}
			$test = yield from $this->request($test);
			$this->player->sendMessage($test.", ".__FUNCTION__);
		}catch(AwaitFormException $exception){

		}
	}

	public function optionsB() : \Generator{
		try{
			$test = yield from $this->request([
				[Button::simple("a"), "a"], //Even if you use duplicate keys, Awaitformoption will resolve it
			]);
			$this->player->sendMessage($test.", ".__FUNCTION__);
		}catch(AwaitFormException $exception){

		}
	}

	public function getOptions() : array{
		return [
			$this->optionsB(),
			$this->optionsA(),
		];
	}
}

Reusing Menu Options

Just like form options, menu options can be reused as well:

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"]),
                ],
                neverRejects: false,
                throwExceptionInCaller: false
            );
        } catch (FormValidationException) {
        }
    });
}

Image

🧩 Menu Advanced Usage: Attaching Objects to Buttons

Normally, Button::simple("label") returns a Button that maps to a string value. But what if you want to associate a more complex object, like a Player, Entity, or CustomData — with each button?

You can do this easily by passing [Button::simple(...), $value] into the menu array.

$selected  = yield from $this->request([
    [Button::simple("Label A"), $someObject],
    [Button::simple("Label B"), "custom-id"],
    [Button::simple("Label C"), 123],
]);

In this format:

  • The first element is always a Button object.
  • The second element is the value that will be returned if the button is selected.
  • The returned result is mapped correctly even for duplicate labels or repeated values.
  • You can use any scalar or object, including players, entities, and custom classes.

Example

public function onUse(PlayerItemUseEvent $event): void{
    $player = $event->getPlayer();
    if(!$player->isSneaking()){
        return;
    }
    Await::f2c(function() use ($player) {
        try {

            $entities = [];
            $world = $player->getWorld();
            foreach($world->getEntities() as $entity){
                if(!$entity instanceof Living){
                    continue;
                }
                $entities[] = $entity;
            }

            yield from AwaitFormOptions::sendMenuAsync(
                player: $player,
                title: "Food Assistance",
                content: "Please select an option",
                buttons: [
                    new EntityNameMenuOptions($player, $entities),
                ],
                neverRejects: true,
                throwExceptionInCaller: false
            );
        } catch (FormValidationException) {
            // The form was cancelled or failed
        }
    });
}

EntityNameMenuOptions.php

<?php

namespace daisukedaisuke\test;

use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use pocketmine\player\Player;
use cosmicpe\awaitform\Button;
use pocketmine\entity\Entity;
use cosmicpe\awaitform\AwaitFormException;

class EntityNameMenuOptions extends MenuOptions {
	public function __construct(private Player $player, private array $entities) {}

	public function chooseEntity(): \Generator {
		try {
			$buttons = [];

			foreach ($this->entities as $entity) {
				// Display name, attach Entity instance
				$buttons[] = [Button::simple($entity->getName()), $entity];
			}

			/** @var Entity $selected */
			$selected = yield from $this->request($buttons);

			$this->player->sendMessage("You chose: " . $selected->getName());
			return $selected;
		} catch (AwaitFormException) {
			// Closed
		}
	}

	public function getOptions(): array {
		return [$this->chooseEntity()];
	}
}

Generator Return Values Are Captured

Each generator that you define in your FormOptions or MenuOptions class can return a value using the return statement. When the form is submitted, all return values from each generator are automatically collected into an array and returned from AwaitFormOptions::sendFormAsync() or sendMenuAsync().

This allows you to treat each form step as a small function that produces a result, just like any other callable.

Menu Example

Here, the selected button id is returned directly from the generator:

Note

Please Note that if the form fails, any return values from the child generators will be ignored and null will be returned

public function onUse(PlayerItemUseEvent $event): void{
    $player = $event->getPlayer();
    if(!$player->isSneaking()){
        return;
    }
    Await::f2c(function() use ($player) {
        try {
            $selected = yield from AwaitFormOptions::sendMenuAsync(
                player: $player,
                title: "Food Assistance",
                content: "Please select an option",
                buttons: [
                    new SimpleButton("test1", 0),
                    new SimpleButton("test2", 2),
                ],
                neverRejects: true,
                throwExceptionInCaller: false
            );
            var_dump($selected);
        } catch (FormValidationException) {
            // The form was cancelled or failed
        }
    });
}

SimpleButton

<?php

namespace daisukedaisuke\test;

use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use cosmicpe\awaitform\Button;
use cosmicpe\awaitform\AwaitFormException;

class SimpleButton extends MenuOptions{
	public function __construct(private string $name, private int $id){
	}

	public function choose(int $offset) : \Generator{
		try{
			yield from $this->request(
				[Button::simple($this->name), 0]
			);
			return $this->id + $offset;
		}catch(AwaitFormException){
			// Closed
		}
	}

	public function getOptions() : array{
		return [
			$this->choose(0),
			$this->choose(1),
		];
	}
}

result

Any of the following

int(0)
int(1)
int(2)
int(3)
NULL

Form Example

Forms can retrieve the return value of a generator in the same way, note that in this case it maps to the keys of the option array.

Note

Note that when $neverRejects is true, child generator processing is forcefully terminated, so an empty array is returned if an error occurs in the form
sendFormAsync will collect all generator return values even if the form fails as long as neverRejects is false. Note that this is different behavior from menu.

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),
                ],
                neverRejects: true,
                throwExceptionInCaller: false
            );
            var_dump($selected);
        } catch (FormValidationException) {
            // The form was cancelled or failed
        }
    });
}

SimpleInput.php

<?php

namespace daisukedaisuke\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){
	}

	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)];
	}

	public function getOptions() : array{
		return [
			$this->input(0),
			$this->input(1),
		];
	}
}

result

array(2) {
  ["input1"]=>
  array(2) {
    [0]=>
    string(4) "test"
    [1]=>
    string(4) "test"
  }
  ["input2"]=>
  array(2) {
    [0]=>
    string(5) "test2"
    [1]=>
    string(5) "test2"
  }
}

Example

🐲 MobKillerOptions (Entity Interaction via Menu)

AwaitFormOptions can be used for more than just player configuration, it also allows you to handle dynamic entities such as mobs or NPCs using menu interactions. Here is a concrete example that lets a player select entities in their current world and kill them via a menu.

Image

public function onUse(PlayerItemUseEvent $event): void{
    $player = $event->getPlayer();
    $world = $player->getWorld();

    if(!$player->isSneaking()){
        return;
    }

    $forms = [];
    foreach($world->getEntities() as $entity){
        if($entity === $player || !$entity instanceof Living){
            continue;
        }
        $forms[] = new MobKillerForm($entity);
    }

    Await::f2c(function() use ($player, $forms) : \Generator{
        yield from AwaitFormOptions::sendMenuAsync(
            player: $player,
            title: "Mob Terminator",
            content: "Choose a mob to eliminate:",
            buttons: $forms,
            neverRejects: true,
            throwExceptionInCaller: false
        );
    });
}

🧪 Option Class Example: MobKillerForm

<?php

namespace daisukedaisuke\test;

use DaisukeDaisuke\AwaitFormOptions\MenuOptions;
use pocketmine\entity\Entity;
use cosmicpe\awaitform\Button;

class MobKillerForm extends MenuOptions{

	public function __construct(private readonly Entity $entity){
	}

	public function KillerForm() : \Generator{
		yield from $this->request([
			[Button::simple($this->entity->getName() . " (" . $this->entity->getId() . ")"), "a"],
		]);
		$this->entity->kill();
	}

	public function getOptions() : array{
		return [
			$this->KillerForm(),
		];
	}
}

Non-Cancellable Form (Forced Confirmation)

Sometimes, you want to prevent players from skipping or cancelling a form unless they acknowledge a specific phrase or condition — such as typing "yes". With AwaitFormOptions, this can be done cleanly by combining input validation and throwExceptionInCaller: true.

Usage

Image

	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()],
						neverRejects: true,
						throwExceptionInCaller: true
					);
					//generator returns
					$typed = $result["output"][0];
					if(strtolower(trim($typed)) === "yes"){
						$player->sendToastNotification("Confirmed", "Thanks for typing!");
						break;
					}

				}catch(AwaitFormException $exception){
					if($exception->getCode() !== AwaitFormException::ERR_PLAYER_REJECTED){
						break;
					}
				}
				$player->sendToastNotification("You must type 'yes'.", "please Type 'Yes'");
			}
		});
	}

🧪 Option Class: ConfirmInputForm

<?php

namespace daisukedaisuke\test;

use DaisukeDaisuke\AwaitFormOptions\FormOptions;
use cosmicpe\awaitform\FormControl;

class ConfirmInputForm extends FormOptions{
	public function confirmOnce(): \Generator {
		[$input] = yield from $this->request([
			FormControl::input("Type 'yes' to confirm", "yes", ""),
		]);
		return $input;
	}

	public function getOptions(): array {
		return [$this->confirmOnce()];
	}
}

🍖 HP-Dependent Form Options (Dynamic Option Filtering)

You can conditionally include different form options by selecting which yield generators are returned from getOptions(), this is a key strength of AwaitFormOptions over flat form construction.

Image

HpBasedFoodOptions.php

<?php

namespace daisukedaisuke\test;

use pocketmine\player\Player;
use pocketmine\item\VanillaItems;
use cosmicpe\awaitform\FormControl;
use cosmicpe\awaitform\Button;
use DaisukeDaisuke\AwaitFormOptions\MenuOptions;

class HpBasedFoodOptions extends MenuOptions{

	public function __construct(private readonly Player $player){
	}

	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");
	}

	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");
	}

	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");
	}

	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;
	}
}

Usage

public function onUse(PlayerItemUseEvent $event): void{
    $player = $event->getPlayer();
    if(!$player->isSneaking()){
        return;
    }
    Await::f2c(function() use ($player) {
        try {
            yield from AwaitFormOptions::sendMenuAsync(
                player: $player,
                title: "Food Assistance",
                content: "Please select an option",
                buttons: [
                    new HpBasedFoodOptions($player),
                ],
                neverRejects: true,
                throwExceptionInCaller: true
            );
        } catch (FormValidationException|AwaitFormException) {
            // The form was cancelled or failed
        }
    });

}

Summary

✅ Modular
✅ Async
✅ Clean separation of form logic
✅ Handles multiple options, cancellations, and reuse easily

Build dynamic, plugin-ready forms with AwaitFormOptions.