From b117ab4356cc41a41fc39316513c3679c5d59e23 Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:17:06 +0700 Subject: [PATCH 1/8] v2 preparation --- .gitignore | 2 ++ .idea/PMServerUI.iml | 1 - .idea/vcs.xml | 4 +++- .poggit.yml | 2 +- LICENSE | 1 + README.md | 8 ++++---- composer.json | 4 ++-- src/{DavyCraft648 => Azvyl}/PMServerUI/ActionFormData.php | 6 +++--- .../PMServerUI/ActionFormResponse.php | 6 +++--- .../PMServerUI/FormCancelationReason.php | 6 +++--- .../PMServerUI/FormDataHandler.php | 6 +++--- .../PMServerUI/FormRejectError.php | 6 +++--- .../PMServerUI/FormRejectReason.php | 6 +++--- src/{DavyCraft648 => Azvyl}/PMServerUI/FormResponse.php | 6 +++--- .../PMServerUI/MessageFormData.php | 6 +++--- .../PMServerUI/MessageFormResponse.php | 6 +++--- src/{DavyCraft648 => Azvyl}/PMServerUI/ModalFormData.php | 6 +++--- .../PMServerUI/ModalFormResponse.php | 6 +++--- src/{DavyCraft648 => Azvyl}/PMServerUI/PMServerUI.php | 8 ++++---- src/{DavyCraft648 => Azvyl}/PMServerUI/Promise.php | 6 +++--- src/{DavyCraft648 => Azvyl}/PMServerUI/UIManager.php | 6 +++--- virion.yml | 4 ++-- 22 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 .gitignore rename src/{DavyCraft648 => Azvyl}/PMServerUI/ActionFormData.php (95%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/ActionFormResponse.php (89%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/FormCancelationReason.php (64%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/FormDataHandler.php (96%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/FormRejectError.php (83%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/FormRejectReason.php (67%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/FormResponse.php (83%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/MessageFormData.php (93%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/MessageFormResponse.php (87%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/ModalFormData.php (98%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/ModalFormResponse.php (97%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/PMServerUI.php (90%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/Promise.php (95%) rename src/{DavyCraft648 => Azvyl}/PMServerUI/UIManager.php (95%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ce5adb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +vendor diff --git a/.idea/PMServerUI.iml b/.idea/PMServerUI.iml index 67452d9..b7d6b2f 100644 --- a/.idea/PMServerUI.iml +++ b/.idea/PMServerUI.iml @@ -2,7 +2,6 @@ - diff --git a/.idea/vcs.xml b/.idea/vcs.xml index d843f34..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,4 +1,6 @@ - + + + \ No newline at end of file diff --git a/.poggit.yml b/.poggit.yml index 4a3012f..6fe256d 100644 --- a/.poggit.yml +++ b/.poggit.yml @@ -1,4 +1,4 @@ ---- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/DavyCraft648/PMServerUI +--- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/Azvyl/PMServerUI build-by-default: true branches: - master diff --git a/LICENSE b/LICENSE index d832b61..949c4b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2025 DavyCraft648 +Copyright (c) 2026 Azvyl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 11d38ff..a36a1f3 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ library that other plugins *include* in their code. following command: ```bash - composer require davycraft648/pmserver-ui + composer require azvyl/pmserver-ui ``` After running the command, your `composer.json` file will be updated. You can inspect it to see the new dependency @@ -42,7 +42,7 @@ library that other plugins *include* in their code. { "require": { // other dependencies ... - "davycraft648/pmserver-ui": "^1.0" + "azvyl/pmserver-ui": "^1.0" } } ``` @@ -56,7 +56,7 @@ library that other plugins *include* in their code. projects: YourPlugin: libs: - - src: DavyCraft648/PMServerUI/PMServerUI + - src: Azvyl/PMServerUI/PMServerUI version: ^1.0.2 ``` @@ -64,7 +64,7 @@ library that other plugins *include* in their code. ## Example Usage (For Developers) -See plugin example [here](https://github.com/DavyCraft648/PMServerUI-Example) +See plugin example [here](https://github.com/Azvyl/PMServerUI-Example) --- diff --git a/composer.json b/composer.json index e6070e5..b31f2f8 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "davycraft648/pmserver-ui", + "name": "azvyl/pmserver-ui", "description": "Integrate UI features available in the Script API to PocketMine-MP", "type": "library", "license": "MIT", @@ -12,7 +12,7 @@ "extra": { "virion": { "spec": "3.0", - "namespace-root": "DavyCraft648\\PMServerUI" + "namespace-root": "Azvyl\\PMServerUI" } } } \ No newline at end of file diff --git a/src/DavyCraft648/PMServerUI/ActionFormData.php b/src/Azvyl/PMServerUI/ActionFormData.php similarity index 95% rename from src/DavyCraft648/PMServerUI/ActionFormData.php rename to src/Azvyl/PMServerUI/ActionFormData.php index ffeb1f0..51ae292 100644 --- a/src/DavyCraft648/PMServerUI/ActionFormData.php +++ b/src/Azvyl/PMServerUI/ActionFormData.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\lang\Translatable; use pocketmine\player\Player; diff --git a/src/DavyCraft648/PMServerUI/ActionFormResponse.php b/src/Azvyl/PMServerUI/ActionFormResponse.php similarity index 89% rename from src/DavyCraft648/PMServerUI/ActionFormResponse.php rename to src/Azvyl/PMServerUI/ActionFormResponse.php index 8e68cba..ff32511 100644 --- a/src/DavyCraft648/PMServerUI/ActionFormResponse.php +++ b/src/Azvyl/PMServerUI/ActionFormResponse.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\form\FormValidationException; use function is_numeric; diff --git a/src/DavyCraft648/PMServerUI/FormCancelationReason.php b/src/Azvyl/PMServerUI/FormCancelationReason.php similarity index 64% rename from src/DavyCraft648/PMServerUI/FormCancelationReason.php rename to src/Azvyl/PMServerUI/FormCancelationReason.php index cf90159..1735cec 100644 --- a/src/DavyCraft648/PMServerUI/FormCancelationReason.php +++ b/src/Azvyl/PMServerUI/FormCancelationReason.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; enum FormCancelationReason{ case UserClosed; diff --git a/src/DavyCraft648/PMServerUI/FormDataHandler.php b/src/Azvyl/PMServerUI/FormDataHandler.php similarity index 96% rename from src/DavyCraft648/PMServerUI/FormDataHandler.php rename to src/Azvyl/PMServerUI/FormDataHandler.php index 7d5b78b..b6ee40c 100644 --- a/src/DavyCraft648/PMServerUI/FormDataHandler.php +++ b/src/Azvyl/PMServerUI/FormDataHandler.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\form\FormValidationException; use pocketmine\lang\Translatable; diff --git a/src/DavyCraft648/PMServerUI/FormRejectError.php b/src/Azvyl/PMServerUI/FormRejectError.php similarity index 83% rename from src/DavyCraft648/PMServerUI/FormRejectError.php rename to src/Azvyl/PMServerUI/FormRejectError.php index c8e1d94..1789df7 100644 --- a/src/DavyCraft648/PMServerUI/FormRejectError.php +++ b/src/Azvyl/PMServerUI/FormRejectError.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; class FormRejectError extends \Error{ diff --git a/src/DavyCraft648/PMServerUI/FormRejectReason.php b/src/Azvyl/PMServerUI/FormRejectReason.php similarity index 67% rename from src/DavyCraft648/PMServerUI/FormRejectReason.php rename to src/Azvyl/PMServerUI/FormRejectReason.php index 99b170a..124b2b5 100644 --- a/src/DavyCraft648/PMServerUI/FormRejectReason.php +++ b/src/Azvyl/PMServerUI/FormRejectReason.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; enum FormRejectReason{ case MalformedResponse; diff --git a/src/DavyCraft648/PMServerUI/FormResponse.php b/src/Azvyl/PMServerUI/FormResponse.php similarity index 83% rename from src/DavyCraft648/PMServerUI/FormResponse.php rename to src/Azvyl/PMServerUI/FormResponse.php index da24d5a..470adcd 100644 --- a/src/DavyCraft648/PMServerUI/FormResponse.php +++ b/src/Azvyl/PMServerUI/FormResponse.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; /** * Base type for a form response. diff --git a/src/DavyCraft648/PMServerUI/MessageFormData.php b/src/Azvyl/PMServerUI/MessageFormData.php similarity index 93% rename from src/DavyCraft648/PMServerUI/MessageFormData.php rename to src/Azvyl/PMServerUI/MessageFormData.php index 14d8951..210b293 100644 --- a/src/DavyCraft648/PMServerUI/MessageFormData.php +++ b/src/Azvyl/PMServerUI/MessageFormData.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\lang\Translatable; use pocketmine\player\Player; diff --git a/src/DavyCraft648/PMServerUI/MessageFormResponse.php b/src/Azvyl/PMServerUI/MessageFormResponse.php similarity index 87% rename from src/DavyCraft648/PMServerUI/MessageFormResponse.php rename to src/Azvyl/PMServerUI/MessageFormResponse.php index e4ba48f..e5406e7 100644 --- a/src/DavyCraft648/PMServerUI/MessageFormResponse.php +++ b/src/Azvyl/PMServerUI/MessageFormResponse.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\form\FormValidationException; diff --git a/src/DavyCraft648/PMServerUI/ModalFormData.php b/src/Azvyl/PMServerUI/ModalFormData.php similarity index 98% rename from src/DavyCraft648/PMServerUI/ModalFormData.php rename to src/Azvyl/PMServerUI/ModalFormData.php index 0b30d89..f32a314 100644 --- a/src/DavyCraft648/PMServerUI/ModalFormData.php +++ b/src/Azvyl/PMServerUI/ModalFormData.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\lang\Translatable; use pocketmine\player\Player; diff --git a/src/DavyCraft648/PMServerUI/ModalFormResponse.php b/src/Azvyl/PMServerUI/ModalFormResponse.php similarity index 97% rename from src/DavyCraft648/PMServerUI/ModalFormResponse.php rename to src/Azvyl/PMServerUI/ModalFormResponse.php index 2e7a3a9..d99d45a 100644 --- a/src/DavyCraft648/PMServerUI/ModalFormResponse.php +++ b/src/Azvyl/PMServerUI/ModalFormResponse.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\form\FormValidationException; use pocketmine\network\PacketHandlingException; diff --git a/src/DavyCraft648/PMServerUI/PMServerUI.php b/src/Azvyl/PMServerUI/PMServerUI.php similarity index 90% rename from src/DavyCraft648/PMServerUI/PMServerUI.php rename to src/Azvyl/PMServerUI/PMServerUI.php index 504a940..8946eb5 100644 --- a/src/DavyCraft648/PMServerUI/PMServerUI.php +++ b/src/Azvyl/PMServerUI/PMServerUI.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\plugin\Plugin; @@ -44,7 +44,7 @@ public static function register(Plugin $plugin, bool $tl = false): void{ //disable player input. //enforce string dropdown. - if(__NAMESPACE__ === "DavyCraft648\\PMServerUI"){ + if(__NAMESPACE__ === "Azvyl\\PMServerUI"){ self::getLogger()->notice("It is recommended to shade virions, go to https://poggit.pmmp.io/virion to see virion documentation."); } } diff --git a/src/DavyCraft648/PMServerUI/Promise.php b/src/Azvyl/PMServerUI/Promise.php similarity index 95% rename from src/DavyCraft648/PMServerUI/Promise.php rename to src/Azvyl/PMServerUI/Promise.php index e067e73..9c55c5f 100644 --- a/src/DavyCraft648/PMServerUI/Promise.php +++ b/src/Azvyl/PMServerUI/Promise.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; class Promise{ private mixed $result = null; diff --git a/src/DavyCraft648/PMServerUI/UIManager.php b/src/Azvyl/PMServerUI/UIManager.php similarity index 95% rename from src/DavyCraft648/PMServerUI/UIManager.php rename to src/Azvyl/PMServerUI/UIManager.php index a676546..506957d 100644 --- a/src/DavyCraft648/PMServerUI/UIManager.php +++ b/src/Azvyl/PMServerUI/UIManager.php @@ -2,9 +2,9 @@ /* * PMServerUI - * https://github.com/DavyCraft648/PMServerUI + * https://github.com/Azvyl/PMServerUI * - * Copyright (c) 2025 DavyCraft648 + * Copyright (c) 2026 Azvyl * * Licensed under the MIT License. * See LICENSE file in the project root for details. @@ -12,7 +12,7 @@ declare(strict_types=1); -namespace DavyCraft648\PMServerUI; +namespace Azvyl\PMServerUI; use pocketmine\event\EventPriority; use pocketmine\event\player\PlayerQuitEvent; diff --git a/virion.yml b/virion.yml index 0d760ad..6e21c5c 100644 --- a/virion.yml +++ b/virion.yml @@ -1,6 +1,6 @@ name: PMServerUI version: 1.0.2 -antigen: DavyCraft648\PMServerUI +antigen: Azvyl\PMServerUI api: 5.0.0 -authors: [DavyCraft648] +authors: [Azvyl] description: Integrate UI features available in the Script API to PocketMine-MP From 4d1c5d7c80e5693be22e4f6e87b0fa28eef49bef Mon Sep 17 00:00:00 2001 From: "Asfadavy Aulia A." <67502532+Azvyl@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:34:21 +0700 Subject: [PATCH 2/8] delete .idea --- .idea/.gitignore | 8 ------- .idea/PMServerUI.iml | 32 --------------------------- .idea/discord.xml | 14 ------------ .idea/modules.xml | 8 ------- .idea/php.xml | 52 -------------------------------------------- .idea/phpspec.xml | 10 --------- .idea/vcs.xml | 6 ----- 7 files changed, 130 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/PMServerUI.iml delete mode 100644 .idea/discord.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/php.xml delete mode 100644 .idea/phpspec.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/PMServerUI.iml b/.idea/PMServerUI.iml deleted file mode 100644 index b7d6b2f..0000000 --- a/.idea/PMServerUI.iml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index 912db82..0000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index bf789b9..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index 9c0dab8..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml deleted file mode 100644 index 33107fd..0000000 --- a/.idea/phpspec.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 3c22ed99534fee9be0a07626f55bf7da93f3fcae Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Sat, 7 Feb 2026 07:26:08 +0700 Subject: [PATCH 3/8] Refactor --- src/Azvyl/PMServerUI/ActionFormData.php | 97 ------- src/Azvyl/PMServerUI/ActionFormResponse.php | 42 --- src/Azvyl/PMServerUI/FormDataHandler.php | 100 ------- src/Azvyl/PMServerUI/FormRejectError.php | 26 -- src/Azvyl/PMServerUI/MessageFormData.php | 75 ------ src/Azvyl/PMServerUI/MessageFormResponse.php | 37 --- src/Azvyl/PMServerUI/ModalFormData.php | 201 -------------- src/Azvyl/PMServerUI/ModalFormResponse.php | 118 -------- src/Azvyl/PMServerUI/PMServerUI.php | 15 +- src/Azvyl/PMServerUI/Promise.php | 16 +- src/Azvyl/PMServerUI/UIManager.php | 85 ------ src/Azvyl/PMServerUI/UIRawMessage.php | 67 +++++ src/Azvyl/PMServerUI/ddui/CustomForm.php | 162 +++++++++++ .../DDUI.php} | 7 +- src/Azvyl/PMServerUI/ddui/DropdownItem.php | 27 ++ src/Azvyl/PMServerUI/ddui/MessageBox.php | 84 ++++++ src/Azvyl/PMServerUI/ddui/Observable.php | 82 ++++++ src/Azvyl/PMServerUI/ddui/Subscription.php | 42 +++ src/Azvyl/PMServerUI/ui/ActionFormData.php | 99 +++++++ .../PMServerUI/ui/ActionFormResponse.php | 27 ++ .../FormCancelationReason.php} | 14 +- src/Azvyl/PMServerUI/ui/FormRejectError.php | 28 ++ src/Azvyl/PMServerUI/ui/FormRejectReason.php | 24 ++ .../PMServerUI/{ => ui}/FormResponse.php | 12 +- src/Azvyl/PMServerUI/ui/MessageFormData.php | 78 ++++++ .../PMServerUI/ui/MessageFormResponse.php | 27 ++ src/Azvyl/PMServerUI/ui/ModalFormData.php | 253 ++++++++++++++++++ src/Azvyl/PMServerUI/ui/ModalFormResponse.php | 29 ++ src/Azvyl/PMServerUI/ui/ServerUI.php | 38 +++ src/Azvyl/PMServerUI/ui/UIManager.php | 144 ++++++++++ 30 files changed, 1241 insertions(+), 815 deletions(-) delete mode 100644 src/Azvyl/PMServerUI/ActionFormData.php delete mode 100644 src/Azvyl/PMServerUI/ActionFormResponse.php delete mode 100644 src/Azvyl/PMServerUI/FormDataHandler.php delete mode 100644 src/Azvyl/PMServerUI/FormRejectError.php delete mode 100644 src/Azvyl/PMServerUI/MessageFormData.php delete mode 100644 src/Azvyl/PMServerUI/MessageFormResponse.php delete mode 100644 src/Azvyl/PMServerUI/ModalFormData.php delete mode 100644 src/Azvyl/PMServerUI/ModalFormResponse.php delete mode 100644 src/Azvyl/PMServerUI/UIManager.php create mode 100644 src/Azvyl/PMServerUI/UIRawMessage.php create mode 100644 src/Azvyl/PMServerUI/ddui/CustomForm.php rename src/Azvyl/PMServerUI/{FormCancelationReason.php => ddui/DDUI.php} (70%) create mode 100644 src/Azvyl/PMServerUI/ddui/DropdownItem.php create mode 100644 src/Azvyl/PMServerUI/ddui/MessageBox.php create mode 100644 src/Azvyl/PMServerUI/ddui/Observable.php create mode 100644 src/Azvyl/PMServerUI/ddui/Subscription.php create mode 100644 src/Azvyl/PMServerUI/ui/ActionFormData.php create mode 100644 src/Azvyl/PMServerUI/ui/ActionFormResponse.php rename src/Azvyl/PMServerUI/{FormRejectReason.php => ui/FormCancelationReason.php} (55%) create mode 100644 src/Azvyl/PMServerUI/ui/FormRejectError.php create mode 100644 src/Azvyl/PMServerUI/ui/FormRejectReason.php rename src/Azvyl/PMServerUI/{ => ui}/FormResponse.php (64%) create mode 100644 src/Azvyl/PMServerUI/ui/MessageFormData.php create mode 100644 src/Azvyl/PMServerUI/ui/MessageFormResponse.php create mode 100644 src/Azvyl/PMServerUI/ui/ModalFormData.php create mode 100644 src/Azvyl/PMServerUI/ui/ModalFormResponse.php create mode 100644 src/Azvyl/PMServerUI/ui/ServerUI.php create mode 100644 src/Azvyl/PMServerUI/ui/UIManager.php diff --git a/src/Azvyl/PMServerUI/ActionFormData.php b/src/Azvyl/PMServerUI/ActionFormData.php deleted file mode 100644 index 51ae292..0000000 --- a/src/Azvyl/PMServerUI/ActionFormData.php +++ /dev/null @@ -1,97 +0,0 @@ -handler = new FormDataHandler(["content" => "", "elements" => null, "title" => "", "type" => "form"], ActionFormResponse::class, $this->buttonCount); - } - - public static function create(): ActionFormData{ - return new self(); - } - - /** - * Method that sets the body text for the modal form. - */ - public function body(Translatable|string $bodyText): ActionFormData{ - $this->handler->add("content", FormDataHandler::tryTl($bodyText)); - return $this; - } - - /** - * Adds a button to this form with an icon from a resource pack. - */ - public function button(Translatable|string $text, ?string $iconPath = null, string $iconType = "path"): ActionFormData{ - $this->handler->arrAdd("elements", ["image" => $iconPath !== null ? ["data" => $iconPath, "type" => $iconType] : null, "text" => FormDataHandler::tryTl($text), "type" => "button"]); - $this->buttonCount++; - return $this; - } - - /** - * Adds a section divider to the form. - */ - public function divider(): ActionFormData{ - $this->handler->arrAdd("elements", ["text" => "", "type" => "divider"]); - return $this; - } - - /** - * Adds a header to the form. - * - * @param Translatable|string $text Text to display. - */ - public function header(Translatable|string $text): ActionFormData{ - $this->handler->arrAdd("elements", ["text" => FormDataHandler::tryTl($text), "type" => "header"]); - return $this; - } - - /** - * Adds a label to the form. - * - * @param Translatable|string $text Text to display. - */ - public function label(Translatable|string $text): ActionFormData{ - $this->handler->arrAdd("elements", ["text" => FormDataHandler::tryTl($text), "type" => "label"]); - return $this; - } - - /** - * Creates and shows this modal popup form. Returns asynchronously when the player confirms or cancels the dialog. - * - * @param Player $player Player to show this dialog to. - */ - public function show(Player $player): Promise{ - return $this->handler->show($player); - } - - /** - * This builder method sets the title for the modal dialog. - */ - public function title(Translatable|string $titleText): ActionFormData{ - $this->handler->add("title", FormDataHandler::tryTl($titleText)); - return $this; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ActionFormResponse.php b/src/Azvyl/PMServerUI/ActionFormResponse.php deleted file mode 100644 index ff32511..0000000 --- a/src/Azvyl/PMServerUI/ActionFormResponse.php +++ /dev/null @@ -1,42 +0,0 @@ -= $buttonCount){ - throw new FormValidationException("Button $data does not exist"); - } - return $data; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/FormDataHandler.php b/src/Azvyl/PMServerUI/FormDataHandler.php deleted file mode 100644 index b6ee40c..0000000 --- a/src/Azvyl/PMServerUI/FormDataHandler.php +++ /dev/null @@ -1,100 +0,0 @@ -data[$key] = $value; - } - - public function arrAdd(string $key, mixed $value): void{ - if(!isset($this->data[$key]) || !is_array($this->data[$key])){ - $this->data[$key] = []; - } - $this->data[$key][] = $value; - } - - public static function tryTl(Translatable|string $value): array|string{ - return PMServerUI::$tl && $value instanceof Translatable ? ["rawtext" => [self::getTl($value)]] : (string)$value; - } - - private static function getTl(Translatable $translatable): array{ - $params = []; - foreach($translatable->getParameters() as $param){ - $params[] = $param instanceof Translatable ? self::getTl($param) : ["text" => $param]; - } - $ret = ["translate" => $translatable->getText()]; - if($params !== []){ - $ret["with"] = ["rawtext" => $params]; - } - return $ret; - } - - public function show(Player $player): Promise{ - $promise = new Promise(); - (function(FormDataHandler $form, Promise $promise): void{ - /** @noinspection PhpUndefinedFieldInspection */ - $id = $this->formIdCounter++; - /** @noinspection PhpUndefinedMethodInspection */ - if($this->getNetworkSession()->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($form, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)))){ - /** @noinspection PhpParamsInspection */ - PMServerUI::getUIManager()->trackForm($this, $id, $form, $promise); - } - })->call($player, $this, $promise); - return $promise; - } - - public function handleCancel(Player $player, int $cancelReason, Promise $promise): void{ - $reason = FormCancelationReason::cases()[$cancelReason] ?? null; - if($reason !== null){ - $promise->resolve($player, (new \ReflectionClass($this->responseClass))->newInstance($reason, true, null)); - }else{ - throw new FormValidationException("Player {$player->getName()} sent unknown cancel reason"); - } - } - - public function handleResponse(Player $player, ?string $rawData, Promise $promise): void{ - if($rawData === null || $rawData === "null" || $rawData === ""){ - throw new FormValidationException("Form response can't be null without cancel reason"); - } - $promise->resolve($player, (new \ReflectionClass($this->responseClass))->newInstance(null, false, [$this->responseClass, "validate"]($rawData, $this->sharedInfo))); - } - - public function informCrash(\Throwable $t, Promise $promise): void{ - if($t instanceof FormValidationException){ - $promise->reject(FormRejectError::create(FormRejectReason::MalformedResponse, "Failed to process form: {$t->getMessage()}", previous: $t)); - return; - } - $promise->reject(FormRejectError::create(FormRejectReason::ServerShutdown, "Crashed when handling packet: {$t->getMessage()}", previous: $t)); - } - - public function informQuit(Player $player, Promise $promise): void{ - $promise->reject(FormRejectError::create(FormRejectReason::PlayerQuit, "Player {$player->getName()} quit before responding")); - } - - public function jsonSerialize(): array{ - return $this->data; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/FormRejectError.php b/src/Azvyl/PMServerUI/FormRejectError.php deleted file mode 100644 index 1789df7..0000000 --- a/src/Azvyl/PMServerUI/FormRejectError.php +++ /dev/null @@ -1,26 +0,0 @@ -handler = new FormDataHandler(["button1" => "", "button2" => "", "content" => "", "title" => "", "type" => "modal"], MessageFormResponse::class); - } - - public static function create(): MessageFormData{ - return new self(); - } - - /** - * Method that sets the body text for the modal form. - */ - public function body(Translatable|string $bodyText): MessageFormData{ - $this->handler->add("content", FormDataHandler::tryTl($bodyText)); - return $this; - } - - /** - * Method that sets the text for the first button of the dialog. - */ - public function button1(Translatable|string $text): MessageFormData{ - $this->handler->add("button1", FormDataHandler::tryTl($text)); - return $this; - } - - /** - * This method sets the text for the second button on the dialog. - */ - public function button2(Translatable|string $text): MessageFormData{ - $this->handler->add("button2", FormDataHandler::tryTl($text)); - return $this; - } - - /** - * Creates and shows this modal popup form. Returns asynchronously when the player confirms or cancels the dialog. - * - * @param Player $player Player to show this dialog to. - */ - public function show(Player $player): Promise{ - return $this->handler->show($player); - } - - /** - * This builder method sets the title for the modal dialog. - */ - public function title(Translatable|string $titleText): MessageFormData{ - $this->handler->add("title", FormDataHandler::tryTl($titleText)); - return $this; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/MessageFormResponse.php b/src/Azvyl/PMServerUI/MessageFormResponse.php deleted file mode 100644 index e5406e7..0000000 --- a/src/Azvyl/PMServerUI/MessageFormResponse.php +++ /dev/null @@ -1,37 +0,0 @@ -handler = new FormDataHandler(["content" => null, "icon" => null, "title" => "", "type" => "custom_form"], ModalFormResponse::class, $this->elements); - } - - public static function create(): ModalFormData{ - return new self(); - } - - /** - * Adds a section divider to the form. - */ - public function divider(): ModalFormData{ - $this->handler->arrAdd("content", ["text" => "", "type" => "divider"]); - $this->elements[] = "divider"; - return $this; - } - - /** - * Adds a dropdown with choices to the form. - * - * @param Translatable|string $label The label to display for the dropdown. - * @param (Translatable|string)[] $items The selectable items for the dropdown. - * @param int|null $default The default selected item index. It will be zero in case of not setting this value. - * @param Translatable|string|null $tooltip It will show an exclamation icon that will display a tooltip if it is hovered. - */ - public function dropdown(Translatable|string $label, array $items, ?int $default = null, Translatable|string|null $tooltip = null): ModalFormData{ - $items = array_values($items); - foreach($items as $i => $item){ - if(!$item instanceof Translatable && !is_string($item)){ - throw new \InvalidArgumentException("Each item in the \$items array must be an instance of Translatable or a string"); - } - $items[$i] = FormDataHandler::tryTl($item); - } - $val = ["options" => $items, "text" => FormDataHandler::tryTl($label), "type" => "dropdown"]; - if($default !== null){ - if(!isset($items[$default])){ - throw new \InvalidArgumentException("No option at index $default, cannot set as default"); - } - $val["default"] = $default; - } - if($tooltip !== null){ - $val["tooltip"] = FormDataHandler::tryTl($tooltip); - } - $this->handler->arrAdd("content", $val); - $this->elements[] = ["dropdown", count($items)]; - return $this; - } - - /** - * Adds a header to the form. - * - * @param Translatable|string $text Text to display. - */ - public function header(Translatable|string $text): ModalFormData{ - $this->handler->arrAdd("content", ["text" => FormDataHandler::tryTl($text), "type" => "header"]); - $this->elements[] = "header"; - return $this; - } - - /** - * Adds a label to the form. - * - * @param Translatable|string $text Text to display. - */ - public function label(Translatable|string $text): ModalFormData{ - $this->handler->arrAdd("content", ["text" => FormDataHandler::tryTl($text), "type" => "label"]); - $this->elements[] = "label"; - return $this; - } - - /** - * Creates and shows this modal popup form. Returns asynchronously when the player confirms or cancels the dialog. - * - * @param Player $player Player to show this dialog to. - */ - public function show(Player $player): Promise{ - return $this->handler->show($player); - } - - /** - * Adds a numeric slider to the form. - * - * @param Translatable|string $label The label to display for the slider. - * @param int $minimum The minimum selectable possible value. - * @param int $maximum The maximum selectable possible value. - * @param int|null $step Defines the increment of values that the slider generates when moved. It will be '1' in case of not providing this. - * @param int|null $default The default value for the slider. - * @param Translatable|string|null $tooltip It will show an exclamation icon that will display a tooltip if it is hovered. - */ - public function slider(Translatable|string $label, int $minimum, int $maximum, ?int $step = null, ?int $default = null, Translatable|string|null $tooltip = null): ModalFormData{ - if($minimum > $maximum){ - throw new \InvalidArgumentException("Slider min value should be less than max value"); - } - $val = ["max" => $maximum, "min" => $minimum, "step" => 1, "text" => FormDataHandler::tryTl($label), "type" => "slider"]; - if($step !== null){ - if($step <= 0){ - throw new \InvalidArgumentException("Step must be greater than zero"); - } - $val["step"] = $step; - } - if($default !== null){ - if($default > $maximum || $default < $minimum){ - throw new \InvalidArgumentException("Default must be in range $minimum ... $maximum"); - } - $val["default"] = $default; - } - if($tooltip !== null){ - $val["tooltip"] = FormDataHandler::tryTl($tooltip); - } - $this->handler->arrAdd("content", $val); - $this->elements[] = ["slider", $minimum, $maximum]; - return $this; - } - - public function submitButton(Translatable|string $submitButtonText): ModalFormData{ - $this->handler->add("submit", FormDataHandler::tryTl($submitButtonText)); - return $this; - } - - /** - * Adds a textbox to the form. - * - * @param Translatable|string $label The label to display for the textfield. - * @param Translatable|string|null $placeholder The place holder text to display. - * @param string|null $default The default value for the textfield. - * @param Translatable|string|null $tooltip It will show an exclamation icon that will display a tooltip if it is hovered. - */ - public function textField(Translatable|string $label, Translatable|string|null $placeholder = null, ?string $default = null, Translatable|string|null $tooltip = null): ModalFormData{ - $val = ["text" => FormDataHandler::tryTl($label), "type" => "input"]; - if($placeholder !== null){ - $val["placeholder"] = FormDataHandler::tryTl($placeholder); - } - if($default !== null){ - $val["default"] = $default; - } - if($tooltip !== null){ - $val["tooltip"] = FormDataHandler::tryTl($tooltip); - } - $this->handler->arrAdd("content", $val); - $this->elements[] = "input"; - return $this; - } - - /** - * This builder method sets the title for the modal dialog. - */ - public function title(Translatable|string $titleText): ModalFormData{ - $this->handler->add("title", FormDataHandler::tryTl($titleText)); - return $this; - } - - /** - * Adds a toggle checkbox button to the form. - * - * @param Translatable|string $label The label to display for the toggle. - * @param bool|null $default The default value for the toggle. - * @param Translatable|string|null $tooltip It will show an exclamation icon that will display a tooltip if it is hovered. - */ - public function toggle(Translatable|string $label, ?bool $default = null, Translatable|string|null $tooltip = null): ModalFormData{ - $val = ["text" => FormDataHandler::tryTl($label), "type" => "toggle"]; - if($default !== null){ - $val["default"] = $default; - } - if($tooltip !== null){ - $val["tooltip"] = FormDataHandler::tryTl($tooltip); - } - $this->handler->arrAdd("content", $val); - $this->elements[] = "toggle"; - return $this; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ModalFormResponse.php b/src/Azvyl/PMServerUI/ModalFormResponse.php deleted file mode 100644 index d99d45a..0000000 --- a/src/Azvyl/PMServerUI/ModalFormResponse.php +++ /dev/null @@ -1,118 +0,0 @@ - $expected){ - throw new FormValidationException("Too many result elements, expected $expected, got $actual"); - }elseif($actual < $expected){ - //In 1.21.70, the client doesn't send nulls for labels, so we need to polyfill them here to - //maintain the old behaviour - $noLabelsIndexMapping = []; - foreach($elements as $index => $element){ - if($element === "label"){ - $noLabelsIndexMapping[] = $index; - } - } - $expectedWithoutLabels = count($noLabelsIndexMapping); - if($actual !== $expectedWithoutLabels){ - throw new FormValidationException("Wrong number of result elements, expected either $expected (with label values) or $expectedWithoutLabels (without label values, 1.21.70), got $actual"); - } - - //polyfill the missing nulls - $mappedData = array_fill(0, $expected, null); - foreach($data as $givenIndex => $value){ - $internalIndex = $noLabelsIndexMapping[$givenIndex] ?? null; - if($internalIndex === null){ - throw new FormValidationException("Can't map given offset $givenIndex to an internal element offset (while correcting for labels)"); - } - //set the appropriate values according to the given index - //this could (?) still leave unexpected nulls, but the validation below will catch that - $mappedData[$internalIndex] = $value; - } - if(count($mappedData) !== $expected){ - throw new AssumptionFailedError("This should always match"); - } - $data = $mappedData; - } - - foreach($data as $index => $value){ - if(!isset($elements[$index])){ - throw new FormValidationException("Element at offset $index does not exist"); - } - $element = $elements[$index]; - if(is_array($element)){ - if($element[0] === "dropdown"){ - if(!is_int($value)){ - throw new FormValidationException("Dropdown: Expected int, got " . gettype($value)); - } - if($value < 0 || $value >= $element[1]){ - throw new FormValidationException("Dropdown: Option $value does not exist"); - } - }elseif($element[0] === "slider"){ - if(!is_float($value) && !is_int($value)){ - throw new FormValidationException("Slider: Expected float, got " . gettype($value)); - } - if($value < $element[1] || $value > $element[2]){ - throw new FormValidationException("Slider: Value $value is out of bounds (min $element[1], max $element[2])"); - } - } - }elseif($element === "input"){ - if(!is_string($value)){ - throw new FormValidationException("Input: Expected string, got " . gettype($value)); - } - }elseif($element === "toggle"){ - if(!is_bool($value)){ - throw new FormValidationException("Toggle: Expected bool, got " . gettype($value)); - } - }elseif($value !== null){ - throw new FormValidationException("The value of $element must be null"); - } - } - return $data; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/PMServerUI.php b/src/Azvyl/PMServerUI/PMServerUI.php index 8946eb5..0810e5a 100644 --- a/src/Azvyl/PMServerUI/PMServerUI.php +++ b/src/Azvyl/PMServerUI/PMServerUI.php @@ -14,20 +14,20 @@ namespace Azvyl\PMServerUI; +use Azvyl\PMServerUI\ui\UIManager; use pocketmine\plugin\Plugin; final class PMServerUI{ - private function __construct(){} + private function __construct(){ } private static ?Plugin $plugin = null; private static \PrefixedLogger $logger; private static UIManager $uiManager; - public static bool $tl = false; - public static function register(Plugin $plugin, bool $tl = false): void{ + public static function register(Plugin $plugin) : void{ if(self::$plugin instanceof Plugin){ - throw new \InvalidArgumentException("{$plugin->getName()} tries to register PMServerUI that has been registered by " . self::$plugin->getName()); + return; } self::$plugin = $plugin; @@ -35,7 +35,6 @@ public static function register(Plugin $plugin, bool $tl = false): void{ self::$uiManager = new UIManager($plugin); //TODO: make options easier to configure. - self::$tl = $tl; //crash on unhandled error. //kick player sending malformed response. //validate response length before performing json_decode. @@ -49,15 +48,15 @@ public static function register(Plugin $plugin, bool $tl = false): void{ } } - public static function getPlugin(): Plugin{ + public static function getPlugin() : Plugin{ return self::$plugin ?? throw new \LogicException("PMServerUI has not been registered"); } - public static function getLogger(): \PrefixedLogger{ + public static function getLogger() : \PrefixedLogger{ return self::$logger ?? throw new \LogicException("PMServerUI has not been registered"); } - public static function getUIManager(): UIManager{ + public static function getUIManager() : UIManager{ return self::$uiManager ?? throw new \LogicException("PMServerUI has not been registered"); } } \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/Promise.php b/src/Azvyl/PMServerUI/Promise.php index 9c55c5f..cfa9dd6 100644 --- a/src/Azvyl/PMServerUI/Promise.php +++ b/src/Azvyl/PMServerUI/Promise.php @@ -14,7 +14,7 @@ namespace Azvyl\PMServerUI; -class Promise{ +final class Promise{ private mixed $result = null; private ?\Throwable $error = null; private bool $isResolved = false; @@ -29,7 +29,7 @@ public function __construct(?callable $executor = null){ } } - public function run(callable $executor): void{ + public function run(callable $executor) : void{ try{ $executor(fn(mixed ...$value) => $this->resolve(...$value), fn(\Throwable $t) => $this->reject($t)); }catch(\Throwable $e){ @@ -37,7 +37,7 @@ public function run(callable $executor): void{ } } - public function resolve(mixed ...$value): void{ + public function resolve(mixed ...$value) : void{ if($this->isResolved || $this->isRejected) return; $this->isResolved = true; @@ -50,7 +50,7 @@ public function resolve(mixed ...$value): void{ $this->onFulfilledCallbacks = []; } - public function reject(\Throwable $reason): void{ + public function reject(\Throwable $reason) : void{ if($this->isResolved || $this->isRejected) return; $this->isRejected = true; @@ -63,8 +63,8 @@ public function reject(\Throwable $reason): void{ $this->onRejectedCallbacks = []; } - public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise{ - return new Promise(function($resolve, $reject) use ($onFulfilled, $onRejected){ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : Promise{ + return new self(function($resolve, $reject) use ($onFulfilled, $onRejected){ $handleFulfilled = function(...$value) use ($onFulfilled, $resolve, $reject){ if($onFulfilled){ try{ @@ -102,7 +102,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null }); } - public function catch(?callable $onRejected = null): Promise{ + public function catch(?callable $onRejected = null) : Promise{ return $this->then(null, $onRejected); } -} \ No newline at end of file +} diff --git a/src/Azvyl/PMServerUI/UIManager.php b/src/Azvyl/PMServerUI/UIManager.php deleted file mode 100644 index 506957d..0000000 --- a/src/Azvyl/PMServerUI/UIManager.php +++ /dev/null @@ -1,85 +0,0 @@ -getPluginManager(); - $plManager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event): void{ - $player = $event->getPlayer(); - if(!isset($this->playerForms[$player->getId()])){ - return; - } - /** - * @var FormDataHandler $form - * @var Promise $promise - */ - foreach($this->playerForms[$player->getId()] as [$form, $promise]){ - $form->informQuit($player, $promise); - } - unset($this->playerForms[$player->getId()]); - }, EventPriority::LOWEST, $plugin); - $plManager->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event): void{ - $packet = $event->getPacket(); - if($packet instanceof ModalFormResponsePacket){ - $player = $event->getOrigin()->getPlayer(); - /** - * @var FormDataHandler $form - * @var Promise $promise - */ - [$form, $promise] = $this->playerForms[$player->getId()][$packet->formId] ?? [null, null]; - if($form !== null){ - unset($this->playerForms[$player->getId()][$packet->formId]); - $event->cancel(); - - try{ - if($packet->cancelReason !== null){ - $form->handleCancel($player, $packet->cancelReason, $promise); - }elseif($packet->formData !== null){ - $form->handleResponse($player, trim($packet->formData), $promise); - }else{ - throw new PacketHandlingException("Expected either formData or cancelReason to be set in ModalFormResponsePacket"); - } - }catch(\Throwable $e){ - PMServerUI::getLogger()->logException($e); - try{ - $form->informCrash($e, $promise); - }catch(\Throwable $t){ - PMServerUI::getLogger()->logException($t); - } - } - } - } - }, EventPriority::LOW, $plugin); - } - - public function trackForm(Player $player, int $formId, FormDataHandler $handler, Promise $promise): void{ - $this->playerForms[$player->getId()][$formId] = [$handler, $promise]; - } -} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/UIRawMessage.php b/src/Azvyl/PMServerUI/UIRawMessage.php new file mode 100644 index 0000000..952065d --- /dev/null +++ b/src/Azvyl/PMServerUI/UIRawMessage.php @@ -0,0 +1,67 @@ +rawtext !== null){ + Utils::validateArrayValueType($this->rawtext, fn(UIRawMessage $_) => null); + } + if(is_array($this->with)){ + Utils::validateArrayValueType($this->with, fn(string $_) => null); + } + } + + public function encode() : array{ + $entries = []; + + if($this->translate !== null){ + $entry = ["translate" => $this->translate]; + + if($this->with !== null){ + if($this->with instanceof UIRawMessage){ + $encoded = $this->with->encode(); + $entry["with"] = $encoded["rawtext"] ?? []; + }elseif(is_array($this->with)){ + $entry["with"] = array_map(fn(string $s) => ["text" => $s], $this->with); + } + } + + $entries[] = $entry; + } + + if($this->text !== null && $this->translate === null && $this->rawtext === null){ + $entries[] = ["text" => $this->text]; + } + + if($this->rawtext !== null){ + // Todo: Verify if this is correct + $entries[] = ["rawtext" => array_map(fn(UIRawMessage $child) => $child->encode(), $this->rawtext)]; + } + + return ["rawtext" => $entries]; + } +} diff --git a/src/Azvyl/PMServerUI/ddui/CustomForm.php b/src/Azvyl/PMServerUI/ddui/CustomForm.php new file mode 100644 index 0000000..d6682ff --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/CustomForm.php @@ -0,0 +1,162 @@ +|string|UIRawMessage $title The title of the form. + */ + public static function create(Player $player, Observable|string|UIRawMessage $title) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a button into the Custom form. onClick is called when the button is pressed. + * + * @param Observable|string|UIRawMessage $label The text to display on the button. + * @param callable() : void $onClick The function to call when the button is clicked. + * @param bool|Observable $disabled + * @param Observable|string|UIRawMessage|null $tooltip The tooltip to display when hovering over the button. + * @param bool|Observable $visible + */ + public function button(Observable|string|UIRawMessage $label, callable $onClick, bool|Observable $disabled = null, Observable|string|UIRawMessage $tooltip = null, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Can this form be shown to the player right now? + */ + public function canShow() : bool{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Closes the form. Throws an error if the form is not open. + */ + public function close() : void{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Adds a close "X" button to the form. + */ + public function closeButton() : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a divider (i.e. a line) into the Custom form. + * + * @param bool|Observable $visible Whether the divider is visible. + */ + public function divider(bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a dropdown into the Custom form with the provided items. The value is based on the items value that + * selected. + * + * @param Observable|string|UIRawMessage $label The text to display above the dropdown. + * @param Observable $value The currently selected index in the dropdown. + * @param array $items An array of DropdownItem to show in the dropdown. + * @param Observable|string|UIRawMessage|null $description + * @param bool|Observable $disabled + * @param bool|Observable $visible + */ + public function dropdown(Observable|string|UIRawMessage $label, Observable $value, array $items, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a label (i.e. medium-sized text) into the Custom form. + * + * @param Observable|string|UIRawMessage $text The text to display in the label. + * @param bool|Observable $visible + */ + public function label(Observable|string|UIRawMessage $text, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Shows the form to the player. Will throw errors if the form is currently being shown or if another behavior pack + * is showing a form. + */ + public function show() : Promise{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Creates a slider that lets players pick a number between minValue and maxValue. + * + * @param Observable|string|UIRawMessage $label The text to display above the slider. + * @param Observable $value The current value observable (client-writable). + * @param int|float $minValue The minimum selectable value. + * @param int|float $maxValue The maximum selectable value. + * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param Observable|int|float|null $step The step size (increment) for the slider. + * @param bool|Observable|null $visible + */ + public function slider(Observable|string|UIRawMessage $label, Observable $value, int|float $minValue, int|float $maxValue, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, int|float|Observable $step = null, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a space into the Custom form. + * + * @param bool|Observable|null $visible Whether the spacer is visible. + */ + public function spacer(bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts a text field into the Custom for that players can enter text into. + * + * @param Observable|string|UIRawMessage $label The label shown above the text field. + * @param Observable $text The text observable bound to the field (client-writable). + * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param bool|Observable|null $visible + */ + public function textField(Observable|string|UIRawMessage $label, Observable $text, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Inserts an on/off toggle that players can interact with into the Custom form. + * + * @param Observable|string|UIRawMessage $label The label shown beside the toggle. + * @param Observable $toggled The boolean observable bound to the toggle (client-writable). + * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param bool|Observable|null $visible + */ + public function toggle(Observable|string|UIRawMessage $label, Observable $toggled, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ + throw new \RuntimeException("Not implemented"); + } +} diff --git a/src/Azvyl/PMServerUI/FormCancelationReason.php b/src/Azvyl/PMServerUI/ddui/DDUI.php similarity index 70% rename from src/Azvyl/PMServerUI/FormCancelationReason.php rename to src/Azvyl/PMServerUI/ddui/DDUI.php index 1735cec..4787554 100644 --- a/src/Azvyl/PMServerUI/FormCancelationReason.php +++ b/src/Azvyl/PMServerUI/ddui/DDUI.php @@ -12,9 +12,8 @@ declare(strict_types=1); -namespace Azvyl\PMServerUI; +namespace Azvyl\PMServerUI\ddui; -enum FormCancelationReason{ - case UserClosed; - case UserBusy; +abstract class DDUI{ + protected function __construct(){ } } \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ddui/DropdownItem.php b/src/Azvyl/PMServerUI/ddui/DropdownItem.php new file mode 100644 index 0000000..18aa369 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/DropdownItem.php @@ -0,0 +1,27 @@ +|string|UIRawMessage $text + */ + public function body(Observable|string|UIRawMessage $text) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Sets the data for the top button in the form. + * + * @param Observable|string|UIRawMessage $label + * @param Observable|string|UIRawMessage|null $tooltip + */ + public function button1(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Sets the data for the bottom button in the form. + * + * @param Observable|string|UIRawMessage $label + * @param Observable|string|UIRawMessage|null $tooltip + */ + public function button2(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Closes the form. Will throw an error if the form is not open. + */ + public function close() : void{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Show this modal to the player. Will throw an error if the modal is already showing. + */ + public function show(Player $player) : Promise{ + throw new \RuntimeException("Not implemented"); + } + + /** + * Sets the title of the form. + * + * @param Observable|string|UIRawMessage $text + */ + public function title(Observable|string|UIRawMessage $text) : self{ + throw new \RuntimeException("Not implemented"); + } +} diff --git a/src/Azvyl/PMServerUI/ddui/Observable.php b/src/Azvyl/PMServerUI/ddui/Observable.php new file mode 100644 index 0000000..f57fb88 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/Observable.php @@ -0,0 +1,82 @@ + */ + private array $listeners = []; + + /** + * @param T $data + * @param bool $clientWritable + */ + private function __construct(private bool|float|int|string $data, private readonly bool $clientWritable = false){ } + + /** + * Create an observable. + * + * @param T $data + * @param bool $clientWritable + * + * @return Observable + */ + public static function create(bool|float|int|string $data, bool $clientWritable = false) : self{ + return new self($data, $clientWritable); + } + + /** + * Get the data. + * @return T + */ + public function getData() : bool|float|int|string{ + return $this->data; + } + + /** + * Set the data and notify subscribers. + * + * @param T $data + */ + public function setData(bool|float|int|string $data) : void{ + $this->data = $data; + foreach($this->listeners as $listener){ + $listener($data); + } + } + + /** + * Subscribe to changes. Returns a Subscription which can be used to unsubscribe. + * + * @param callable(T): void $listener + * + * @return Subscription + */ + public function subscribe(callable $listener) : Subscription{ + $id = count($this->listeners) + 1; + $this->listeners[$id] = $listener; + return new Subscription(function() use ($id) : void{ + unset($this->listeners[$id]); + }); + } + + public function isClientWritable() : bool{ + return $this->clientWritable; + } +} diff --git a/src/Azvyl/PMServerUI/ddui/Subscription.php b/src/Azvyl/PMServerUI/ddui/Subscription.php new file mode 100644 index 0000000..1a64264 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/Subscription.php @@ -0,0 +1,42 @@ +onUnsubscribe = $onUnsubscribe; + } + + public function unsubscribe() : void{ + if(!$this->active){ + return; + } + $this->active = false; + if(is_callable($this->onUnsubscribe)){ + ($this->onUnsubscribe)(); + } + } + + public function isActive() : bool{ + return $this->active; + } +} diff --git a/src/Azvyl/PMServerUI/ui/ActionFormData.php b/src/Azvyl/PMServerUI/ui/ActionFormData.php new file mode 100644 index 0000000..91c39fc --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/ActionFormData.php @@ -0,0 +1,99 @@ +title = $title instanceof UIRawMessage ? $title->encode() : $title; + return $this; + } + + public function body(string|UIRawMessage $body) : self{ + $this->body = $body instanceof UIRawMessage ? $body->encode() : $body; + return $this; + } + + public function button(string|UIRawMessage $text, ?string $iconPath = null, ?string $iconUrl = null) : self{ + $this->elements[] = [ + 'type' => 'button', + 'text' => $text instanceof UIRawMessage ? $text->encode() : $text, + 'image' => $iconPath !== null || $iconUrl !== null ? [ + 'data' => $iconPath ?? $iconUrl, + 'type' => $iconPath !== null ? 'path' : 'url', + ] : null, + ]; + $this->buttonCount++; + return $this; + } + + public function divider() : self{ + $this->elements[] = ['type' => 'divider', 'text' => '']; + return $this; + } + + public function header(string|UIRawMessage $text) : self{ + $this->elements[] = ['type' => 'header', 'text' => $text instanceof UIRawMessage ? $text->encode() : $text]; + return $this; + } + + public function label(string|UIRawMessage $text) : self{ + $this->elements[] = ['type' => 'label', 'text' => $text instanceof UIRawMessage ? $text->encode() : $text]; + return $this; + } + + /** + * @internal + */ + public function toPacketFormData() : array{ + return [ + 'type' => 'form', + 'title' => $this->title ?? '', + 'content' => $this->body ?? '', + 'elements' => $this->elements, + ]; + } + + /** + * @internal + */ + public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : ActionFormResponse{ + if($cancelReason !== null){ + return new ActionFormResponse($cancelReason, null); + } + if($rawData !== null){ + if(!is_numeric($rawData)){ + throw new FormValidationException("Expected int, got $rawData"); + } + $data = (int) $rawData; + if($data < 0 || $data >= $this->buttonCount){ + throw new FormValidationException("Button $data does not exist"); + } + return new ActionFormResponse(null, $data); + } + throw new \InvalidArgumentException("Expected rawData to be non-null"); + } +} diff --git a/src/Azvyl/PMServerUI/ui/ActionFormResponse.php b/src/Azvyl/PMServerUI/ui/ActionFormResponse.php new file mode 100644 index 0000000..508e432 --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/ActionFormResponse.php @@ -0,0 +1,27 @@ +title = $title instanceof UIRawMessage ? $title->encode() : $title; + return $this; + } + + public function body(string|UIRawMessage $body) : self{ + $this->body = $body instanceof UIRawMessage ? $body->encode() : $body; + return $this; + } + + public function button1(string|UIRawMessage $text) : self{ + $this->button1 = $text instanceof UIRawMessage ? $text->encode() : $text; + return $this; + } + + public function button2(string|UIRawMessage $text) : self{ + $this->button2 = $text instanceof UIRawMessage ? $text->encode() : $text; + return $this; + } + + /** + * @internal + */ + public function toPacketFormData() : array{ + return [ + 'type' => 'modal', + 'title' => $this->title ?? '', + 'content' => $this->body ?? '', + 'button1' => $this->button1 ?? '', + 'button2' => $this->button2 ?? '', + ]; + } + + /** + * @internal + */ + public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : MessageFormResponse{ + if($cancelReason !== null){ + return new MessageFormResponse($cancelReason, null); + } + if($rawData !== null){ + $trimmed = trim($rawData); + if($trimmed === "true" || $trimmed === "false"){ + return new MessageFormResponse(null, $trimmed === "true" ? 0 : 1); + } + throw new FormValidationException("Expected bool, got $trimmed"); + } + throw new \InvalidArgumentException("Expected rawData to be non-null"); + } +} diff --git a/src/Azvyl/PMServerUI/ui/MessageFormResponse.php b/src/Azvyl/PMServerUI/ui/MessageFormResponse.php new file mode 100644 index 0000000..801d218 --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/MessageFormResponse.php @@ -0,0 +1,27 @@ +> */ + private array $controls = []; + /** @var array */ + private array $validators = []; + + public function title(string|UIRawMessage $title) : self{ + $this->title = $title instanceof UIRawMessage ? $title->encode() : $title; + return $this; + } + + public function divider() : self{ + $this->controls[] = ['type' => 'divider', 'text' => '']; + $this->validators[] = null; + return $this; + } + + /** + * @param string[]|UIRawMessage[] $items + */ + public function dropdown(string|UIRawMessage $label, array $items, int $defaultValueIndex = null, string|UIRawMessage $tooltip = null) : self{ + $dropdownElement = [ + 'type' => 'dropdown', + 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, + 'options' => array_map(fn(string|UIRawMessage $item) => $item instanceof UIRawMessage ? $item->encode() : $item, $items) + ]; + if($defaultValueIndex !== null){ + $dropdownElement['default'] = $defaultValueIndex; + } + if($tooltip !== null){ + $dropdownElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; + } + + $this->controls[] = $dropdownElement; + $this->validators[] = static function($value) use ($items) : int{ + if(!is_int($value)){ + throw new FormValidationException("Expected integer index for dropdown response, got " . gettype($value)); + } + $maxIndex = count($items) - 1; + if($value < 0 || $value > $maxIndex){ + throw new FormValidationException("Dropdown response index out of range (0..$maxIndex), got $value"); + } + return $value; + }; + + return $this; + } + + public function header(string|UIRawMessage $text) : self{ + $this->controls[] = ['type' => 'header', 'text' => $text instanceof UIRawMessage ? $text->encode() : $text]; + $this->validators[] = null; + return $this; + } + + public function label(string|UIRawMessage $text) : self{ + $this->controls[] = ['type' => 'label', 'text' => $text instanceof UIRawMessage ? $text->encode() : $text]; + $this->validators[] = null; + return $this; + } + + public function slider(string|UIRawMessage $label, int $minValue, int $maxValue, int $defaultValue = null, string|UIRawMessage $tooltip = null, int $valueStep = null) : self{ + $sliderElement = [ + 'type' => 'slider', + 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, + 'min' => (float) $minValue, + 'max' => (float) $maxValue, + 'step' => (float) ($valueStep ?? 1.0), + 'timeout' => 100.0,//TODO: Find out what this does (1.26.0.29) + ]; + if($defaultValue !== null){ + $sliderElement['default'] = $defaultValue; + } + if($tooltip !== null){ + $sliderElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; + } + $this->controls[] = $sliderElement; + $this->validators[] = static function($value) use ($minValue, $maxValue) : float{ + if(!is_int($value) && !is_float($value)){ + throw new FormValidationException("Expected numeric value for slider response, got " . gettype($value)); + } + $numeric = (float) $value; + if($numeric < $minValue || $numeric > $maxValue){ + throw new FormValidationException("Slider response out of range ($minValue..$maxValue), got $numeric"); + } + return $numeric; + }; + + return $this; + } + + public function submitButton(string|UIRawMessage $text) : self{ + $this->submit = $text instanceof UIRawMessage ? $text->encode() : $text; + return $this; + } + + public function textField(string|UIRawMessage $label, string|UIRawMessage $placeholderText, string|UIRawMessage $defaultValue = null, string|UIRawMessage $tooltip = null) : self{ + $textFieldElement = [ + 'type' => 'input', + 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, + 'placeholder' => $placeholderText instanceof UIRawMessage ? $placeholderText->encode() : $placeholderText, + ]; + if($defaultValue !== null){ + $textFieldElement['default'] = $defaultValue instanceof UIRawMessage ? $defaultValue->encode() : $defaultValue; + } + if($tooltip !== null){ + $textFieldElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; + } + $this->controls[] = $textFieldElement; + $this->validators[] = static function($value) : string{ + if(!is_string($value)){ + throw new FormValidationException("Expected string for text field response, got " . gettype($value)); + } + return $value; + }; + + return $this; + } + + public function toggle(string|UIRawMessage $label, bool $defaultValue = null, string|UIRawMessage $tooltip = null) : self{ + $toggleElement = [ + 'type' => 'toggle', + 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, + ]; + if($defaultValue !== null){ + $toggleElement['default'] = $defaultValue; + } + if($tooltip !== null){ + $toggleElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; + } + $this->controls[] = $toggleElement; + $this->validators[] = static function($value) : bool{ + if(!is_bool($value)){ + throw new FormValidationException("Expected boolean for toggle response, got " . gettype($value)); + } + return $value; + }; + + return $this; + } + + /** + * @internal + */ + public function toPacketFormData() : array{ + $data = [ + 'type' => 'custom_form', + 'icon' => null, + 'title' => $this->title ?? '', + 'content' => $this->controls, + ]; + if($this->submit !== null){ + $data['submit'] = $this->submit; + } + return $data; + } + + /** + * @internal + */ + public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : ModalFormResponse{ + if($cancelReason !== null){ + return new ModalFormResponse($cancelReason, null); + } + if($rawData !== null){ + try{ + $data = json_decode($rawData, true, 2, JSON_THROW_ON_ERROR); + }catch(\JsonException $e){ + throw PacketHandlingException::wrap($e, "Failed to decode form response data"); + } + if(!is_array($data)){ + throw new FormValidationException("Expected array, got $rawData"); + } + + $controlCount = count($this->controls); + $interactiveCount = 0; + foreach($this->validators as $v){ + if($v !== null) $interactiveCount++; + } + $actual = count($data); + + if($actual === $controlCount){ + $results = []; + foreach($this->validators as $index => $validator){ + if($validator === null){ + $results[] = null; + continue; + } + if(!array_key_exists($index, $data)){ + throw new FormValidationException("Missing expected result element at index $index"); + } + $results[] = $validator($data[$index]); + } + return new ModalFormResponse(null, $results); + }elseif($actual < $controlCount){ + // TODO: Verify this with 1.21.70 clients + $filteredData = array_values(array_filter($data, static fn($value) => $value !== null)); + if(count($filteredData) === $interactiveCount){ + $results = []; + $interactiveIndex = 0; + foreach($this->validators as $validator){ + if($validator === null){ + $results[] = null; + continue; + } + $results[] = $validator($filteredData[$interactiveIndex]); + $interactiveIndex++; + } + return new ModalFormResponse(null, $results); + } + } + + throw new FormValidationException("Unexpected number of result elements: expected either $controlCount or $interactiveCount (compact), got $actual"); + } + throw new \InvalidArgumentException("Expected rawData to be non-null"); + } +} diff --git a/src/Azvyl/PMServerUI/ui/ModalFormResponse.php b/src/Azvyl/PMServerUI/ui/ModalFormResponse.php new file mode 100644 index 0000000..746fd5e --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/ModalFormResponse.php @@ -0,0 +1,29 @@ +|null $formValues + */ + public function __construct(?FormCancelationReason $cancelationReason, public ?array $formValues){ + parent::__construct($cancelationReason, $cancelationReason !== null); + } +} diff --git a/src/Azvyl/PMServerUI/ui/ServerUI.php b/src/Azvyl/PMServerUI/ui/ServerUI.php new file mode 100644 index 0000000..a8eed4e --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/ServerUI.php @@ -0,0 +1,38 @@ +___send($player, $this); + } + + abstract public function toPacketFormData() : array; + + abstract public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : FormResponse; + +} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ui/UIManager.php b/src/Azvyl/PMServerUI/ui/UIManager.php new file mode 100644 index 0000000..8b1a9d6 --- /dev/null +++ b/src/Azvyl/PMServerUI/ui/UIManager.php @@ -0,0 +1,144 @@ +> */ + private array $playerForms = []; + + public function __construct(Plugin $plugin){ + $plManager = Server::getInstance()->getPluginManager(); + $plManager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{ + $player = $event->getPlayer(); + $playerId = $player->getId(); + if(isset($this->playerForms[$playerId])){ + foreach($this->playerForms[$playerId] as [, $promise]){ + try{ + $promise->reject(FormRejectError::create(FormRejectReason::PlayerQuit, "Player disconnected")); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + unset($this->playerForms[$playerId]); + } + }, EventPriority::LOWEST, $plugin); + $plManager->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event) : void{ + $packet = $event->getPacket(); + if($packet instanceof ModalFormResponsePacket){ + $player = $event->getOrigin()->getPlayer(); + if($player === null){ + return; + } + $playerId = $player->getId(); + if(isset($this->playerForms[$playerId][$packet->formId])){ + [$ui, $promise] = $this->playerForms[$playerId][$packet->formId]; + unset($this->playerForms[$playerId][$packet->formId]); + $event->cancel(); + + try{ + if($packet->cancelReason !== null){ + $reason = FormCancelationReason::cases()[$packet->cancelReason] ?? null; + if($reason !== null){ + $promise->resolve($ui->processResponse(cancelReason: $reason)); + }else{ + throw new FormValidationException("Player {$player->getName()} sent unknown cancel reason"); + } + }elseif($packet->formData !== null){ + var_dump($packet->formData); + $maxFormResponseSize = 10 * 1024; + if(strlen($packet->formData) > $maxFormResponseSize){ + throw new PacketHandlingException("Form response data too large, refusing to decode (received" . strlen($packet->formData) . " bytes, max $maxFormResponseSize bytes)"); + } + $trimmedData = trim($packet->formData); + if($trimmedData === "null" || $trimmedData === ""){ + throw new FormValidationException("Form response can't be null without cancel reason"); + } + $promise->resolve($ui->processResponse(rawData: $packet->formData)); + }else{ + throw new PacketHandlingException("Expected either formData or cancelReason to be set in ModalFormResponsePacket"); + } + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + try{ + if($t instanceof FormValidationException){ + $promise->reject(FormRejectError::create(FormRejectReason::MalformedResponse, "Failed to process form: {$t->getMessage()}", previous: $t)); + return; + } + $promise->reject(FormRejectError::create(FormRejectReason::ServerShutdown, "Crashed when handling packet: {$t->getMessage()}", previous: $t)); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + } + } + }, EventPriority::LOW, $plugin); + } + + /** + * Close all forms for a player. + */ + public function closeAllForms(Player $player) : void{ + throw new \RuntimeException("Not implemented yet"); + } + + /** + * @internal + */ + public function ___track(Player $player, int $formId, ServerUI $ui, Promise $promise) : void{ + // Note: A vanilla client should only have one form open at a time + if(($this->playerForms[$player->getId()] ?? []) !== []){ + PMServerUI::getLogger()->debug("Player {$player->getName()} ({$player->getId()}) had multiple open forms. This may indicate a suspicious client or a bug in a UI plugin."); + } + $this->playerForms[$player->getId()][$formId] = [$ui, $promise]; + } + + /** + * @internal + */ + public function ___send(Player $player, ServerUI $ui) : Promise{ + $promise = new Promise(); + (function(UIManager $UIManager, ServerUI $ui, Promise $promise) : void{ + /** @noinspection PhpUndefinedFieldInspection */ + $id = $this->formIdCounter++;//$this is Player instance due to closure binding + /** @noinspection PhpUndefinedMethodInspection */ + if($this->getNetworkSession()->sendDataPacket(ModalFormRequestPacket::create($id, json_encode($ui->toPacketFormData(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)))){ + /** @noinspection PhpParamsInspection */ + $UIManager->___track($this, $id, $ui, $promise); + } + })->call($player, $this, $ui, $promise); + return $promise; + } +} From 403f01f076b74b32625a80e68f55f1c3f8177c71 Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Sat, 7 Feb 2026 07:30:10 +0700 Subject: [PATCH 4/8] ... --- src/Azvyl/PMServerUI/UIRawMessage.php | 4 +--- src/Azvyl/PMServerUI/ddui/CustomForm.php | 16 ++++----------- src/Azvyl/PMServerUI/ddui/DropdownItem.php | 4 +--- src/Azvyl/PMServerUI/ddui/MessageBox.php | 20 ++++++------------- src/Azvyl/PMServerUI/ddui/Subscription.php | 4 +--- src/Azvyl/PMServerUI/ui/ActionFormData.php | 12 +++-------- .../PMServerUI/ui/ActionFormResponse.php | 4 +--- .../PMServerUI/ui/FormCancelationReason.php | 4 +--- src/Azvyl/PMServerUI/ui/FormRejectError.php | 4 +--- src/Azvyl/PMServerUI/ui/FormRejectReason.php | 4 +--- src/Azvyl/PMServerUI/ui/MessageFormData.php | 12 +++-------- .../PMServerUI/ui/MessageFormResponse.php | 4 +--- src/Azvyl/PMServerUI/ui/ModalFormData.php | 12 +++-------- src/Azvyl/PMServerUI/ui/ServerUI.php | 4 +--- src/Azvyl/PMServerUI/ui/UIManager.php | 16 ++++----------- 15 files changed, 32 insertions(+), 92 deletions(-) diff --git a/src/Azvyl/PMServerUI/UIRawMessage.php b/src/Azvyl/PMServerUI/UIRawMessage.php index 952065d..460bb93 100644 --- a/src/Azvyl/PMServerUI/UIRawMessage.php +++ b/src/Azvyl/PMServerUI/UIRawMessage.php @@ -17,9 +17,7 @@ use pocketmine\utils\Utils; use function array_map; -/** - * A subset of the RawMessage type, and is used for UI messages. - */ +/** A subset of the RawMessage type, and is used for UI messages. */ final class UIRawMessage{ /** diff --git a/src/Azvyl/PMServerUI/ddui/CustomForm.php b/src/Azvyl/PMServerUI/ddui/CustomForm.php index d6682ff..89987f3 100644 --- a/src/Azvyl/PMServerUI/ddui/CustomForm.php +++ b/src/Azvyl/PMServerUI/ddui/CustomForm.php @@ -18,9 +18,7 @@ use Azvyl\PMServerUI\UIRawMessage; use pocketmine\player\Player; -/** - * A customizable form that lets you put buttons, labels, toggles, dropdowns, sliders, and more into a form. - */ +/** A customizable form that lets you put buttons, labels, toggles, dropdowns, sliders, and more into a form. */ final class CustomForm extends DDUI{ /** @@ -46,23 +44,17 @@ public function button(Observable|string|UIRawMessage $label, callable $onClick, throw new \RuntimeException("Not implemented"); } - /** - * Can this form be shown to the player right now? - */ + /** Can this form be shown to the player right now? */ public function canShow() : bool{ throw new \RuntimeException("Not implemented"); } - /** - * Closes the form. Throws an error if the form is not open. - */ + /** Closes the form. Throws an error if the form is not open. */ public function close() : void{ throw new \RuntimeException("Not implemented"); } - /** - * Adds a close "X" button to the form. - */ + /** Adds a close "X" button to the form. */ public function closeButton() : self{ throw new \RuntimeException("Not implemented"); } diff --git a/src/Azvyl/PMServerUI/ddui/DropdownItem.php b/src/Azvyl/PMServerUI/ddui/DropdownItem.php index 18aa369..7bad118 100644 --- a/src/Azvyl/PMServerUI/ddui/DropdownItem.php +++ b/src/Azvyl/PMServerUI/ddui/DropdownItem.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ddui; -/** - * Dropdown data for use in CustomForm. - */ +/** Dropdown data for use in CustomForm. */ final class DropdownItem{ /** * @param string $label The label of the dropdown item in the dropdown. diff --git a/src/Azvyl/PMServerUI/ddui/MessageBox.php b/src/Azvyl/PMServerUI/ddui/MessageBox.php index 5c450dc..90cafd2 100644 --- a/src/Azvyl/PMServerUI/ddui/MessageBox.php +++ b/src/Azvyl/PMServerUI/ddui/MessageBox.php @@ -18,14 +18,10 @@ use Azvyl\PMServerUI\UIRawMessage; use pocketmine\player\Player; -/** - * A simple 2-button modal message box. - */ +/** A simple 2-button modal message box. */ final class MessageBox extends DDUI{ - /** - * Creates a message form for a certain player. - */ + /** Creates a message form for a certain player. */ public static function create(Player $player) : self{ throw new \RuntimeException("Not implemented"); } @@ -42,7 +38,7 @@ public function body(Observable|string|UIRawMessage $text) : self{ /** * Sets the data for the top button in the form. * - * @param Observable|string|UIRawMessage $label + * @param Observable|string|UIRawMessage $label * @param Observable|string|UIRawMessage|null $tooltip */ public function button1(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ @@ -52,23 +48,19 @@ public function button1(Observable|string|UIRawMessage $label, Observable|string /** * Sets the data for the bottom button in the form. * - * @param Observable|string|UIRawMessage $label + * @param Observable|string|UIRawMessage $label * @param Observable|string|UIRawMessage|null $tooltip */ public function button2(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ throw new \RuntimeException("Not implemented"); } - /** - * Closes the form. Will throw an error if the form is not open. - */ + /** Closes the form. Will throw an error if the form is not open. */ public function close() : void{ throw new \RuntimeException("Not implemented"); } - /** - * Show this modal to the player. Will throw an error if the modal is already showing. - */ + /** Show this modal to the player. Will throw an error if the modal is already showing. */ public function show(Player $player) : Promise{ throw new \RuntimeException("Not implemented"); } diff --git a/src/Azvyl/PMServerUI/ddui/Subscription.php b/src/Azvyl/PMServerUI/ddui/Subscription.php index 1a64264..f110504 100644 --- a/src/Azvyl/PMServerUI/ddui/Subscription.php +++ b/src/Azvyl/PMServerUI/ddui/Subscription.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ddui; -/** - * Represents a subscription returned from Observable::subscribe. - */ +/** Represents a subscription returned from Observable::subscribe. */ final class Subscription{ private bool $active = true; /** @var callable|null */ diff --git a/src/Azvyl/PMServerUI/ui/ActionFormData.php b/src/Azvyl/PMServerUI/ui/ActionFormData.php index 91c39fc..7cf8320 100644 --- a/src/Azvyl/PMServerUI/ui/ActionFormData.php +++ b/src/Azvyl/PMServerUI/ui/ActionFormData.php @@ -18,9 +18,7 @@ use pocketmine\form\FormValidationException; use function is_numeric; -/** - * Builder for a simple action form with a list of buttons. - */ +/** Builder for a simple action form with a list of buttons. */ final class ActionFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $body = null; @@ -65,9 +63,7 @@ public function label(string|UIRawMessage $text) : self{ return $this; } - /** - * @internal - */ + /** @internal */ public function toPacketFormData() : array{ return [ 'type' => 'form', @@ -77,9 +73,7 @@ public function toPacketFormData() : array{ ]; } - /** - * @internal - */ + /** @internal */ public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : ActionFormResponse{ if($cancelReason !== null){ return new ActionFormResponse($cancelReason, null); diff --git a/src/Azvyl/PMServerUI/ui/ActionFormResponse.php b/src/Azvyl/PMServerUI/ui/ActionFormResponse.php index 508e432..be78a4f 100644 --- a/src/Azvyl/PMServerUI/ui/ActionFormResponse.php +++ b/src/Azvyl/PMServerUI/ui/ActionFormResponse.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ui; -/** - * Response for ActionFormData. Contains the selected button index. - */ +/** Response for ActionFormData. Contains the selected button index. */ readonly class ActionFormResponse extends FormResponse{ /** * @param int|null $selection Returns the index of the button that was pushed. diff --git a/src/Azvyl/PMServerUI/ui/FormCancelationReason.php b/src/Azvyl/PMServerUI/ui/FormCancelationReason.php index 0401499..db386c8 100644 --- a/src/Azvyl/PMServerUI/ui/FormCancelationReason.php +++ b/src/Azvyl/PMServerUI/ui/FormCancelationReason.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ui; -/** - * Reasons why a form was canceled. - */ +/** Reasons why a form was canceled. */ enum FormCancelationReason: string{ case UserBusy = 'UserBusy'; case UserClosed = 'UserClosed'; diff --git a/src/Azvyl/PMServerUI/ui/FormRejectError.php b/src/Azvyl/PMServerUI/ui/FormRejectError.php index 159717c..a3c1d75 100644 --- a/src/Azvyl/PMServerUI/ui/FormRejectError.php +++ b/src/Azvyl/PMServerUI/ui/FormRejectError.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ui; -/** - * Error thrown when a form response is rejected for a reason. - */ +/** Error thrown when a form response is rejected for a reason. */ class FormRejectError extends \RuntimeException{ public function __construct(public FormRejectReason $reason, string $message = "Form rejected", ?\Throwable $previous = null){ parent::__construct($message, previous: $previous); diff --git a/src/Azvyl/PMServerUI/ui/FormRejectReason.php b/src/Azvyl/PMServerUI/ui/FormRejectReason.php index 02b0e41..eb60287 100644 --- a/src/Azvyl/PMServerUI/ui/FormRejectReason.php +++ b/src/Azvyl/PMServerUI/ui/FormRejectReason.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ui; -/** - * Reasons why a form response was rejected. - */ +/** Reasons why a form response was rejected. */ enum FormRejectReason: string{ case MalformedResponse = 'MalformedResponse'; case PlayerQuit = 'PlayerQuit'; diff --git a/src/Azvyl/PMServerUI/ui/MessageFormData.php b/src/Azvyl/PMServerUI/ui/MessageFormData.php index bb7512a..e3467ac 100644 --- a/src/Azvyl/PMServerUI/ui/MessageFormData.php +++ b/src/Azvyl/PMServerUI/ui/MessageFormData.php @@ -17,9 +17,7 @@ use Azvyl\PMServerUI\UIRawMessage; use pocketmine\form\FormValidationException; -/** - * Builder for a simple message form with two buttons. - */ +/** Builder for a simple message form with two buttons. */ final class MessageFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $body = null; @@ -46,9 +44,7 @@ public function button2(string|UIRawMessage $text) : self{ return $this; } - /** - * @internal - */ + /** @internal */ public function toPacketFormData() : array{ return [ 'type' => 'modal', @@ -59,9 +55,7 @@ public function toPacketFormData() : array{ ]; } - /** - * @internal - */ + /** @internal */ public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : MessageFormResponse{ if($cancelReason !== null){ return new MessageFormResponse($cancelReason, null); diff --git a/src/Azvyl/PMServerUI/ui/MessageFormResponse.php b/src/Azvyl/PMServerUI/ui/MessageFormResponse.php index 801d218..428ec40 100644 --- a/src/Azvyl/PMServerUI/ui/MessageFormResponse.php +++ b/src/Azvyl/PMServerUI/ui/MessageFormResponse.php @@ -14,9 +14,7 @@ namespace Azvyl\PMServerUI\ui; -/** - * Response for MessageFormData. Contains selection index (0 or 1) or null. - */ +/** Response for MessageFormData. Contains selection index (0 or 1) or null. */ readonly class MessageFormResponse extends FormResponse{ /** * @param int|null $selection Returns the index of the button that was pushed. diff --git a/src/Azvyl/PMServerUI/ui/ModalFormData.php b/src/Azvyl/PMServerUI/ui/ModalFormData.php index b3dd61f..f20fe27 100644 --- a/src/Azvyl/PMServerUI/ui/ModalFormData.php +++ b/src/Azvyl/PMServerUI/ui/ModalFormData.php @@ -30,9 +30,7 @@ use function is_string; use function json_decode; -/** - * Builder for a customizable modal form. - */ +/** Builder for a customizable modal form. */ final class ModalFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $submit = null; @@ -175,9 +173,7 @@ public function toggle(string|UIRawMessage $label, bool $defaultValue = null, st return $this; } - /** - * @internal - */ + /** @internal */ public function toPacketFormData() : array{ $data = [ 'type' => 'custom_form', @@ -191,9 +187,7 @@ public function toPacketFormData() : array{ return $data; } - /** - * @internal - */ + /** @internal */ public function processResponse(string $rawData = null, FormCancelationReason $cancelReason = null) : ModalFormResponse{ if($cancelReason !== null){ return new ModalFormResponse($cancelReason, null); diff --git a/src/Azvyl/PMServerUI/ui/ServerUI.php b/src/Azvyl/PMServerUI/ui/ServerUI.php index a8eed4e..bc67ee2 100644 --- a/src/Azvyl/PMServerUI/ui/ServerUI.php +++ b/src/Azvyl/PMServerUI/ui/ServerUI.php @@ -24,9 +24,7 @@ public static function create() : static{ return new static(); } - /** - * Show the form to a player. Returns a Promise that resolves with a FormResponse. - */ + /** Show the form to a player. Returns a Promise that resolves with a FormResponse. */ final public function show(Player $player) : Promise{ return PMServerUI::getUIManager()->___send($player, $this); } diff --git a/src/Azvyl/PMServerUI/ui/UIManager.php b/src/Azvyl/PMServerUI/ui/UIManager.php index 8b1a9d6..9a6b6ed 100644 --- a/src/Azvyl/PMServerUI/ui/UIManager.php +++ b/src/Azvyl/PMServerUI/ui/UIManager.php @@ -31,9 +31,7 @@ use function trim; use function var_dump; -/** - * Manager responsible for opening and closing UI forms. - */ +/** Manager responsible for opening and closing UI forms. */ final class UIManager{ /** @var array> */ @@ -107,16 +105,12 @@ public function __construct(Plugin $plugin){ }, EventPriority::LOW, $plugin); } - /** - * Close all forms for a player. - */ + /** Close all forms for a player. */ public function closeAllForms(Player $player) : void{ throw new \RuntimeException("Not implemented yet"); } - /** - * @internal - */ + /** @internal */ public function ___track(Player $player, int $formId, ServerUI $ui, Promise $promise) : void{ // Note: A vanilla client should only have one form open at a time if(($this->playerForms[$player->getId()] ?? []) !== []){ @@ -125,9 +119,7 @@ public function ___track(Player $player, int $formId, ServerUI $ui, Promise $pro $this->playerForms[$player->getId()][$formId] = [$ui, $promise]; } - /** - * @internal - */ + /** @internal */ public function ___send(Player $player, ServerUI $ui) : Promise{ $promise = new Promise(); (function(UIManager $UIManager, ServerUI $ui, Promise $promise) : void{ From a5bd97a3b2cbbadd45f84373132809b5a254d7f3 Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:35:19 +0700 Subject: [PATCH 5/8] Reflect latest server ui changes --- src/Azvyl/PMServerUI/PMServerUI.php | 7 +++ src/Azvyl/PMServerUI/ddui/CustomForm.php | 59 +++++++++++++++------ src/Azvyl/PMServerUI/ddui/DDUIManager.php | 33 ++++++++++++ src/Azvyl/PMServerUI/ddui/MessageBox.php | 32 +++++++---- src/Azvyl/PMServerUI/ui/ActionFormData.php | 2 +- src/Azvyl/PMServerUI/ui/MessageFormData.php | 2 +- src/Azvyl/PMServerUI/ui/ModalFormData.php | 40 +++++++------- 7 files changed, 127 insertions(+), 48 deletions(-) create mode 100644 src/Azvyl/PMServerUI/ddui/DDUIManager.php diff --git a/src/Azvyl/PMServerUI/PMServerUI.php b/src/Azvyl/PMServerUI/PMServerUI.php index 0810e5a..71b1dab 100644 --- a/src/Azvyl/PMServerUI/PMServerUI.php +++ b/src/Azvyl/PMServerUI/PMServerUI.php @@ -14,6 +14,7 @@ namespace Azvyl\PMServerUI; +use Azvyl\PMServerUI\ddui\DDUIManager; use Azvyl\PMServerUI\ui\UIManager; use pocketmine\plugin\Plugin; @@ -24,6 +25,7 @@ private function __construct(){ } private static ?Plugin $plugin = null; private static \PrefixedLogger $logger; private static UIManager $uiManager; + private static ?DDUIManager $dduiManager = null; public static function register(Plugin $plugin) : void{ if(self::$plugin instanceof Plugin){ @@ -33,6 +35,7 @@ public static function register(Plugin $plugin) : void{ self::$plugin = $plugin; self::$logger = new \PrefixedLogger($plugin->getLogger(), "PMServerUI"); self::$uiManager = new UIManager($plugin); + self::$dduiManager = new DDUIManager($plugin); //TODO: make options easier to configure. //crash on unhandled error. @@ -59,4 +62,8 @@ public static function getLogger() : \PrefixedLogger{ public static function getUIManager() : UIManager{ return self::$uiManager ?? throw new \LogicException("PMServerUI has not been registered"); } + + public static function getDDUIManager() : DDUIManager{ + return self::$dduiManager ?? throw new \LogicException("PMServerUI has not been registered"); + } } \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ddui/CustomForm.php b/src/Azvyl/PMServerUI/ddui/CustomForm.php index 89987f3..6633662 100644 --- a/src/Azvyl/PMServerUI/ddui/CustomForm.php +++ b/src/Azvyl/PMServerUI/ddui/CustomForm.php @@ -28,7 +28,9 @@ final class CustomForm extends DDUI{ * @param Observable|string|UIRawMessage $title The title of the form. */ public static function create(Player $player, Observable|string|UIRawMessage $title) : self{ - throw new \RuntimeException("Not implemented"); + $instance = new self(); + // TODO: to be implemented + return $instance; } /** @@ -41,22 +43,19 @@ public static function create(Player $player, Observable|string|UIRawMessage $ti * @param bool|Observable $visible */ public function button(Observable|string|UIRawMessage $label, callable $onClick, bool|Observable $disabled = null, Observable|string|UIRawMessage $tooltip = null, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); - } - - /** Can this form be shown to the player right now? */ - public function canShow() : bool{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** Closes the form. Throws an error if the form is not open. */ public function close() : void{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented } /** Adds a close "X" button to the form. */ public function closeButton() : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -65,7 +64,8 @@ public function closeButton() : self{ * @param bool|Observable $visible Whether the divider is visible. */ public function divider(bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -80,7 +80,8 @@ public function divider(bool|Observable $visible = null) : self{ * @param bool|Observable $visible */ public function dropdown(Observable|string|UIRawMessage $label, Observable $value, array $items, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -90,7 +91,25 @@ public function dropdown(Observable|string|UIRawMessage $label, Observable $valu * @param bool|Observable $visible */ public function label(Observable|string|UIRawMessage $text, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; + } + + /** + * Inserts a header (i.e. large-sized text) into the Custom form. + * + * @param Observable|string|UIRawMessage $text The text to display in the header. + * @param bool|Observable $visible Whether the header is visible. + */ + public function header(Observable|string|UIRawMessage $text, bool|Observable $visible = null) : self{ + // TODO: to be implemented + return $this; + } + + /** Returns true if the form is currently being shown to the player. */ + public function isShowing() : bool{ + // TODO: to be implemented + return false; } /** @@ -98,7 +117,9 @@ public function label(Observable|string|UIRawMessage $text, bool|Observable $vis * is showing a form. */ public function show() : Promise{ - throw new \RuntimeException("Not implemented"); + $promise = new Promise(); + // TODO: to be implemented + return $promise; } /** @@ -114,7 +135,8 @@ public function show() : Promise{ * @param bool|Observable|null $visible */ public function slider(Observable|string|UIRawMessage $label, Observable $value, int|float $minValue, int|float $maxValue, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, int|float|Observable $step = null, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -123,7 +145,8 @@ public function slider(Observable|string|UIRawMessage $label, Observable $value, * @param bool|Observable|null $visible Whether the spacer is visible. */ public function spacer(bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -136,7 +159,8 @@ public function spacer(bool|Observable $visible = null) : self{ * @param bool|Observable|null $visible */ public function textField(Observable|string|UIRawMessage $label, Observable $text, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -149,6 +173,7 @@ public function textField(Observable|string|UIRawMessage $label, Observable $tex * @param bool|Observable|null $visible */ public function toggle(Observable|string|UIRawMessage $label, Observable $toggled, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } } diff --git a/src/Azvyl/PMServerUI/ddui/DDUIManager.php b/src/Azvyl/PMServerUI/ddui/DDUIManager.php new file mode 100644 index 0000000..7fd4866 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/DDUIManager.php @@ -0,0 +1,33 @@ +getPluginManager(); + + $plManager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{ + + }, EventPriority::LOWEST, $plugin); + + $plManager->registerEvent(DataPacketDecodeEvent::class, function(DataPacketDecodeEvent $event) : void{ + + }, EventPriority::LOWEST, $plugin); + + $plManager->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event) : void{ + + }, EventPriority::LOW, $plugin); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/MessageBox.php b/src/Azvyl/PMServerUI/ddui/MessageBox.php index 90cafd2..b4d2640 100644 --- a/src/Azvyl/PMServerUI/ddui/MessageBox.php +++ b/src/Azvyl/PMServerUI/ddui/MessageBox.php @@ -22,8 +22,10 @@ final class MessageBox extends DDUI{ /** Creates a message form for a certain player. */ - public static function create(Player $player) : self{ - throw new \RuntimeException("Not implemented"); + public static function create(Player $player, Observable|string|UIRawMessage $title) : self{ + $instance = new self(); + // TODO: to be implemented + return $instance; } /** @@ -32,7 +34,8 @@ public static function create(Player $player) : self{ * @param Observable|string|UIRawMessage $text */ public function body(Observable|string|UIRawMessage $text) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -42,7 +45,8 @@ public function body(Observable|string|UIRawMessage $text) : self{ * @param Observable|string|UIRawMessage|null $tooltip */ public function button1(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** @@ -52,17 +56,26 @@ public function button1(Observable|string|UIRawMessage $label, Observable|string * @param Observable|string|UIRawMessage|null $tooltip */ public function button2(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } /** Closes the form. Will throw an error if the form is not open. */ public function close() : void{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + } + + /** Returns true if the message box is currently being shown to the player. */ + public function isShowing() : bool{ + // TODO: to be implemented + return false; } /** Show this modal to the player. Will throw an error if the modal is already showing. */ - public function show(Player $player) : Promise{ - throw new \RuntimeException("Not implemented"); + public function show() : Promise{ + $promise = new Promise(); + //TODO: to be implemented + return $promise; } /** @@ -71,6 +84,7 @@ public function show(Player $player) : Promise{ * @param Observable|string|UIRawMessage $text */ public function title(Observable|string|UIRawMessage $text) : self{ - throw new \RuntimeException("Not implemented"); + // TODO: to be implemented + return $this; } } diff --git a/src/Azvyl/PMServerUI/ui/ActionFormData.php b/src/Azvyl/PMServerUI/ui/ActionFormData.php index 7cf8320..84645d6 100644 --- a/src/Azvyl/PMServerUI/ui/ActionFormData.php +++ b/src/Azvyl/PMServerUI/ui/ActionFormData.php @@ -19,7 +19,7 @@ use function is_numeric; /** Builder for a simple action form with a list of buttons. */ -final class ActionFormData extends ServerUI{ +class ActionFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $body = null; private array $elements = []; diff --git a/src/Azvyl/PMServerUI/ui/MessageFormData.php b/src/Azvyl/PMServerUI/ui/MessageFormData.php index e3467ac..c57be75 100644 --- a/src/Azvyl/PMServerUI/ui/MessageFormData.php +++ b/src/Azvyl/PMServerUI/ui/MessageFormData.php @@ -18,7 +18,7 @@ use pocketmine\form\FormValidationException; /** Builder for a simple message form with two buttons. */ -final class MessageFormData extends ServerUI{ +class MessageFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $body = null; private array|null|string $button1 = null; diff --git a/src/Azvyl/PMServerUI/ui/ModalFormData.php b/src/Azvyl/PMServerUI/ui/ModalFormData.php index f20fe27..df7ed1b 100644 --- a/src/Azvyl/PMServerUI/ui/ModalFormData.php +++ b/src/Azvyl/PMServerUI/ui/ModalFormData.php @@ -31,7 +31,7 @@ use function json_decode; /** Builder for a customizable modal form. */ -final class ModalFormData extends ServerUI{ +class ModalFormData extends ServerUI{ private array|null|string $title = null; private array|null|string $submit = null; /** @var array> */ @@ -53,14 +53,14 @@ public function divider() : self{ /** * @param string[]|UIRawMessage[] $items */ - public function dropdown(string|UIRawMessage $label, array $items, int $defaultValueIndex = null, string|UIRawMessage $tooltip = null) : self{ + public function dropdown(string|UIRawMessage $label, array $items, int $defaultIndex = null, string|UIRawMessage $tooltip = null) : self{ $dropdownElement = [ 'type' => 'dropdown', 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, 'options' => array_map(fn(string|UIRawMessage $item) => $item instanceof UIRawMessage ? $item->encode() : $item, $items) ]; - if($defaultValueIndex !== null){ - $dropdownElement['default'] = $defaultValueIndex; + if($defaultIndex !== null){ + $dropdownElement['default'] = $defaultIndex; } if($tooltip !== null){ $dropdownElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; @@ -93,29 +93,29 @@ public function label(string|UIRawMessage $text) : self{ return $this; } - public function slider(string|UIRawMessage $label, int $minValue, int $maxValue, int $defaultValue = null, string|UIRawMessage $tooltip = null, int $valueStep = null) : self{ + public function slider(string|UIRawMessage $label, int $min, int $max, int $default = null, string|UIRawMessage $tooltip = null, int $step = null) : self{ $sliderElement = [ 'type' => 'slider', 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, - 'min' => (float) $minValue, - 'max' => (float) $maxValue, - 'step' => (float) ($valueStep ?? 1.0), + 'min' => (float) $min, + 'max' => (float) $max, + 'step' => (float) ($step ?? 1.0), 'timeout' => 100.0,//TODO: Find out what this does (1.26.0.29) ]; - if($defaultValue !== null){ - $sliderElement['default'] = $defaultValue; + if($default !== null){ + $sliderElement['default'] = $default; } if($tooltip !== null){ $sliderElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; } $this->controls[] = $sliderElement; - $this->validators[] = static function($value) use ($minValue, $maxValue) : float{ + $this->validators[] = static function($value) use ($min, $max) : float{ if(!is_int($value) && !is_float($value)){ throw new FormValidationException("Expected numeric value for slider response, got " . gettype($value)); } $numeric = (float) $value; - if($numeric < $minValue || $numeric > $maxValue){ - throw new FormValidationException("Slider response out of range ($minValue..$maxValue), got $numeric"); + if($numeric < $min || $numeric > $max){ + throw new FormValidationException("Slider response out of range ($min..$max), got $numeric"); } return $numeric; }; @@ -128,14 +128,14 @@ public function submitButton(string|UIRawMessage $text) : self{ return $this; } - public function textField(string|UIRawMessage $label, string|UIRawMessage $placeholderText, string|UIRawMessage $defaultValue = null, string|UIRawMessage $tooltip = null) : self{ + public function textField(string|UIRawMessage $label, string|UIRawMessage $placeholderText = null, string|UIRawMessage $default = null, string|UIRawMessage $tooltip = null) : self{ $textFieldElement = [ 'type' => 'input', 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, - 'placeholder' => $placeholderText instanceof UIRawMessage ? $placeholderText->encode() : $placeholderText, + 'placeholder' => $placeholderText instanceof UIRawMessage ? $placeholderText->encode() : $placeholderText ?? "", ]; - if($defaultValue !== null){ - $textFieldElement['default'] = $defaultValue instanceof UIRawMessage ? $defaultValue->encode() : $defaultValue; + if($default !== null){ + $textFieldElement['default'] = $default instanceof UIRawMessage ? $default->encode() : $default; } if($tooltip !== null){ $textFieldElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; @@ -151,13 +151,13 @@ public function textField(string|UIRawMessage $label, string|UIRawMessage $place return $this; } - public function toggle(string|UIRawMessage $label, bool $defaultValue = null, string|UIRawMessage $tooltip = null) : self{ + public function toggle(string|UIRawMessage $label, bool $default = null, string|UIRawMessage $tooltip = null) : self{ $toggleElement = [ 'type' => 'toggle', 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, ]; - if($defaultValue !== null){ - $toggleElement['default'] = $defaultValue; + if($default !== null){ + $toggleElement['default'] = $default; } if($tooltip !== null){ $toggleElement['tooltip'] = $tooltip instanceof UIRawMessage ? $tooltip->encode() : $tooltip; From 6838a9a9a2981ddc9b8df4962e07961fb36ca222 Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:21:21 +0700 Subject: [PATCH 6/8] Full CustomForm DDUI support This ddui changes has been tested by 1.26.11 client --- src/Azvyl/PMServerUI/PMServerUI.php | 2 +- src/Azvyl/PMServerUI/Promise.php | 37 +- src/Azvyl/PMServerUI/UIRawMessage.php | 24 +- src/Azvyl/PMServerUI/ddui/CustomForm.php | 195 ++++++--- .../ddui/CustomFormPayloadComposer.php | 67 +++ .../ddui/CustomFormRenderContext.php | 133 ++++++ src/Azvyl/PMServerUI/ddui/DDUI.php | 12 +- src/Azvyl/PMServerUI/ddui/DDUIManager.php | 404 +++++++++++++++++- src/Azvyl/PMServerUI/ddui/DropdownItem.php | 16 +- src/Azvyl/PMServerUI/ddui/MessageBox.php | 29 +- src/Azvyl/PMServerUI/ddui/Observable.php | 52 ++- src/Azvyl/PMServerUI/ddui/Subscription.php | 12 +- .../ddui/elements/ButtonElement.php | 52 +++ .../ddui/elements/CustomFormElement.php | 17 + .../ddui/elements/DividerElement.php | 23 + .../ddui/elements/DropdownElement.php | 58 +++ .../ddui/elements/HeaderElement.php | 30 ++ .../PMServerUI/ddui/elements/LabelElement.php | 30 ++ .../ddui/elements/SliderElement.php | 49 +++ .../ddui/elements/SpacerElement.php | 23 + .../ddui/elements/TextFieldElement.php | 39 ++ .../ddui/elements/ToggleElement.php | 39 ++ .../packets/ClientboundDataStorePacket.php | 39 ++ .../types/BoolDataStorePropertyValue.php | 30 ++ .../ddui/packets/types/DataStoreChange.php | 57 +++ .../ddui/packets/types/DataStoreMapEntry.php | 32 ++ .../packets/types/DataStorePropertyType.php | 15 + .../packets/types/DataStorePropertyValue.php | 36 ++ .../types/Int64DataStorePropertyValue.php | 30 ++ .../types/ListDataStorePropertyValue.php | 46 ++ .../types/MapDataStorePropertyValue.php | 46 ++ .../types/NoneDataStorePropertyValue.php | 21 + .../types/StringDataStorePropertyValue.php | 30 ++ src/Azvyl/PMServerUI/ui/ActionFormData.php | 2 +- src/Azvyl/PMServerUI/ui/FormResponse.php | 4 +- src/Azvyl/PMServerUI/ui/ModalFormData.php | 8 +- src/Azvyl/PMServerUI/ui/ServerUI.php | 6 +- src/Azvyl/PMServerUI/ui/UIManager.php | 15 +- 38 files changed, 1595 insertions(+), 165 deletions(-) create mode 100644 src/Azvyl/PMServerUI/ddui/CustomFormPayloadComposer.php create mode 100644 src/Azvyl/PMServerUI/ddui/CustomFormRenderContext.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/ButtonElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/CustomFormElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/DividerElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/DropdownElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/HeaderElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/LabelElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/SliderElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/SpacerElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/TextFieldElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/elements/ToggleElement.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/ClientboundDataStorePacket.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/BoolDataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/DataStoreChange.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/DataStoreMapEntry.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/DataStorePropertyType.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/DataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/Int64DataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/ListDataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/MapDataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/NoneDataStorePropertyValue.php create mode 100644 src/Azvyl/PMServerUI/ddui/packets/types/StringDataStorePropertyValue.php diff --git a/src/Azvyl/PMServerUI/PMServerUI.php b/src/Azvyl/PMServerUI/PMServerUI.php index 71b1dab..0752b1b 100644 --- a/src/Azvyl/PMServerUI/PMServerUI.php +++ b/src/Azvyl/PMServerUI/PMServerUI.php @@ -20,7 +20,7 @@ final class PMServerUI{ - private function __construct(){ } + private function __construct(){} private static ?Plugin $plugin = null; private static \PrefixedLogger $logger; diff --git a/src/Azvyl/PMServerUI/Promise.php b/src/Azvyl/PMServerUI/Promise.php index cfa9dd6..49e4686 100644 --- a/src/Azvyl/PMServerUI/Promise.php +++ b/src/Azvyl/PMServerUI/Promise.php @@ -14,37 +14,50 @@ namespace Azvyl\PMServerUI; +/** + * @template TValue + */ final class Promise{ + /** @var TValue|null */ private mixed $result = null; private ?\Throwable $error = null; private bool $isResolved = false; private bool $isRejected = false; + /** @var array */ private array $onFulfilledCallbacks = []; + /** @var array */ private array $onRejectedCallbacks = []; + /** + * @param null|callable(callable(TValue):void, callable(\Throwable):void):void $executor + */ public function __construct(?callable $executor = null){ if($executor !== null){ $this->run($executor); } } + /** + * @param callable(callable(TValue):void, callable(\Throwable):void):void $executor + */ public function run(callable $executor) : void{ try{ - $executor(fn(mixed ...$value) => $this->resolve(...$value), fn(\Throwable $t) => $this->reject($t)); + $executor(fn(mixed $value) => $this->resolve($value), fn(\Throwable $t) => $this->reject($t)); }catch(\Throwable $e){ $this->reject($e); } } - public function resolve(mixed ...$value) : void{ + /** @param TValue $value */ + public function resolve(mixed $value) : void{ if($this->isResolved || $this->isRejected) return; $this->isResolved = true; $this->result = $value; foreach($this->onFulfilledCallbacks as $cb){ - $cb(...$value); + $cb($value); } $this->onFulfilledCallbacks = []; @@ -63,12 +76,18 @@ public function reject(\Throwable $reason) : void{ $this->onRejectedCallbacks = []; } + /** + * @template TNext + * @param null|callable(TValue):TNext $onFulfilled + * @param null|callable(\Throwable):TNext $onRejected + * @return Promise + */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : Promise{ return new self(function($resolve, $reject) use ($onFulfilled, $onRejected){ - $handleFulfilled = function(...$value) use ($onFulfilled, $resolve, $reject){ + $handleFulfilled = function(mixed $value) use ($onFulfilled, $resolve, $reject){ if($onFulfilled){ try{ - $result = $onFulfilled(...$value); + $result = $onFulfilled($value); $resolve($result); }catch(\Throwable $e){ $reject($e); @@ -78,7 +97,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null } }; - $handleRejected = function($reason) use ($onRejected, $resolve, $reject){ + $handleRejected = function(\Throwable $reason) use ($onRejected, $resolve, $reject){ if($onRejected){ try{ $result = $onRejected($reason); @@ -94,7 +113,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null if($this->isResolved){ $handleFulfilled($this->result); }elseif($this->isRejected){ - $handleRejected($this->error); + $handleRejected($this->error ?? new \RuntimeException("Promise rejected without reason")); }else{ $this->onFulfilledCallbacks[] = $handleFulfilled; $this->onRejectedCallbacks[] = $handleRejected; @@ -102,6 +121,10 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null }); } + /** + * @param null|callable(\Throwable):TValue $onRejected + * @return Promise + */ public function catch(?callable $onRejected = null) : Promise{ return $this->then(null, $onRejected); } diff --git a/src/Azvyl/PMServerUI/UIRawMessage.php b/src/Azvyl/PMServerUI/UIRawMessage.php index 460bb93..cc0b358 100644 --- a/src/Azvyl/PMServerUI/UIRawMessage.php +++ b/src/Azvyl/PMServerUI/UIRawMessage.php @@ -21,7 +21,7 @@ final class UIRawMessage{ /** - * @param UIRawMessage[] $rawtext + * @param UIRawMessage[] $rawtext * @param string[]|UIRawMessage $with */ public function __construct(public ?array $rawtext = null, public ?string $text = null, public ?string $translate = null, public null|array|UIRawMessage $with = null){ @@ -33,6 +33,7 @@ public function __construct(public ?array $rawtext = null, public ?string $text } } + /** Encodes this value into the Bedrock RawMessage payload format for network packets. */ public function encode() : array{ $entries = []; @@ -41,10 +42,15 @@ public function encode() : array{ if($this->with !== null){ if($this->with instanceof UIRawMessage){ - $encoded = $this->with->encode(); - $entry["with"] = $encoded["rawtext"] ?? []; + $entry["with"] = $this->with->encode(); }elseif(is_array($this->with)){ - $entry["with"] = array_map(fn(string $s) => ["text" => $s], $this->with); + $entry["with"] = ["rawtext" => array_map(function($v){ + if($v instanceof UIRawMessage){ + $enc = $v->encode(); + return $enc["rawtext"][0] ?? []; + } + return ["text" => (string)$v]; + }, $this->with)]; } } @@ -56,8 +62,14 @@ public function encode() : array{ } if($this->rawtext !== null){ - // Todo: Verify if this is correct - $entries[] = ["rawtext" => array_map(fn(UIRawMessage $child) => $child->encode(), $this->rawtext)]; + foreach($this->rawtext as $child){ + if(!($child instanceof UIRawMessage)) continue; + $childEnc = $child->encode(); + $childEntries = $childEnc["rawtext"] ?? []; + foreach($childEntries as $ce){ + $entries[] = $ce; + } + } } return ["rawtext" => $entries]; diff --git a/src/Azvyl/PMServerUI/ddui/CustomForm.php b/src/Azvyl/PMServerUI/ddui/CustomForm.php index 6633662..1ca972b 100644 --- a/src/Azvyl/PMServerUI/ddui/CustomForm.php +++ b/src/Azvyl/PMServerUI/ddui/CustomForm.php @@ -1,60 +1,95 @@ */ + private \WeakReference $playerRef; + /** @var Observable|Observable|string|UIRawMessage */ + private mixed $title; + /** @var int|null */ + private ?int $formId = null; + /** @var array */ + private array $elements = []; + private bool $showing = false; + private bool $closeButtonEnabled = false; + private CustomFormPayloadComposer $payloadComposer; /** * Create a CustomForm for a specific player. * - * @param Player $player The player to show the form to. - * @param Observable|string|UIRawMessage $title The title of the form. + * @param Player $player The player to show the form to. + * @param Observable|Observable|string|UIRawMessage $title The title of the form. */ public static function create(Player $player, Observable|string|UIRawMessage $title) : self{ $instance = new self(); - // TODO: to be implemented + $instance->playerRef = \WeakReference::create($player); + $instance->title = $title; + $instance->elements = []; + $instance->showing = false; + $instance->closeButtonEnabled = false; + $instance->payloadComposer = new CustomFormPayloadComposer(); return $instance; } /** * Inserts a button into the Custom form. onClick is called when the button is pressed. * - * @param Observable|string|UIRawMessage $label The text to display on the button. - * @param callable() : void $onClick The function to call when the button is clicked. - * @param bool|Observable $disabled - * @param Observable|string|UIRawMessage|null $tooltip The tooltip to display when hovering over the button. - * @param bool|Observable $visible + * @param Observable|Observable|string|UIRawMessage $label The text to display on the button. + * @param callable() : void $onClick The function to call when the button is clicked. + * @param bool|Observable $disabled + * @param Observable|Observable|string|UIRawMessage|null $tooltip The tooltip to display when hovering over the button. + * @param bool|Observable $visible */ public function button(Observable|string|UIRawMessage $label, callable $onClick, bool|Observable $disabled = null, Observable|string|UIRawMessage $tooltip = null, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new ButtonElement($label, $onClick, $disabled, $tooltip, $visible); return $this; } - /** Closes the form. Throws an error if the form is not open. */ + /** + * Close the form programmatically. + * + * Sends a close packet to the client. This method expects the form to be currently shown; it will throw if not. + */ public function close() : void{ - // TODO: to be implemented + if(!$this->showing){ + throw new \RuntimeException("Form is not open"); + } + $player = $this->playerRef->get(); + if($player === null){ + throw new \RuntimeException("Player reference is gone"); + } + $fid = $this->formId; + $player->getNetworkSession()->sendDataPacket(ClientboundDataDrivenUICloseScreenPacket::create($fid)); + $this->showing = false; } - /** Adds a close "X" button to the form. */ + /** Enable the standard close (X) control shown by the client. */ public function closeButton() : self{ - // TODO: to be implemented + $this->closeButtonEnabled = true; return $this; } @@ -64,7 +99,7 @@ public function closeButton() : self{ * @param bool|Observable $visible Whether the divider is visible. */ public function divider(bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new DividerElement($visible); return $this; } @@ -72,70 +107,100 @@ public function divider(bool|Observable $visible = null) : self{ * Inserts a dropdown into the Custom form with the provided items. The value is based on the items value that * selected. * - * @param Observable|string|UIRawMessage $label The text to display above the dropdown. - * @param Observable $value The currently selected index in the dropdown. - * @param array $items An array of DropdownItem to show in the dropdown. - * @param Observable|string|UIRawMessage|null $description - * @param bool|Observable $disabled - * @param bool|Observable $visible + * @param Observable|Observable|string|UIRawMessage $label The text to display above the dropdown. + * @param Observable|Observable $value The currently selected index in the dropdown. + * @param DropdownItem[] $items An array of DropdownItem to show in the dropdown. + * @param Observable|Observable|string|UIRawMessage|null $description + * @param bool|Observable $disabled + * @param bool|Observable $visible */ public function dropdown(Observable|string|UIRawMessage $label, Observable $value, array $items, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new DropdownElement($label, $value, $items, $description, $disabled, $visible); return $this; } /** * Inserts a label (i.e. medium-sized text) into the Custom form. * - * @param Observable|string|UIRawMessage $text The text to display in the label. - * @param bool|Observable $visible + * @param Observable|Observable|string|UIRawMessage $text The text to display in the label. + * @param bool|Observable $visible */ public function label(Observable|string|UIRawMessage $text, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new LabelElement($text, $visible); return $this; } /** * Inserts a header (i.e. large-sized text) into the Custom form. * - * @param Observable|string|UIRawMessage $text The text to display in the header. - * @param bool|Observable $visible Whether the header is visible. + * @param Observable|Observable|string|UIRawMessage $text The text to display in the header. + * @param bool|Observable $visible Whether the header is visible. */ public function header(Observable|string|UIRawMessage $text, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new HeaderElement($text, $visible); return $this; } - /** Returns true if the form is currently being shown to the player. */ + /** Returns true if this CustomForm has been shown and not yet acknowledged/closed. */ public function isShowing() : bool{ - // TODO: to be implemented - return false; + return $this->showing ?? false; } /** * Shows the form to the player. Will throw errors if the form is currently being shown or if another behavior pack * is showing a form. + * + * @return Promise */ public function show() : Promise{ + /** @var Promise $promise */ $promise = new Promise(); - // TODO: to be implemented + if($this->showing){ + throw new \RuntimeException("Form is already open"); + } + $player = $this->playerRef->get(); + if($player === null){ + throw new \RuntimeException("Player reference is gone"); + } + $manager = PMServerUI::getDDUIManager(); + $playerUuid = $player->getUniqueId()->getBytes(); + if($manager->hasActiveForm($playerUuid)){ + throw new \RuntimeException("Another DDUI form is already active for this player"); + } + $formId = $manager->nextFormId(); + + $this->formId = $formId; + $player->getNetworkSession()->sendDataPacket(ClientboundDataDrivenUIShowScreenPacket::create("minecraft:custom_form", $formId, null)); + + $mapValue = $this->payloadComposer->compose($manager, $playerUuid, $formId, $this->title, $this->closeButtonEnabled, $this->elements); + $updateCount = $manager->nextUpdateCountFor($playerUuid); + + $player->getNetworkSession()->sendDataPacket(ClientboundDataStorePacket::create([ + new ClientboundDataStoreChange('minecraft', 'custom_form_data', $updateCount, $mapValue), + new ClientboundDataStoreChange('minecraft', 'ddui_form_active', $updateCount, new BoolDataStorePropertyValue(true)) + ])); + + $this->showing = true; + + $manager->registerFormPromise($playerUuid, $formId, $promise); + return $promise; } /** * Creates a slider that lets players pick a number between minValue and maxValue. * - * @param Observable|string|UIRawMessage $label The text to display above the slider. - * @param Observable $value The current value observable (client-writable). - * @param int|float $minValue The minimum selectable value. - * @param int|float $maxValue The maximum selectable value. - * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. - * @param bool|Observable|null $disabled - * @param Observable|int|float|null $step The step size (increment) for the slider. - * @param bool|Observable|null $visible + * @param Observable|Observable|string|UIRawMessage $label The text to display above the slider. + * @param Observable|Observable $value The current value observable (client-writable). + * @param Observable|Observable|int|float $minValue The minimum selectable value. + * @param Observable|Observable|int|float $maxValue The maximum selectable value. + * @param Observable|Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param Observable|Observable|int|float|null $step The step size (increment) for the slider. + * @param bool|Observable|null $visible */ - public function slider(Observable|string|UIRawMessage $label, Observable $value, int|float $minValue, int|float $maxValue, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, int|float|Observable $step = null, bool|Observable $visible = null) : self{ - // TODO: to be implemented + public function slider(Observable|string|UIRawMessage $label, Observable $value, int|float|Observable $minValue, int|float|Observable $maxValue, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, int|float|Observable $step = null, bool|Observable $visible = null) : self{ + $this->elements[] = new SliderElement($label, $value, $minValue, $maxValue, $description, $disabled, $step, $visible); return $this; } @@ -145,35 +210,35 @@ public function slider(Observable|string|UIRawMessage $label, Observable $value, * @param bool|Observable|null $visible Whether the spacer is visible. */ public function spacer(bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new SpacerElement($visible); return $this; } /** * Inserts a text field into the Custom for that players can enter text into. * - * @param Observable|string|UIRawMessage $label The label shown above the text field. - * @param Observable $text The text observable bound to the field (client-writable). - * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. - * @param bool|Observable|null $disabled - * @param bool|Observable|null $visible + * @param Observable|Observable|string|UIRawMessage $label The label shown above the text field. + * @param Observable $text The text observable bound to the field (client-writable). + * @param Observable|Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param bool|Observable|null $visible */ public function textField(Observable|string|UIRawMessage $label, Observable $text, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new TextFieldElement($label, $text, $description, $disabled, $visible); return $this; } /** * Inserts an on/off toggle that players can interact with into the Custom form. * - * @param Observable|string|UIRawMessage $label The label shown beside the toggle. - * @param Observable $toggled The boolean observable bound to the toggle (client-writable). - * @param Observable|string|UIRawMessage|null $description Optional description shown in the UI. - * @param bool|Observable|null $disabled - * @param bool|Observable|null $visible + * @param Observable|Observable|string|UIRawMessage $label The label shown beside the toggle. + * @param Observable $toggled The boolean observable bound to the toggle (client-writable). + * @param Observable|Observable|string|UIRawMessage|null $description Optional description shown in the UI. + * @param bool|Observable|null $disabled + * @param bool|Observable|null $visible */ public function toggle(Observable|string|UIRawMessage $label, Observable $toggled, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ - // TODO: to be implemented + $this->elements[] = new ToggleElement($label, $toggled, $description, $disabled, $visible); return $this; } } diff --git a/src/Azvyl/PMServerUI/ddui/CustomFormPayloadComposer.php b/src/Azvyl/PMServerUI/ddui/CustomFormPayloadComposer.php new file mode 100644 index 0000000..3d5526d --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/CustomFormPayloadComposer.php @@ -0,0 +1,67 @@ + $elements + */ + public function compose( + DDUIManager $manager, + string $playerUuid, + int $formId, + Observable|string|UIRawMessage $title, + bool $closeButtonEnabled, + array $elements, + ) : MapDataStorePropertyValue{ + $context = new CustomFormRenderContext($manager, $playerUuid, $formId); + + $closeButtonEntries = [ + new DataStoreMapEntry('button_visible', new BoolDataStorePropertyValue($closeButtonEnabled)), + new DataStoreMapEntry('label', new StringDataStorePropertyValue('Close')), + new DataStoreMapEntry('onClick', new Int64DataStorePropertyValue(0)), + ]; + + $layoutEntries = []; + foreach($elements as $index => $element){ + $elementEntries = $element->buildEntries($context, $index); + $visible = true; + $disabled = false; + foreach($elementEntries as $entry){ + $key = $entry->getKey(); + $value = $entry->getValue(); + if(!$value instanceof BoolDataStorePropertyValue){ + continue; + } + if($key === 'visible'){ + $visible = $value->getValue(); + }elseif($key === 'disabled'){ + $disabled = $value->getValue(); + } + } + $manager->registerElementInteractionState($playerUuid, $formId, $index, $visible, $disabled); + $layoutEntries[] = new DataStoreMapEntry((string) $index, new MapDataStorePropertyValue($elementEntries)); + } + $layoutEntries[] = new DataStoreMapEntry('length', new Int64DataStorePropertyValue(count($elements))); + + $titleText = $context->resolveText($title, 'title'); + + return new MapDataStorePropertyValue([ + new DataStoreMapEntry('closeButton', new MapDataStorePropertyValue($closeButtonEntries)), + new DataStoreMapEntry('layout', new MapDataStorePropertyValue($layoutEntries)), + new DataStoreMapEntry('title', $context->toTextPropertyValue($titleText)), + ]); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/CustomFormRenderContext.php b/src/Azvyl/PMServerUI/ddui/CustomFormRenderContext.php new file mode 100644 index 0000000..ead39cb --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/CustomFormRenderContext.php @@ -0,0 +1,133 @@ +manager->registerObservableBinding($this->playerUuid, $this->formId, $path, $observable); + } + + public function registerClickHandler(string $path, callable $handler) : void{ + $this->manager->registerClickHandler($this->playerUuid, $this->formId, $path, $handler); + } + + public function resolveBool(bool|Observable|null $value, string $path, bool $default = false) : bool{ + if($value instanceof Observable){ + $this->registerBinding($path, $value); + return (bool)$value->getData(); + } + if($value === null){ + return $default; + } + return (bool)$value; + } + + public function resolveInt(int|float|Observable|null $value, string $path, int $default = 0) : int{ + if($value instanceof Observable){ + $this->registerBinding($path, $value); + return (int)$value->getData(); + } + if($value === null){ + return $default; + } + return (int)$value; + } + + public function resolveText(Observable|string|UIRawMessage|null $value, string $path, string|UIRawMessage $default = '') : string|UIRawMessage{ + if($value instanceof Observable){ + $this->registerBinding($path, $value); + $value = $value->getData(); + } + + if($value instanceof UIRawMessage){ + return $value; + } + + if($value === null){ + return $default; + } + + return (string)$value; + } + + public function toTextPropertyValue(mixed $value) : DataStorePropertyValue{ + if($value instanceof UIRawMessage){ + return $this->toPropertyValue($value->encode()); + } + return new StringDataStorePropertyValue((string)($value ?? '')); + } + + public function toPropertyValue(mixed $value) : DataStorePropertyValue{ + if($value === null){ + return new NoneDataStorePropertyValue(); + } + + if(is_array($value)){ + $isList = array_keys($value) === range(0, count($value) - 1); + if($isList){ + $entries = []; + foreach($value as $item){ + $entries[] = $this->toPropertyValue($item); + } + return new ListDataStorePropertyValue($entries); + } + + $mapEntries = []; + foreach($value as $key => $entryValue){ + if($key === 'with' && is_array($entryValue) && array_keys($entryValue) === range(0, count($entryValue) - 1)){ + $rawTextEntries = []; + foreach($entryValue as $item){ + $rawTextEntries[] = $this->toPropertyValue($item); + } + $mapEntries[] = new DataStoreMapEntry('with', new MapDataStorePropertyValue([ + new DataStoreMapEntry('rawtext', new ListDataStorePropertyValue($rawTextEntries)), + ])); + continue; + } + + $mapEntries[] = new DataStoreMapEntry((string)$key, $this->toPropertyValue($entryValue)); + } + + return new MapDataStorePropertyValue($mapEntries); + } + + if(is_string($value)){ + return new StringDataStorePropertyValue($value); + } + if(is_int($value) || is_float($value)){ + return new Int64DataStorePropertyValue((int)$value); + } + if(is_bool($value)){ + return new BoolDataStorePropertyValue($value); + } + + return new NoneDataStorePropertyValue(); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/DDUI.php b/src/Azvyl/PMServerUI/ddui/DDUI.php index 4787554..4ad9549 100644 --- a/src/Azvyl/PMServerUI/ddui/DDUI.php +++ b/src/Azvyl/PMServerUI/ddui/DDUI.php @@ -1,19 +1,9 @@ */ + private array $updateCounts = []; + + /** @var array */ + private array $activeFormIds = []; + + /** + * Per-player promises for open DDUI forms. + * Structure: [playerUuid => [formId => Promise]] + * + * @var array>> + */ + private array $formPromises = []; + + /** + * Bindings of observables for shown forms. + * Structure: [playerUuid => [formId => [path => Observable]]] + * + * @var array>> + */ + private array $bindings = []; + + /** + * Reverse index of bindings by observable object id. + * Structure: [spl_object_id => [['player' => playerUuid, 'form' => formId, 'path' => path], ...]] + * + * @var array>> + */ + private array $bindingsByObservable = []; + + /** Click handlers for buttons: [playerUuid => [formId => [path => callable]]] */ + private array $clickHandlers = []; + + /** Path update counters per player and path to produce pathUpdateCount */ + private array $pathUpdateCounts = []; + + /** + * Per-element interaction state used to validate inbound updates. + * Structure: [playerUuid => [formId => [elementIndex => ['visible' => bool, 'disabled' => bool]]]] + * + * @var array>> + */ + private array $elementInteractionState = []; public function __construct(Plugin $plugin){ $plManager = Server::getInstance()->getPluginManager(); $plManager->registerEvent(PlayerQuitEvent::class, function(PlayerQuitEvent $event) : void{ - + $this->clearPlayerState($event->getPlayer()->getUniqueId()->getBytes(), "Player disconnected"); }, EventPriority::LOWEST, $plugin); $plManager->registerEvent(DataPacketDecodeEvent::class, function(DataPacketDecodeEvent $event) : void{ - - }, EventPriority::LOWEST, $plugin); + if($event->getPacketId() === ServerboundDataStorePacket::NETWORK_ID || $event->getPacketId() === ServerboundDataDrivenScreenClosedPacket::NETWORK_ID){ + $event->uncancel(); + } + }, EventPriority::LOWEST, $plugin, true); $plManager->registerEvent(DataPacketReceiveEvent::class, function(DataPacketReceiveEvent $event) : void{ + $packet = $event->getPacket(); + $player = $event->getOrigin()->getPlayer(); + if($player === null){ + return; + } + if($packet instanceof ServerboundDataStorePacket){ + $this->handleServerboundDataStore($event, $player, $packet); + }elseif($packet instanceof ServerboundDataDrivenScreenClosedPacket){ + $this->handleServerboundScreenClosed($event, $player, $packet); + } }, EventPriority::LOW, $plugin); } -} + public function hasActiveForm(string $playerUuid) : bool{ + return isset($this->activeFormIds[$playerUuid]); + } + + /** + * @param DataStore[] $entries + */ + public function sendDataStoreEntries(Player $player, array $entries) : void{ + $player->getNetworkSession()->sendDataPacket(ClientboundDataStorePacket::create($entries)); + } + + /** + * Register a promise for a shown form so DDUIManager can resolve/reject it. + * + * @param string $playerUuid raw uuid bytes + * @param int $formId + * @param Promise $promise + */ + public function registerFormPromise(string $playerUuid, int $formId, Promise $promise) : void{ + if(isset($this->activeFormIds[$playerUuid]) && $this->activeFormIds[$playerUuid] !== $formId){ + throw new \RuntimeException("A DDUI form is already active for this player"); + } + $this->formPromises[$playerUuid][$formId] = $promise; + $this->activeFormIds[$playerUuid] = $formId; + } + + /** + * Register an observable binding for a shown form path. + * The path is the DataStore path, e.g. "title" or "layout[3].visible". + */ + public function registerObservableBinding(string $playerUuid, int $formId, string $path, Observable $observable) : void{ + $this->bindings[$playerUuid][$formId][$path] = $observable; + $id = spl_object_id($observable); + $this->bindingsByObservable[$id][] = ['player' => $playerUuid, 'form' => $formId, 'path' => $path]; + } + + /** + * Unregister bindings for a form (called when form closes). + */ + public function unregisterBindingsForForm(string $playerUuid, int $formId) : void{ + if(!isset($this->bindings[$playerUuid][$formId])) return; + foreach($this->bindings[$playerUuid][$formId] as $path => $obs){ + $id = spl_object_id($obs); + if(isset($this->bindingsByObservable[$id])){ + $this->bindingsByObservable[$id] = array_filter($this->bindingsByObservable[$id], function($e) use ($playerUuid, $formId, $path){ + return !($e['player'] === $playerUuid && $e['form'] === $formId && $e['path'] === $path); + }); + if($this->bindingsByObservable[$id] === []){ + unset($this->bindingsByObservable[$id]); + } + } + } + unset($this->bindings[$playerUuid][$formId]); + } + + /** + * Register a click handler for a specific button path in a shown form. + */ + public function registerClickHandler(string $playerUuid, int $formId, string $path, callable $handler) : void{ + $this->clickHandlers[$playerUuid][$formId][$path] = $handler; + } + + public function registerElementInteractionState(string $playerUuid, int $formId, int $elementIndex, bool $visible, bool $disabled) : void{ + $this->elementInteractionState[$playerUuid][$formId][$elementIndex] = [ + 'visible' => $visible, + 'disabled' => $disabled, + ]; + } + + private function getDataStoreValue(DataStoreValue $val) : mixed{ + if(method_exists($val, 'getValue')){ + return $val->getValue(); + } + return null; + } + + /** + * Called by Observable when server code sets an observable value. Sends DataStore updates to all bound clients. + */ + public function notifyObservableChanged(Observable $observable) : void{ + $id = spl_object_id($observable); + if(!isset($this->bindingsByObservable[$id])) return; + + foreach($this->bindingsByObservable[$id] as $entry){ + $playerUuid = $entry['player']; + $formId = $entry['form']; + $path = $entry['path']; + + try{ + $this->sendObservableUpdateToBinding($playerUuid, $formId, $path, $observable->getData()); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + } + + private function handleServerboundDataStore(DataPacketReceiveEvent $event, Player $player, ServerboundDataStorePacket $packet) : void{ + try{ + $update = $packet->getUpdate(); + if($update->getName() !== "minecraft" || $update->getProperty() !== "custom_form_data"){ + return; + } + + $path = $update->getPath(); + $playerUuid = $player->getUniqueId()->getBytes(); + $formId = $this->activeFormIds[$playerUuid] ?? null; + if($formId === null){ + return; + } + + if($this->isInteractionBlockedByState($playerUuid, $formId, $path)){ + $event->cancel(); + return; + } + + if($path === "closeButton.onClick"){ + $event->cancel(); + $player->getNetworkSession()->sendDataPacket(ClientboundDataDrivenUICloseScreenPacket::create($formId)); + return; + } + + if(isset($this->clickHandlers[$playerUuid][$formId][$path])){ + $event->cancel(); + ($this->clickHandlers[$playerUuid][$formId][$path])($update->getData()); + return; + } + + if(isset($this->bindings[$playerUuid][$formId][$path])){ + $event->cancel(); + $observable = $this->bindings[$playerUuid][$formId][$path]; + if(!$observable->isClientWritable()){ + return; + } + $newValue = $this->getDataStoreValue($update->getData()); + $this->trackElementInteractionPathUpdate($playerUuid, $formId, $path, $newValue); + $observable->applyClientUpdate($newValue); + $this->notifyObservableChangedFromClient($observable, $playerUuid, $formId, $path); + } + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + + private function notifyObservableChangedFromClient(Observable $observable, string $sourcePlayerUuid, int $sourceFormId, string $sourcePath) : void{ + $id = spl_object_id($observable); + if(!isset($this->bindingsByObservable[$id])){ + return; + } + + $value = $observable->getData(); + foreach($this->bindingsByObservable[$id] as $entry){ + $playerUuid = $entry['player']; + $formId = $entry['form']; + $path = $entry['path']; + + if($playerUuid === $sourcePlayerUuid && $formId === $sourceFormId && $path === $sourcePath){ + continue; + } + + try{ + $this->sendObservableUpdateToBinding($playerUuid, $formId, $path, $value); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + } + + private function sendObservableUpdateToBinding(string $playerUuid, int $formId, string $path, bool|float|int|string|UIRawMessage $value) : void{ + if(($this->activeFormIds[$playerUuid] ?? null) !== $formId){ + return; + } + + $player = Server::getInstance()->getPlayerByRawUUID($playerUuid); + if($player === null){ + return; + } + + $data = $this->toDataStoreValue($value); + if($data === null){ + PMServerUI::getLogger()->warning("Unimplemented data type for DataStoreValue conversion: " . get_debug_type($value)); + return; + } + + $updateCount = $this->updateCounts[$playerUuid];// use the same value as previously sent to open the form + $pathUpdate = ($this->pathUpdateCounts[$playerUuid][$path] ?? 0) + 1; + $this->pathUpdateCounts[$playerUuid][$path] = $pathUpdate; + $this->trackElementInteractionPathUpdate($playerUuid, $formId, $path, $value); + + $this->sendDataStoreEntries($player, [new DataStoreUpdate( + "minecraft", + "custom_form_data", + $path, + $data, + $updateCount, + $pathUpdate + )]); + } + + private function toDataStoreValue(bool|float|int|string|UIRawMessage $value) : ?DataStoreValue{ + if(is_bool($value)){ + return new BoolDataStoreValue($value); + } + if(is_int($value) || is_float($value)){ + return new DoubleDataStoreValue((float) $value); + } + if(is_string($value)){ + return new StringDataStoreValue($value); + } + if($value instanceof UIRawMessage){ + return new StringDataStoreValue(json_encode($value->encode(), JSON_UNESCAPED_SLASHES)); + } + + return null; + } + + private function handleServerboundScreenClosed(DataPacketReceiveEvent $event, Player $player, ServerboundDataDrivenScreenClosedPacket $packet) : void{ + $event->cancel(); + $playerUuid = $player->getUniqueId()->getBytes(); + $updateCount = $this->nextUpdateCountFor($playerUuid); + + try{ + $this->sendDataStoreEntries($player, [ + new ClientboundDataStoreChange("minecraft", "ddui_form_active", $updateCount, new BoolDataStorePropertyValue(false)), + new ClientboundDataStoreChange("minecraft", "custom_form_data", $updateCount, new NoneDataStorePropertyValue()), + ]); + + $formId = $this->getScreenClosedFormId($packet); + if(isset($this->formPromises[$playerUuid][$formId])){ + $this->formPromises[$playerUuid][$formId]->resolve(true); + unset($this->formPromises[$playerUuid][$formId]); + if($this->formPromises[$playerUuid] === []){ + unset($this->formPromises[$playerUuid]); + } + } + + $this->unregisterBindingsForForm($playerUuid, $formId); + unset($this->clickHandlers[$playerUuid][$formId]); + + if(($this->activeFormIds[$playerUuid] ?? null) === $formId){ + unset($this->activeFormIds[$playerUuid]); + } + unset($this->elementInteractionState[$playerUuid][$formId]); + if(($this->elementInteractionState[$playerUuid] ?? []) === []){ + unset($this->elementInteractionState[$playerUuid]); + } + unset($this->pathUpdateCounts[$playerUuid]); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + + private function getScreenClosedFormId(ServerboundDataDrivenScreenClosedPacket $packet) : int{ + if(method_exists($packet, 'getFormId')){ + /** @var int */ + return $packet->getFormId(); + } + return (new \ReflectionProperty($packet, 'formId'))->getValue($packet); + } + + private function clearPlayerState(string $playerUuid, string $reason) : void{ + unset($this->updateCounts[$playerUuid], $this->activeFormIds[$playerUuid], $this->pathUpdateCounts[$playerUuid], $this->clickHandlers[$playerUuid], $this->bindings[$playerUuid], $this->elementInteractionState[$playerUuid]); + if(!isset($this->formPromises[$playerUuid])){ + return; + } + foreach($this->formPromises[$playerUuid] as $promise){ + try{ + $promise->reject(new \RuntimeException($reason)); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + unset($this->formPromises[$playerUuid]); + } + + /** Generate and return a new unique form id. */ + public function nextFormId() : int{ + return $this->formIdCounter++; + } + + /** Increment and return the next update count for a given player UUID (raw bytes string). */ + public function nextUpdateCountFor(string $playerUuid) : int{ + $this->updateCounts[$playerUuid] = ($this->updateCounts[$playerUuid] ?? 0) + 1; + return $this->updateCounts[$playerUuid]; + } + + private function isInteractionBlockedByState(string $playerUuid, int $formId, string $path) : bool{ + if(!preg_match('/^layout\[(\d+)]\.(.+)$/', $path, $matches)){ + return false; + } + + $property = $matches[2]; + if($property === 'visible' || $property === 'disabled'){ + return false; + } + + $elementIndex = (int) $matches[1]; + $state = $this->elementInteractionState[$playerUuid][$formId][$elementIndex] ?? null; + if($state === null){ + return false; + } + + return $state['disabled'] || !$state['visible']; + } + + private function trackElementInteractionPathUpdate(string $playerUuid, int $formId, string $path, mixed $value) : void{ + if(!preg_match('/^layout\[(\d+)]\.(visible|disabled)$/', $path, $matches)){ + return; + } + + $elementIndex = (int) $matches[1]; + $property = $matches[2]; + $state = $this->elementInteractionState[$playerUuid][$formId][$elementIndex] ?? ['visible' => true, 'disabled' => false]; + $state[$property] = (bool) $value; + $this->elementInteractionState[$playerUuid][$formId][$elementIndex] = $state; + } +} diff --git a/src/Azvyl/PMServerUI/ddui/DropdownItem.php b/src/Azvyl/PMServerUI/ddui/DropdownItem.php index 7bad118..9438f68 100644 --- a/src/Azvyl/PMServerUI/ddui/DropdownItem.php +++ b/src/Azvyl/PMServerUI/ddui/DropdownItem.php @@ -1,15 +1,5 @@ |string|UIRawMessage $text + * @param Observable|Observable|string|UIRawMessage $text */ public function body(Observable|string|UIRawMessage $text) : self{ // TODO: to be implemented @@ -41,8 +31,8 @@ public function body(Observable|string|UIRawMessage $text) : self{ /** * Sets the data for the top button in the form. * - * @param Observable|string|UIRawMessage $label - * @param Observable|string|UIRawMessage|null $tooltip + * @param Observable|Observable|string|UIRawMessage $label + * @param Observable|Observable|string|UIRawMessage|null $tooltip */ public function button1(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ // TODO: to be implemented @@ -52,8 +42,8 @@ public function button1(Observable|string|UIRawMessage $label, Observable|string /** * Sets the data for the bottom button in the form. * - * @param Observable|string|UIRawMessage $label - * @param Observable|string|UIRawMessage|null $tooltip + * @param Observable|Observable|string|UIRawMessage $label + * @param Observable|Observable|string|UIRawMessage|null $tooltip */ public function button2(Observable|string|UIRawMessage $label, Observable|string|UIRawMessage $tooltip = null) : self{ // TODO: to be implemented @@ -71,8 +61,13 @@ public function isShowing() : bool{ return false; } - /** Show this modal to the player. Will throw an error if the modal is already showing. */ + /** + * Show this modal to the player. Will throw an error if the modal is already showing. + * + * @return Promise + */ public function show() : Promise{ + /** @var Promise $promise */ $promise = new Promise(); //TODO: to be implemented return $promise; @@ -81,7 +76,7 @@ public function show() : Promise{ /** * Sets the title of the form. * - * @param Observable|string|UIRawMessage $text + * @param Observable|Observable|string|UIRawMessage $text */ public function title(Observable|string|UIRawMessage $text) : self{ // TODO: to be implemented diff --git a/src/Azvyl/PMServerUI/ddui/Observable.php b/src/Azvyl/PMServerUI/ddui/Observable.php index f57fb88..a9c67fc 100644 --- a/src/Azvyl/PMServerUI/ddui/Observable.php +++ b/src/Azvyl/PMServerUI/ddui/Observable.php @@ -1,51 +1,45 @@ */ private array $listeners = []; /** - * @param T $data + * @param T $data * @param bool $clientWritable */ - private function __construct(private bool|float|int|string $data, private readonly bool $clientWritable = false){ } + private function __construct(private bool|float|int|string|UIRawMessage $data, private readonly bool $clientWritable = false){} /** * Create an observable. * - * @param T $data + * @param T $data * @param bool $clientWritable * * @return Observable */ - public static function create(bool|float|int|string $data, bool $clientWritable = false) : self{ + public static function create(bool|float|int|string|UIRawMessage $data, bool $clientWritable = false) : self{ return new self($data, $clientWritable); } /** * Get the data. + * * @return T */ - public function getData() : bool|float|int|string{ + public function getData() : bool|float|int|string|UIRawMessage{ return $this->data; } @@ -54,7 +48,31 @@ public function getData() : bool|float|int|string{ * * @param T $data */ - public function setData(bool|float|int|string $data) : void{ + public function setData(bool|float|int|string|UIRawMessage $data) : void{ + $this->data = $data; + foreach($this->listeners as $listener){ + try{ + $listener($data); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + } + + try{ + PMServerUI::getDDUIManager()->notifyObservableChanged($this); + }catch(\Throwable $e){ + PMServerUI::getLogger()->logException($e); + } + } + + /** + * @internal + * Apply a value received from the client. This updates the observable and notifies listeners + * but DOES NOT echo the value back to the client (to avoid feedback loops). + * + * @param T $data + */ + public function applyClientUpdate(bool|float|int|string|UIRawMessage $data) : void{ $this->data = $data; foreach($this->listeners as $listener){ $listener($data); diff --git a/src/Azvyl/PMServerUI/ddui/Subscription.php b/src/Azvyl/PMServerUI/ddui/Subscription.php index f110504..40be36a 100644 --- a/src/Azvyl/PMServerUI/ddui/Subscription.php +++ b/src/Azvyl/PMServerUI/ddui/Subscription.php @@ -1,19 +1,11 @@ onClick = $onClick(...); + } + + public function buildEntries(CustomFormRenderContext $context, int $index) : array{ + $label = $context->resolveText($this->label, $context->path($index, 'label')); + $tooltip = $context->resolveText($this->tooltip, $context->path($index, 'tooltip')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + $disabled = $context->resolveBool($this->disabled, $context->path($index, 'disabled'), false); + + $context->registerClickHandler($context->path($index, 'onClick'), function() : void{ + try{ + ($this->onClick)(); + }catch(\Throwable $t){ + PMServerUI::getLogger()->logException($t); + } + }); + + return [ + new DataStoreMapEntry('button_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('disabled', new BoolDataStorePropertyValue($disabled)), + new DataStoreMapEntry('label', $context->toTextPropertyValue($label)), + new DataStoreMapEntry('onClick', new Int64DataStorePropertyValue(0)), + new DataStoreMapEntry('tooltip', $context->toTextPropertyValue($tooltip)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/CustomFormElement.php b/src/Azvyl/PMServerUI/ddui/elements/CustomFormElement.php new file mode 100644 index 0000000..19d6824 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/CustomFormElement.php @@ -0,0 +1,17 @@ + + */ + public function buildEntries(CustomFormRenderContext $context, int $index) : array; +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/DividerElement.php b/src/Azvyl/PMServerUI/ddui/elements/DividerElement.php new file mode 100644 index 0000000..1c5ca96 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/DividerElement.php @@ -0,0 +1,23 @@ +resolveBool($this->visible, $context->path($index, 'visible'), true); + return [ + new DataStoreMapEntry('divider_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/DropdownElement.php b/src/Azvyl/PMServerUI/ddui/elements/DropdownElement.php new file mode 100644 index 0000000..85ac6e8 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/DropdownElement.php @@ -0,0 +1,58 @@ + $items + */ + public function __construct( + private Observable|string|UIRawMessage $label, + private Observable $value, + private array $items, + private Observable|string|UIRawMessage|null $description = null, + private bool|Observable|null $disabled = null, + private bool|Observable|null $visible = null, + ){} + + public function buildEntries(CustomFormRenderContext $context, int $index) : array{ + $description = $context->resolveText($this->description, $context->path($index, 'description')); + $label = $context->resolveText($this->label, $context->path($index, 'label')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + $disabled = $context->resolveBool($this->disabled, $context->path($index, 'disabled'), false); + $value = $context->resolveInt($this->value, $context->path($index, 'value')); + + $itemEntries = []; + foreach($this->items as $itemIndex => $item){ + $itemMap = [ + new DataStoreMapEntry('label', $context->toTextPropertyValue($item->label)), + new DataStoreMapEntry('value', new Int64DataStorePropertyValue((int)$item->value)), + ]; + $itemEntries[] = new DataStoreMapEntry((string)$itemIndex, new MapDataStorePropertyValue($itemMap)); + } + $itemEntries[] = new DataStoreMapEntry('length', new Int64DataStorePropertyValue(count($this->items))); + + return [ + new DataStoreMapEntry('description', $context->toTextPropertyValue($description)), + new DataStoreMapEntry('disabled', new BoolDataStorePropertyValue($disabled)), + new DataStoreMapEntry('dropdown_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + new DataStoreMapEntry('label', $context->toTextPropertyValue($label)), + new DataStoreMapEntry('items', new MapDataStorePropertyValue($itemEntries)), + new DataStoreMapEntry('value', new Int64DataStorePropertyValue($value)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/HeaderElement.php b/src/Azvyl/PMServerUI/ddui/elements/HeaderElement.php new file mode 100644 index 0000000..5e54d22 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/HeaderElement.php @@ -0,0 +1,30 @@ +resolveText($this->text, $context->path($index, 'text')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + + return [ + new DataStoreMapEntry('header_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + new DataStoreMapEntry('text', $context->toTextPropertyValue($text)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/LabelElement.php b/src/Azvyl/PMServerUI/ddui/elements/LabelElement.php new file mode 100644 index 0000000..a823ce3 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/LabelElement.php @@ -0,0 +1,30 @@ +resolveText($this->text, $context->path($index, 'text')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + + return [ + new DataStoreMapEntry('label_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('text', $context->toTextPropertyValue($text)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/SliderElement.php b/src/Azvyl/PMServerUI/ddui/elements/SliderElement.php new file mode 100644 index 0000000..071cdf0 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/SliderElement.php @@ -0,0 +1,49 @@ +resolveText($this->description, $context->path($index, 'description')); + $label = $context->resolveText($this->label, $context->path($index, 'label')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + $disabled = $context->resolveBool($this->disabled, $context->path($index, 'disabled'), false); + $min = $context->resolveInt($this->min, $context->path($index, 'minValue'), 0); + $max = $context->resolveInt($this->max, $context->path($index, 'maxValue'), 100); + $step = $context->resolveInt($this->step, $context->path($index, 'step'), 1); + $value = $context->resolveInt($this->value, $context->path($index, 'value')); + + return [ + new DataStoreMapEntry('description', $context->toTextPropertyValue($description)), + new DataStoreMapEntry('slider_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('disabled', new BoolDataStorePropertyValue($disabled)), + new DataStoreMapEntry('label', $context->toTextPropertyValue($label)), + new DataStoreMapEntry('maxValue', new Int64DataStorePropertyValue($max)), + new DataStoreMapEntry('minValue', new Int64DataStorePropertyValue($min)), + new DataStoreMapEntry('step', new Int64DataStorePropertyValue($step)), + new DataStoreMapEntry('value', new Int64DataStorePropertyValue($value)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/SpacerElement.php b/src/Azvyl/PMServerUI/ddui/elements/SpacerElement.php new file mode 100644 index 0000000..17b442a --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/SpacerElement.php @@ -0,0 +1,23 @@ +resolveBool($this->visible, $context->path($index, 'visible'), true); + return [ + new DataStoreMapEntry('spacer_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/TextFieldElement.php b/src/Azvyl/PMServerUI/ddui/elements/TextFieldElement.php new file mode 100644 index 0000000..365e861 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/TextFieldElement.php @@ -0,0 +1,39 @@ +resolveText($this->description, $context->path($index, 'description')); + $label = $context->resolveText($this->label, $context->path($index, 'label')); + $text = $context->resolveText($this->text, $context->path($index, 'text')); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + $disabled = $context->resolveBool($this->disabled, $context->path($index, 'disabled'), false); + + return [ + new DataStoreMapEntry('description', $context->toTextPropertyValue($description)), + new DataStoreMapEntry('disabled', new BoolDataStorePropertyValue($disabled)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + new DataStoreMapEntry('label', $context->toTextPropertyValue($label)), + new DataStoreMapEntry('text', $context->toTextPropertyValue($text)), + new DataStoreMapEntry('textfield_visible', new BoolDataStorePropertyValue(true)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/elements/ToggleElement.php b/src/Azvyl/PMServerUI/ddui/elements/ToggleElement.php new file mode 100644 index 0000000..03a34c8 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/elements/ToggleElement.php @@ -0,0 +1,39 @@ +resolveText($this->description, $context->path($index, 'description')); + $label = $context->resolveText($this->label, $context->path($index, 'label')); + $toggled = $context->resolveBool($this->toggled, $context->path($index, 'toggled'), false); + $visible = $context->resolveBool($this->visible, $context->path($index, 'visible'), true); + $disabled = $context->resolveBool($this->disabled, $context->path($index, 'disabled'), false); + + return [ + new DataStoreMapEntry('description', $context->toTextPropertyValue($description)), + new DataStoreMapEntry('visible', new BoolDataStorePropertyValue($visible)), + new DataStoreMapEntry('disabled', new BoolDataStorePropertyValue($disabled)), + new DataStoreMapEntry('label', $context->toTextPropertyValue($label)), + new DataStoreMapEntry('toggle_visible', new BoolDataStorePropertyValue(true)), + new DataStoreMapEntry('toggled', new BoolDataStorePropertyValue($toggled)), + ]; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/ClientboundDataStorePacket.php b/src/Azvyl/PMServerUI/ddui/packets/ClientboundDataStorePacket.php new file mode 100644 index 0000000..2a8f43f --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/ClientboundDataStorePacket.php @@ -0,0 +1,39 @@ + $values + */ + public static function create(array $values) : self{ + $result = new self; + $result->values = $values; + return $result; + } + + protected function decodePayload(ByteBufferReader $in) : void{ + $this->values = []; + for($i = 0, $len = VarInt::readUnsignedInt($in); $i < $len; ++$i){ + $this->values[] = match(VarInt::readUnsignedInt($in)){ + DataStoreType::UPDATE => DataStoreUpdate::read($in), + DataStoreType::CHANGE => DataStoreChange::read($in), + DataStoreType::REMOVAL => DataStoreRemoval::read($in), + default => throw new PacketDecodeException("Unknown DataStore type"), + }; + } + } +} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/BoolDataStorePropertyValue.php b/src/Azvyl/PMServerUI/ddui/packets/types/BoolDataStorePropertyValue.php new file mode 100644 index 0000000..fc31124 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/BoolDataStorePropertyValue.php @@ -0,0 +1,30 @@ +value; } + + public function getTypeId() : int{ return self::ID; } + + protected function writePayload(ByteBufferWriter $out) : void{ + CommonTypes::putBool($out, $this->value); + } + + public static function read(ByteBufferReader $in) : self{ + return new self(CommonTypes::getBool($in)); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreChange.php b/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreChange.php new file mode 100644 index 0000000..a52cf07 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreChange.php @@ -0,0 +1,57 @@ +name; } + + public function getProperty() : string{ return $this->property; } + + public function getUpdateCount() : int{ return $this->updateCount; } + + public function getNewValue() : DataStorePropertyValue{ return $this->newValue; } + + public static function read(ByteBufferReader $in) : self{ + $name = CommonTypes::getString($in); + $property = CommonTypes::getString($in); + $updateCount = LE::readUnsignedInt($in); + + $data = DataStorePropertyValue::read($in); + + return new self( + $name, + $property, + $updateCount, + $data, + ); + } + + public function write(ByteBufferWriter $out) : void{ + CommonTypes::putString($out, $this->name); + CommonTypes::putString($out, $this->property); + LE::writeUnsignedInt($out, $this->updateCount); + $this->newValue->writeWithType($out); + } +} \ No newline at end of file diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreMapEntry.php b/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreMapEntry.php new file mode 100644 index 0000000..f3b177d --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/DataStoreMapEntry.php @@ -0,0 +1,32 @@ +key; } + + public function getValue() : DataStorePropertyValue{ return $this->value; } + + public static function read(ByteBufferReader $in) : self{ + $key = CommonTypes::getString($in); + $value = DataStorePropertyValue::read($in); + return new self($key, $value); + } + + public function write(ByteBufferWriter $out) : void{ + CommonTypes::putString($out, $this->key); + $this->value->writeWithType($out); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/DataStorePropertyType.php b/src/Azvyl/PMServerUI/ddui/packets/types/DataStorePropertyType.php new file mode 100644 index 0000000..0e00036 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/DataStorePropertyType.php @@ -0,0 +1,15 @@ +getTypeId()); + $this->writePayload($out); + } + + public static function read(ByteBufferReader $in) : self{ + $type = LE::readSignedInt($in); + return match($type){ + DataStorePropertyType::NONE => NoneDataStorePropertyValue::read($in), + DataStorePropertyType::BOOL => BoolDataStorePropertyValue::read($in), + DataStorePropertyType::INT64 => Int64DataStorePropertyValue::read($in), + DataStorePropertyType::STRING => StringDataStorePropertyValue::read($in), + DataStorePropertyType::LIST => ListDataStorePropertyValue::read($in), + DataStorePropertyType::MAP => MapDataStorePropertyValue::read($in), + default => throw new PacketDecodeException("Unknown DataStorePropertyType"), + }; + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/Int64DataStorePropertyValue.php b/src/Azvyl/PMServerUI/ddui/packets/types/Int64DataStorePropertyValue.php new file mode 100644 index 0000000..fbe8b30 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/Int64DataStorePropertyValue.php @@ -0,0 +1,30 @@ +value; } + + public function getTypeId() : int{ return self::ID; } + + protected function writePayload(ByteBufferWriter $out) : void{ + LE::writeSignedLong($out, $this->value); + } + + public static function read(ByteBufferReader $in) : self{ + return new self(LE::readSignedLong($in)); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/ListDataStorePropertyValue.php b/src/Azvyl/PMServerUI/ddui/packets/types/ListDataStorePropertyValue.php new file mode 100644 index 0000000..8b120bd --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/ListDataStorePropertyValue.php @@ -0,0 +1,46 @@ + $entries + */ + public function __construct( + private readonly array $entries, + ){} + + /** + * @return DataStorePropertyValue[] + * @phpstan-return list + */ + public function getEntries() : array{ return $this->entries; } + + public function getTypeId() : int{ return self::ID; } + + protected function writePayload(ByteBufferWriter $out) : void{ + VarInt::writeUnsignedInt($out, count($this->entries)); + foreach($this->entries as $entry){ + $entry->writeWithType($out); + } + } + + public static function read(ByteBufferReader $in) : self{ + $entries = []; + for($i = 0, $len = VarInt::readUnsignedInt($in); $i < $len; ++$i){ + $entries[] = DataStorePropertyValue::read($in); + } + return new self($entries); + } +} + + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/MapDataStorePropertyValue.php b/src/Azvyl/PMServerUI/ddui/packets/types/MapDataStorePropertyValue.php new file mode 100644 index 0000000..cd1abde --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/MapDataStorePropertyValue.php @@ -0,0 +1,46 @@ + $entries + */ + public function __construct( + private readonly array $entries, + ){} + + /** + * @return DataStoreMapEntry[] + * @phpstan-return list + */ + public function getEntries() : array{ return $this->entries; } + + public function getTypeId() : int{ return self::ID; } + + protected function writePayload(ByteBufferWriter $out) : void{ + VarInt::writeUnsignedInt($out, count($this->entries)); + foreach($this->entries as $entry){ + $entry->write($out); + } + } + + public static function read(ByteBufferReader $in) : self{ + $entries = []; + for($i = 0, $len = VarInt::readUnsignedInt($in); $i < $len; ++$i){ + $entries[] = DataStoreMapEntry::read($in); + } + return new self($entries); + } +} + diff --git a/src/Azvyl/PMServerUI/ddui/packets/types/NoneDataStorePropertyValue.php b/src/Azvyl/PMServerUI/ddui/packets/types/NoneDataStorePropertyValue.php new file mode 100644 index 0000000..0a9a4c5 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/packets/types/NoneDataStorePropertyValue.php @@ -0,0 +1,21 @@ +value; } + + public function getTypeId() : int{ return self::ID; } + + protected function writePayload(ByteBufferWriter $out) : void{ + CommonTypes::putString($out, $this->value); + } + + public static function read(ByteBufferReader $in) : self{ + return new self(CommonTypes::getString($in)); + } +} + diff --git a/src/Azvyl/PMServerUI/ui/ActionFormData.php b/src/Azvyl/PMServerUI/ui/ActionFormData.php index 84645d6..9cad37a 100644 --- a/src/Azvyl/PMServerUI/ui/ActionFormData.php +++ b/src/Azvyl/PMServerUI/ui/ActionFormData.php @@ -82,7 +82,7 @@ public function processResponse(string $rawData = null, FormCancelationReason $c if(!is_numeric($rawData)){ throw new FormValidationException("Expected int, got $rawData"); } - $data = (int) $rawData; + $data = (int)$rawData; if($data < 0 || $data >= $this->buttonCount){ throw new FormValidationException("Button $data does not exist"); } diff --git a/src/Azvyl/PMServerUI/ui/FormResponse.php b/src/Azvyl/PMServerUI/ui/FormResponse.php index a034e51..bb8b965 100644 --- a/src/Azvyl/PMServerUI/ui/FormResponse.php +++ b/src/Azvyl/PMServerUI/ui/FormResponse.php @@ -17,7 +17,7 @@ readonly class FormResponse{ /** * @param FormCancelationReason|null $cancelationReason Contains additional details as to why a form was canceled. - * @param bool $canceled If true, the form was canceled by the player (e.g., they selected the pop-up X close button). + * @param bool $canceled If true, the form was canceled by the player (e.g., they selected the pop-up X close button). */ - protected function __construct(public ?FormCancelationReason $cancelationReason, public bool $canceled){ } + protected function __construct(public ?FormCancelationReason $cancelationReason, public bool $canceled){} } diff --git a/src/Azvyl/PMServerUI/ui/ModalFormData.php b/src/Azvyl/PMServerUI/ui/ModalFormData.php index df7ed1b..52a15be 100644 --- a/src/Azvyl/PMServerUI/ui/ModalFormData.php +++ b/src/Azvyl/PMServerUI/ui/ModalFormData.php @@ -97,9 +97,9 @@ public function slider(string|UIRawMessage $label, int $min, int $max, int $defa $sliderElement = [ 'type' => 'slider', 'text' => $label instanceof UIRawMessage ? $label->encode() : $label, - 'min' => (float) $min, - 'max' => (float) $max, - 'step' => (float) ($step ?? 1.0), + 'min' => (float)$min, + 'max' => (float)$max, + 'step' => (float)($step ?? 1.0), 'timeout' => 100.0,//TODO: Find out what this does (1.26.0.29) ]; if($default !== null){ @@ -113,7 +113,7 @@ public function slider(string|UIRawMessage $label, int $min, int $max, int $defa if(!is_int($value) && !is_float($value)){ throw new FormValidationException("Expected numeric value for slider response, got " . gettype($value)); } - $numeric = (float) $value; + $numeric = (float)$value; if($numeric < $min || $numeric > $max){ throw new FormValidationException("Slider response out of range ($min..$max), got $numeric"); } diff --git a/src/Azvyl/PMServerUI/ui/ServerUI.php b/src/Azvyl/PMServerUI/ui/ServerUI.php index bc67ee2..fdab2f1 100644 --- a/src/Azvyl/PMServerUI/ui/ServerUI.php +++ b/src/Azvyl/PMServerUI/ui/ServerUI.php @@ -24,7 +24,11 @@ public static function create() : static{ return new static(); } - /** Show the form to a player. Returns a Promise that resolves with a FormResponse. */ + /** + * Show the form to a player. Returns a Promise that resolves with a FormResponse. + * + * @return Promise + */ final public function show(Player $player) : Promise{ return PMServerUI::getUIManager()->___send($player, $this); } diff --git a/src/Azvyl/PMServerUI/ui/UIManager.php b/src/Azvyl/PMServerUI/ui/UIManager.php index 9a6b6ed..33c412f 100644 --- a/src/Azvyl/PMServerUI/ui/UIManager.php +++ b/src/Azvyl/PMServerUI/ui/UIManager.php @@ -29,12 +29,11 @@ use function json_encode; use function strlen; use function trim; -use function var_dump; /** Manager responsible for opening and closing UI forms. */ final class UIManager{ - /** @var array> */ + /** @var array}>> */ private array $playerForms = []; public function __construct(Plugin $plugin){ @@ -75,7 +74,6 @@ public function __construct(Plugin $plugin){ throw new FormValidationException("Player {$player->getName()} sent unknown cancel reason"); } }elseif($packet->formData !== null){ - var_dump($packet->formData); $maxFormResponseSize = 10 * 1024; if(strlen($packet->formData) > $maxFormResponseSize){ throw new PacketHandlingException("Form response data too large, refusing to decode (received" . strlen($packet->formData) . " bytes, max $maxFormResponseSize bytes)"); @@ -110,7 +108,10 @@ public function closeAllForms(Player $player) : void{ throw new \RuntimeException("Not implemented yet"); } - /** @internal */ + /** + * @internal + * @param Promise $promise + */ public function ___track(Player $player, int $formId, ServerUI $ui, Promise $promise) : void{ // Note: A vanilla client should only have one form open at a time if(($this->playerForms[$player->getId()] ?? []) !== []){ @@ -119,8 +120,12 @@ public function ___track(Player $player, int $formId, ServerUI $ui, Promise $pro $this->playerForms[$player->getId()][$formId] = [$ui, $promise]; } - /** @internal */ + /** + * @internal + * @return Promise + */ public function ___send(Player $player, ServerUI $ui) : Promise{ + /** @var Promise $promise */ $promise = new Promise(); (function(UIManager $UIManager, ServerUI $ui, Promise $promise) : void{ /** @noinspection PhpUndefinedFieldInspection */ From d82302d2b61f4097766b502eeb217a35c4bbabbb Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Tue, 31 Mar 2026 05:55:23 +0700 Subject: [PATCH 7/8] Increment version to 2.0.0 --- README.md | 4 ++-- virion.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a36a1f3..7d379ee 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ library that other plugins *include* in their code. { "require": { // other dependencies ... - "azvyl/pmserver-ui": "^1.0" + "azvyl/pmserver-ui": "^2.0" } } ``` @@ -57,7 +57,7 @@ library that other plugins *include* in their code. YourPlugin: libs: - src: Azvyl/PMServerUI/PMServerUI - version: ^1.0.2 + version: ^2.0.0 ``` --- diff --git a/virion.yml b/virion.yml index 6e21c5c..c7038e1 100644 --- a/virion.yml +++ b/virion.yml @@ -1,5 +1,5 @@ name: PMServerUI -version: 1.0.2 +version: 2.0.0 antigen: Azvyl\PMServerUI api: 5.0.0 authors: [Azvyl] From 19aa2a47a77c2409e6bd565b9d85572d1dfe5eeb Mon Sep 17 00:00:00 2001 From: Azvyl <67502532+Azvyl@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:40:46 +0700 Subject: [PATCH 8/8] Update - Fix listener id in `Observable` - Prevent unnecessary updates on `Observable.setData()` - Remove license comment in all file headers - Update `pocketmine/bedrock-protocol` to `56.1.0+bedrock-1.26.10` - Update to reflect latest server-ui changes --- src/Azvyl/PMServerUI/PMServerUI.php | 10 -------- src/Azvyl/PMServerUI/Promise.php | 10 -------- src/Azvyl/PMServerUI/UIRawMessage.php | 10 -------- src/Azvyl/PMServerUI/ddui/CustomForm.php | 12 +++++----- src/Azvyl/PMServerUI/ddui/DDUIManager.php | 13 ++++------ .../ddui/DataDrivenScreenClosedReason.php | 15 ++++++++++++ src/Azvyl/PMServerUI/ddui/DropdownItem.php | 2 +- src/Azvyl/PMServerUI/ddui/FormCloseError.php | 10 ++++++++ src/Azvyl/PMServerUI/ddui/MessageBox.php | 17 ++++--------- .../PMServerUI/ddui/MessageBoxResult.php | 13 ++++++++++ src/Azvyl/PMServerUI/ddui/Observable.php | 9 ++++++- src/Azvyl/PMServerUI/ddui/PlayerLeftError.php | 10 ++++++++ .../PMServerUI/ddui/ServerShutdownError.php | 10 ++++++++ .../PMServerUI/ddui/TextFilteringError.php | 24 +++++++++++++++++++ src/Azvyl/PMServerUI/ui/ActionFormData.php | 10 -------- .../PMServerUI/ui/ActionFormResponse.php | 10 -------- .../PMServerUI/ui/FormCancelationReason.php | 10 -------- src/Azvyl/PMServerUI/ui/FormRejectError.php | 10 -------- src/Azvyl/PMServerUI/ui/FormRejectReason.php | 10 -------- src/Azvyl/PMServerUI/ui/FormResponse.php | 10 -------- src/Azvyl/PMServerUI/ui/MessageFormData.php | 10 -------- .../PMServerUI/ui/MessageFormResponse.php | 10 -------- src/Azvyl/PMServerUI/ui/ModalFormData.php | 10 -------- src/Azvyl/PMServerUI/ui/ModalFormResponse.php | 10 -------- src/Azvyl/PMServerUI/ui/ServerUI.php | 10 -------- src/Azvyl/PMServerUI/ui/UIManager.php | 10 -------- 26 files changed, 105 insertions(+), 180 deletions(-) create mode 100644 src/Azvyl/PMServerUI/ddui/DataDrivenScreenClosedReason.php create mode 100644 src/Azvyl/PMServerUI/ddui/FormCloseError.php create mode 100644 src/Azvyl/PMServerUI/ddui/MessageBoxResult.php create mode 100644 src/Azvyl/PMServerUI/ddui/PlayerLeftError.php create mode 100644 src/Azvyl/PMServerUI/ddui/ServerShutdownError.php create mode 100644 src/Azvyl/PMServerUI/ddui/TextFilteringError.php diff --git a/src/Azvyl/PMServerUI/PMServerUI.php b/src/Azvyl/PMServerUI/PMServerUI.php index 0752b1b..34efd1d 100644 --- a/src/Azvyl/PMServerUI/PMServerUI.php +++ b/src/Azvyl/PMServerUI/PMServerUI.php @@ -1,15 +1,5 @@ $disabled * @param bool|Observable $visible */ - public function dropdown(Observable|string|UIRawMessage $label, Observable $value, array $items, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ + public function dropdown(Observable|string|UIRawMessage $label, Observable $value, array $items, Observable|string|UIRawMessage $description = null, bool|Observable $disabled = null, bool|Observable $visible = null) : self{ // TODO: verify if $description can be Observable $this->elements[] = new DropdownElement($label, $value, $items, $description, $disabled, $visible); return $this; } @@ -143,18 +143,18 @@ public function header(Observable|string|UIRawMessage $text, bool|Observable $vi /** Returns true if this CustomForm has been shown and not yet acknowledged/closed. */ public function isShowing() : bool{ - return $this->showing ?? false; + return $this->showing; } /** - * Shows the form to the player. Will throw errors if the form is currently being shown or if another behavior pack - * is showing a form. + * Shows the form to the player. Will return false if the client was busy (i.e. in another menu or this one is open). + * Will throw if the user disconnects. * * @return Promise */ - public function show() : Promise{ + public function show() : Promise{ // TODO: return Promise /** @var Promise $promise */ - $promise = new Promise(); + $promise = new Promise(); // TODO: reject instead off throwing exception if($this->showing){ throw new \RuntimeException("Form is already open"); } diff --git a/src/Azvyl/PMServerUI/ddui/DDUIManager.php b/src/Azvyl/PMServerUI/ddui/DDUIManager.php index eb58b5f..9dc0e76 100644 --- a/src/Azvyl/PMServerUI/ddui/DDUIManager.php +++ b/src/Azvyl/PMServerUI/ddui/DDUIManager.php @@ -107,6 +107,9 @@ public function __construct(Plugin $plugin){ $this->handleServerboundScreenClosed($event, $player, $packet); } }, EventPriority::LOW, $plugin); + + // Test whether it works if more than 1 plugin uses the PMServerUI virion or another plugin implements its own DDUI packet sending + // TODO: sync form id with DataPacketSendEvent? } public function hasActiveForm(string $playerUuid) : bool{ @@ -335,7 +338,7 @@ private function handleServerboundScreenClosed(DataPacketReceiveEvent $event, Pl new ClientboundDataStoreChange("minecraft", "custom_form_data", $updateCount, new NoneDataStorePropertyValue()), ]); - $formId = $this->getScreenClosedFormId($packet); + $formId = $packet->getFormId(); if(isset($this->formPromises[$playerUuid][$formId])){ $this->formPromises[$playerUuid][$formId]->resolve(true); unset($this->formPromises[$playerUuid][$formId]); @@ -360,14 +363,6 @@ private function handleServerboundScreenClosed(DataPacketReceiveEvent $event, Pl } } - private function getScreenClosedFormId(ServerboundDataDrivenScreenClosedPacket $packet) : int{ - if(method_exists($packet, 'getFormId')){ - /** @var int */ - return $packet->getFormId(); - } - return (new \ReflectionProperty($packet, 'formId'))->getValue($packet); - } - private function clearPlayerState(string $playerUuid, string $reason) : void{ unset($this->updateCounts[$playerUuid], $this->activeFormIds[$playerUuid], $this->pathUpdateCounts[$playerUuid], $this->clickHandlers[$playerUuid], $this->bindings[$playerUuid], $this->elementInteractionState[$playerUuid]); if(!isset($this->formPromises[$playerUuid])){ diff --git a/src/Azvyl/PMServerUI/ddui/DataDrivenScreenClosedReason.php b/src/Azvyl/PMServerUI/ddui/DataDrivenScreenClosedReason.php new file mode 100644 index 0000000..a0f0759 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/DataDrivenScreenClosedReason.php @@ -0,0 +1,15 @@ + + * @return Promise */ public function show() : Promise{ - /** @var Promise $promise */ + /** @var Promise $promise */ $promise = new Promise(); //TODO: to be implemented return $promise; } - - /** - * Sets the title of the form. - * - * @param Observable|Observable|string|UIRawMessage $text - */ - public function title(Observable|string|UIRawMessage $text) : self{ - // TODO: to be implemented - return $this; - } } diff --git a/src/Azvyl/PMServerUI/ddui/MessageBoxResult.php b/src/Azvyl/PMServerUI/ddui/MessageBoxResult.php new file mode 100644 index 0000000..547da94 --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/MessageBoxResult.php @@ -0,0 +1,13 @@ + */ private array $listeners = []; + private int $nextListenerId = 1; + /** * @param T $data * @param bool $clientWritable @@ -43,12 +45,17 @@ public function getData() : bool|float|int|string|UIRawMessage{ return $this->data; } + // TODO: getFilteredText() + /** * Set the data and notify subscribers. * * @param T $data */ public function setData(bool|float|int|string|UIRawMessage $data) : void{ + if($data === $this->data){ + return; + } $this->data = $data; foreach($this->listeners as $listener){ try{ @@ -87,7 +94,7 @@ public function applyClientUpdate(bool|float|int|string|UIRawMessage $data) : vo * @return Subscription */ public function subscribe(callable $listener) : Subscription{ - $id = count($this->listeners) + 1; + $id = $this->nextListenerId++; $this->listeners[$id] = $listener; return new Subscription(function() use ($id) : void{ unset($this->listeners[$id]); diff --git a/src/Azvyl/PMServerUI/ddui/PlayerLeftError.php b/src/Azvyl/PMServerUI/ddui/PlayerLeftError.php new file mode 100644 index 0000000..f97e83f --- /dev/null +++ b/src/Azvyl/PMServerUI/ddui/PlayerLeftError.php @@ -0,0 +1,10 @@ +