Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forms API #1476

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
96cba44
Some base work for forms API
dktapps Oct 1, 2017
9f963dd
Implemented ListForm, refactored some stuff
dktapps Oct 2, 2017
8ce7cb0
fix inspections
dktapps Oct 2, 2017
8f40482
Change namespace to 'form' because @SOF3 is fussy
dktapps Oct 2, 2017
383cb7c
Move "title" property up to `Form` base class
dktapps Oct 6, 2017
d0690b8
ModalForm: fix typo
dktapps Oct 6, 2017
fa7eadb
Added some documentation
dktapps Oct 6, 2017
ecd51b2
Some changes to how response data is handled to make it consistent ac…
dktapps Oct 6, 2017
ca107a6
Refactor ListForm -> MenuForm and Button -> MenuOption
dktapps Oct 6, 2017
5b84b43
Added CustomForm and its components
dktapps Oct 6, 2017
a7df2ca
Added ServerSettingsForm class, refactor icon handling
dktapps Oct 6, 2017
4877f96
fix some issues
dktapps Oct 18, 2017
a0a9eb9
don't allow re-using form objects
dktapps Oct 21, 2017
04965a1
refactor some confusing shit
dktapps Oct 21, 2017
17ad96f
change some exception messages to be more clear
dktapps Oct 21, 2017
66b854d
Implemented form queuing
SOF3 Oct 23, 2017
f87b23b
Changed the argument order of some constructors
SOF3 Nov 11, 2017
9c72600
Merge branch 'master' into forms-api
dktapps Nov 12, 2017
cd5667c
fixed returning forms
dktapps Nov 12, 2017
e4c409a
Fixed client barfing on forms when assoc arrays are used
dktapps Nov 22, 2017
845c3fc
Merge branch 'master' into forms-api
dktapps Nov 22, 2017
3b6ac5f
add constant visibility
dktapps Nov 22, 2017
a449406
Merge branch 'master' into forms-api
dktapps Dec 7, 2017
6d5e8ad
Change hasBeenQueued() to isInUse()
dktapps Dec 7, 2017
7a916fd
Fixed returning $this in onSubmit() or onClose()
dktapps Dec 7, 2017
1ab98db
add some phpdoc
dktapps Dec 7, 2017
5f8599a
Revert "Fixed returning $this in onSubmit() or onClose()"
dktapps Dec 7, 2017
a835179
Fixed returning already-used forms, rev. 2
dktapps Dec 7, 2017
5a76b34
Beware exception throws messing up the queue
dktapps Dec 7, 2017
dfdafd2
Don't block the player, it didn't do anything wrong
dktapps Dec 7, 2017
b0f00b0
Don't break the form queue when DataPacketSendEvent is cancelled
dktapps Dec 7, 2017
402acf5
Added debug for form ID mismatch
dktapps Dec 7, 2017
eadc44d
Merge branch 'master' into forms-api
dktapps Feb 19, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions src/pocketmine/Player.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
use pocketmine\event\player\PlayerTransferEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\event\Timings;
use pocketmine\form\Form;
use pocketmine\inventory\BigCraftingGrid;
use pocketmine\inventory\CraftingGrid;
use pocketmine\inventory\Inventory;
Expand Down Expand Up @@ -115,6 +116,7 @@
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
Expand Down Expand Up @@ -297,6 +299,15 @@ public static function isValidUserName(?string $name) : bool{
*/
protected $lastPingMeasure = 1;

/** @var int */
protected $formIdCounter = 0;
/** @var int|null */
protected $sentFormId = null;
/** @var Form|null */
protected $sentForm = null;
/** @var Form[] */
protected $formQueue = [];

/**
* @return TranslationContainer|string
*/
Expand Down Expand Up @@ -3229,6 +3240,78 @@ public function sendWhisper(string $sender, string $message){
$this->dataPacket($pk);
}

/**
* Sends a Form to the player, or queue to send it if a form is already open.
*
* @param Form $form
* @param bool $prepend if true, the form will be sent immediately after the current form is closed (if any), before other queued forms.
*/
public function sendForm(Form $form, bool $prepend = false) : void{
$form->setInUse();

if($this->sentForm !== null){
if($prepend){
array_unshift($this->formQueue, $form);
}else{
$this->formQueue[] = $form;
}
return;
}

$this->sendFormRequestPacket($form);
}

/**
* @param int $formId
* @param mixed $responseData
*
* @return bool
*/
public function onFormSubmit(int $formId, $responseData) : bool{
if($formId !== $this->sentFormId){
$this->server->getLogger()->debug("Got unexpected response for form $formId, but waiting for response for $this->sentFormId");
return false;
}

$form = null;

try{
$form = $this->sentForm->handleResponse($this, $responseData);
}catch(\Throwable $e){
$this->server->getLogger()->logException($e);
}

$this->sentFormId = null;
$this->sentForm = null;

try{
if($form !== null){
$form->setInUse(); //forms in the queue will already be marked as "in use", we only need to check here
}else{
$form = array_shift($this->formQueue);
}

if($form !== null){
$this->sendFormRequestPacket($form);
}
}catch(\Throwable $e){
$this->server->getLogger()->logException($e);
}

return true;
}

private function sendFormRequestPacket(Form $form) : void{
$id = $this->formIdCounter++;
$pk = new ModalFormRequestPacket();
$pk->formId = $id;
$pk->formData = json_encode($form);
if($this->dataPacket($pk)){
$this->sentFormId = $id;
$this->sentForm = $form;
}
}

/**
* Note for plugin developers: use kick() with the isAdmin
* flag set to kick without the "Kicked by admin" part instead of this method.
Expand Down
105 changes: 105 additions & 0 deletions src/pocketmine/form/CustomForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/

declare(strict_types=1);

namespace pocketmine\form;

use pocketmine\form\element\CustomFormElement;
use pocketmine\Player;
use pocketmine\utils\Utils;

abstract class CustomForm extends Form{

/** @var CustomFormElement[] */
private $elements;

/**
* @param string $title
* @param CustomFormElement[] $elements
*/
public function __construct(string $title, array $elements){
assert(Utils::validateObjectArray($elements, CustomFormElement::class));

parent::__construct($title);
$this->elements = array_values($elements);
}

/**
* @return string
*/
public function getType() : string{
return Form::TYPE_CUSTOM_FORM;
}

/**
* @param int $index
*
* @return CustomFormElement|null
*/
public function getElement(int $index) : ?CustomFormElement{
return $this->elements[$index] ?? null;
}

/**
* @return CustomFormElement[]
*/
public function getAllElements() : array{
return $this->elements;
}

public function onSubmit(Player $player) : ?Form{
return null;
}

/**
* Called when a player closes the form without submitting it.
* @param Player $player
* @return Form|null a form which will be opened immediately (before queued forms) as a response to this form, or null if not applicable.
*/
public function onClose(Player $player) : ?Form{
return null;
}


public function handleResponse(Player $player, $data) : ?Form{
if($data === null){
return $this->onClose($player);
}

if(is_array($data)){
/** @var array $data */
foreach($data as $index => $value){
$this->elements[$index]->setValue($value);
}

return $this->onSubmit($player);
}

throw new \UnexpectedValueException("Expected array or NULL, got " . gettype($data));
}

public function serializeFormData() : array{
return [
"content" => $this->elements
];
}
}
133 changes: 133 additions & 0 deletions src/pocketmine/form/Form.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/

declare(strict_types=1);

/**
* API for Minecraft: Bedrock custom UI (forms)
*/
namespace pocketmine\form;

use pocketmine\Player;

/**
* Base class for a custom form. Forms are serialized to JSON data to be sent to clients.
*/
abstract class Form implements \JsonSerializable{

public const TYPE_MODAL = "modal";
public const TYPE_MENU = "form";
public const TYPE_CUSTOM_FORM = "custom_form";

/** @var string */
protected $title = "";
/** @var bool */
private $inUse = false;

public function __construct(string $title){
$this->title = $title;
}

/**
* Returns the type used to show this form to clients
* @return string
*/
abstract public function getType() : string;

/**
* Returns the text shown on the form title-bar.
* @return string
*/
public function getTitle() : string{
return $this->title;
}

/**
* Handles a form response from a player. Plugins should not override this method, override {@link onSubmit}
* instead.
*
* @param Player $player
* @param mixed $data
*
* @return Form|null a form which will be opened immediately (before queued forms) as a response to this form, or null if not applicable.
*/
abstract public function handleResponse(Player $player, $data) : ?Form;

/**
* Called when a player submits this form. Each form type usually has its own methods for getting relevant data from
* them.
*
* Plugins should extend the class and override this function and add their own code to handle form responses as
* they wish.
*
* @param Player $player
* @return Form|null a form which will be opened immediately (before queued forms) as a response to this form, or null if not applicable.
*/
abstract public function onSubmit(Player $player) : ?Form;

/**
* Returns whether the form has already been sent to a player or not. Note that you cannot send the form again if
* this is true.
*
* @return bool
*/
public function isInUse() : bool{
return $this->inUse;
}

/**
* Called to flag the form as having been sent to prevent it being used again, to avoid concurrency issues.
*/
public function setInUse() : void{
if($this->isInUse()){
throw new \InvalidArgumentException("Form is already in use, create a new one instead");
}
$this->inUse = true;
}

/**
* Clears response data from a form, useful if you want to reuse the same form object several times.
*/
public function clearResponseData() : void{
Copy link
Member

@SOF3 SOF3 Oct 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is very confusing. It is not called by the server, but relies on the developer to call it.

This leads to inconsistency:

  1. Is a Form object expected to be reused for a single player?
  2. Is a Form object expected to be reused for multiple players?
  3. Can the developer use the form values outside onSubmit()?

The answer is:

  • If the same Form instance is only sent once, you can use the form values outside onSubmit() and hold the Form instance forever.
  • If the same Form instance is sent more than once (same player or not), it would be a very bad practice to depend on the value held in the Form instance outside onSubmit(), even in the same tick.

It is particularly tempting to hold a CustomForm instance and use the values there later, since the data cannot be easily extracted. I can foresee this is going to cause a lot of concurrency bugs that can be very difficult to identify. Therefore, it is suggested that, starting from the core, one of either approach ("resend same instance" or "new instance each resend") be adopted to avoid confusion.

  • Adopting the "resend same instance" approach should automatically call the clearResponseData() method after each onSubmit() call.
  • Adopting the "new instance each resend" approach should remove this method and add a flag to identify a Form as sent and throw an exception when attempting to use sendForm() with it.

Deciding which approach to adopt should require extensive consultation.


}

/**
* Serializes the form to JSON for sending to clients.
*
* @return array
*/
final public function jsonSerialize() : array{
$jsonBase = [
"type" => $this->getType(),
"title" => $this->getTitle()
];

return array_merge($jsonBase, $this->serializeFormData());
}

/**
* Serializes additional data needed to show this form to clients.
* @return array
*/
abstract protected function serializeFormData() : array;

}
Loading