diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b606325d..970565c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ name: Build phar on: push: - branches: [master] + branches: [master, dev] jobs: pharynx: name: build phar @@ -9,7 +9,7 @@ jobs: contents: write runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -20,7 +20,7 @@ jobs: with: additional-assets: | icon.png - - uses: actions/upload-artifact@v5 + - uses: actions/upload-artifact@v6 with: name: Smaccer.phar path: ${{steps.pharynx.outputs.output-phar}} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..064117b9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + name: Tests (PHP ${{ matrix.php }}) + runs-on: ${{ matrix.image }} + + strategy: + matrix: + image: [ubuntu-latest] + php: [8.5] + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Setup PHP + uses: pmmp/setup-php-action@3c38c259834f945351a662392836d90eb08abff0 # 3.2.0 + with: + php-version: ${{ matrix.php }} + install-path: "./bin" + pm-version-major: "5" + + - name: Restore Composer package cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5 + with: + path: | + ~/.cache/composer/files + ~/.cache/composer/vcs + key: "composer-v2-cache-${{ matrix.php }}-${{ hashFiles('./composer.lock') }}" + restore-keys: | + composer-v2-cache- + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyze --no-progress --memory-limit=2G \ No newline at end of file diff --git a/.gitignore b/.gitignore index db7a216a..c3a68254 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -# Ignore Composer's vendor directory -vendor/ +/vendor/ +.build/ \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 00000000..2ba2e3a0 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,165 @@ +save(); + +$ruleSet = Config\RuleSet\Php85::create() + //->withHeader($license->header()) + ->withCustomFixers(Config\Fixers::fromFixers( + new Fixer\CommentSurroundedBySpacesFixer(), + new Fixer\CommentedOutFunctionFixer(), + new Fixer\ConstructorEmptyBracesFixer(), + new Fixer\EmptyFunctionBodyFixer(), + new Fixer\NoCommentedOutCodeFixer(), + new Fixer\NoDoctrineMigrationsGeneratedCommentFixer(), + new Fixer\NoImportFromGlobalNamespaceFixer(), + new Fixer\NoLeadingSlashInGlobalNamespaceFixer(), + new Fixer\NoNullableBooleanTypeFixer(), + new Fixer\NoPhpStormGeneratedCommentFixer(), + new Fixer\NoReferenceInFunctionDefinitionFixer(), + new Fixer\NoSuperfluousConcatenationFixer(), + new Fixer\NoTrailingCommaInSinglelineFixer(), + new Fixer\NoUselessCommentFixer(), + new Fixer\NoUselessDirnameCallFixer(), + new Fixer\NoUselessDoctrineRepositoryCommentFixer(), + new Fixer\NoUselessParenthesisFixer(), + new Fixer\NoUselessStrlenFixer(), + new Fixer\PhpdocNoIncorrectVarAnnotationFixer(), + new Fixer\PhpdocNoSuperfluousParamFixer(), + new Fixer\PhpdocParamTypeFixer(), + new Fixer\PhpdocSelfAccessorFixer(), + new Fixer\PhpdocSingleLineVarFixer(), + new Fixer\PhpdocTypesTrimFixer(), + new Fixer\PromotedConstructorPropertyFixer(), + new Fixer\ReadonlyPromotedPropertiesFixer(), + new Fixer\SingleSpaceAfterStatementFixer(), + new Fixer\StringableInterfaceFixer(), + )) + ->withRules(Config\Rules::fromArray([ + 'align_multiline_comment' => [ + 'comment_type' => 'phpdocs_only' + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'declare' + ] + ], + 'blank_line_between_import_groups' => false, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'braces_position' => [ + 'classes_opening_brace' => 'same_line', + 'functions_opening_brace' => 'same_line', + ], + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + "date_time_immutable" => false, + 'error_suppression' => [ + 'noise_remaining_usages' => false, + ], + 'final_class' => false, + 'final_public_method_for_abstract_class' => false, + 'global_namespace_import' => [ + 'import_constants' => true, + 'import_functions' => true, + 'import_classes' => null, + ], + 'header_comment' => [ + 'comment_type' => 'comment', + 'header' => trim($license->header()), + 'location' => 'after_open', + ], + 'mb_str_functions' => false, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'native_constant_invocation' => [ + 'scope' => 'namespaced' + ], + 'native_function_invocation' => [ + 'scope' => 'namespaced', + 'include' => ['@all'], + ], + 'new_with_parentheses' => [ + 'named_class' => true, + 'anonymous_class' => false, + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', + 'function', + 'const', + ], + 'sort_algorithm' => 'alpha' + ], + 'ordered_class_elements' => false, + 'phpdoc_align' => [ + 'align' => 'vertical', + 'tags' => [ + 'param', + ] + ], + 'phpdoc_line_span' => [ + 'property' => 'single', + 'method' => null, + 'const' => null + ], + 'phpdoc_list_type' => false, + 'phpdoc_order' => [ + 'order' => [ + 'param', + 'return', + 'throws', + ], + ], + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'alpha', + ], + 'return_type_declaration' => [ + 'space_before' => 'one' + ], + 'simplified_if_return' => true, + 'single_line_empty_body' => true, + 'static_lambda' => false, + 'strict_param' => true, + 'trailing_comma_in_multiline' => [ + 'after_heredoc' => false, + 'elements' => [ + 'arrays', + ], + ], + 'use_arrow_functions' => true, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + ])); + +$config = Config\Factory::fromRuleSet($ruleSet) + ->setIndent("\t") + ->setLineEnding("\n") + ->setCacheFile(__DIR__ . '/.build/php-cs-fixer/.php-cs-fixer.cache'); +$config->getFinder() + ->in(__DIR__ . '/src'); + +return $config; \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 94628679..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 - 2025 AIPTU - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..8d74feb8 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +# The MIT License (MIT) + +Copyright (c) 2024-2026 AIPTU + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the _Software_), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED **AS IS**, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 207c853e..122d3b49 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,6 @@ npc-default-settings: ## Credits - [Bedrock-Emotes by TwistedAsylumMC](https://github.com/TwistedAsylumMC/Bedrock-Emotes) for providing the emotes. -- [CPlot by ColinHDev](https://github.com/ColinHDev/CPlot) for implementing promises. ## Additional Notes diff --git a/composer.json b/composer.json index 570be22d..fca0b21b 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "MIT", "type": "project", "require": { + "php": "^8.3", "aiptu/libplaceholder": "^1.0", "frago9876543210/forms": "dev-master", "ifera-mc/update-notifier": "dev-master", @@ -11,6 +12,16 @@ "paroxity/commando": "dev-master", "pocketmine/pocketmine-mp": "^5.16" }, + "require-dev": { + "ergebnis/composer-normalize": "^2.42", + "ergebnis/license": "^2.4", + "ergebnis/php-cs-fixer-config": "^6.26", + "friendsofphp/php-cs-fixer": "^3.54", + "kubawerlos/php-cs-fixer-custom-fixers": "^3.21", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0" + }, "repositories": [ { "type": "vcs", @@ -31,5 +42,12 @@ ], "autoload": { "classmap": ["src"] + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true + } } } diff --git a/composer.lock b/composer.lock index c1843845..57c16778 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "01bcaa31d512d1264231d5cd98bd1784", + "content-hash": "10f543e9cb62093138f13471b09b9086", "packages": [ { "name": "adhocore/json-comment", @@ -67,19 +67,20 @@ }, { "name": "aiptu/libplaceholder", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/AIPTU/libplaceholder.git", - "reference": "99afc16093d4b43febdad445b16fe3626d897531" + "reference": "415af1d5df07839cfd3b88100db8111a9b8a4b3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AIPTU/libplaceholder/zipball/99afc16093d4b43febdad445b16fe3626d897531", - "reference": "99afc16093d4b43febdad445b16fe3626d897531", + "url": "https://api.github.com/repos/AIPTU/libplaceholder/zipball/415af1d5df07839cfd3b88100db8111a9b8a4b3f", + "reference": "415af1d5df07839cfd3b88100db8111a9b8a4b3f", "shasum": "" }, "require": { + "php": "^8.3", "pocketmine/pocketmine-mp": "^5.0" }, "type": "library", @@ -101,22 +102,22 @@ "description": "A flexible placeholder library for PocketMine-MP plugins, allowing dynamic insertion of player and global data into messages.", "support": { "issues": "https://github.com/AIPTU/libplaceholder/issues", - "source": "https://github.com/AIPTU/libplaceholder/tree/1.0.0" + "source": "https://github.com/AIPTU/libplaceholder/tree/1.1.0" }, - "time": "2024-09-07T07:12:46+00:00" + "time": "2025-11-30T10:08:58+00:00" }, { "name": "brick/math", - "version": "0.14.0", + "version": "0.14.8", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2" + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", - "reference": "113a8ee2656b882d4c3164fa31aa6e12cbb7aaa2", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", "shasum": "" }, "require": { @@ -155,7 +156,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.0" + "source": "https://github.com/brick/math/tree/0.14.8" }, "funding": [ { @@ -163,7 +164,7 @@ "type": "github" } ], - "time": "2025-08-29T12:40:03+00:00" + "time": "2026-02-10T14:33:43+00:00" }, { "name": "frago9876543210/forms", @@ -333,16 +334,16 @@ }, { "name": "muqsit/simple-packet-handler", - "version": "0.1.5", + "version": "0.1.6", "source": { "type": "git", "url": "https://github.com/Muqsit/SimplePacketHandler.git", - "reference": "3400302e0d77757e716ac69a4099561bde5634b3" + "reference": "4e7b399c61f2def3c088ecde96e15d1f9858914f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Muqsit/SimplePacketHandler/zipball/3400302e0d77757e716ac69a4099561bde5634b3", - "reference": "3400302e0d77757e716ac69a4099561bde5634b3", + "url": "https://api.github.com/repos/Muqsit/SimplePacketHandler/zipball/4e7b399c61f2def3c088ecde96e15d1f9858914f", + "reference": "4e7b399c61f2def3c088ecde96e15d1f9858914f", "shasum": "" }, "require": { @@ -373,22 +374,22 @@ "description": "Handle specific data packets (virion for PMMP API 4.0.0)", "support": { "issues": "https://github.com/Muqsit/SimplePacketHandler/issues", - "source": "https://github.com/Muqsit/SimplePacketHandler/tree/0.1.5" + "source": "https://github.com/Muqsit/SimplePacketHandler/tree/0.1.6" }, - "time": "2025-02-16T23:26:12+00:00" + "time": "2026-02-17T11:40:38+00:00" }, { "name": "netresearch/jsonmapper", - "version": "v5.0.0", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/cweiske/jsonmapper.git", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" + "reference": "980674efdda65913492d29a8fd51c82270dd37bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", - "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/980674efdda65913492d29a8fd51c82270dd37bb", + "reference": "980674efdda65913492d29a8fd51c82270dd37bb", "shasum": "" }, "require": { @@ -424,9 +425,9 @@ "support": { "email": "cweiske@cweiske.de", "issues": "https://github.com/cweiske/jsonmapper/issues", - "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.1" }, - "time": "2024-09-08T10:20:00+00:00" + "time": "2026-02-22T16:28:03+00:00" }, { "name": "paroxity/commando", @@ -434,18 +435,26 @@ "source": { "type": "git", "url": "https://github.com/AIPTU/Commando.git", - "reference": "877a464bf95ba82dda647dc22aa2bea9176c0b2d" + "reference": "9d4a6111d669f0be4806d816461aebccb2637d2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/AIPTU/Commando/zipball/877a464bf95ba82dda647dc22aa2bea9176c0b2d", - "reference": "877a464bf95ba82dda647dc22aa2bea9176c0b2d", + "url": "https://api.github.com/repos/AIPTU/Commando/zipball/9d4a6111d669f0be4806d816461aebccb2637d2e", + "reference": "9d4a6111d669f0be4806d816461aebccb2637d2e", "shasum": "" }, "require": { "muqsit/simple-packet-handler": "^0.1.4", "pocketmine/pocketmine-mp": "^5.0" }, + "require-dev": { + "ergebnis/php-cs-fixer-config": "^6.26", + "friendsofphp/php-cs-fixer": "^3.54", + "kubawerlos/php-cs-fixer-custom-fixers": "^3.21", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0" + }, "default-branch": true, "type": "library", "extra": { @@ -468,7 +477,7 @@ "support": { "source": "https://github.com/AIPTU/Commando/tree/master" }, - "time": "2025-11-12T03:18:56+00:00" + "time": "2026-02-22T09:44:14+00:00" }, { "name": "pocketmine/bedrock-block-upgrade-schema", @@ -498,16 +507,16 @@ }, { "name": "pocketmine/bedrock-data", - "version": "6.2.0+bedrock-1.21.120", + "version": "6.6.0+bedrock-1.26.20", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockData.git", - "reference": "f186e363ac8abbc3242481cad59ae620549dc709" + "reference": "e5a25957497a4520f4672a379e2a3f6b87602e37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/f186e363ac8abbc3242481cad59ae620549dc709", - "reference": "f186e363ac8abbc3242481cad59ae620549dc709", + "url": "https://api.github.com/repos/pmmp/BedrockData/zipball/e5a25957497a4520f4672a379e2a3f6b87602e37", + "reference": "e5a25957497a4520f4672a379e2a3f6b87602e37", "shasum": "" }, "type": "library", @@ -518,22 +527,22 @@ "description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP", "support": { "issues": "https://github.com/pmmp/BedrockData/issues", - "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.120" + "source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.26.20" }, - "time": "2025-10-28T22:15:22+00:00" + "time": "2026-05-09T15:06:58+00:00" }, { "name": "pocketmine/bedrock-item-upgrade-schema", - "version": "1.16.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git", - "reference": "8c48ceaa72d390e89c4dbff9542aa4dfa734057d" + "reference": "e19685d2e7e76eb7446115c556df34e5d627d072" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/8c48ceaa72d390e89c4dbff9542aa4dfa734057d", - "reference": "8c48ceaa72d390e89c4dbff9542aa4dfa734057d", + "url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/e19685d2e7e76eb7446115c556df34e5d627d072", + "reference": "e19685d2e7e76eb7446115c556df34e5d627d072", "shasum": "" }, "type": "library", @@ -544,22 +553,22 @@ "description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves", "support": { "issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues", - "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.16.0" + "source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.17.0" }, - "time": "2025-10-02T13:22:32+00:00" + "time": "2026-05-06T13:12:04+00:00" }, { "name": "pocketmine/bedrock-protocol", - "version": "53.1.0+bedrock-1.21.120", + "version": "57.1.0+bedrock-1.26.20", "source": { "type": "git", "url": "https://github.com/pmmp/BedrockProtocol.git", - "reference": "16a2fa153e93aec4388c89ccf6a7e5202e66afcb" + "reference": "b38ef7d8a87651d8b253645196fac97b6825cb98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/16a2fa153e93aec4388c89ccf6a7e5202e66afcb", - "reference": "16a2fa153e93aec4388c89ccf6a7e5202e66afcb", + "url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/b38ef7d8a87651d8b253645196fac97b6825cb98", + "reference": "b38ef7d8a87651d8b253645196fac97b6825cb98", "shasum": "" }, "require": { @@ -591,22 +600,22 @@ "description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP", "support": { "issues": "https://github.com/pmmp/BedrockProtocol/issues", - "source": "https://github.com/pmmp/BedrockProtocol/tree/53.1.0+bedrock-1.21.120" + "source": "https://github.com/pmmp/BedrockProtocol/tree/57.1.0+bedrock-1.26.20" }, - "time": "2025-11-03T20:07:57+00:00" + "time": "2026-05-09T17:13:12+00:00" }, { "name": "pocketmine/binaryutils", - "version": "0.2.6", + "version": "0.2.7", "source": { "type": "git", "url": "https://github.com/pmmp/BinaryUtils.git", - "reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84" + "reference": "14c044afa33cb581b4a6d1ea04a87e0bc99e824b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/ccfc1899b859d45814ea3592e20ebec4cb731c84", - "reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84", + "url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/14c044afa33cb581b4a6d1ea04a87e0bc99e824b", + "reference": "14c044afa33cb581b4a6d1ea04a87e0bc99e824b", "shasum": "" }, "require": { @@ -615,9 +624,9 @@ }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "~1.10.3", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.0.0", + "phpstan/phpstan": "2.1.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0" }, "type": "library", @@ -633,36 +642,36 @@ "description": "Classes and methods for conveniently handling binary data", "support": { "issues": "https://github.com/pmmp/BinaryUtils/issues", - "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.6" + "source": "https://github.com/pmmp/BinaryUtils/tree/0.2.7" }, - "time": "2024-03-04T15:04:17+00:00" + "time": "2025-12-24T04:20:35+00:00" }, { "name": "pocketmine/callback-validator", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/pmmp/CallbackValidator.git", - "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2" + "reference": "143fa6e13254f1ab90c31b223982016f95635c37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/CallbackValidator/zipball/64787469766bcaa7e5885242e85c23c25e8c55a2", - "reference": "64787469766bcaa7e5885242e85c23c25e8c55a2", + "url": "https://api.github.com/repos/pmmp/CallbackValidator/zipball/143fa6e13254f1ab90c31b223982016f95635c37", + "reference": "143fa6e13254f1ab90c31b223982016f95635c37", "shasum": "" }, "require": { "ext-reflection": "*", - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "replace": { "daverandom/callback-validator": "*" }, "require-dev": { "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "0.12.59", - "phpstan/phpstan-strict-rules": "^0.12.4", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + "phpstan/phpstan": "2.1.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0" }, "type": "library", "autoload": { @@ -683,9 +692,9 @@ "description": "Fork of daverandom/callback-validator - Tools for validating callback signatures", "support": { "issues": "https://github.com/pmmp/CallbackValidator/issues", - "source": "https://github.com/pmmp/CallbackValidator/tree/1.0.3" + "source": "https://github.com/pmmp/CallbackValidator/tree/1.0.4" }, - "time": "2020-12-11T01:45:37+00:00" + "time": "2025-12-24T01:25:05+00:00" }, { "name": "pocketmine/color", @@ -727,24 +736,24 @@ }, { "name": "pocketmine/errorhandler", - "version": "0.7.0", + "version": "0.7.1", "source": { "type": "git", "url": "https://github.com/pmmp/ErrorHandler.git", - "reference": "cae94884368a74ece5294b9ff7fef18732dcd921" + "reference": "84c9ec829163f21ced425d8f3b27d32f908e87fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/cae94884368a74ece5294b9ff7fef18732dcd921", - "reference": "cae94884368a74ece5294b9ff7fef18732dcd921", + "url": "https://api.github.com/repos/pmmp/ErrorHandler/zipball/84c9ec829163f21ced425d8f3b27d32f908e87fb", + "reference": "84c9ec829163f21ced425d8f3b27d32f908e87fb", "shasum": "" }, "require": { "php": "^8.0" }, "require-dev": { - "phpstan/phpstan": "~1.10.3", - "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan": "2.1.33", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0" }, "type": "library", @@ -760,9 +769,9 @@ "description": "Utilities to handle nasty PHP E_* errors in a usable way", "support": { "issues": "https://github.com/pmmp/ErrorHandler/issues", - "source": "https://github.com/pmmp/ErrorHandler/tree/0.7.0" + "source": "https://github.com/pmmp/ErrorHandler/tree/0.7.1" }, - "time": "2024-04-02T18:29:54+00:00" + "time": "2025-12-24T02:26:52+00:00" }, { "name": "pocketmine/log", @@ -891,16 +900,16 @@ }, { "name": "pocketmine/pocketmine-mp", - "version": "5.37.1", + "version": "5.43.1", "source": { "type": "git", "url": "https://github.com/pmmp/PocketMine-MP.git", - "reference": "114556003835003a97c4f525e8ac3129721af139" + "reference": "763354dfadf13e4df00b8485c93da0c9dc2965a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/PocketMine-MP/zipball/114556003835003a97c4f525e8ac3129721af139", - "reference": "114556003835003a97c4f525e8ac3129721af139", + "url": "https://api.github.com/repos/pmmp/PocketMine-MP/zipball/763354dfadf13e4df00b8485c93da0c9dc2965a0", + "reference": "763354dfadf13e4df00b8485c93da0c9dc2965a0", "shasum": "" }, "require": { @@ -934,11 +943,11 @@ "php": "^8.1", "php-64bit": "*", "pocketmine/bedrock-block-upgrade-schema": "~5.2.0+bedrock-1.21.110", - "pocketmine/bedrock-data": "~6.2.0+bedrock-1.21.120", - "pocketmine/bedrock-item-upgrade-schema": "~1.16.0+bedrock-1.21.110", - "pocketmine/bedrock-protocol": "~53.1.0+bedrock-1.21.120", + "pocketmine/bedrock-data": "~6.6.0+bedrock-1.26.20", + "pocketmine/bedrock-item-upgrade-schema": "~1.17.0+bedrock-1.26.20", + "pocketmine/bedrock-protocol": "~57.1.0+bedrock-1.26.20", "pocketmine/binaryutils": "^0.2.1", - "pocketmine/callback-validator": "^1.0.2", + "pocketmine/callback-validator": "~1.0.4", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.7.0", "pocketmine/log": "^0.4.0", @@ -955,7 +964,7 @@ "symfony/polyfill-mbstring": "*" }, "require-dev": { - "phpstan/phpstan": "2.1.31", + "phpstan/phpstan": "2.1.46", "phpstan/phpstan-phpunit": "^2.0.0", "phpstan/phpstan-strict-rules": "^2.0.0", "phpunit/phpunit": "^10.5.24" @@ -966,7 +975,10 @@ "src/CoreConstants.php" ], "psr-4": { - "pocketmine\\": "src/" + "pocketmine\\": [ + "src/", + "generated/" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -977,7 +989,7 @@ "homepage": "https://pmmp.io", "support": { "issues": "https://github.com/pmmp/PocketMine-MP/issues", - "source": "https://github.com/pmmp/PocketMine-MP/tree/5.37.1" + "source": "https://github.com/pmmp/PocketMine-MP/tree/5.43.1" }, "funding": [ { @@ -989,20 +1001,20 @@ "type": "patreon" } ], - "time": "2025-11-07T16:44:39+00:00" + "time": "2026-05-09T17:40:51+00:00" }, { "name": "pocketmine/raklib", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/pmmp/RakLib.git", - "reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b" + "reference": "669eb4d1e644f91437323ef24ce3ee985182b829" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pmmp/RakLib/zipball/a28d05216d34dbd00e8aed827a58df6b4c11510b", - "reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b", + "url": "https://api.github.com/repos/pmmp/RakLib/zipball/669eb4d1e644f91437323ef24ce3ee985182b829", + "reference": "669eb4d1e644f91437323ef24ce3ee985182b829", "shasum": "" }, "require": { @@ -1014,7 +1026,7 @@ "pocketmine/log": "^0.3.0 || ^0.4.0" }, "require-dev": { - "phpstan/phpstan": "2.1.0", + "phpstan/phpstan": "2.1.33", "phpstan/phpstan-strict-rules": "^2.0" }, "type": "library", @@ -1030,9 +1042,9 @@ "description": "A RakNet server implementation written in PHP", "support": { "issues": "https://github.com/pmmp/RakLib/issues", - "source": "https://github.com/pmmp/RakLib/tree/1.2.0" + "source": "https://github.com/pmmp/RakLib/tree/1.2.1" }, - "time": "2025-06-08T17:36:06+00:00" + "time": "2025-12-24T03:02:42+00:00" }, { "name": "pocketmine/raklib-ipc", @@ -1193,20 +1205,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.1", + "version": "4.9.2", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + "reference": "8429c78ca35a09f27565311b98101e2826affde0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", - "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", "shasum": "" }, "require": { - "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -1265,22 +1277,22 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.1" + "source": "https://github.com/ramsey/uuid/tree/4.9.2" }, - "time": "2025-09-04T20:59:21+00:00" + "time": "2025-12-14T04:43:48+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.24", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c507b077756b4e3e09adbbe7975fac81cd3722ca", + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca", "shasum": "" }, "require": { @@ -1317,7 +1329,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.24" + "source": "https://github.com/symfony/filesystem/tree/v6.4.39" }, "funding": [ { @@ -1337,21 +1349,3618 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2026-05-07T13:11:42+00:00" } ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": { - "frago9876543210/forms": 20, - "ifera-mc/update-notifier": 20, - "jasonw4331/libpmquery": 20, - "paroxity/commando": 20 - }, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "ergebnis/agent-detector", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/agent-detector.git", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "infection/infection": "^0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a detector for detecting the presence of an agent.", + "homepage": "https://github.com/ergebnis/agent-detector", + "support": { + "issues": "https://github.com/ergebnis/agent-detector/issues", + "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/agent-detector" + }, + "time": "2026-05-07T08:19:07+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.52.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "988f83f5e51a42cdd2337e5fcd935432f8dfa33c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/988f83f5e51a42cdd2337e5fcd935432f8dfa33c", + "reference": "988f83f5e51a42cdd2337e5fcd935432f8dfa33c", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.4.0", + "ergebnis/json-normalizer": "^4.9.0", + "ergebnis/json-printer": "^3.7.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "localheinz/diff": "^1.3.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "composer/composer": "^2.9.8", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.62.1", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.11", + "phpunit/phpunit": "^9.6.33", + "rector/rector": "^2.4.3", + "symfony/filesystem": "^5.4.41" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "branch-alias": { + "dev-main": "2.52-dev" + }, + "plugin-optional": true, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2026-05-15T15:39:24+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "7b56d2b5d9e897e75b43e2e753075a0904c921b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/7b56d2b5d9e897e75b43e2e753075a0904c921b1", + "reference": "7b56d2b5d9e897e75b43e2e753075a0904c921b1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpstan-rules": "^2.11.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpstan/phpstan-deprecation-rules": "^2.0.3", + "phpstan/phpstan-phpunit": "^2.0.7", + "phpstan/phpstan-strict-rules": "^2.0.6", + "phpunit/phpunit": "^9.6.24", + "rector/rector": "^2.1.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.7-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2025-09-06T09:08:45+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.10.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "77961faf2c651c3f05977b53c6c68e8434febf62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/77961faf2c651c3f05977b53c6c68e8434febf62", + "reference": "77961faf2c651c3f05977b53c6c68e8434febf62", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ergebnis/json-printer": "^3.5.0", + "ergebnis/json-schema-validator": "^4.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "composer/semver": "^3.4.3", + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.19", + "rector/rector": "^1.2.10" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.11-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2025-09-06T09:18:13+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/b58c3c468a7ff109fdf9a255f17de29ecbe5276c", + "reference": "b58c3c468a7ff109fdf9a255f17de29ecbe5276c", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/php-cs-fixer-config": "^6.60.2", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.16.0", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^9.6.34", + "rector/rector": "^2.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.8-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides an abstraction of a JSON pointer.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2026-04-07T14:52:13+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.8.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "211d73fc7ec6daf98568ee6ed6e6d133dee8503e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/211d73fc7ec6daf98568ee6ed6e6d133dee8503e", + "reference": "211d73fc7ec6daf98568ee6ed6e6d133dee8503e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.1", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.21", + "rector/rector": "^1.2.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.9-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2025-09-06T09:59:26+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.5.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "b739527a480a9e3651360ad351ea77e7e9019df2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/b739527a480a9e3651360ad351ea77e7e9019df2", + "reference": "b739527a480a9e3651360ad351ea77e7e9019df2", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.44.0", + "ergebnis/data-provider": "^3.3.0", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.1", + "fakerphp/faker": "^1.24.0", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.20", + "rector/rector": "^1.2.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.6-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2025-09-06T11:37:35+00:00" + }, + { + "name": "ergebnis/license", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/license.git", + "reference": "97c30560b6d0c68d9a213756a2ed59903dc1223e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/license/zipball/97c30560b6d0c68d9a213756a2ed59903dc1223e", + "reference": "97c30560b6d0c68d9a213756a2ed59903dc1223e", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.47.0", + "ergebnis/data-provider": "^3.5.0", + "ergebnis/php-cs-fixer-config": "^6.53.0", + "ergebnis/phpstan-rules": "^2.5.2", + "ergebnis/phpunit-slow-test-detector": "^2.20.0", + "fakerphp/faker": "^1.24.1", + "infection/infection": "~0.26.6", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.10", + "phpstan/phpstan-deprecation-rules": "^1.2.1", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.1", + "phpunit/phpunit": "^9.6.19", + "rector/rector": "^1.2.10", + "symfony/filesystem": "^5.4.41" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.6-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\License\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides an abstraction of an open-source license.", + "homepage": "https://github.com/ergebnis/license", + "keywords": [ + "license" + ], + "support": { + "issues": "https://github.com/ergebnis/license/issues", + "security": "https://github.com/ergebnis/license/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/license" + }, + "time": "2025-08-30T16:40:37+00:00" + }, + { + "name": "ergebnis/php-cs-fixer-config", + "version": "6.62.1", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/php-cs-fixer-config.git", + "reference": "a0ffd53a8b59da9eaa90c5e43511e7707e528f4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/php-cs-fixer-config/zipball/a0ffd53a8b59da9eaa90c5e43511e7707e528f4b", + "reference": "a0ffd53a8b59da9eaa90c5e43511e7707e528f4b", + "shasum": "" + }, + "require": { + "erickskrauch/php-cs-fixer-custom-fixers": "~1.3.1", + "ext-filter": "*", + "friendsofphp/php-cs-fixer": "~3.95.2", + "kubawerlos/php-cs-fixer-custom-fixers": "~3.37.2", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.51.0", + "ergebnis/data-provider": "^3.6.0", + "ergebnis/license": "^2.7.0", + "ergebnis/phpstan-rules": "^2.13.1", + "ergebnis/phpunit-slow-test-detector": "^2.24.0", + "ergebnis/rector-rules": "^1.18.1", + "fakerphp/faker": "^1.24.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.54", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.11", + "phpunit/phpunit": "^9.6.33", + "rector/rector": "^2.4.3", + "symfony/filesystem": "^5.0.0 || ^6.0.0", + "symfony/process": "^5.0.0 || ^6.0.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\PhpCsFixer\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a configuration factory and rule set factories for friendsofphp/php-cs-fixer.", + "homepage": "https://github.com/ergebnis/php-cs-fixer-config", + "support": { + "issues": "https://github.com/ergebnis/php-cs-fixer-config/issues", + "security": "https://github.com/ergebnis/php-cs-fixer-config/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/php-cs-fixer-config" + }, + "time": "2026-05-15T15:14:10+00:00" + }, + { + "name": "erickskrauch/php-cs-fixer-custom-fixers", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/erickskrauch/php-cs-fixer-custom-fixers.git", + "reference": "8c54170266d7a98610f43f8223b3ac653043821a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erickskrauch/php-cs-fixer-custom-fixers/zipball/8c54170266d7a98610f43f8223b3ac653043821a", + "reference": "8c54170266d7a98610f43f8223b3ac653043821a", + "shasum": "" + }, + "require": { + "friendsofphp/php-cs-fixer": "^3", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ely/php-code-style": "^1", + "ergebnis/composer-normalize": "^2.28", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.11.x-dev", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "symfony/phpunit-bridge": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "ErickSkrauch\\PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ErickSkrauch", + "email": "erickskrauch@ely.by" + } + ], + "description": "A set of custom fixers for PHP-CS-Fixer", + "homepage": "https://github.com/erickskrauch/php-cs-fixer-custom-fixers", + "keywords": [ + "Code style", + "dev", + "php-cs-fixer" + ], + "support": { + "issues": "https://github.com/erickskrauch/php-cs-fixer-custom-fixers/issues", + "source": "https://github.com/erickskrauch/php-cs-fixer-custom-fixers/tree/1.3.1" + }, + "time": "2025-11-24T16:54:01+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.95.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a28d88a5e172b27e78d0816992b15a9df3da20f1", + "reference": "a28d88a5e172b27e78d0816992b15a9df3da20f1", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.3", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.5", + "ergebnis/agent-detector": "^1.1.1", + "ext-filter": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.3", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.6", + "react/event-loop": "^1.5", + "react/socket": "^1.16", + "react/stream": "^1.4", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.33", + "symfony/polyfill-php80": "^1.33", + "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3.1 || ^2.11.0", + "infection/infection": "^0.32.7", + "justinrainbow/json-schema": "^6.8.0", + "keradus/cli-executor": "^2.3", + "mikey179/vfsstream": "^1.6.12", + "php-coveralls/php-coveralls": "^2.9.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", + "symfony/polyfill-php85": "^1.33", + "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", + "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.8" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/**/Internal/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.2" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2026-05-15T09:20:44+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.8.2", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.4", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "dev-main", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + }, + "time": "2026-05-05T05:39:01+00:00" + }, + { + "name": "kubawerlos/php-cs-fixer-custom-fixers", + "version": "v3.37.2", + "source": { + "type": "git", + "url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git", + "reference": "678df979ce743466b42ddb6eea46b3f4c9a7bade" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/678df979ce743466b42ddb6eea46b3f4c9a7bade", + "reference": "678df979ce743466b42ddb6eea46b3f4c9a7bade", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-tokenizer": "*", + "friendsofphp/php-cs-fixer": "^3.87", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpCsFixerCustomFixers\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kuba Werłos", + "email": "werlos@gmail.com" + } + ], + "description": "A set of custom fixers for PHP CS Fixer", + "support": { + "issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues", + "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.37.2" + }, + "funding": [ + { + "url": "https://github.com/kubawerlos", + "type": "github" + } + ], + "time": "2026-05-12T16:22:19+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/33bd840935970cda6691c23fc7d94ae764c0734c", + "reference": "33bd840935970cda6691c23fc7d94ae764c0734c", + "shasum": "" + }, + "require": { + "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.0 || ^8.5.23", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/localheinz/diff/issues", + "source": "https://github.com/localheinz/diff/tree/1.3.0" + }, + "time": "2025-08-30T09:44:18+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.55", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9eaac3826ed5e9b8427350a43cac825eeca3f566", + "reference": "9eaac3826ed5e9b8427350a43cac825eeca3f566", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-05-18T11:57:34+00:00" + }, + { + "name": "phpstan/phpstan-strict-rules", + "version": "2.0.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan-strict-rules.git", + "reference": "9b000a578b85b32945b358b172c7b20e91189024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024", + "reference": "9b000a578b85b32945b358b172c7b20e91189024", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.1.39" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "rules.neon" + ] + } + }, + "autoload": { + "psr-4": { + "PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Extra strict and opinionated rules for PHPStan", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11" + }, + "time": "2026-05-02T06:54:10+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/970f0e71945556422ee4570ccbabaedc3cf04ad3", + "reference": "970f0e71945556422ee4570ccbabaedc3cf04ad3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/socket": "^1.16", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.7" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-12-23T15:25:20+00:00" + }, + { + "name": "react/dns", + "version": "v1.14.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.14.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-18T19:34:28+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/promise", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/23444f53a813a3296c1368bb104793ce8d88f04a", + "reference": "23444f53a813a3296c1368bb104793ce8d88f04a", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.12.28 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-08-19T18:57:03+00:00" + }, + { + "name": "react/socket", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.17.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-19T20:47:34+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "8.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b36d33b6e796513de7cb7df053afb3f55eefcd47", + "reference": "b36d33b6e796513de7cb7df053afb3f55eefcd47", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/8.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-05-15T04:58:09+00:00" + }, + { + "name": "symfony/console", + "version": "v8.0.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/3156577f46a38aa1b9323aad223de7a9cd426782", + "reference": "3156577f46a38aa1b9323aad223de7a9cd426782", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4|^8.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.0.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:07:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:51:42+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8da41214757b87d97f181e3d14a4179286151007" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007", + "reference": "8da41214757b87d97f181e3d14a4179286151007", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-26T13:13:48+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T18:47:49+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/26d89e459f037d2873300605d0a07e7a8ef84db0", + "reference": "26d89e459f037d2873300605d0a07e7a8ef84db0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:56:32+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "reference": "85954ed72d5440ea4dc9a10b7e49e01df766ffa3", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/39be2ad058a3c0bd558edca23e65f009865d75ff", + "reference": "39be2ad058a3c0bd558edca23e65f009865d75ff", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:07:53+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "frago9876543210/forms": 20, + "ifera-mc/update-notifier": 20, + "jasonw4331/libpmquery": 20, + "paroxity/commando": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, "platform-dev": {}, "plugin-api-version": "2.9.0" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..f8f330e9 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +parameters: + level: 9 + paths: + - src + treatPhpDocTypesAsCertain: false + ignoreErrors: + - identifier: missingType.iterableValue \ No newline at end of file diff --git a/plugin.yml b/plugin.yml index 32b3a209..d86d99d4 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,7 +1,7 @@ name: Smaccer main: aiptu\smaccer\Smaccer version: 1.1.0 -api: [5.36.0] +api: [5.39.0] author: AIPTU permissions: smaccer.bypass.cooldown: diff --git a/src/aiptu/smaccer/EventHandler.php b/src/aiptu/smaccer/EventHandler.php index 718632ac..675fe7e3 100644 --- a/src/aiptu/smaccer/EventHandler.php +++ b/src/aiptu/smaccer/EventHandler.php @@ -1,7 +1,7 @@ canExecuteCommands($damager)) { - $entity->executeCommands($damager); - } + if ($entity->canExecuteCommands($damager)) { + $entity->executeCommands($damager); + } + $event->cancel(); return; } @@ -143,7 +144,7 @@ public function onAttack(EntityDamageEvent $event) : void { if (!$entity->isOwnedBy($damager) && !$damager->hasPermission(Permissions::COMMAND_EDIT_OTHERS)) { $damager->sendMessage(TextFormat::RED . "You don't have permission to edit this entity!"); } else { - FormManager::sendEditMenuForm($damager, $entity); + EditForms::sendMenu($damager, $entity); } break; @@ -151,7 +152,9 @@ public function onAttack(EntityDamageEvent $event) : void { if (!$entity->isOwnedBy($damager) && !$damager->hasPermission(Permissions::COMMAND_DELETE_OTHERS)) { $damager->sendMessage(TextFormat::RED . "You don't have permission to delete this entity!"); } else { - SmaccerHandler::getInstance()->despawnNPC($entity->getCreatorId(), $entity)->onCompletion( + SmaccerHandler::getInstance()->despawnNPC( + $entity->getCreatorId(), + $entity, function (bool $success) use ($damager, $npcId, $entity) : void { $damager->sendMessage(TextFormat::GREEN . 'NPC ' . $entity->getName() . ' with ID ' . $npcId . ' despawned successfully.'); }, @@ -207,4 +210,4 @@ public function onInteract(PlayerEntityInteractEvent $event) : void { $event->cancel(); } } -} \ No newline at end of file +} diff --git a/src/aiptu/smaccer/NPCDefaultSettings.php b/src/aiptu/smaccer/NPCDefaultSettings.php index 45aa346d..09773ef4 100644 --- a/src/aiptu/smaccer/NPCDefaultSettings.php +++ b/src/aiptu/smaccer/NPCDefaultSettings.php @@ -1,7 +1,7 @@ emoteManager = new EmoteManager($emotes); + $this->emoteManager = new EmoteManager($cachedFile['emotes']); + + if ($sender === null) { + $this->getLogger()->info('Loaded ' . count($cachedFile['emotes']) . ' emotes from cache'); + } + + $this->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask($cacheFilePath, $cachedFile['commit_id'], $sender)); } else { - $this->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask(EmoteUtils::getEmoteCachePath())); + $this->emoteManager = new EmoteManager([]); + + if ($sender === null) { + $this->getLogger()->info('No emote cache found, loading emotes asynchronously...'); + } else { + $sender->sendMessage('[Smaccer] §eLoading emotes from repository...'); + } + + $this->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask($cacheFilePath, null, $sender)); } } + /** + * Reload emotes from repository. + * Can be called from reload command to notify the player. + */ + public function reloadEmotes(?CommandSender $sender = null) : void { + if ($sender !== null) { + $sender->sendMessage('[Smaccer] §eReloading emotes...'); + } + + $this->loadEmotes($sender); + } + public function getDefaultSettings() : NPCDefaultSettings { return $this->npcDefaultSettings; } diff --git a/src/aiptu/smaccer/command/SmaccerCommand.php b/src/aiptu/smaccer/command/SmaccerCommand.php index 93781946..66457150 100644 --- a/src/aiptu/smaccer/command/SmaccerCommand.php +++ b/src/aiptu/smaccer/command/SmaccerCommand.php @@ -1,7 +1,7 @@ getRegisteredNPC()); - return array_map('strtolower', $names); + return array_values(SmaccerHandler::getInstance()->getRegisteredNPC()); } public function getTypeName() : string { diff --git a/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php b/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php index cdb39834..73660e25 100644 --- a/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php +++ b/src/aiptu/smaccer/command/argument/ReloadTypeArgument.php @@ -1,7 +1,7 @@ self::CONFIG, 'emotes' => self::EMOTES, ]; @@ -34,6 +36,8 @@ public function getEnumName() : string { } public function parse(string $argument, CommandSender $sender) : string { - return (string) $this->getValue($argument); + $value = $this->getValue($argument); + assert(is_string($value)); + return $value; } } diff --git a/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php b/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php index 4aa103d1..42b05caa 100644 --- a/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php +++ b/src/aiptu/smaccer/command/subcommand/AboutSubCommand.php @@ -1,7 +1,7 @@ setNameTag($nameTag) ->setScale($scale) ->setBaby($isBaby); - SmaccerHandler::getInstance()->spawnNPC($entityType, $target, $npcData)->onCompletion( + SmaccerHandler::getInstance()->spawnNPC( + $target, + $npcData, function (Entity $entity) use ($sender, $target) : void { if (($entity instanceof HumanSmaccer) || ($entity instanceof EntitySmaccer)) { $npcName = $entity->getName(); diff --git a/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php b/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php index eda79545..78683fcb 100644 --- a/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php +++ b/src/aiptu/smaccer/command/subcommand/DeleteSubCommand.php @@ -1,7 +1,7 @@ sendMessage(TextFormat::GREEN . 'Configuration reloaded successfully.'); break; case ReloadTypeArgument::EMOTES: - $plugin->getServer()->getAsyncPool()->submitTask(new LoadEmotesTask(EmoteUtils::getEmoteCachePath())); - $sender->sendMessage(TextFormat::GREEN . 'Emotes reloaded successfully.'); + $plugin->reloadEmotes($sender); break; default: $sender->sendMessage(TextFormat::RED . 'Invalid reload type specified.'); diff --git a/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php b/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php index 04ed1ea3..b8b17a10 100644 --- a/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php +++ b/src/aiptu/smaccer/command/subcommand/TeleportSubCommand.php @@ -1,7 +1,7 @@ nameTag; + public function getType() : string { + return $this->type; } - public function setNameTag(?string $nameTag) : self { - $this->nameTag = $nameTag; - return $this; + public function getLocation() : ?Location { + return $this->location; + } + + public function getMotion() : ?Vector3 { + return $this->motion; + } + + public function getNameTag() : ?string { + return $this->nameTag; } public function getScale() : float { return $this->scale; } - public function setScale(float $scale) : self { - $this->scale = $scale; + public function isBaby() : bool { + return $this->isBaby; + } + + public function isRotationEnabled() : bool { + return $this->rotationEnabled; + } + + public function isNametagVisible() : bool { + return $this->nametagVisible; + } + + public function getVisibility() : EntityVisibility { + return $this->visibility; + } + + public function getSlapBack() : bool { + return $this->slapBack; + } + + public function getActionEmote() : ?EmoteType { + return $this->actionEmote; + } + + public function getEmote() : ?EmoteType { + return $this->emote; + } + + public function hasGravity() : bool { + return $this->gravityEnabled; + } + + public function setLocation(?Location $location) : self { + $this->location = $location; return $this; } - public function isBaby() : bool { - return $this->baby; + public function setMotion(?Vector3 $motion) : self { + $this->motion = $motion; + return $this; } - public function setBaby(bool $baby) : self { - $this->baby = $baby; + public function setNameTag(?string $nameTag) : self { + $this->nameTag = $nameTag; return $this; } - public function isRotationEnabled() : bool { - return $this->rotationEnabled; + public function setScale(float $scale) : self { + $this->scale = $scale; + return $this; } - public function setRotationEnabled(bool $rotationEnabled) : self { - $this->rotationEnabled = $rotationEnabled; + public function setBaby(bool $isBaby) : self { + $this->isBaby = $isBaby; return $this; } - public function isNametagVisible() : bool { - return $this->nametagVisible; + public function setRotationEnabled(bool $rotationEnabled) : self { + $this->rotationEnabled = $rotationEnabled; + return $this; } public function setNametagVisible(bool $nametagVisible) : self { @@ -77,46 +126,26 @@ public function setNametagVisible(bool $nametagVisible) : self { return $this; } - public function getVisibility() : EntityVisibility { - return $this->visibility; - } - public function setVisibility(EntityVisibility $visibility) : self { $this->visibility = $visibility; return $this; } - public function getSlapBack() : bool { - return $this->slapBack; - } - public function setSlapBack(bool $slapBack) : self { $this->slapBack = $slapBack; return $this; } - public function getActionEmote() : ?EmoteType { - return $this->actionEmote; - } - public function setActionEmote(?EmoteType $actionEmote) : self { $this->actionEmote = $actionEmote; return $this; } - public function getEmote() : ?EmoteType { - return $this->emote; - } - public function setEmote(?EmoteType $emote) : self { $this->emote = $emote; return $this; } - public function hasGravity() : bool { - return $this->gravityEnabled; - } - public function setHasGravity(bool $gravityEnabled) : self { $this->gravityEnabled = $gravityEnabled; return $this; diff --git a/src/aiptu/smaccer/entity/SmaccerHandler.php b/src/aiptu/smaccer/entity/SmaccerHandler.php index e9b9b9c2..15fc791a 100644 --- a/src/aiptu/smaccer/entity/SmaccerHandler.php +++ b/src/aiptu/smaccer/entity/SmaccerHandler.php @@ -1,7 +1,7 @@ AllaySmaccer::class, - 'Armadillo' => ArmadilloSmaccer::class, - 'Axolotl' => AxolotlSmaccer::class, - 'Bat' => BatSmaccer::class, - 'Bee' => BeeSmaccer::class, - 'Blaze' => BlazeSmaccer::class, - 'Bogged' => BoggedSmaccer::class, - 'Breeze' => BreezeSmaccer::class, - 'Camel' => CamelSmaccer::class, - 'Cat' => CatSmaccer::class, - 'CaveSpider' => CaveSpiderSmaccer::class, - 'Chicken' => ChickenSmaccer::class, - 'Cod' => CodSmaccer::class, - 'Cow' => CowSmaccer::class, - 'Creaking' => CreakingSmaccer::class, - 'Creeper' => CreeperSmaccer::class, - 'Dolphin' => DolphinSmaccer::class, - 'Donkey' => DonkeySmaccer::class, - 'Drowned' => DrownedSmaccer::class, - 'ElderGuardian' => ElderGuardianSmaccer::class, - 'EnderDragon' => EnderDragonSmaccer::class, - 'Enderman' => EndermanSmaccer::class, - 'Endermite' => EndermiteSmaccer::class, - 'EvocationIllager' => EvocationIllagerSmaccer::class, - 'Fox' => FoxSmaccer::class, - 'Frog' => FrogSmaccer::class, - 'Ghast' => GhastSmaccer::class, - 'GlowSquid' => GlowSquidSmaccer::class, - 'Goat' => GoatSmaccer::class, - 'Guardian' => GuardianSmaccer::class, - 'Hoglin' => HoglinSmaccer::class, - 'Horse' => HorseSmaccer::class, - 'Husk' => HuskSmaccer::class, - 'IronGolem' => IronGolemSmaccer::class, - 'Llama' => LlamaSmaccer::class, - 'MagmaCube' => MagmaCubeSmaccer::class, - 'Mooshroom' => MooshroomSmaccer::class, - 'Mule' => MuleSmaccer::class, - 'Ocelot' => OcelotSmaccer::class, - 'Panda' => PandaSmaccer::class, - 'Parrot' => ParrotSmaccer::class, - 'Phantom' => PhantomSmaccer::class, - 'Pig' => PigSmaccer::class, - 'PiglinBrute' => PiglinBruteSmaccer::class, - 'Piglin' => PiglinSmaccer::class, - 'Pillager' => PillagerSmaccer::class, - 'PolarBear' => PolarBearSmaccer::class, - 'Pufferfish' => PufferfishSmaccer::class, - 'Rabbit' => RabbitSmaccer::class, - 'Ravager' => RavagerSmaccer::class, - 'Salmon' => SalmonSmaccer::class, - 'Sheep' => SheepSmaccer::class, - 'Shulker' => ShulkerSmaccer::class, - 'Silverfish' => SilverfishSmaccer::class, - 'SkeletonHorse' => SkeletonHorseSmaccer::class, - 'Skeleton' => SkeletonSmaccer::class, - 'Slime' => SlimeSmaccer::class, - 'Sniffer' => SnifferSmaccer::class, - 'SnowGolem' => SnowGolemSmaccer::class, - 'Spider' => SpiderSmaccer::class, - 'Squid' => SquidSmaccer::class, - 'Stray' => StraySmaccer::class, - 'Strider' => StriderSmaccer::class, - 'Tadpole' => TadpoleSmaccer::class, - 'TraderLlama' => TraderLlamaSmaccer::class, - 'Tropicalfish' => TropicalfishSmaccer::class, - 'Turtle' => TurtleSmaccer::class, - 'Vex' => VexSmaccer::class, - 'Villager' => VillagerSmaccer::class, - 'VillagerV2' => VillagerV2Smaccer::class, - 'Vindicator' => VindicatorSmaccer::class, - 'WanderingTrader' => WanderingTraderSmaccer::class, - 'Warden' => WardenSmaccer::class, - 'Witch' => WitchSmaccer::class, - 'WitherSkeleton' => WitherSkeletonSmaccer::class, - 'Wither' => WitherSmaccer::class, - 'Wolf' => WolfSmaccer::class, - 'Zoglin' => ZoglinSmaccer::class, - 'ZombieHorse' => ZombieHorseSmaccer::class, - 'Zombie' => ZombieSmaccer::class, - 'ZombieVillager' => ZombieVillagerSmaccer::class, - 'ZombieVillagerV2' => ZombieVillagerV2Smaccer::class, - ]; - + /** @var array> */ private array $registered_npcs = []; /** @var array> */ private array $playerNPCs = []; + /** + * Registers all default NPC types from the SmaccerType enum. + */ public function registerAll() : void { - $this->registerEntity('Human', HumanSmaccer::class); + foreach (SmaccerType::cases() as $type) { + $this->registerNPC($type->value, $type->getClass()); + } + } + + /** + * Registers a custom NPC type. + * + * @param class-string $entityClass + * + * @throws InvalidArgumentException + */ + public function registerNPC(string $identifier, string $entityClass) : void { + $lowerId = strtolower($identifier); + + if (isset($this->registered_npcs[$lowerId])) { + throw new InvalidArgumentException("NPC type '{$identifier}' is already registered."); + } - foreach ($this->npcs as $type => $class) { - $this->registerEntity($type, $class); + if (!is_a($entityClass, EntitySmaccer::class, true) && !is_a($entityClass, HumanSmaccer::class, true)) { + throw new InvalidArgumentException("Class {$entityClass} must be a subclass of " . EntitySmaccer::class . ' or ' . HumanSmaccer::class); } + + $this->registerEntity($identifier, $entityClass); } - private function registerEntity(string $type, string $entityClass) : void { - if (!is_subclass_of($entityClass, Entity::class)) { - throw new \InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); + /** + * Registers an entity class with the EntityFactory. + * + * @param class-string $entityClass + * + * @throws InvalidArgumentException + */ + private function registerEntity(string $identifier, string $entityClass) : void { + if (!is_a($entityClass, Entity::class, true)) { + throw new InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); } $registerFunction = function (World $world, CompoundTag $nbt) use ($entityClass) : Entity { - if (is_a($entityClass, HumanSmaccer::class, true)) { - return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); + if (is_a($entityClass, EntitySmaccer::class, true)) { + return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), $nbt); } - return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), $nbt); + return new $entityClass(EntityDataHelper::parseLocation($nbt, $world), Human::parseSkinNBT($nbt), $nbt); }; - EntityFactory::getInstance()->register($entityClass, $registerFunction, array_merge([$entityClass], Utils::getClassNamespace($entityClass))); - $this->registered_npcs[$type] = $entityClass; + EntityFactory::getInstance()->register($entityClass, $registerFunction, array_merge([$identifier, $entityClass], Utils::getClassNamespace($entityClass))); + $this->registered_npcs[strtolower($identifier)] = $entityClass; } + /** + * Gets all registered NPC type identifiers. + * + * @return array + */ public function getRegisteredNPC() : array { - return $this->registered_npcs; + $enumNames = array_map(fn (SmaccerType $type) : string => $type->value, SmaccerType::cases()); + $customNames = array_keys($this->registered_npcs); + + return array_unique(array_merge($enumNames, $customNames)); } + /** + * Gets the entity class for a given NPC type identifier. + * + * @return class-string|null + */ public function getNPC(string $entityName) : ?string { - foreach ($this->registered_npcs as $type => $class) { - if (strtolower($type) === strtolower($entityName)) { - return $class; - } + $type = SmaccerType::fromString($entityName); + if ($type !== null) { + return $type->getClass(); } - return null; + return $this->registered_npcs[strtolower($entityName)] ?? null; } - public function createEntity(string $type, Location $location, CompoundTag $nbt) : ?Entity { - $entityClass = $this->getNPC($type); - if ($entityClass === null) { - return null; + /** + * Gets the entity class for a given NPC type identifier, throws exception if not found. + * + * @return class-string + * + * @throws InvalidArgumentException + */ + public function getNPCStrict(string $entityName) : string { + $class = $this->getNPC($entityName); + if ($class === null) { + throw new InvalidArgumentException("NPC type '{$entityName}' is not registered"); } - if (!is_subclass_of($entityClass, Entity::class)) { - throw new \InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); - } + return $class; + } - $createFunction = function (Location $location, CompoundTag $nbt) use ($entityClass) { - if (is_a($entityClass, HumanSmaccer::class, true)) { - return new $entityClass($location, Human::parseSkinNBT($nbt), $nbt); + /** + * Gets the identifier for a registered entity class. + */ + public function getIdentifierByClass(Entity $entity) : ?string { + $className = $entity::class; + foreach ($this->registered_npcs as $identifier => $class) { + if ($class === $className) { + return $identifier; } + } + + return null; + } + + /** + * Creates an entity instance from NBT data. + * + * @throws InvalidArgumentException + */ + public function createEntity(string $type, Location $location, CompoundTag $nbt) : EntitySmaccer|HumanSmaccer { + $entityClass = $this->getNPCStrict($type); + if (!is_a($entityClass, Entity::class, true)) { + throw new InvalidArgumentException("Class {$entityClass} must be a subclass of " . Entity::class); + } + + if (is_a($entityClass, EntitySmaccer::class, true)) { return new $entityClass($location, $nbt); - }; + } - return $createFunction($location, $nbt); + return new $entityClass($location, Human::parseSkinNBT($nbt), $nbt); } + /** + * Creates base NBT data for entity spawning. + */ private static function createBaseNBT(Vector3 $pos, ?Vector3 $motion = null, float $yaw = 0.0, float $pitch = 0.0) : CompoundTag { return CompoundTag::create() ->setTag('Pos', new ListTag([ @@ -295,216 +205,199 @@ private static function createBaseNBT(Vector3 $pos, ?Vector3 $motion = null, flo } /** - * @return Promise + * Spawns a new NPC in the world. + * + * @param \Closure(Entity) : void $onSuccess + * @param \Closure(Throwable) : void $onError */ public function spawnNPC( - string $type, Player $player, NPCData $npcData, - ?Location $customPos = null, - ?Vector3 $motion = null - ) : Promise { - $resolver = new PromiseResolver(); - $promise = $resolver->getPromise(); - - $pos = $customPos ?? $player->getLocation(); - $yaw = $pos->getYaw(); - $pitch = $pos->getPitch(); - $motion ??= $player->getMotion(); - - $playerId = $player->getUniqueId()->getBytes(); - $nameTag = $npcData->getNameTag(); - $scale = $npcData->getScale(); - $rotationEnabled = $npcData->isRotationEnabled(); - $nametagVisible = $npcData->isNametagVisible(); - $visibility = $npcData->getVisibility(); - $isBaby = $npcData->isBaby(); - $slapBack = $npcData->getSlapBack(); - $actionEmote = $npcData->getActionEmote(); - $emote = $npcData->getEmote(); - $gravityEnabled = $npcData->hasGravity(); - - $entityClass = $this->getNPC($type); - if ($entityClass === null) { - $resolver->reject(new \InvalidArgumentException("Invalid NPC type: {$type}")); - return $promise; - } - - $nbt = self::createBaseNBT($pos, $motion, $yaw, $pitch); - $nbt->setString(EntityTag::CREATOR, $playerId) - ->setFloat(EntityTag::SCALE, $scale) - ->setByte(EntityTag::ROTATE_TO_PLAYERS, (int) $rotationEnabled) - ->setByte(EntityTag::NAMETAG_VISIBLE, (int) $nametagVisible) - ->setInt(EntityTag::VISIBILITY, $visibility->value) - ->setByte(EntityTag::GRAVITY, (int) $gravityEnabled); - - if (is_a($entityClass, EntityAgeable::class, true)) { - $nbt->setByte(EntityTag::BABY, (int) $isBaby); - } - - if (is_a($entityClass, HumanSmaccer::class, true)) { - $skin = $player->getSkin(); + ?\Closure $onSuccess = null, + ?\Closure $onError = null + ) : void { + try { + $type = $npcData->getType(); + $entityClass = $this->getNPCStrict($type); + + $pos = $npcData->getLocation() ?? $player->getLocation(); + $motion = $npcData->getMotion() ?? $player->getMotion(); + $yaw = $pos->getYaw(); + $pitch = $pos->getPitch(); + + $playerId = $player->getUniqueId()->getBytes(); + + $nbt = self::createBaseNBT($pos, $motion, $yaw, $pitch); + $nbt->setString(EntityTag::CREATOR, $playerId) + ->setFloat(EntityTag::SCALE, $npcData->getScale()) + ->setByte(EntityTag::ROTATE_TO_PLAYERS, (int) $npcData->isRotationEnabled()) + ->setByte(EntityTag::NAMETAG_VISIBLE, (int) $npcData->isNametagVisible()) + ->setInt(EntityTag::VISIBILITY, $npcData->getVisibility()->value) + ->setByte(EntityTag::GRAVITY, (int) $npcData->hasGravity()); + + if (is_a($entityClass, EntityAgeable::class, true)) { + $nbt->setByte(EntityTag::BABY, (int) $npcData->isBaby()); + } - $nbt->setTag( - 'Skin', - CompoundTag::create() + if (is_a($entityClass, HumanSmaccer::class, true)) { + $skin = $player->getSkin(); + $nbt->setTag('Skin', CompoundTag::create() ->setString('Name', $skin->getSkinId()) ->setByteArray('Data', $skin->getSkinData()) ->setByteArray('CapeData', $skin->getCapeData()) ->setString('GeometryName', $skin->getGeometryName()) - ->setByteArray('GeometryData', $skin->getGeometryData()) - ); - - $nbt->setByte(EntityTag::SLAP_BACK, (int) $slapBack); + ->setByteArray('GeometryData', $skin->getGeometryData())); - if ($actionEmote !== null) { - $nbt->setString(EntityTag::ACTION_EMOTE, $actionEmote->getUuid()); - } + $nbt->setByte(EntityTag::SLAP_BACK, (int) $npcData->getSlapBack()); + if (($ae = $npcData->getActionEmote()) !== null) { + $nbt->setString(EntityTag::ACTION_EMOTE, $ae->getUuid()); + } - if ($emote !== null) { - $nbt->setString(EntityTag::EMOTE, $emote->getUuid()); + if (($e = $npcData->getEmote()) !== null) { + $nbt->setString(EntityTag::EMOTE, $e->getUuid()); + } } - } - - $entity = $this->createEntity($type, $pos, $nbt); - if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { - $resolver->reject(new \RuntimeException("Failed to create NPC entity: {$type}")); - return $promise; - } - if ($nameTag !== null) { - $entity->setNameTag($nameTag); - } + $entity = $this->createEntity($type, $pos, $nbt); - $entity->setScale($scale); - $entity->setRotateToPlayers($rotationEnabled); - $entity->setNameTagAlwaysVisible($nametagVisible); - $entity->setNameTagVisible($nametagVisible); + if (($nt = $npcData->getNameTag()) !== null) { + $entity->setNameTag($nt); + } - if ($entity instanceof EntityAgeable) { - $entity->setBaby($isBaby); - } + $entity->setScale($npcData->getScale()); + $entity->setRotateToPlayers($npcData->isRotationEnabled()); + $entity->setNameTagAlwaysVisible($npcData->isNametagVisible()); + $entity->setNameTagVisible($npcData->isNametagVisible()); + if ($entity instanceof EntityAgeable) { + $entity->setBaby($npcData->isBaby()); + } - if ($entity instanceof HumanSmaccer) { - $entity->setSlapBack($slapBack); + if ($entity instanceof HumanSmaccer) { + $entity->setSlapBack($npcData->getSlapBack()); + if (($ae = $npcData->getActionEmote()) !== null) { + $entity->setActionEmote($ae); + } - if ($actionEmote !== null) { - $entity->setActionEmote($actionEmote); + if (($e = $npcData->getEmote()) !== null) { + $entity->setEmote($e); + } } - if ($emote !== null) { - $entity->setEmote($emote); + $entity->setVisibility($npcData->getVisibility()); + $entity->setHasGravity($npcData->hasGravity()); + + $ev = new NPCSpawnEvent($entity); + $ev->call(); + if ($ev->isCancelled()) { + throw new RuntimeException('NPC spawn event was cancelled by another plugin'); } - } - $entity->setVisibility($visibility); - $entity->setHasGravity($gravityEnabled); + $this->playerNPCs[$playerId][$entity->getActorId()] = $entity; - $ev = new NPCSpawnEvent($entity); - $ev->call(); - if ($ev->isCancelled()) { - $resolver->reject(new \RuntimeException('NPC spawn event was cancelled')); - return $promise; + if ($onSuccess !== null) { + $onSuccess($entity); + } + } catch (Throwable $t) { + if ($onError !== null) { + $onError($t); + } } - - $entityId = $entity->getActorId(); - $this->playerNPCs[$playerId][$entityId] = $entity; - - $resolver->resolve($entity); - return $promise; } /** - * @return Promise + * Despawns an NPC from the world. + * + * @param \Closure(bool) : void $onSuccess + * @param \Closure(Throwable) : void $onError */ - public function despawnNPC(string $creatorId, Entity $entity) : Promise { - $resolver = new PromiseResolver(); - $promise = $resolver->getPromise(); + public function despawnNPC(string $creatorId, Entity $entity, ?\Closure $onSuccess = null, ?\Closure $onError = null) : void { + try { + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + throw new InvalidArgumentException('Provided entity is not a valid Smaccer NPC'); + } - if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { - $resolver->reject(new \InvalidArgumentException('Invalid entity type')); - return $promise; - } + $ev = new NPCDespawnEvent($entity); + $ev->call(); + if ($ev->isCancelled()) { + throw new RuntimeException('NPC despawn event was cancelled by another plugin'); + } - $entityId = $entity->getActorId(); + $entityId = $entity->getActorId(); + $entity->flagForDespawn(); + $entity->removeActorId(); + unset($this->playerNPCs[$creatorId][$entityId]); - $ev = new NPCDespawnEvent($entity); - $ev->call(); - if ($ev->isCancelled()) { - $resolver->reject(new \RuntimeException('NPC despawn event was cancelled')); - return $promise; + if ($onSuccess !== null) { + $onSuccess(true); + } + } catch (Throwable $t) { + if ($onError !== null) { + $onError($t); + } } - - $entity->flagForDespawn(); - $entity->removeActorId(); - unset($this->playerNPCs[$creatorId][$entityId]); - - $resolver->resolve(true); - return $promise; } /** - * @return Promise + * Edits an existing NPC's configuration. + * + * @param \Closure(bool) : void $onSuccess + * @param \Closure(Throwable) : void $onError */ - public function editNPC(Player $player, Entity $entity, NPCData $npcData) : Promise { - $resolver = new PromiseResolver(); - $promise = $resolver->getPromise(); - - if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { - $resolver->reject(new \InvalidArgumentException('Invalid entity type')); - return $promise; - } + public function editNPC(Player $player, Entity $entity, NPCData $npcData, ?\Closure $onSuccess = null, ?\Closure $onError = null) : void { + try { + if (!$entity instanceof EntitySmaccer && !$entity instanceof HumanSmaccer) { + throw new InvalidArgumentException('Provided entity is not a valid Smaccer NPC'); + } - $ev = new NPCUpdateEvent($entity, $npcData); - $ev->call(); - if ($ev->isCancelled()) { - $resolver->reject(new \RuntimeException('NPC update event was cancelled')); - return $promise; - } + $ev = new NPCUpdateEvent($entity, $npcData); + $ev->call(); + if ($ev->isCancelled()) { + throw new RuntimeException('NPC update event was cancelled by another plugin'); + } - $nameTag = $ev->getNPCData()->getNameTag(); - $scale = $ev->getNPCData()->getScale(); - $rotationEnabled = $ev->getNPCData()->isRotationEnabled(); - $nametagVisible = $ev->getNPCData()->isNametagVisible(); - $visibility = $ev->getNPCData()->getVisibility(); - $isBaby = $ev->getNPCData()->isBaby(); - $slapBack = $ev->getNPCData()->getSlapBack(); - $actionEmote = $ev->getNPCData()->getActionEmote(); - $emote = $ev->getNPCData()->getEmote(); - $gravityEnabled = $npcData->hasGravity(); - - if ($nameTag !== null) { - $entity->setNameTag($nameTag); - } + $data = $ev->getNPCData(); + if (($nt = $data->getNameTag()) !== null) { + $entity->setNameTag($nt); + } - $entity->setScale($scale); - $entity->setRotateToPlayers($rotationEnabled); - $entity->setNameTagAlwaysVisible($nametagVisible); - $entity->setNameTagVisible($nametagVisible); + $entity->setScale($data->getScale()); + $entity->setRotateToPlayers($data->isRotationEnabled()); + $entity->setNameTagAlwaysVisible($data->isNametagVisible()); + $entity->setNameTagVisible($data->isNametagVisible()); - if ($entity instanceof EntityAgeable) { - $entity->setBaby($isBaby); - } + if ($entity instanceof EntityAgeable) { + $entity->setBaby($data->isBaby()); + } - if ($entity instanceof HumanSmaccer) { - $entity->setSlapBack($slapBack); + if ($entity instanceof HumanSmaccer) { + $entity->setSlapBack($data->getSlapBack()); + if (($ae = $data->getActionEmote()) !== null) { + $entity->setActionEmote($ae); + } - if ($actionEmote !== null) { - $entity->setActionEmote($actionEmote); + if (($e = $data->getEmote()) !== null) { + $entity->setEmote($e); + } } - if ($emote !== null) { - $entity->setEmote($emote); + $entity->setVisibility($data->getVisibility()); + $entity->setHasGravity($data->hasGravity()); + + if ($onSuccess !== null) { + $onSuccess(true); + } + } catch (Throwable $t) { + if ($onError !== null) { + $onError($t); } } - - $entity->setVisibility($visibility); - $entity->setHasGravity($gravityEnabled); - - $resolver->resolve(true); - return $promise; } + /** + * Gets information about NPCs in the world. + * + * @return array{count: int, infoList: list} + */ public function getEntitiesInfo(?Player $player = null, bool $collectInfo = false) : array { $entityCount = 0; $entityInfoList = []; @@ -525,7 +418,20 @@ public function getEntitiesInfo(?Player $player = null, bool $collectInfo = fals foreach ($entities as $entityId => $entity) { ++$entityCount; if ($collectInfo) { - $entityInfoList[] = TextFormat::YELLOW . 'ID: (' . $entityId . ') ' . TextFormat::GREEN . $entity->getNameTag() . TextFormat::GRAY . ' -- ' . TextFormat::AQUA . $entity->getWorld()->getFolderName() . ': ' . $entity->getLocation()->getFloorX() . '/' . $entity->getLocation()->getFloorY() . '/' . $entity->getLocation()->getFloorZ(); + $location = $entity->getLocation(); + $entityInfoList[] = sprintf( + '%sID: (%d) %s%s%s -- %s%s: %d/%d/%d', + TextFormat::YELLOW, + $entityId, + TextFormat::GREEN, + $entity->getNameTag(), + TextFormat::GRAY, + TextFormat::AQUA, + $entity->getWorld()->getFolderName(), + $location->getFloorX(), + $location->getFloorY(), + $location->getFloorZ() + ); } } diff --git a/src/aiptu/smaccer/entity/SmaccerType.php b/src/aiptu/smaccer/entity/SmaccerType.php new file mode 100644 index 00000000..87e1d2ee --- /dev/null +++ b/src/aiptu/smaccer/entity/SmaccerType.php @@ -0,0 +1,416 @@ + + */ + public function getClass() : string { + return match ($this) { + self::HUMAN => HumanSmaccer::class, + self::ALLAY => AllaySmaccer::class, + self::ARMADILLO => ArmadilloSmaccer::class, + self::ARMOR_STAND => ArmorStandSmaccer::class, + self::ARROW => ArrowSmaccer::class, + self::AXOLOTL => AxolotlSmaccer::class, + self::BAT => BatSmaccer::class, + self::BEE => BeeSmaccer::class, + self::BLAZE => BlazeSmaccer::class, + self::BOAT => BoatSmaccer::class, + self::BOGGED => BoggedSmaccer::class, + self::BREEZE => BreezeSmaccer::class, + self::BREEZE_WIND_CHARGE_PROJECTILE => BreezeWindChargeProjectileSmaccer::class, + self::CAMEL_HUSK => CamelHuskSmaccer::class, + self::CAMEL => CamelSmaccer::class, + self::CAT => CatSmaccer::class, + self::CAVE_SPIDER => CaveSpiderSmaccer::class, + self::CHEST_BOAT => ChestBoatSmaccer::class, + self::CHEST_MINECART => ChestMinecartSmaccer::class, + self::CHICKEN => ChickenSmaccer::class, + self::COD => CodSmaccer::class, + self::COMMAND_BLOCK_MINECART => CommandBlockMinecartSmaccer::class, + self::COPPER_GOLEM => CopperGolemSmaccer::class, + self::COW => CowSmaccer::class, + self::CREAKING => CreakingSmaccer::class, + self::CREEPER => CreeperSmaccer::class, + self::DOLPHIN => DolphinSmaccer::class, + self::DONKEY => DonkeySmaccer::class, + self::DRAGON_FIREBALL => DragonFireballSmaccer::class, + self::DROWNED => DrownedSmaccer::class, + self::EGG => EggSmaccer::class, + self::ELDER_GUARDIAN => ElderGuardianSmaccer::class, + self::ENDER_CRYSTAL => EnderCrystalSmaccer::class, + self::ENDER_DRAGON => EnderDragonSmaccer::class, + self::ENDER_PEARL => EnderPearlSmaccer::class, + self::ENDERMAN => EndermanSmaccer::class, + self::ENDERMITE => EndermiteSmaccer::class, + self::EVOCATION_ILLAGER => EvocationIllagerSmaccer::class, + self::EYE_OF_ENDER_SIGNAL => EyeOfEnderSignalSmaccer::class, + self::FIREBALL => FireballSmaccer::class, + self::FIREWORKS_ROCKET => FireworksRocketSmaccer::class, + self::FISHING_HOOK => FishingHookSmaccer::class, + self::FOX => FoxSmaccer::class, + self::FROG => FrogSmaccer::class, + self::GHAST => GhastSmaccer::class, + self::GLOW_SQUID => GlowSquidSmaccer::class, + self::GOAT => GoatSmaccer::class, + self::GUARDIAN => GuardianSmaccer::class, + self::HAPPY_GHAST => HappyGhastSmaccer::class, + self::HOGLIN => HoglinSmaccer::class, + self::HOPPER_MINECART => HopperMinecartSmaccer::class, + self::HORSE => HorseSmaccer::class, + self::HUSK => HuskSmaccer::class, + self::IRON_GOLEM => IronGolemSmaccer::class, + self::LINGERING_POTION => LingeringPotionSmaccer::class, + self::LLAMA => LlamaSmaccer::class, + self::LLAMA_SPIT => LlamaSpitSmaccer::class, + self::MAGMA_CUBE => MagmaCubeSmaccer::class, + self::MINECART => MinecartSmaccer::class, + self::MOOSHROOM => MooshroomSmaccer::class, + self::MULE => MuleSmaccer::class, + self::NAUTILUS => NautilusSmaccer::class, + self::OCELOT => OcelotSmaccer::class, + self::PANDA => PandaSmaccer::class, + self::PARCHED => ParchedSmaccer::class, + self::PARROT => ParrotSmaccer::class, + self::PHANTOM => PhantomSmaccer::class, + self::PIG => PigSmaccer::class, + self::PIGLIN_BRUTE => PiglinBruteSmaccer::class, + self::PIGLIN => PiglinSmaccer::class, + self::PILLAGER => PillagerSmaccer::class, + self::POLAR_BEAR => PolarBearSmaccer::class, + self::PUFFERFISH => PufferfishSmaccer::class, + self::RABBIT => RabbitSmaccer::class, + self::RAVAGER => RavagerSmaccer::class, + self::SALMON => SalmonSmaccer::class, + self::SHEEP => SheepSmaccer::class, + self::SHULKER => ShulkerSmaccer::class, + self::SHULKER_BULLET => ShulkerBulletSmaccer::class, + self::SILVERFISH => SilverfishSmaccer::class, + self::SKELETON_HORSE => SkeletonHorseSmaccer::class, + self::SKELETON => SkeletonSmaccer::class, + self::SLIME => SlimeSmaccer::class, + self::SMALL_FIREBALL => SmallFireballSmaccer::class, + self::SNIFFER => SnifferSmaccer::class, + self::SNOW_GOLEM => SnowGolemSmaccer::class, + self::SNOWBALL => SnowballSmaccer::class, + self::SPIDER => SpiderSmaccer::class, + self::SPLASH_POTION => SplashPotionSmaccer::class, + self::SQUID => SquidSmaccer::class, + self::STRAY => StraySmaccer::class, + self::STRIDER => StriderSmaccer::class, + self::TADPOLE => TadpoleSmaccer::class, + self::THROWN_TRIDENT => ThrownTridentSmaccer::class, + self::TNT_MINECART => TntMinecartSmaccer::class, + self::TNT => TntSmaccer::class, + self::TRADER_LLAMA => TraderLlamaSmaccer::class, + self::TRIPOD_CAMERA => TripodCameraSmaccer::class, + self::TROPICALFISH => TropicalfishSmaccer::class, + self::TURTLE => TurtleSmaccer::class, + self::VEX => VexSmaccer::class, + self::VILLAGER => VillagerSmaccer::class, + self::VILLAGER_V2 => VillagerV2Smaccer::class, + self::VINDICATOR => VindicatorSmaccer::class, + self::WANDERING_TRADER => WanderingTraderSmaccer::class, + self::WARDEN => WardenSmaccer::class, + self::WIND_CHARGE_PROJECTILE => WindChargeProjectileSmaccer::class, + self::WITCH => WitchSmaccer::class, + self::WITHER_SKELETON => WitherSkeletonSmaccer::class, + self::WITHER_SKULL_DANGEROUS => WitherSkullDangerousSmaccer::class, + self::WITHER_SKULL => WitherSkullSmaccer::class, + self::WITHER => WitherSmaccer::class, + self::WOLF => WolfSmaccer::class, + self::XP_BOTTLE => XpBottleSmaccer::class, + self::XP_ORB => XpOrbSmaccer::class, + self::ZOGLIN => ZoglinSmaccer::class, + self::ZOMBIE_HORSE => ZombieHorseSmaccer::class, + self::ZOMBIE_NAUTILUS => ZombieNautilusSmaccer::class, + self::ZOMBIE_PIGMAN => ZombiePigmanSmaccer::class, + self::ZOMBIE => ZombieSmaccer::class, + self::ZOMBIE_VILLAGER => ZombieVillagerSmaccer::class, + self::ZOMBIE_VILLAGER_V2 => ZombieVillagerV2Smaccer::class, + }; + } + + /** + * Gets the display name for this NPC type. + */ + public function getDisplayName() : string { + return $this->value; + } + + /** + * Attempts to create an enum instance from a string identifier. + * Case-insensitive matching. + */ + public static function fromString(string $name) : ?self { + $lowerName = strtolower($name); + foreach (self::cases() as $type) { + if (strtolower($type->value) === $lowerName) { + return $type; + } + } + + return null; + } +} diff --git a/src/aiptu/smaccer/entity/command/CommandHandler.php b/src/aiptu/smaccer/entity/command/CommandHandler.php index 57c9327d..dfe8438b 100644 --- a/src/aiptu/smaccer/entity/command/CommandHandler.php +++ b/src/aiptu/smaccer/entity/command/CommandHandler.php @@ -1,7 +1,7 @@ */ private array $commands = []; diff --git a/src/aiptu/smaccer/entity/emote/EmoteManager.php b/src/aiptu/smaccer/entity/emote/EmoteManager.php index 58a01f7f..d20c2827 100644 --- a/src/aiptu/smaccer/entity/emote/EmoteManager.php +++ b/src/aiptu/smaccer/entity/emote/EmoteManager.php @@ -1,7 +1,7 @@ */ - private array $emotes = []; + /** @var array */ + private array $emotesByUuid = []; + + /** @var array */ + private array $emotesByTitle = []; /** - * @param array{ - * array{ - * uuid: string, - * title: string, - * image: string - * } - * } $emotes the array of emotes list + * @param list $emotes */ public function __construct(array $emotes) { $this->loadEmotes($emotes); } /** - * Load emote from the given array. + * Load emotes from the given array. * - * @param array{ - * array{ - * uuid: string, - * title: string, - * image: string - * } - * } $emotes the array of emotes list + * @param list $emotes */ public function loadEmotes(array $emotes) : void { - // TODO: if your want to force load from github using a single command. so its should be empty first - $this->emotes = []; + $this->emotesByUuid = []; + $this->emotesByTitle = []; - foreach ($emotes as $emote) { - extract($emote); + foreach ($emotes as $emoteData) { + $uuid = $emoteData['uuid']; + $title = $emoteData['title']; + $image = $emoteData['image']; $originalTitle = $title; $counter = 2; - while ($this->ensureUniqueTitle($title)) { + while (array_key_exists($title, $this->emotesByTitle)) { $title = $originalTitle . ' ' . $counter; ++$counter; } - $this->emotes[] = new EmoteType($uuid, $title, $image); + $emote = new EmoteType($uuid, $title, $image); + + $this->emotesByUuid[$uuid] = $emote; + $this->emotesByTitle[$title] = $emote; } } /** - * Ensure none of the title are the same. - * - * @param string $title The title that will be checked - * - * @return bool Returns `true` when the title is the same as the one listed and `false` when the title is Unique + * Check if a title already exists. */ - public function ensureUniqueTitle(string $title) { - foreach ($this->emotes as $emote) { - if ($emote->getTitle() === $title) { - return true; - } - } - - return false; + public function ensureUniqueTitle(string $title) : bool { + return array_key_exists($title, $this->emotesByTitle); } /** - * Get an emote by its uuid. - * - * @param string $uuid the UUID of the emote - * - * @return EmoteType|null returns `EmoteType` class when the uuid exists and `null` if the UUID doesn`t exists + * Get an emote by its UUID. */ public function getEmote(string $uuid) : ?EmoteType { - foreach ($this->emotes as $emote) { - if ($emote->getUuid() === $uuid) { - return $emote; - } - } - - return null; + return $this->emotesByUuid[$uuid] ?? null; } /** - * Return all emotes. + * Return all emotes indexed by UUID. * - * @return array Returns all of the `EmoteType` + * @return array */ public function getAll() : array { - return $this->emotes; + return $this->emotesByUuid; } } diff --git a/src/aiptu/smaccer/entity/emote/EmoteType.php b/src/aiptu/smaccer/entity/emote/EmoteType.php index 075c1cb8..3447c1b9 100644 --- a/src/aiptu/smaccer/entity/emote/EmoteType.php +++ b/src/aiptu/smaccer/entity/emote/EmoteType.php @@ -1,7 +1,7 @@ uuid; } /** - * The Title of the emote. + * Get the title of the emote. */ public function getTitle() : string { return $this->title; } /** - * The Image Url of the emote. + * Get the image URL of the emote. */ public function getImage() : string { return $this->image; diff --git a/src/aiptu/smaccer/entity/npc/BatSmaccer.php b/src/aiptu/smaccer/entity/npc/ambient/BatSmaccer.php similarity index 84% rename from src/aiptu/smaccer/entity/npc/BatSmaccer.php rename to src/aiptu/smaccer/entity/npc/ambient/BatSmaccer.php index e59f4ec4..43dcaf8d 100644 --- a/src/aiptu/smaccer/entity/npc/BatSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/ambient/BatSmaccer.php @@ -1,7 +1,7 @@ getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2.375; + } + + public function getWidth() : float { + return 1.7; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CAMEL_HUSK; + } + + public function getName() : string { + return 'Camel Husk'; + } + + public function getCategory() : string { + return 'hostile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/CaveSpiderSmaccer.php similarity index 84% rename from src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php rename to src/aiptu/smaccer/entity/npc/hostile/CaveSpiderSmaccer.php index 79285f46..f87501e1 100644 --- a/src/aiptu/smaccer/entity/npc/CaveSpiderSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/hostile/CaveSpiderSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.425 : 0.85; + return $this->isBaby() ? 0.95 : 1.9; } public function getWidth() : float { - return $this->isBaby() ? 0.425 : 0.85; + return $this->isBaby() ? 0.3 : 0.6; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Hoglin'; } + + public function getCategory() : string { + return 'hostile'; + } } diff --git a/src/aiptu/smaccer/entity/npc/HuskSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/HuskSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/HuskSmaccer.php rename to src/aiptu/smaccer/entity/npc/hostile/HuskSmaccer.php index cfcefc44..40ed4aac 100644 --- a/src/aiptu/smaccer/entity/npc/HuskSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/hostile/HuskSmaccer.php @@ -1,7 +1,7 @@ getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.9; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::PARCHED; + } + + public function getName() : string { + return 'Parched'; + } + + public function getCategory() : string { + return 'hostile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/PhantomSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/PhantomSmaccer.php similarity index 84% rename from src/aiptu/smaccer/entity/npc/PhantomSmaccer.php rename to src/aiptu/smaccer/entity/npc/hostile/PhantomSmaccer.php index 0e58793f..c06b9c4d 100644 --- a/src/aiptu/smaccer/entity/npc/PhantomSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/hostile/PhantomSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.425 : 0.85; + return $this->isBaby() ? 0.7 : 1.4; } public function getWidth() : float { - return $this->isBaby() ? 0.425 : 0.85; + return $this->isBaby() ? 0.7 : 1.4; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Zoglin'; } + + public function getCategory() : string { + return 'hostile'; + } } diff --git a/src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/ZombieHorseSmaccer.php similarity index 77% rename from src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php rename to src/aiptu/smaccer/entity/npc/hostile/ZombieHorseSmaccer.php index 985dc541..9d09848d 100644 --- a/src/aiptu/smaccer/entity/npc/ZombieHorseSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/hostile/ZombieHorseSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.8 : 1.6; + return $this->isBaby() ? 1.12 : 2.24; } public function getWidth() : float { - return $this->isBaby() ? 0.7 : 1.4; + return $this->isBaby() ? 0.98 : 1.96; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Zombie Horse'; } + + public function getCategory() : string { + return 'hostile'; + } } diff --git a/src/aiptu/smaccer/entity/npc/hostile/ZombieNautilusSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/ZombieNautilusSmaccer.php new file mode 100644 index 00000000..6151aed2 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/hostile/ZombieNautilusSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.95; + } + + public function getWidth() : float { + return 0.875; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE_NAUTILUS; + } + + public function getName() : string { + return 'Zombie Nautilus'; + } + + public function getCategory() : string { + return 'hostile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/hostile/ZombiePigmanSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/ZombiePigmanSmaccer.php new file mode 100644 index 00000000..cda86263 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/hostile/ZombiePigmanSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 1.9; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.3 : 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ZOMBIE_PIGMAN; + } + + public function getName() : string { + return 'Zombie Pigman'; + } + + public function getCategory() : string { + return 'hostile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/ZombieSmaccer.php b/src/aiptu/smaccer/entity/npc/hostile/ZombieSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/ZombieSmaccer.php rename to src/aiptu/smaccer/entity/npc/hostile/ZombieSmaccer.php index 307a68d9..1f538cf7 100644 --- a/src/aiptu/smaccer/entity/npc/ZombieSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/hostile/ZombieSmaccer.php @@ -1,7 +1,7 @@ getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.98; + } + + public function getWidth() : float { + return 0.6; + } + + public static function getNetworkTypeId() : string { + return EntityIds::COPPER_GOLEM; + } + + public function getName() : string { + return 'Copper Golem'; + } + + public function getCategory() : string { + return 'passive'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/CowSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/CowSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/CowSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/CowSmaccer.php index ebad3624..6fa1f9b8 100644 --- a/src/aiptu/smaccer/entity/npc/CowSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/CowSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.475 : 0.95; + return $this->isBaby() ? 0.4 : 0.8; } public function getWidth() : float { - return $this->isBaby() ? 0.475 : 0.95; + return $this->isBaby() ? 0.4 : 0.8; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Glow Squid'; } + + public function getCategory() : string { + return 'passive'; + } } diff --git a/src/aiptu/smaccer/entity/npc/passive/HappyGhastSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/HappyGhastSmaccer.php new file mode 100644 index 00000000..cd52b87e --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/passive/HappyGhastSmaccer.php @@ -0,0 +1,48 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.95 : 4; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.95 : 4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::HAPPY_GHAST; + } + + public function getName() : string { + return 'Happy Ghast'; + } + + public function getCategory() : string { + return 'passive'; + } + + public function getBabyScale() : float { + return 0.2375; + } +} diff --git a/src/aiptu/smaccer/entity/npc/HorseSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/HorseSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/HorseSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/HorseSmaccer.php index 7936d601..c35a372c 100644 --- a/src/aiptu/smaccer/entity/npc/HorseSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/HorseSmaccer.php @@ -1,7 +1,7 @@ getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return $this->isBaby() ? 0.5 : 0.5; + } + + public function getWidth() : float { + return $this->isBaby() ? 0.44 : 0.44; + } + + public static function getNetworkTypeId() : string { + return EntityIds::NAUTILUS; + } + + public function getName() : string { + return 'Nautilus'; + } + + public function getCategory() : string { + return 'passive'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/OcelotSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/OcelotSmaccer.php similarity index 86% rename from src/aiptu/smaccer/entity/npc/OcelotSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/OcelotSmaccer.php index 82c62697..09e07f79 100644 --- a/src/aiptu/smaccer/entity/npc/OcelotSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/OcelotSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.402 : 0.67; + return $this->isBaby() ? 0.6 : 1; } public function getWidth() : float { - return $this->isBaby() ? 0.402 : 0.67; + return $this->isBaby() ? 0.36 : 0.6; } public static function getNetworkTypeId() : string { @@ -38,6 +38,10 @@ public function getName() : string { return 'Rabbit'; } + public function getCategory() : string { + return 'passive'; + } + public function getBabyScale() : float { return 0.6; } diff --git a/src/aiptu/smaccer/entity/npc/SalmonSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/SalmonSmaccer.php similarity index 84% rename from src/aiptu/smaccer/entity/npc/SalmonSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/SalmonSmaccer.php index 103dac19..adb9ed20 100644 --- a/src/aiptu/smaccer/entity/npc/SalmonSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/SalmonSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.8 : 1.6; + return $this->isBaby() ? 1.12 : 2.24; } public function getWidth() : float { - return $this->isBaby() ? 0.7 : 1.4; + return $this->isBaby() ? 0.98 : 1.96; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Skeleton Horse'; } + + public function getCategory() : string { + return 'passive'; + } } diff --git a/src/aiptu/smaccer/entity/npc/SnifferSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/SnifferSmaccer.php similarity index 86% rename from src/aiptu/smaccer/entity/npc/SnifferSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/SnifferSmaccer.php index 9c0d9c82..9800b2cb 100644 --- a/src/aiptu/smaccer/entity/npc/SnifferSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/SnifferSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.475 : 0.95; + return $this->isBaby() ? 0.4 : 0.8; } public function getWidth() : float { - return $this->isBaby() ? 0.475 : 0.95; + return $this->isBaby() ? 0.4 : 0.8; } public static function getNetworkTypeId() : string { @@ -37,4 +37,8 @@ public static function getNetworkTypeId() : string { public function getName() : string { return 'Squid'; } + + public function getCategory() : string { + return 'passive'; + } } diff --git a/src/aiptu/smaccer/entity/npc/StriderSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/StriderSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/StriderSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/StriderSmaccer.php index 27ea907b..a7529578 100644 --- a/src/aiptu/smaccer/entity/npc/StriderSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/StriderSmaccer.php @@ -1,7 +1,7 @@ isBaby() ? 0.032 : 0.2; + return $this->isBaby() ? 0.21333328 : 1.333333; } public function getWidth() : float { - return $this->isBaby() ? 0.096 : 0.6; + return $this->isBaby() ? 0.64 : 4; } public static function getNetworkTypeId() : string { @@ -38,6 +38,10 @@ public function getName() : string { return 'Turtle'; } + public function getCategory() : string { + return 'passive'; + } + public function getBabyScale() : float { return 0.16; } diff --git a/src/aiptu/smaccer/entity/npc/VillagerSmaccer.php b/src/aiptu/smaccer/entity/npc/passive/VillagerSmaccer.php similarity index 85% rename from src/aiptu/smaccer/entity/npc/VillagerSmaccer.php rename to src/aiptu/smaccer/entity/npc/passive/VillagerSmaccer.php index a31f1c01..6c2fe8b2 100644 --- a/src/aiptu/smaccer/entity/npc/VillagerSmaccer.php +++ b/src/aiptu/smaccer/entity/npc/passive/VillagerSmaccer.php @@ -1,7 +1,7 @@ getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.975; + } + + public function getWidth() : float { + return 0.5; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ARMOR_STAND; + } + + public function getName() : string { + return 'Armor Stand'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/BoatSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/BoatSmaccer.php new file mode 100644 index 00000000..57640e1a --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/BoatSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.455; + } + + public function getWidth() : float { + return 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BOAT; + } + + public function getName() : string { + return 'Boat'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/ChestBoatSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/ChestBoatSmaccer.php new file mode 100644 index 00000000..083a675f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/ChestBoatSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.455; + } + + public function getWidth() : float { + return 1.4; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CHEST_BOAT; + } + + public function getName() : string { + return 'Chest Boat'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/ChestMinecartSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/ChestMinecartSmaccer.php new file mode 100644 index 00000000..eb7f474e --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/ChestMinecartSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.7; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::CHEST_MINECART; + } + + public function getName() : string { + return 'Chest Minecart'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/CommandBlockMinecartSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/CommandBlockMinecartSmaccer.php new file mode 100644 index 00000000..f5834c25 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/CommandBlockMinecartSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.7; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::COMMAND_BLOCK_MINECART; + } + + public function getName() : string { + return 'Command Block Minecart'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/EnderCrystalSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/EnderCrystalSmaccer.php new file mode 100644 index 00000000..e8891dcf --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/EnderCrystalSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 2; + } + + public function getWidth() : float { + return 2; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ENDER_CRYSTAL; + } + + public function getName() : string { + return 'Ender Crystal'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/HopperMinecartSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/HopperMinecartSmaccer.php new file mode 100644 index 00000000..78942111 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/HopperMinecartSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.7; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::HOPPER_MINECART; + } + + public function getName() : string { + return 'Hopper Minecart'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/MinecartSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/MinecartSmaccer.php new file mode 100644 index 00000000..82ed50c7 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/MinecartSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.7; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::MINECART; + } + + public function getName() : string { + return 'Minecart'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/TntMinecartSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/TntMinecartSmaccer.php new file mode 100644 index 00000000..b8bf7daa --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/TntMinecartSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.7; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TNT_MINECART; + } + + public function getName() : string { + return 'Tnt Minecart'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/TntSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/TntSmaccer.php new file mode 100644 index 00000000..17d55735 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/TntSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.98; + } + + public function getWidth() : float { + return 0.98; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TNT; + } + + public function getName() : string { + return 'Tnt'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/TripodCameraSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/TripodCameraSmaccer.php new file mode 100644 index 00000000..084c3d3e --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/TripodCameraSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1.8; + } + + public function getWidth() : float { + return 0.75; + } + + public static function getNetworkTypeId() : string { + return EntityIds::TRIPOD_CAMERA; + } + + public function getName() : string { + return 'Tripod Camera'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/placed/XpOrbSmaccer.php b/src/aiptu/smaccer/entity/npc/placed/XpOrbSmaccer.php new file mode 100644 index 00000000..c334a36d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/placed/XpOrbSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::XP_ORB; + } + + public function getName() : string { + return 'Xp Orb'; + } + + public function getCategory() : string { + return 'placed'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/ArrowSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/ArrowSmaccer.php new file mode 100644 index 00000000..f5e138a4 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/ArrowSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ARROW; + } + + public function getName() : string { + return 'Arrow'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/BreezeWindChargeProjectileSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/BreezeWindChargeProjectileSmaccer.php new file mode 100644 index 00000000..d8b1ea66 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/BreezeWindChargeProjectileSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.3125; + } + + public function getWidth() : float { + return 0.3125; + } + + public static function getNetworkTypeId() : string { + return EntityIds::BREEZE_WIND_CHARGE_PROJECTILE; + } + + public function getName() : string { + return 'Breeze Wind Charge Projectile'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/DragonFireballSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/DragonFireballSmaccer.php new file mode 100644 index 00000000..4ff62530 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/DragonFireballSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.31; + } + + public function getWidth() : float { + return 0.31; + } + + public static function getNetworkTypeId() : string { + return EntityIds::DRAGON_FIREBALL; + } + + public function getName() : string { + return 'Dragon Fireball'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/EggSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/EggSmaccer.php new file mode 100644 index 00000000..0864fe73 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/EggSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::EGG; + } + + public function getName() : string { + return 'Egg'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/EnderPearlSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/EnderPearlSmaccer.php new file mode 100644 index 00000000..cdfda01d --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/EnderPearlSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::ENDER_PEARL; + } + + public function getName() : string { + return 'Ender Pearl'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/EyeOfEnderSignalSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/EyeOfEnderSignalSmaccer.php new file mode 100644 index 00000000..83b255eb --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/EyeOfEnderSignalSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::EYE_OF_ENDER_SIGNAL; + } + + public function getName() : string { + return 'Eye Of Ender Signal'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/FireballSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/FireballSmaccer.php new file mode 100644 index 00000000..1008e289 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/FireballSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 1; + } + + public function getWidth() : float { + return 1; + } + + public static function getNetworkTypeId() : string { + return EntityIds::FIREBALL; + } + + public function getName() : string { + return 'Fireball'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/FireworksRocketSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/FireworksRocketSmaccer.php new file mode 100644 index 00000000..f81968d6 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/FireworksRocketSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::FIREWORKS_ROCKET; + } + + public function getName() : string { + return 'Fireworks Rocket'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/FishingHookSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/FishingHookSmaccer.php new file mode 100644 index 00000000..ccaa6802 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/FishingHookSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.15; + } + + public function getWidth() : float { + return 0.15; + } + + public static function getNetworkTypeId() : string { + return EntityIds::FISHING_HOOK; + } + + public function getName() : string { + return 'Fishing Hook'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/LingeringPotionSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/LingeringPotionSmaccer.php new file mode 100644 index 00000000..9c366a51 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/LingeringPotionSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::LINGERING_POTION; + } + + public function getName() : string { + return 'Lingering Potion'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/LlamaSpitSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/LlamaSpitSmaccer.php new file mode 100644 index 00000000..4d3d2649 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/LlamaSpitSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.31; + } + + public function getWidth() : float { + return 0.31; + } + + public static function getNetworkTypeId() : string { + return EntityIds::LLAMA_SPIT; + } + + public function getName() : string { + return 'Llama Spit'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/ShulkerBulletSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/ShulkerBulletSmaccer.php new file mode 100644 index 00000000..f65605cf --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/ShulkerBulletSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.625; + } + + public function getWidth() : float { + return 0.625; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SHULKER_BULLET; + } + + public function getName() : string { + return 'Shulker Bullet'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/SmallFireballSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/SmallFireballSmaccer.php new file mode 100644 index 00000000..63297da8 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/SmallFireballSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.31; + } + + public function getWidth() : float { + return 0.31; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SMALL_FIREBALL; + } + + public function getName() : string { + return 'Small Fireball'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/SnowballSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/SnowballSmaccer.php new file mode 100644 index 00000000..350c75da --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/SnowballSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SNOWBALL; + } + + public function getName() : string { + return 'Snowball'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/SplashPotionSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/SplashPotionSmaccer.php new file mode 100644 index 00000000..e36808ce --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/SplashPotionSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::SPLASH_POTION; + } + + public function getName() : string { + return 'Splash Potion'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/ThrownTridentSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/ThrownTridentSmaccer.php new file mode 100644 index 00000000..0a9bdc66 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/ThrownTridentSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.35; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::THROWN_TRIDENT; + } + + public function getName() : string { + return 'Thrown Trident'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/WindChargeProjectileSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/WindChargeProjectileSmaccer.php new file mode 100644 index 00000000..43e0b782 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/WindChargeProjectileSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.3125; + } + + public function getWidth() : float { + return 0.3125; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WIND_CHARGE_PROJECTILE; + } + + public function getName() : string { + return 'Wind Charge Projectile'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/WitherSkullDangerousSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/WitherSkullDangerousSmaccer.php new file mode 100644 index 00000000..f69a72f4 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/WitherSkullDangerousSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.15; + } + + public function getWidth() : float { + return 0.15; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WITHER_SKULL_DANGEROUS; + } + + public function getName() : string { + return 'Wither Skull Dangerous'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/WitherSkullSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/WitherSkullSmaccer.php new file mode 100644 index 00000000..9752bf0f --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/WitherSkullSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.15; + } + + public function getWidth() : float { + return 0.15; + } + + public static function getNetworkTypeId() : string { + return EntityIds::WITHER_SKULL; + } + + public function getName() : string { + return 'Wither Skull'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/npc/projectile/XpBottleSmaccer.php b/src/aiptu/smaccer/entity/npc/projectile/XpBottleSmaccer.php new file mode 100644 index 00000000..71b11756 --- /dev/null +++ b/src/aiptu/smaccer/entity/npc/projectile/XpBottleSmaccer.php @@ -0,0 +1,44 @@ +getHeight(), $this->getWidth()); + } + + public function getHeight() : float { + return 0.25; + } + + public function getWidth() : float { + return 0.25; + } + + public static function getNetworkTypeId() : string { + return EntityIds::XP_BOTTLE; + } + + public function getName() : string { + return 'Xp Bottle'; + } + + public function getCategory() : string { + return 'projectile'; + } +} diff --git a/src/aiptu/smaccer/entity/query/QueryHandler.php b/src/aiptu/smaccer/entity/query/QueryHandler.php index fedba07b..9a8c1fc8 100644 --- a/src/aiptu/smaccer/entity/query/QueryHandler.php +++ b/src/aiptu/smaccer/entity/query/QueryHandler.php @@ -1,7 +1,7 @@ }> */ private array $queries = []; diff --git a/src/aiptu/smaccer/entity/query/QueryInfo.php b/src/aiptu/smaccer/entity/query/QueryInfo.php index 7cd23613..8a7811f8 100644 --- a/src/aiptu/smaccer/entity/query/QueryInfo.php +++ b/src/aiptu/smaccer/entity/query/QueryInfo.php @@ -1,7 +1,7 @@ getCommandHandler(); + + $form = new MenuForm( + 'Edit Commands', + 'Choose a command operation:', + onSubmit: fn (Player $player, Button $selected) => match ($selected->text) { + 'Add' => self::sendAdd($player, $npc), + 'List' => self::sendList($player, $npc), + 'Clear' => self::confirmClear($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + if (count($commandHandler->getAll()) > 0) { + $form->appendOptions('Add', 'List', 'Clear'); + } else { + $form->appendOptions('Add'); + } + + $player->sendForm($form); + } + + private static function sendAdd(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(new CustomForm( + 'Add Command', + [ + new Input('Enter command', 'command', ''), + new Dropdown('Select command type', [ + EntityTag::COMMAND_TYPE_PLAYER, + EntityTag::COMMAND_TYPE_SERVER, + ]), + ], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $command = $response->getInput()->getValue(); + $commandType = $response->getDropdown()->getSelectedOption(); + + if ($npc->addCommand($command, $commandType) !== null) { + $player->sendMessage(TextFormat::GREEN . "Command added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add command for NPC {$npc->getName()}."); + } + } + )); + } + + private static function sendList(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $commands = $npc->getCommands(); + + $buttons = array_map( + fn ($id, $data) => new Button("Command: {$data['command']} (Type: {$data['type']})"), + array_keys($commands), + $commands + ); + + $player->sendForm(new MenuForm( + 'List Commands', + 'Commands for NPC:', + $buttons, + function (Player $player, Button $selected) use ($npc, $commands) : void { + $selectedText = $selected->text; + foreach ($commands as $id => $data) { + if ("Command: {$data['command']} (Type: {$data['type']})" === $selectedText) { + self::sendEditOrRemove($player, $npc, $id, $data['command'], $data['type']); + break; + } + } + } + )); + } + + private static function sendEditOrRemove(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(MenuForm::withOptions( + 'Edit or Remove Command', + "Command: {$command} (Type: {$type})", + ['Edit', 'Remove'], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEdit($player, $npc, $commandId, $command, $type), + 1 => self::confirmRemove($player, $npc, $commandId), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + )); + } + + private static function sendEdit(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $defaultTypeIndex = array_search($type, [EntityTag::COMMAND_TYPE_PLAYER, EntityTag::COMMAND_TYPE_SERVER], true); + + $player->sendForm(new CustomForm( + 'Edit Command', + [ + new Input('Edit command', 'command', $command), + new Dropdown('Select command type', [ + EntityTag::COMMAND_TYPE_PLAYER, + EntityTag::COMMAND_TYPE_SERVER, + ], $defaultTypeIndex !== false ? $defaultTypeIndex : 0), + ], + function (Player $player, CustomFormResponse $response) use ($npc, $commandId) : void { + $newCommand = $response->getInput()->getValue(); + $newType = $response->getDropdown()->getSelectedOption(); + + if ($npc->editCommand($commandId, $newCommand, $newType)) { + $player->sendMessage(TextFormat::GREEN . "Command updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update command for NPC {$npc->getName()}."); + } + } + )); + } + + private static function confirmRemove(Player $player, Entity $npc, int $commandId) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(ModalForm::confirm( + 'Confirm Remove Command', + "Are you sure you want to remove this command from NPC: {$npc->getName()}?", + function (Player $player) use ($npc, $commandId) : void { + $npc->removeCommandById($commandId); + $player->sendMessage(TextFormat::GREEN . "Command removed from NPC {$npc->getName()}."); + } + )); + } + + private static function confirmClear(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(ModalForm::confirm( + 'Confirm Clear Commands', + "Are you sure you want to clear all commands from NPC: {$npc->getName()}?", + function (Player $player) use ($npc) : void { + $npc->clearCommands(); + $player->sendMessage(TextFormat::GREEN . "All commands cleared from NPC {$npc->getName()}."); + } + )); + } +} diff --git a/src/aiptu/smaccer/forms/EditForms.php b/src/aiptu/smaccer/forms/EditForms.php new file mode 100644 index 00000000..2fe00b00 --- /dev/null +++ b/src/aiptu/smaccer/forms/EditForms.php @@ -0,0 +1,198 @@ + match ($selected->getValue()) { + 0 => self::sendGeneralSettings($player, $npc), + 1 => CommandForms::sendMenu($player, $npc), + 2 => self::sendTeleport($player, $npc, 'npc_to_player'), + 3 => self::sendTeleport($player, $npc, 'player_to_npc'), + 4 => QueryForms::sendMenu($player, $npc), + 5 => HumanForms::sendEmoteMenu($player, $npc), + 6 => HumanForms::sendSkinMenu($player, $npc), + 7 => HumanForms::sendArmorMenu($player, $npc), + 8 => HumanForms::equipHeldItem($player, $npc), + 9 => HumanForms::equipOffHandItem($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + if ($npc instanceof HumanSmaccer) { + $form->appendOptions( + 'Emote Settings', + 'Skin Settings', + 'Armor Settings', + 'Equip Held Item', + 'Equip Off-Hand Item' + ); + } + + $player->sendForm($form); + } + + public static function sendGeneralSettings(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $visibilityValues = array_values(EntityVisibility::getAll()); + $currentVisibility = $npc->getVisibility()->name; + $defaultVisibilityIndex = array_search($currentVisibility, $visibilityValues, true); + $defaultVisibility = Smaccer::getInstance()->getDefaultSettings()->getEntityVisibility()->value; + + $formElements = [ + new Input('Edit NPC name tag', 'NPC Name', $npc->getNameTag()), + new Input('Set NPC scale (0.1 - 10.0)', '1.0', (string) $npc->getScale()), + new Toggle('Enable rotation?', $npc->canRotateToPlayers()), + new Toggle('Set name tag visible', $npc->isNameTagVisible()), + new StepSlider('Select visibility', $visibilityValues, $defaultVisibilityIndex !== false ? (int) $defaultVisibilityIndex : $defaultVisibility), + new Toggle('Enable gravity?', $npc->hasGravity()), + ]; + + if ($npc instanceof EntityAgeable) { + $formElements[] = new Toggle('Is baby?', $npc->isBaby()); + } + + if ($npc instanceof HumanSmaccer) { + $formElements[] = new Toggle('Enable slapback?', $npc->canSlapBack()); + } + + $player->sendForm(new CustomForm( + 'Edit NPC', + $formElements, + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $values = $response->getValues(); + + if (!is_string($values[0]) || !is_numeric($values[1]) || !is_bool($values[2]) || !is_bool($values[3]) || !is_string($values[4]) || !is_bool($values[5])) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + $scale = (float) $values[1]; + if ($scale < 0.1 || $scale > 10.0) { + $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); + return; + } + + $type = SmaccerHandler::getInstance()->getIdentifierByClass($npc); + if ($type === null) { + $player->sendMessage(TextFormat::RED . 'Could not determine NPC type.'); + return; + } + + $npcData = NPCData::create($type) + ->setNameTag($values[0]) + ->setScale($scale) + ->setRotationEnabled($values[2]) + ->setNametagVisible($values[3]) + ->setVisibility(EntityVisibility::fromString($values[4])) + ->setHasGravity($values[5]); + + $index = 6; + + if ($npc instanceof EntityAgeable && isset($values[$index])) { + $npcData->setBaby((bool) $values[$index]); + ++$index; + } + + if ($npc instanceof HumanSmaccer && isset($values[$index])) { + $npcData->setSlapBack((bool) $values[$index]); + } + + SmaccerHandler::getInstance()->editNPC( + $player, + $npc, + $npcData, + function (bool $success) use ($player, $npc) : void { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' updated successfully!'); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to edit NPC: ' . $e->getMessage()); + } + ); + } + )); + } + + public static function sendTeleport(Player $player, Entity $npc, string $action) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $server = Smaccer::getInstance()->getServer(); + $playerNames = array_values(array_map(fn ($player) => $player->getName(), $server->getOnlinePlayers())); + + $player->sendForm(MenuForm::withOptions( + 'Teleport Options', + 'Select a player:', + $playerNames, + function (Player $player, Button $selected) use ($server, $npc, $action) : void { + $selectedPlayerName = $selected->text; + $selectedPlayer = $server->getPlayerExact($selectedPlayerName); + + if ($selectedPlayer !== null) { + if ($action === 'npc_to_player') { + $npc->teleport($selectedPlayer->getLocation()); + $player->sendMessage(TextFormat::GREEN . "NPC {$npc->getName()} has been teleported to {$selectedPlayerName}'s location."); + } elseif ($action === 'player_to_npc') { + $player->teleport($npc->getLocation()); + $player->sendMessage(TextFormat::GREEN . "You have been teleported to NPC {$npc->getName()}'s location."); + } + } else { + $player->sendMessage(TextFormat::RED . 'Player not found.'); + } + } + )); + } +} diff --git a/src/aiptu/smaccer/forms/HumanForms.php b/src/aiptu/smaccer/forms/HumanForms.php new file mode 100644 index 00000000..548329c3 --- /dev/null +++ b/src/aiptu/smaccer/forms/HumanForms.php @@ -0,0 +1,290 @@ +sendForm(MenuForm::withOptions( + 'Edit Emote', + 'Choose an emote option:', + ['Action Emote', 'Emote'], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendActionEmote($player, $npc), + 1 => self::sendEmote($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + )); + } + + private static function sendActionEmote(Player $player, HumanSmaccer $npc, int $page = 0) : void { + $emoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); + $currentEmote = $npc->getActionEmote(); + $currentEmoteName = $currentEmote === null ? 'None' : $currentEmote->getTitle(); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($emoteOptions)); + + $buttons = array_values(array_map(function (EmoteType $emote) { + $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; + return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); + }, array_slice($emoteOptions, $start, self::ITEMS_PER_PAGE))); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($emoteOptions)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm(new MenuForm( + 'Action Emote', + 'Current action emote: ' . $currentEmoteName, + $buttons, + function (Player $player, Button $selected) use ($npc, $page, $emoteOptions, $start) : void { + $buttonText = $selected->text; + $buttonValue = $selected->getValue(); + + if ($buttonText === self::PREVIOUS_PAGE) { + self::sendActionEmote($player, $npc, $page - 1); + } elseif ($buttonText === self::NEXT_PAGE) { + self::sendActionEmote($player, $npc, $page + 1); + } else { + $npc->setActionEmote($buttonText !== 'None' ? $emoteOptions[$start + $buttonValue] : null); + $player->sendMessage(TextFormat::GREEN . "Action emote updated for NPC {$npc->getName()}."); + } + } + )); + } + + private static function sendEmote(Player $player, HumanSmaccer $npc, int $page = 0) : void { + $emoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); + $currentEmote = $npc->getEmote(); + $currentEmoteName = $currentEmote === null ? 'None' : $currentEmote->getTitle(); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($emoteOptions)); + + $buttons = array_values(array_map(function (EmoteType $emote) { + $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; + return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); + }, array_slice($emoteOptions, $start, self::ITEMS_PER_PAGE))); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($emoteOptions)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm(new MenuForm( + 'Emote', + 'Current emote: ' . $currentEmoteName, + $buttons, + function (Player $player, Button $selected) use ($npc, $page, $emoteOptions, $start) : void { + $buttonText = $selected->text; + $buttonValue = $selected->getValue(); + + if ($buttonText === self::PREVIOUS_PAGE) { + self::sendEmote($player, $npc, $page - 1); + } elseif ($buttonText === self::NEXT_PAGE) { + self::sendEmote($player, $npc, $page + 1); + } else { + $npc->setEmote($buttonText !== 'None' ? $emoteOptions[$start + $buttonValue] : null); + $player->sendMessage(TextFormat::GREEN . "Emote updated for NPC {$npc->getName()}."); + } + } + )); + } + + public static function sendSkinMenu(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(MenuForm::withOptions( + 'Skin Settings', + 'Select an option:', + ['Edit Skin', 'Edit Cape'], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendSkinOptions($player, $npc), + 1 => self::sendCapeForm($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + )); + } + + private static function sendSkinOptions(Player $player, HumanSmaccer $npc) : void { + $player->sendForm(MenuForm::withOptions( + 'Edit Skin', + 'Select an option:', + ['Change Skin from Player', 'Change Skin from URL'], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendSkinFromPlayer($player, $npc), + 1 => self::sendSkinFromURL($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + )); + } + + private static function sendSkinFromPlayer(Player $player, HumanSmaccer $npc) : void { + $server = Smaccer::getInstance()->getServer(); + $playerNames = array_values(array_map(fn ($p) => $p->getName(), $server->getOnlinePlayers())); + + $player->sendForm(MenuForm::withOptions( + 'Change Skin from Player', + 'Select a player:', + $playerNames, + function (Player $player, Button $selected) use ($server, $npc) : void { + $selectedPlayer = $server->getPlayerExact($selected->text); + + if ($selectedPlayer !== null) { + $npc->setSkin($selectedPlayer->getSkin()); + $npc->sendSkin(); + $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from player {$selected->text}."); + } else { + $player->sendMessage(TextFormat::RED . 'Player not found.'); + } + } + )); + } + + private static function sendSkinFromURL(Player $player, HumanSmaccer $npc) : void { + $player->sendForm(new CustomForm( + 'Change Skin from URL', + [new Input('Enter skin URL', 'https://example.com/skin.png')], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $url = $response->getInput()->getValue(); + + SkinUtils::skinFromURL( + $url, + function (string $skinBytes) use ($player, $npc) : void { + $npc->changeSkin($skinBytes); + $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from URL."); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to update skin from URL: ' . $e->getMessage()); + } + ); + } + )); + } + + private static function sendCapeForm(Player $player, HumanSmaccer $npc) : void { + $player->sendForm(new CustomForm( + 'Change Cape from URL', + [new Input('Enter cape URL', 'https://example.com/cape.png')], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $url = $response->getInput()->getValue(); + + SkinUtils::capeFromURL( + $url, + function (string $capeBytes) use ($player, $npc) : void { + $npc->changeCape($capeBytes); + $player->sendMessage(TextFormat::GREEN . "Cape updated for NPC {$npc->getName()} from URL."); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to update cape from URL: ' . $e->getMessage()); + } + ); + } + )); + } + + public static function sendArmorMenu(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(MenuForm::withOptions( + 'Armor Settings', + 'Choose an armor option:', + ['Equip All Armor', 'Equip Helmet', 'Equip Chestplate', 'Equip Leggings', 'Equip Boots'], + function (Player $player, Button $selected) use ($npc) : void { + $piece = match ($selected->getValue()) { + 0 => 'all', + 1 => 'helmet', + 2 => 'chestplate', + 3 => 'leggings', + 4 => 'boots', + default => null, + }; + + if ($piece === null) { + $player->sendMessage(TextFormat::RED . 'Invalid option selected.'); + return; + } + + match ($piece) { + 'helmet' => $npc->setHelmet($player), + 'chestplate' => $npc->setChestplate($player), + 'leggings' => $npc->setLeggings($player), + 'boots' => $npc->setBoots($player), + 'all' => $npc->setArmor($player), + }; + + $message = $piece === 'all' ? 'All armor' : ucfirst($piece); + $player->sendMessage(TextFormat::GREEN . "{$message} equipped to NPC {$npc->getName()}."); + } + )); + } + + public static function equipHeldItem(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $npc->setItemInHand($player->getInventory()->getItemInHand()); + $player->sendMessage(TextFormat::GREEN . "Held item equipped to NPC {$npc->getName()}."); + } + + public static function equipOffHandItem(Player $player, Entity $npc) : void { + if (!$npc instanceof HumanSmaccer) { + return; + } + + $npc->setOffHandItem($player->getOffHandInventory()->getItem(0)); + $player->sendMessage(TextFormat::GREEN . "Off-hand item equipped to NPC {$npc->getName()}."); + } +} diff --git a/src/aiptu/smaccer/forms/NPCForms.php b/src/aiptu/smaccer/forms/NPCForms.php new file mode 100644 index 00000000..37e05ea1 --- /dev/null +++ b/src/aiptu/smaccer/forms/NPCForms.php @@ -0,0 +1,259 @@ + match ($selected->getValue()) { + 0 => self::sendEntitySelection($player, 0), + 1 => self::sendNPCIdSelection($player, self::ACTION_DELETE), + 2 => self::sendNPCIdSelection($player, self::ACTION_EDIT), + 3 => self::sendNPCList($player), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + $entityData = SmaccerHandler::getInstance()->getEntitiesInfo($player); + $ownedEntityCount = $entityData['count']; + + if ($ownedEntityCount > 0) { + $form->appendOptions('Create NPC', 'Delete NPC', 'Edit NPC', 'List NPCs'); + } else { + $form->appendOptions('Create NPC'); + } + + $player->sendForm($form); + } + + public static function sendEntitySelection(Player $player, int $page) : void { + $entityTypes = SmaccerHandler::getInstance()->getRegisteredNPC(); + + $start = $page * self::ITEMS_PER_PAGE; + $end = min($start + self::ITEMS_PER_PAGE, count($entityTypes)); + + $buttons = array_map( + fn ($type) => new Button($type, Image::url("https://raw.githubusercontent.com/AIPTU/Smaccer/assets/faces/{$type}.png")), + array_slice(array_values($entityTypes), $start, self::ITEMS_PER_PAGE) + ); + + if ($page > 0) { + $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); + } + + if ($end < count($entityTypes)) { + $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); + } + + $player->sendForm(new MenuForm( + 'Select Entity', + 'Choose an entity to create:', + $buttons, + function (Player $player, Button $selected) use ($entityTypes, $page) : void { + $selectedText = $selected->text; + if ($selectedText === self::PREVIOUS_PAGE) { + self::sendEntitySelection($player, $page - 1); + } elseif ($selectedText === self::NEXT_PAGE) { + self::sendEntitySelection($player, $page + 1); + } else { + $selectedEntityType = $entityTypes[$selectedText]; + self::sendCreateNPC($player, $selectedEntityType); + } + } + )); + } + + public static function sendCreateNPC(Player $player, string $entityType) : void { + $entityClass = SmaccerHandler::getInstance()->getNPCStrict($entityType); + + $settings = Smaccer::getInstance()->getDefaultSettings(); + $formElements = [ + new Input('Enter NPC name tag', 'NPC Name', ''), + new Input('Set NPC scale (0.1 - 10.0)', '1.0', '1.0'), + new Toggle('Enable rotation?', $settings->isRotationEnabled()), + new Toggle('Set name tag visible', $settings->isNametagVisible()), + new StepSlider('Select visibility', array_values(EntityVisibility::getAll()), $settings->getEntityVisibility()->value), + new Toggle('Enable gravity?', $settings->isGravityEnabled()), + ]; + + if (is_a($entityClass, EntityAgeable::class, true)) { + $formElements[] = new Toggle('Is baby?', false); + } + + if (is_a($entityClass, HumanSmaccer::class, true)) { + $formElements[] = new Toggle('Enable slapback?', $settings->isSlapEnabled()); + } + + $player->sendForm(new CustomForm( + 'Spawn NPC', + $formElements, + function (Player $player, CustomFormResponse $response) use ($entityType, $entityClass) : void { + $values = $response->getValues(); + + if (!is_string($values[0]) || !is_numeric($values[1]) || !is_bool($values[2]) || !is_bool($values[3]) || !is_string($values[4]) || !is_bool($values[5])) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + $scale = (float) $values[1]; + if ($scale < 0.1 || $scale > 10.0) { + $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); + return; + } + + $npcData = NPCData::create($entityType) + ->setNameTag($values[0]) + ->setScale($scale) + ->setRotationEnabled($values[2]) + ->setNametagVisible($values[3]) + ->setVisibility(EntityVisibility::fromString($values[4])) + ->setHasGravity($values[5]); + + $index = 6; + if (is_a($entityClass, EntityAgeable::class, true) && isset($values[$index])) { + $npcData->setBaby((bool) $values[$index]); + ++$index; + } + + if (is_a($entityClass, HumanSmaccer::class, true) && isset($values[$index])) { + $npcData->setSlapBack((bool) $values[$index]); + } + + SmaccerHandler::getInstance()->spawnNPC( + $player, + $npcData, + function (Entity $entity) use ($player) : void { + if (($entity instanceof HumanSmaccer) || ($entity instanceof EntitySmaccer)) { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $entity->getName() . ' created successfully! ID: ' . $entity->getActorId()); + } + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to spawn npc: ' . $e->getMessage()); + } + ); + } + )); + } + + public static function sendNPCIdSelection(Player $player, string $action) : void { + $player->sendForm(new CustomForm( + 'Select NPC', + [new Input("Enter the ID of the NPC to {$action}", 'NPC ID', '')], + function (Player $player, CustomFormResponse $response) use ($action) : void { + $npcId = (int) $response->getInput()->getValue(); + $npc = ActorHandler::findEntity($npcId); + + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + $player->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); + return; + } + + $hasPermission = match ($action) { + self::ACTION_DELETE => $player->hasPermission(Permissions::COMMAND_DELETE_OTHERS), + self::ACTION_EDIT => $player->hasPermission(Permissions::COMMAND_EDIT_OTHERS), + default => false, + }; + + if (!$npc->isOwnedBy($player) && !$hasPermission) { + $player->sendMessage(TextFormat::RED . "You don't have permission to {$action} this entity!"); + return; + } + + match ($action) { + self::ACTION_DELETE => self::confirmDelete($player, $npc), + self::ACTION_EDIT => EditForms::sendMenu($player, $npc), + default => $player->sendMessage(TextFormat::RED . 'Invalid action.'), + }; + } + )); + } + + public static function confirmDelete(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(ModalForm::confirm( + 'Confirm Deletion', + "Are you sure you want to delete NPC: {$npc->getName()}?", + function (Player $player) use ($npc) : void { + SmaccerHandler::getInstance()->despawnNPC( + $npc->getCreatorId(), + $npc, + function (bool $success) use ($player, $npc) : void { + $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' with ID ' . $npc->getActorId() . ' despawned successfully.'); + }, + function (\Throwable $e) use ($player) : void { + $player->sendMessage(TextFormat::RED . 'Failed to despawn npc: ' . $e->getMessage()); + } + ); + } + )); + } + + public static function sendNPCList(Player $player) : void { + $entityData = SmaccerHandler::getInstance()->getEntitiesInfo(null, true); + $totalEntityCount = $entityData['count']; + $entities = $entityData['infoList']; + + if ($totalEntityCount > 0) { + $content = TextFormat::RED . 'NPC List and Locations: (' . $totalEntityCount . ')'; + $content .= "\n" . TextFormat::WHITE . '- ' . implode("\n - ", $entities); + } else { + $content = TextFormat::RED . 'No NPCs found in any world.'; + } + + $player->sendForm(new MenuForm('List NPCs', $content)); + } +} diff --git a/src/aiptu/smaccer/forms/QueryForms.php b/src/aiptu/smaccer/forms/QueryForms.php new file mode 100644 index 00000000..d9a17427 --- /dev/null +++ b/src/aiptu/smaccer/forms/QueryForms.php @@ -0,0 +1,230 @@ + match ($selected->text) { + 'Add Server Query' => self::sendAddServer($player, $npc), + 'Add World Query' => self::sendAddWorld($player, $npc), + 'Edit/Remove Server Query' => self::sendEditRemove($player, $npc, QueryHandler::TYPE_SERVER), + 'Edit/Remove World Query' => self::sendEditRemove($player, $npc, QueryHandler::TYPE_WORLD), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + ); + + $player->sendForm($form); + } + + private static function sendAddServer(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(new CustomForm( + 'Add Server Query', + [ + new Input('Enter IP/Domain', 'ip_or_domain'), + new Input('Enter Port', 'port'), + ], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $values = $response->getValues(); + + if (!is_string($values[0]) || !is_numeric($values[1])) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + if ($npc->getQueryHandler()->addServerQuery($values[0], (int) $values[1]) !== null) { + $player->sendMessage(TextFormat::GREEN . "Server query added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add server query for NPC {$npc->getName()}."); + } + } + )); + } + + private static function sendAddWorld(Player $player, Entity $npc) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(new CustomForm( + 'Add World Query', + [new Input('Enter world name', 'world_name')], + function (Player $player, CustomFormResponse $response) use ($npc) : void { + $worldName = $response->getInput()->getValue(); + + if ($npc->getQueryHandler()->addWorldQuery($worldName) !== null) { + $player->sendMessage(TextFormat::GREEN . "World query added to NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to add world query for NPC {$npc->getName()}."); + } + } + )); + } + + private static function sendEditRemove(Player $player, Entity $npc, string $queryType) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $queries = $npc->getQueryHandler()->getAll(); + $buttons = array_values(array_filter(array_map( + fn ($id, $data) => $queryType === $data['type'] + ? new Button( + $data['type'] === QueryHandler::TYPE_SERVER + ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" + : "World: {$data['value']['world_name']}" + ) + : null, + array_keys($queries), + $queries + ))); + + if (count($buttons) === 0) { + $player->sendMessage(TextFormat::RED . 'No queries found for the selected type.'); + return; + } + + $player->sendForm(new MenuForm( + 'Edit/Remove Query', + 'Select a query to edit/remove:', + $buttons, + function (Player $player, Button $selected) use ($npc, $queries) : void { + $selectedText = $selected->text; + foreach ($queries as $id => $data) { + $expectedText = $data['type'] === QueryHandler::TYPE_SERVER + ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" + : "World: {$data['value']['world_name']}"; + if ($expectedText === $selectedText) { + self::sendEditOrRemoveOptions($player, $npc, $id, $data['type'], $data['value']); + return; + } + } + } + )); + } + + private static function sendEditOrRemoveOptions(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(MenuForm::withOptions( + 'Edit or Remove Query', + $queryType === QueryHandler::TYPE_SERVER + ? "IP: {$queryValue['ip']} Port: {$queryValue['port']}" + : "World: {$queryValue['world_name']}", + ['Edit', 'Remove'], + fn (Player $player, Button $selected) => match ($selected->getValue()) { + 0 => self::sendEdit($player, $npc, $queryId, $queryType, $queryValue), + 1 => self::confirmRemove($player, $npc, $queryId), + default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), + } + )); + } + + private static function sendEdit(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + if ($queryType === QueryHandler::TYPE_SERVER) { + $player->sendForm(new CustomForm( + 'Edit Server Query', + [ + new Input('Edit IP/Domain', 'ip_or_domain', $queryValue['ip']), + new Input('Edit Port', 'port', (string) $queryValue['port']), + ], + function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { + $values = $response->getValues(); + + if (!is_string($values[0]) || !is_numeric($values[1])) { + $player->sendMessage(TextFormat::RED . 'Invalid form values.'); + return; + } + + if ($npc->getQueryHandler()->editServerQuery($queryId, $values[0], (int) $values[1])) { + $player->sendMessage(TextFormat::GREEN . "Server query updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update server query for NPC {$npc->getName()}."); + } + } + )); + } else { + $player->sendForm(new CustomForm( + 'Edit World Query', + [new Input('Edit world name', 'world_name', $queryValue['world_name'])], + function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { + $newWorldName = $response->getInput()->getValue(); + + if ($npc->getQueryHandler()->editWorldQuery($queryId, $newWorldName)) { + $player->sendMessage(TextFormat::GREEN . "World query updated for NPC {$npc->getName()}."); + } else { + $player->sendMessage(TextFormat::RED . "Failed to update world query for NPC {$npc->getName()}."); + } + } + )); + } + } + + private static function confirmRemove(Player $player, Entity $npc, int $queryId) : void { + if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { + return; + } + + $player->sendForm(ModalForm::confirm( + 'Confirm Remove Query', + "Are you sure you want to remove this query from NPC: {$npc->getName()}?", + function (Player $player) use ($npc, $queryId) : void { + $npc->getQueryHandler()->removeById($queryId); + $player->sendMessage(TextFormat::GREEN . "Query removed from NPC {$npc->getName()}."); + } + )); + } +} diff --git a/src/aiptu/smaccer/tasks/LoadEmotesTask.php b/src/aiptu/smaccer/tasks/LoadEmotesTask.php index 5f4a2136..9c94164d 100644 --- a/src/aiptu/smaccer/tasks/LoadEmotesTask.php +++ b/src/aiptu/smaccer/tasks/LoadEmotesTask.php @@ -1,7 +1,7 @@ playerName = $sender?->getName(); + } public function onRun() : void { $currentCommitId = EmoteUtils::getCurrentCommitId(); - $cachedFile = EmoteUtils::getEmotesFromCache($this->cachedFilePath); if ($currentCommitId === null) { - throw new RuntimeException('Failed to fetch current commit ID'); + $this->setResult([ + 'status' => 'error', + 'message' => 'Failed to fetch current commit ID', + ]); + return; } - if ($cachedFile === null || $cachedFile['commit_id'] !== $currentCommitId) { - $emotes = EmoteUtils::getEmotes(); - if ($emotes === null) { - throw new RuntimeException('Failed to fetch emote list'); - } + if ($this->cachedCommitId === $currentCommitId) { + $this->setResult([ + 'status' => 'unchanged', + 'message' => 'Emote cache is up-to-date', + ]); + return; + } - EmoteUtils::saveEmoteToCache($this->cachedFilePath, $currentCommitId, $emotes); + $emotes = EmoteUtils::getEmotes(); - $this->setResult($emotes); + if ($emotes === null) { + $this->setResult([ + 'status' => 'error', + 'message' => 'Failed to fetch emotes from repository', + ]); return; } - $this->setResult($cachedFile['emotes']); + EmoteUtils::saveEmoteToCache($this->cachedFilePath, $currentCommitId, $emotes); + + $this->setResult([ + 'status' => 'updated', + 'emotes' => $emotes, + 'commit_id' => $currentCommitId, + ]); } public function onCompletion() : void { - /** @var array{array{uuid: string, title: string, image: string}} $result */ $result = $this->getResult(); - if (!is_array($result)) { - throw new RuntimeException('Emotes result is not an array'); + + if (!is_array($result) || !isset($result['status'])) { + Smaccer::getInstance()->getLogger()->error('[Smaccer] Invalid emote task result'); + $this->sendMessage('§cAn unexpected error occurred while loading emotes.'); + return; + } + + $status = $result['status']; + $message = isset($result['message']) && is_string($result['message']) ? $result['message'] : 'Unknown error'; + + if ($status === 'unchanged') { + Smaccer::getInstance()->getLogger()->debug($message); + $this->sendMessage('§aEmotes are already up-to-date!'); + return; } - Smaccer::getInstance()->setEmoteManager(new EmoteManager($result)); + if ($status === 'error') { + Smaccer::getInstance()->getLogger()->warning($message); + $this->sendMessage('§cFailed to load emotes: §f' . $message); + return; + } + + if ($status === 'updated' && isset($result['emotes']) && is_array($result['emotes'])) { + /** @var list $emotes */ + $emotes = $result['emotes']; + + $emoteManager = new EmoteManager($emotes); + Smaccer::getInstance()->setEmoteManager($emoteManager); + + $count = (string) (isset($result['count']) && is_int($result['count']) ? $result['count'] : count($emotes)); + $commitId = isset($result['commit_id']) && is_string($result['commit_id']) ? $result['commit_id'] : 'unknown'; + + Smaccer::getInstance()->getLogger()->info('Successfully loaded ' . $count . ' emotes (commit: ' . $commitId . ')'); + $this->sendMessage('§aSuccessfully loaded §e' . $count . '§a emotes!'); + } + } + + /** + * Send message to the command sender who initiated the reload. + * If no sender (background update), message is not sent. + */ + private function sendMessage(string $message) : void { + if ($this->playerName === null) { + return; + } + + $sender = Server::getInstance()->getPlayerExact($this->playerName); + if ($sender !== null && $sender->isOnline()) { + $sender->sendMessage($message); + } } } diff --git a/src/aiptu/smaccer/tasks/QueryServerTask.php b/src/aiptu/smaccer/tasks/QueryServerTask.php index 87523479..98435e32 100644 --- a/src/aiptu/smaccer/tasks/QueryServerTask.php +++ b/src/aiptu/smaccer/tasks/QueryServerTask.php @@ -1,7 +1,7 @@ getBody(), true, flags: JSON_THROW_ON_ERROR); + $data = json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); + if (!is_array($data) || !isset($data['sha']) || !is_string($data['sha'])) { return null; } @@ -48,20 +47,9 @@ public static function getCurrentCommitId() : ?string { } /** - * Retrieve a list of emotes in emotes.json from this github repository https://github.com/TwistedAsylumMC/Bedrock-Emotes. - * - * @return array{ - * array{ - * uuid: string, - * title: string, - * image: string - * } - * }|null An array of associative arrays, each containing: - * - 'uuid' (string): The unique identifier of the emote. - * - 'title' (string): The title of the emote. - * - 'image' (string): The URL to the thumbnail image of the emote. + * Retrieve and validate the list of emotes from the repository. * - * Or `null` if there is an issue with fetching the emotes. + * @return list|null */ public static function getEmotes() : ?array { $response = Internet::getURL(self::EMOTES_URL); @@ -69,82 +57,89 @@ public static function getEmotes() : ?array { return null; } - /** @var array{array{uuid: string, title: string, image: string}} $data */ - $data = json_decode($response->getBody(), true, flags: JSON_THROW_ON_ERROR); - if (!is_array($data)) { - return null; - } + $data = json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR); - foreach ($data as $emote) { - if (!isset($emote['uuid'], $emote['title'], $emote['image']) || !is_string($emote['uuid']) || !is_string($emote['title']) || !is_string($emote['image'])) { - return null; - } + if (!is_array($data) || !self::validateEmotesStructure($data)) { + return null; } + /** @var list $data */ return $data; } /** - * Retrieve emotes from a cache file. - * - * @param string $cacheFilePath the path to the cache file + * Retrieve emotes from the cache file. * - * @return array{ - * commit_id: string, - * emotes: array - * }|null Returns an associative array with `commit_id` and `emotes` if the cache file exists, - * or `null` if the file does not exist + * @return array{commit_id: string, emotes: list}|null */ public static function getEmotesFromCache(string $cacheFilePath) : ?array { - if (file_exists($cacheFilePath)) { - $data = json_decode(Filesystem::fileGetContents($cacheFilePath), true, flags: JSON_THROW_ON_ERROR); - if (!is_array($data) || !isset($data['commit_id'], $data['emotes']) || !is_string($data['commit_id']) || !is_array($data['emotes'])) { - return null; - } + if (!file_exists($cacheFilePath)) { + return null; + } - foreach ($data['emotes'] as $emote) { - if (!isset($emote['uuid'], $emote['title'], $emote['image']) || !is_string($emote['uuid']) || !is_string($emote['title']) || !is_string($emote['image'])) { - return null; - } - } + $content = Filesystem::fileGetContents($cacheFilePath); + $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); - return $data; + if (!is_array($data) || !isset($data['commit_id'], $data['emotes'])) { + return null; } - return null; + if (!is_string($data['commit_id']) || !is_array($data['emotes'])) { + return null; + } + + if (!self::validateEmotesStructure($data['emotes'])) { + return null; + } + + /** @var array{commit_id: string, emotes: list} $data */ + return $data; } /** - * Save emotes to a cache file. + * Save emotes to the cache file. * - * @param string $cacheFilePath the path to the cache file will be saved - * @param string $commitId the Current Commit ID - * @param array{ - * array{ - * uuid: string, - * title: string, - * image: string - * } - * } $emotes the array of emotes list + * @param list $emotes */ public static function saveEmoteToCache(string $cacheFilePath, string $commitId, array $emotes) : void { $jsonData = json_encode([ 'commit_id' => $commitId, 'emotes' => $emotes, ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR); - if ($jsonData === false) { - throw new \RuntimeException('Failed to encode emotes to JSON.'); - } Filesystem::safeFilePutContents($cacheFilePath, $jsonData); } /** - * Get the emote file path. - * - * @return string the emote file path + * Get the emote cache file path. */ public static function getEmoteCachePath() : string { return Smaccer::getInstance()->getDataFolder() . 'emotes_cache.json'; } + + /** + * Validate the structure of emotes data. + */ + private static function validateEmotesStructure(mixed $data) : bool { + if (!is_array($data)) { + return false; + } + + foreach ($data as $emote) { + if (!is_array($emote)) { + return false; + } + + if ( + !isset($emote['uuid'], $emote['title'], $emote['image']) + || !is_string($emote['uuid']) + || !is_string($emote['title']) + || !is_string($emote['image']) + ) { + return false; + } + } + + return true; + } } diff --git a/src/aiptu/smaccer/utils/FormManager.php b/src/aiptu/smaccer/utils/FormManager.php deleted file mode 100644 index 66c6d80f..00000000 --- a/src/aiptu/smaccer/utils/FormManager.php +++ /dev/null @@ -1,1229 +0,0 @@ - match ($selected->getValue()) { - 0 => self::sendEntitySelectionForm($player, 0, $onSubmit), - 1 => self::sendNPCIdSelectionForm($player, self::ACTION_DELETE), - 2 => self::sendNPCIdSelectionForm($player, self::ACTION_EDIT), - 3 => self::sendNPCListForm($player), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ); - - $entityData = SmaccerHandler::getInstance()->getEntitiesInfo($player); - $ownedEntityCount = $entityData['count']; - - if ($ownedEntityCount > 0) { - $form->appendOptions( - 'Create NPC', - 'Delete NPC', - 'Edit NPC', - 'List NPCs' - ); - } else { - $form->appendOptions('Create NPC'); - } - - $player->sendForm($form); - } - - public static function sendEntitySelectionForm(Player $player, int $page, callable $onEntitySelected) : void { - $entityTypes = array_keys(SmaccerHandler::getInstance()->getRegisteredNPC()); - - $start = $page * self::ITEMS_PER_PAGE; - $end = min($start + self::ITEMS_PER_PAGE, count($entityTypes)); - - $buttons = array_map( - fn ($type) => new Button($type, Image::url("https://raw.githubusercontent.com/AIPTU/Smaccer/assets/faces/{$type}.png")), - array_slice($entityTypes, $start, self::ITEMS_PER_PAGE) - ); - - if ($page > 0) { - $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); - } - - if ($end < count($entityTypes)) { - $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); - } - - $player->sendForm( - new MenuForm( - 'Select Entity', - 'Choose an entity to create:', - $buttons, - function (Player $player, Button $selected) use ($entityTypes, $page, $onEntitySelected) : void { - $selectedText = $selected->text; - if ($selectedText === self::PREVIOUS_PAGE) { - self::sendEntitySelectionForm($player, $page - 1, $onEntitySelected); - } elseif ($selectedText === self::NEXT_PAGE) { - self::sendEntitySelectionForm($player, $page + 1, $onEntitySelected); - } else { - $selectedEntityType = $entityTypes[array_search($selectedText, $entityTypes, true)]; - $onEntitySelected($player, $selectedEntityType); - } - } - ) - ); - } - - public static function sendCreateNPCForm(Player $player, string $entityType, callable $onNPCFormSubmit) : void { - $entityClass = SmaccerHandler::getInstance()->getNPC($entityType); - if ($entityClass === null) { - return; - } - - $settings = Smaccer::getInstance()->getDefaultSettings(); - $rotationEnabled = $settings->isRotationEnabled(); - $nametagVisible = $settings->isNametagVisible(); - $defaultVisibility = $settings->getEntityVisibility()->value; - $gravityEnabled = $settings->isGravityEnabled(); - - $formElements = [ - new Input('Enter NPC name tag', 'NPC Name', ''), - new Input('Set NPC scale (0.1 - 10.0)', '1.0', '1.0'), - new Toggle('Enable rotation?', $rotationEnabled), - new Toggle('Set name tag visible', $nametagVisible), - new StepSlider('Select visibility', array_values(EntityVisibility::getAll()), $defaultVisibility), - new Toggle('Enable gravity?', $gravityEnabled), - ]; - - if (is_a($entityClass, EntityAgeable::class, true)) { - $formElements[] = new Toggle('Is baby?', false); - } - - if (is_a($entityClass, HumanSmaccer::class, true)) { - $formElements[] = new Toggle('Enable slapback?', $settings->isSlapEnabled()); - } - - $player->sendForm( - new CustomForm( - 'Spawn NPC', - $formElements, - function (Player $player, CustomFormResponse $response) use ($entityType, $onNPCFormSubmit) : void { - $onNPCFormSubmit($player, $response, $entityType); - } - ) - ); - } - - public static function handleCreateNPCResponse(Player $player, CustomFormResponse $response, string $entityType) : void { - $entityClass = SmaccerHandler::getInstance()->getNPC($entityType); - if ($entityClass === null) { - return; - } - - $values = $response->getValues(); - - $nameTag = $values[0]; - $scaleStr = $values[1]; - $rotationEnabled = $values[2]; - $nameTagVisible = $values[3]; - $visibility = $values[4]; - $gravityEnabled = $values[5]; - - if (!is_string($nameTag) || !is_numeric($scaleStr) || !is_bool($rotationEnabled) || !is_bool($nameTagVisible) || !is_string($visibility) || !is_bool($gravityEnabled)) { - $player->sendMessage(TextFormat::RED . 'Invalid form values.'); - return; - } - - $scale = (float) $scaleStr; - if ($scale < 0.1 || $scale > 10.0) { - $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); - return; - } - - $visibilityEnum = EntityVisibility::fromString($visibility); - - $npcData = NPCData::create() - ->setNameTag($nameTag) - ->setScale($scale) - ->setRotationEnabled($rotationEnabled) - ->setNametagVisible($nameTagVisible) - ->setVisibility($visibilityEnum) - ->setHasGravity($gravityEnabled); - - $index = 6; - - if (is_a($entityClass, EntityAgeable::class, true) && isset($values[$index])) { - $isBaby = (bool) $values[$index]; - $npcData->setBaby($isBaby); - ++$index; - } - - if (is_a($entityClass, HumanSmaccer::class, true)) { - if (isset($values[$index])) { - $enableSlapback = (bool) $values[$index]; - $npcData->setSlapBack($enableSlapback); - ++$index; - } - } - - SmaccerHandler::getInstance()->spawnNPC($entityType, $player, $npcData)->onCompletion( - function (Entity $entity) use ($player) : void { - if (($entity instanceof HumanSmaccer) || ($entity instanceof EntitySmaccer)) { - $player->sendMessage(TextFormat::GREEN . 'NPC ' . $entity->getName() . ' created successfully! ID: ' . $entity->getActorId()); - } - }, - function (\Throwable $e) use ($player) : void { - $player->sendMessage(TextFormat::RED . 'Failed to spawn npc: ' . $e->getMessage()); - } - ); - } - - public static function sendNPCIdSelectionForm(Player $player, string $action) : void { - $player->sendForm( - new CustomForm( - 'Select NPC', - [ - new Input("Enter the ID of the NPC to {$action}", 'NPC ID', ''), - ], - function (Player $player, CustomFormResponse $response) use ($action) : void { - $npcId = (int) $response->getInput()->getValue(); - $npc = ActorHandler::findEntity($npcId); - - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - $player->sendMessage(TextFormat::RED . 'NPC with ID ' . $npcId . ' not found!'); - return; - } - - $hasPermission = match ($action) { - self::ACTION_DELETE => $player->hasPermission(Permissions::COMMAND_DELETE_OTHERS), - self::ACTION_EDIT => $player->hasPermission(Permissions::COMMAND_EDIT_OTHERS), - default => false, - }; - - if (!$npc->isOwnedBy($player) && !$hasPermission) { - $player->sendMessage(TextFormat::RED . "You don't have permission to {$action} this entity!"); - return; - } - - match ($action) { - self::ACTION_DELETE => self::confirmDeleteNPC($player, $npc), - self::ACTION_EDIT => self::sendEditMenuForm($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid action.'), - }; - } - ) - ); - } - - public static function confirmDeleteNPC(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - ModalForm::confirm( - 'Confirm Deletion', - "Are you sure you want to delete NPC: {$npc->getName()}?", - function (Player $player) use ($npc) : void { - SmaccerHandler::getInstance()->despawnNPC($npc->getCreatorId(), $npc)->onCompletion( - function (bool $success) use ($player, $npc) : void { - $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' with ID ' . $npc->getActorId() . ' despawned successfully.'); - }, - function (\Throwable $e) use ($player) : void { - $player->sendMessage(TextFormat::RED . 'Failed to despawn npc: ' . $e->getMessage()); - } - ); - } - ) - ); - } - - public static function sendEditMenuForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $form = MenuForm::withOptions( - 'Edit NPC', - 'Choose an edit option:', - [ - 'General Settings', - 'Commands', - 'Teleport NPC to Player', - 'Teleport Player to NPC', - 'Query Settings', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendEditNPCForm($player, $npc), - 1 => self::sendEditCommandsForm($player, $npc), - 2 => self::sendTeleportOptionsForm($player, $npc, self::TELEPORT_NPC_TO_PLAYER), - 3 => self::sendTeleportOptionsForm($player, $npc, self::TELEPORT_PLAYER_TO_NPC), - 4 => self::sendQueryManagementForm($player, $npc), - 5 => self::handleEmoteSelection($player, $npc), - 6 => self::sendEditSkinSettingsForm($player, $npc), - 7 => self::sendArmorSettingsForm($player, $npc), - 8 => self::equipHeldItem($player, $npc), - 9 => self::equipOffHandItem($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ); - - if ($npc instanceof HumanSmaccer) { - $form->appendOptions( - 'Emote Settings', - 'Skin Settings', - 'Armor Settings', - 'Equip Held Item', - 'Equip Off-Hand Item' - ); - } - - $player->sendForm($form); - } - - public static function sendEditNPCForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $visibilityValues = array_values(EntityVisibility::getAll()); - $currentVisibility = $npc->getVisibility()->name; - $defaultVisibilityIndex = array_search($currentVisibility, $visibilityValues, true); - $defaultVisibility = Smaccer::getInstance()->getDefaultSettings()->getEntityVisibility()->value; - - $formElements = [ - new Input('Edit NPC name tag', 'NPC Name', $npc->getNameTag()), - new Input('Set NPC scale (0.1 - 10.0)', '1.0', (string) $npc->getScale()), - new Toggle('Enable rotation?', $npc->canRotateToPlayers()), - new Toggle('Set name tag visible', $npc->isNameTagVisible()), - new StepSlider('Select visibility', $visibilityValues, $defaultVisibilityIndex !== false ? (int) $defaultVisibilityIndex : $defaultVisibility), - new Toggle('Enable gravity?', $npc->hasGravity()), - ]; - - if ($npc instanceof EntityAgeable) { - $formElements[] = new Toggle('Is baby?', $npc->isBaby()); - } - - if ($npc instanceof HumanSmaccer) { - $formElements[] = new Toggle('Enable slapback?', $npc->canSlapBack()); - } - - $player->sendForm( - new CustomForm( - 'Edit NPC', - $formElements, - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $values = $response->getValues(); - - $nameTag = $values[0]; - $scaleStr = $values[1]; - $rotationEnabled = $values[2]; - $nameTagVisible = $values[3]; - $visibility = $values[4]; - $gravityEnabled = $values[5]; - - if (!is_string($nameTag) || !is_numeric($scaleStr) || !is_bool($rotationEnabled) || !is_bool($nameTagVisible) || !is_string($visibility) || !is_bool($gravityEnabled)) { - $player->sendMessage(TextFormat::RED . 'Invalid form values.'); - return; - } - - $scale = (float) $scaleStr; - if ($scale < 0.1 || $scale > 10.0) { - $player->sendMessage(TextFormat::RED . 'Invalid scale value. Please enter a number between 0.1 and 10.0.'); - return; - } - - $visibilityEnum = EntityVisibility::fromString($visibility); - - $npcData = NPCData::create() - ->setNameTag($nameTag) - ->setScale($scale) - ->setRotationEnabled($rotationEnabled) - ->setNametagVisible($nameTagVisible) - ->setVisibility($visibilityEnum) - ->setHasGravity($gravityEnabled); - - $index = 6; - - if ($npc instanceof EntityAgeable && isset($values[$index])) { - $isBaby = (bool) $values[$index]; - $npcData->setBaby($isBaby); - ++$index; - } - - if ($npc instanceof HumanSmaccer) { - if (isset($values[$index])) { - $enableSlapback = (bool) $values[$index]; - $npcData->setSlapBack($enableSlapback); - ++$index; - } - } - - SmaccerHandler::getInstance()->editNPC($player, $npc, $npcData)->onCompletion( - function (bool $success) use ($player, $npc) : void { - $player->sendMessage(TextFormat::GREEN . 'NPC ' . $npc->getName() . ' updated successfully!'); - }, - function (\Throwable $e) use ($player) : void { - $player->sendMessage(TextFormat::RED . 'Failed to edit NPC: ' . $e->getMessage()); - } - ); - } - ) - ); - } - - public static function sendEditCommandsForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $commandHandler = $npc->getCommandHandler(); - - $form = new MenuForm( - 'Edit Commands', - 'Choose a command operation:', - onSubmit: fn (Player $player, Button $selected) => match ($selected->text) { - 'Add' => self::sendAddCommandForm($player, $npc), - 'List' => self::sendListCommandsForm($player, $npc), - 'Clear' => self::confirmClearCommands($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ); - - if (count($commandHandler->getAll()) > 0) { - $form->appendOptions( - 'Add', - 'List', - 'Clear' - ); - } else { - $form->appendOptions('Add'); - } - - $player->sendForm($form); - } - - public static function sendAddCommandForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - new CustomForm( - 'Add Command', - [ - new Input('Enter command', 'command', ''), - new Dropdown('Select command type', [ - EntityTag::COMMAND_TYPE_PLAYER, - EntityTag::COMMAND_TYPE_SERVER, - ]), - ], - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $command = $response->getInput()->getValue(); - $commandType = $response->getDropdown()->getSelectedOption(); - - if ($npc->addCommand($command, $commandType) !== null) { - $player->sendMessage(TextFormat::GREEN . "Command added to NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to add command for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function sendListCommandsForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $commands = $npc->getCommands(); - - $buttons = array_map( - fn ($id, $data) => new Button("Command: {$data['command']} (Type: {$data['type']})"), - array_keys($commands), - $commands - ); - - $player->sendForm( - new MenuForm( - 'List Commands', - 'Commands for NPC:', - $buttons, - function (Player $player, Button $selected) use ($npc, $commands) : void { - $selectedText = $selected->text; - foreach ($commands as $id => $data) { - if ("Command: {$data['command']} (Type: {$data['type']})" === $selectedText) { - self::handleCommandSelection($player, $npc, $id, $data['command'], $data['type']); - break; - } - } - } - ) - ); - } - - public static function handleCommandSelection(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - MenuForm::withOptions( - 'Edit or Remove Command', - "Command: {$command} (Type: {$type})", - [ - 'Edit', - 'Remove', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendEditCommandForm($player, $npc, $commandId, $command, $type), - 1 => self::confirmRemoveCommand($player, $npc, $commandId), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ) - ); - } - - public static function sendEditCommandForm(Player $player, Entity $npc, int $commandId, string $command, string $type) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $defaultTypeIndex = array_search($type, [EntityTag::COMMAND_TYPE_PLAYER, EntityTag::COMMAND_TYPE_SERVER], true); - - $player->sendForm( - new CustomForm( - 'Edit Command', - [ - new Input('Edit command', 'command', $command), - new Dropdown('Select command type', [ - EntityTag::COMMAND_TYPE_PLAYER, - EntityTag::COMMAND_TYPE_SERVER, - ], $defaultTypeIndex !== false ? $defaultTypeIndex : 0), - ], - function (Player $player, CustomFormResponse $response) use ($npc, $commandId) : void { - $newCommand = $response->getInput()->getValue(); - $newType = $response->getDropdown()->getSelectedOption(); - - if ($npc->editCommand($commandId, $newCommand, $newType)) { - $player->sendMessage(TextFormat::GREEN . "Command updated for NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to update command for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function confirmRemoveCommand(Player $player, Entity $npc, int $commandId) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - ModalForm::confirm( - 'Confirm Remove Command', - "Are you sure you want to remove this command from NPC: {$npc->getName()}?", - function (Player $player) use ($npc, $commandId) : void { - $npc->removeCommandById($commandId); - $player->sendMessage(TextFormat::GREEN . "Command removed from NPC {$npc->getName()}."); - } - ) - ); - } - - public static function confirmClearCommands(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - ModalForm::confirm( - 'Confirm Clear Commands', - "Are you sure you want to clear all commands from NPC: {$npc->getName()}?", - function (Player $player) use ($npc) : void { - $npc->clearCommands(); - $player->sendMessage(TextFormat::GREEN . "All commands cleared from NPC {$npc->getName()}."); - } - ) - ); - } - - public static function sendTeleportOptionsForm(Player $player, Entity $npc, string $action) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $server = Smaccer::getInstance()->getServer(); - $playerNames = array_map(fn ($player) => $player->getName(), $server->getOnlinePlayers()); - - $player->sendForm( - MenuForm::withOptions( - 'Teleport Options', - 'Select a player:', - $playerNames, - function (Player $player, Button $selected) use ($server, $npc, $action) : void { - $selectedPlayerName = $selected->text; - $selectedPlayer = $server->getPlayerExact($selectedPlayerName); - - if ($selectedPlayer !== null) { - if ($action === self::TELEPORT_NPC_TO_PLAYER) { - $npc->teleport($selectedPlayer->getLocation()); - $player->sendMessage(TextFormat::GREEN . "NPC {$npc->getName()} has been teleported to {$selectedPlayerName}'s location."); - } elseif ($action === self::TELEPORT_PLAYER_TO_NPC) { - $player->teleport($npc->getLocation()); - $player->sendMessage(TextFormat::GREEN . "You have been teleported to NPC {$npc->getName()}'s location."); - } - } else { - $player->sendMessage(TextFormat::RED . 'Player not found.'); - } - } - ) - ); - } - - public static function handleEmoteSelection(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - MenuForm::withOptions( - 'Edit Emote', - 'Choose an emote option:', - [ - 'Action Emote', - 'Emote', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendEditActionEmoteForm($player, $npc), - 1 => self::sendEditEmoteForm($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ) - ); - } - - public static function sendEditActionEmoteForm(Player $player, Entity $npc, int $page = 0) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $actionEmoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); - $defaultActionEmote = $npc->getActionEmote(); - $currentActionEmote = $defaultActionEmote === null ? 'None' : $defaultActionEmote->getTitle(); - - $start = $page * self::ITEMS_PER_PAGE; - $end = min($start + self::ITEMS_PER_PAGE, count($actionEmoteOptions)); - - $buttons = array_map(function (EmoteType $emote) { - $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; - return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); - }, array_slice($actionEmoteOptions, $start, self::ITEMS_PER_PAGE)); - - if ($page > 0) { - $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); - } - - if ($end < count($actionEmoteOptions)) { - $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); - } - - $player->sendForm( - new MenuForm( - 'Action Emote', - 'Current action emote: ' . $currentActionEmote, - $buttons, - function (Player $player, Button $selected) use ($npc, $page, $actionEmoteOptions, $start) : void { - $buttonText = $selected->text; - $buttonValue = $selected->getValue(); - - if ($buttonText === self::PREVIOUS_PAGE) { - self::sendEditActionEmoteForm($player, $npc, $page - 1); - } elseif ($buttonText === self::NEXT_PAGE) { - self::sendEditActionEmoteForm($player, $npc, $page + 1); - } else { - if ($buttonText !== 'None') { - $actionEmote = $actionEmoteOptions[$start + $buttonValue]; - - $npc->setActionEmote($actionEmote); - } else { - $npc->setActionEmote(null); - } - - $player->sendMessage(TextFormat::GREEN . "Action emote updated for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function sendEditEmoteForm(Player $player, Entity $npc, int $page = 0) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $emoteOptions = array_merge([new EmoteType('', 'None', '')], Smaccer::getInstance()->getEmoteManager()->getAll()); - $defaultEmote = $npc->getEmote(); - $currentEmote = $defaultEmote === null ? 'None' : $defaultEmote->getTitle(); - - $start = $page * self::ITEMS_PER_PAGE; - $end = min($start + self::ITEMS_PER_PAGE, count($emoteOptions)); - - $buttons = array_map(function (EmoteType $emote) { - $image = $emote->getTitle() !== 'None' ? $emote->getImage() : null; - return $image !== null ? new Button($emote->getTitle(), Image::url($image)) : new Button($emote->getTitle()); - }, array_slice($emoteOptions, $start, self::ITEMS_PER_PAGE)); - - if ($page > 0) { - $buttons[] = new Button(self::PREVIOUS_PAGE, Image::path('textures/ui/arrowLeft.png')); - } - - if ($end < count($emoteOptions)) { - $buttons[] = new Button(self::NEXT_PAGE, Image::path('textures/ui/arrowRight.png')); - } - - $player->sendForm( - new MenuForm( - 'Emote', - 'Current emote: ' . $currentEmote, - $buttons, - function (Player $player, Button $selected) use ($npc, $page, $emoteOptions, $start) : void { - $buttonText = $selected->text; - $buttonValue = $selected->getValue(); - - if ($buttonText === self::PREVIOUS_PAGE) { - self::sendEditEmoteForm($player, $npc, $page - 1); - } elseif ($buttonText === self::NEXT_PAGE) { - self::sendEditEmoteForm($player, $npc, $page + 1); - } else { - if ($buttonText !== 'None') { - $emote = $emoteOptions[$start + $buttonValue]; - - $npc->setEmote($emote); - } else { - $npc->setEmote(null); - } - - $player->sendMessage(TextFormat::GREEN . "Emote updated for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function sendEditSkinSettingsForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - MenuForm::withOptions( - 'Skin Settings', - 'Select an option:', - [ - 'Edit Skin', - 'Edit Cape', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendEditSkinForm($player, $npc), - 1 => self::sendEditCapeForm($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ) - ); - } - - public static function sendEditSkinForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - MenuForm::withOptions( - 'Edit Skin', - 'Select an option:', - [ - 'Change Skin from Player', - 'Change Skin from URL', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendChangeSkinFromPlayerForm($player, $npc), - 1 => self::sendChangeSkinFromURLForm($player, $npc), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ) - ); - } - - public static function sendChangeSkinFromPlayerForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $server = Smaccer::getInstance()->getServer(); - $onlinePlayers = $server->getOnlinePlayers(); - $playerNames = array_map(fn ($player) => $player->getName(), $onlinePlayers); - - $player->sendForm( - MenuForm::withOptions( - 'Change Skin from Player', - 'Select a player:', - $playerNames, - function (Player $player, Button $selected) use ($server, $npc) : void { - $selectedPlayerName = $selected->text; - $selectedPlayer = $server->getPlayerExact($selectedPlayerName); - - if ($selectedPlayer !== null) { - $npc->setSkin($selectedPlayer->getSkin()); - $npc->sendSkin(); - $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from player {$selectedPlayerName}."); - } else { - $player->sendMessage(TextFormat::RED . 'Player not found.'); - } - } - ) - ); - } - - public static function sendChangeSkinFromURLForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $formElements = [ - new Input('Enter skin URL', 'https://example.com/skin.png'), - ]; - - $player->sendForm( - new CustomForm( - 'Change Skin from URL', - $formElements, - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $url = $response->getInput()->getValue(); - - SkinUtils::skinFromURL($url)->onCompletion( - function (string $skinBytes) use ($player, $npc) : void { - $npc->changeSkin($skinBytes); - $player->sendMessage(TextFormat::GREEN . "Skin updated for NPC {$npc->getName()} from URL."); - }, - function (\Throwable $e) use ($player) : void { - $player->sendMessage(TextFormat::RED . 'Failed to update skin from URL: ' . $e->getMessage()); - } - ); - } - ) - ); - } - - public static function sendEditCapeForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $formElements = [ - new Input('Enter cape URL', 'https://example.com/cape.png'), - ]; - - $player->sendForm( - new CustomForm( - 'Change Cape from URL', - $formElements, - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $url = $response->getInput()->getValue(); - - SkinUtils::capeFromURL($url)->onCompletion( - function (string $capeBytes) use ($player, $npc) : void { - $npc->changeCape($capeBytes); - $player->sendMessage(TextFormat::GREEN . "Cape updated for NPC {$npc->getName()} from URL."); - }, - function (\Throwable $e) use ($player) : void { - $player->sendMessage(TextFormat::RED . 'Failed to update cape from URL: ' . $e->getMessage()); - } - ); - } - ) - ); - } - - public static function sendArmorSettingsForm(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - $player->sendMessage(TextFormat::RED . 'This NPC cannot wear armor.'); - return; - } - - $form = MenuForm::withOptions( - 'Armor Settings', - 'Choose an armor option:', - [ - 'Equip All Armor', - 'Equip Helmet', - 'Equip Chestplate', - 'Equip Leggings', - 'Equip Boots', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::equipArmorPiece($player, $npc, self::ARMOR_ALL), - 1 => self::equipArmorPiece($player, $npc, self::ARMOR_HELMET), - 2 => self::equipArmorPiece($player, $npc, self::ARMOR_CHESTPLATE), - 3 => self::equipArmorPiece($player, $npc, self::ARMOR_LEGGINGS), - 4 => self::equipArmorPiece($player, $npc, self::ARMOR_BOOTS), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ); - - $player->sendForm($form); - } - - public static function equipArmorPiece(Player $player, HumanSmaccer $npc, string $piece) : void { - $armorInventory = $player->getArmorInventory(); - - switch ($piece) { - case self::ARMOR_HELMET: - $npc->setHelmet($player); - break; - case self::ARMOR_CHESTPLATE: - $npc->setChestplate($player); - break; - case self::ARMOR_LEGGINGS: - $npc->setLeggings($player); - break; - case self::ARMOR_BOOTS: - $npc->setBoots($player); - break; - case self::ARMOR_ALL: - $npc->setArmor($player); - - $player->sendMessage(TextFormat::GREEN . "All armor equipped to NPC {$npc->getName()}."); - return; - default: - $player->sendMessage(TextFormat::RED . 'Invalid armor piece specified.'); - return; - } - - $player->sendMessage(TextFormat::GREEN . ucfirst($piece) . " equipped to NPC {$npc->getName()}."); - } - - public static function equipHeldItem(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $item = $player->getInventory()->getItemInHand(); - $npc->setItemInHand($item); - - $player->sendMessage(TextFormat::GREEN . "Held item equipped to NPC {$npc->getName()}."); - } - - public static function equipOffHandItem(Player $player, Entity $npc) : void { - if (!$npc instanceof HumanSmaccer) { - return; - } - - $item = $player->getOffHandInventory()->getItem(0); - $npc->setOffHandItem($item); - - $player->sendMessage(TextFormat::GREEN . "Off-hand item equipped to NPC {$npc->getName()}."); - } - - public static function sendNPCListForm(Player $player) : void { - $entityData = SmaccerHandler::getInstance()->getEntitiesInfo(null, true); - $totalEntityCount = $entityData['count']; - $entities = $entityData['infoList']; - - if ($totalEntityCount > 0) { - $content = TextFormat::RED . 'NPC List and Locations: (' . $totalEntityCount . ')'; - $content .= "\n" . TextFormat::WHITE . '- ' . implode("\n - ", $entities); - } else { - $content = TextFormat::RED . 'No NPCs found in any world.'; - } - - $player->sendForm(new MenuForm('List NPCs', $content)); - } - - public static function sendQueryManagementForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $form = MenuForm::withOptions( - 'Manage Queries', - 'Select a query type:', - [ - 'Add Server Query', - 'Add World Query', - 'Edit/Remove Server Query', - 'Edit/Remove World Query', - ], - fn (Player $player, Button $selected) => match ($selected->text) { - 'Add Server Query' => self::sendAddServerQueryForm($player, $npc), - 'Add World Query' => self::sendAddWorldQueryForm($player, $npc), - 'Edit/Remove Server Query' => self::sendEditRemoveQueryForm($player, $npc, QueryHandler::TYPE_SERVER), - 'Edit/Remove World Query' => self::sendEditRemoveQueryForm($player, $npc, QueryHandler::TYPE_WORLD), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ); - - $player->sendForm($form); - } - - public static function sendAddServerQueryForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - new CustomForm( - 'Add Server Query', - [ - new Input('Enter IP/Domain', 'ip_or_domain'), - new Input('Enter Port', 'port'), - ], - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $values = $response->getValues(); - - $ipOrDomain = $values[0]; - $port = $values[1]; - - if (!is_string($ipOrDomain) || !is_numeric($port)) { - $player->sendMessage(TextFormat::RED . 'Invalid form values.'); - return; - } - - if ($npc->getQueryHandler()->addServerQuery($ipOrDomain, (int) $port) !== null) { - $player->sendMessage(TextFormat::GREEN . "Server query added to NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to add server query for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function sendAddWorldQueryForm(Player $player, Entity $npc) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - new CustomForm( - 'Add World Query', - [ - new Input('Enter world name', 'world_name'), - ], - function (Player $player, CustomFormResponse $response) use ($npc) : void { - $worldName = $response->getInput()->getValue(); - - if ($npc->getQueryHandler()->addWorldQuery($worldName) !== null) { - $player->sendMessage(TextFormat::GREEN . "World query added to NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to add world query for NPC {$npc->getName()}."); - } - } - ) - ); - } - - public static function sendEditRemoveQueryForm(Player $player, Entity $npc, string $queryType) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $queries = $npc->getQueryHandler()->getAll(); - $buttons = array_values(array_filter(array_map( - fn ($id, $data) => $queryType === $data['type'] - ? new Button( - $data['type'] === QueryHandler::TYPE_SERVER - ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" - : "World: {$data['value']['world_name']}" - ) - : null, - array_keys($queries), - $queries - ))); - - if (count($buttons) === 0) { - $player->sendMessage(TextFormat::RED . 'No queries found for the selected type.'); - return; - } - - $player->sendForm( - new MenuForm( - 'Edit/Remove Query', - 'Select a query to edit/remove:', - $buttons, - function (Player $player, Button $selected) use ($npc, $queries) : void { - $selectedText = $selected->text; - foreach ($queries as $id => $data) { - $expectedText = $data['type'] === QueryHandler::TYPE_SERVER - ? "IP: {$data['value']['ip']} Port: {$data['value']['port']}" - : "World: {$data['value']['world_name']}"; - if ($expectedText === $selectedText) { - self::handleQuerySelection($player, $npc, $id, $data['type'], $data['value']); - return; - } - } - - $player->sendMessage(TextFormat::RED . 'Failed to match the selected query.'); - } - ) - ); - } - - public static function handleQuerySelection(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - MenuForm::withOptions( - 'Edit or Remove Query', - $queryType === QueryHandler::TYPE_SERVER - ? "IP: {$queryValue['ip']} Port: {$queryValue['port']}" - : "World: {$queryValue['world_name']}", - [ - 'Edit', - 'Remove', - ], - fn (Player $player, Button $selected) => match ($selected->getValue()) { - 0 => self::sendEditQueryForm($player, $npc, $queryId, $queryType, $queryValue), - 1 => self::confirmRemoveQuery($player, $npc, $queryId), - default => $player->sendMessage(TextFormat::RED . 'Invalid option selected.'), - } - ) - ); - } - - public static function sendEditQueryForm(Player $player, Entity $npc, int $queryId, string $queryType, array $queryValue) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - if ($queryType === QueryHandler::TYPE_SERVER) { - $player->sendForm( - new CustomForm( - 'Edit Server Query', - [ - new Input('Edit IP/Domain', 'ip_or_domain', $queryValue['ip']), - new Input('Edit Port', 'port', (string) $queryValue['port']), - ], - function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { - $values = $response->getValues(); - - $newIpOrDomain = $values[0]; - $newPort = $values[1]; - - if (!is_string($newIpOrDomain) || !is_numeric($newPort)) { - $player->sendMessage(TextFormat::RED . 'Invalid form values.'); - return; - } - - if ($npc->getQueryHandler()->editServerQuery($queryId, $newIpOrDomain, (int) $newPort)) { - $player->sendMessage(TextFormat::GREEN . "Server query updated for NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to update server query for NPC {$npc->getName()}."); - } - } - ) - ); - } else { - $player->sendForm( - new CustomForm( - 'Edit World Query', - [ - new Input('Edit world name', 'world_name', $queryValue['world_name']), - ], - function (Player $player, CustomFormResponse $response) use ($npc, $queryId) : void { - $newWorldName = $response->getInput()->getValue(); - - if ($npc->getQueryHandler()->editWorldQuery($queryId, $newWorldName)) { - $player->sendMessage(TextFormat::GREEN . "World query updated for NPC {$npc->getName()}."); - } else { - $player->sendMessage(TextFormat::RED . "Failed to update world query for NPC {$npc->getName()}."); - } - } - ) - ); - } - } - - public static function confirmRemoveQuery(Player $player, Entity $npc, int $queryId) : void { - if (!$npc instanceof EntitySmaccer && !$npc instanceof HumanSmaccer) { - return; - } - - $player->sendForm( - ModalForm::confirm( - 'Confirm Remove Query', - "Are you sure you want to remove this query from NPC: {$npc->getName()}?", - function (Player $player) use ($npc, $queryId) : void { - $npc->getQueryHandler()->removeById($queryId); - $player->sendMessage(TextFormat::GREEN . "Query removed from NPC {$npc->getName()}."); - } - ) - ); - } -} diff --git a/src/aiptu/smaccer/utils/Permissions.php b/src/aiptu/smaccer/utils/Permissions.php index 3a742724..7aa2c1db 100644 --- a/src/aiptu/smaccer/utils/Permissions.php +++ b/src/aiptu/smaccer/utils/Permissions.php @@ -1,7 +1,7 @@ a promise that resolves to the skin bytes + * @param string $url PNG image URL + * @param Closure $onSuccess Callback on success: function(string $skinBytes) + * @param Closure $onError Callback on error: function(Throwable $error) * - * @throws \InvalidArgumentException if the URL is invalid or not a PNG + * @throws InvalidArgumentException if URL format invalid */ - public static function skinFromURL(string $url) : Promise { - $resolver = new PromiseResolver(); - + public static function skinFromURL(string $url, Closure $onSuccess, Closure $onError) : void { try { - self::validateUrl($url); self::validatePngUrl($url); - - Utils::fetchAsync($url, function ($result) use ($resolver) : void { - if ($result === null) { - $resolver->reject(new \RuntimeException('Failed to download skin.')); - return; - } - - $skinData = $result->getBody(); - $filePath = self::saveSkinToFile($skinData); - - $skinBytes = self::skinFromFile($filePath); - $resolver->resolve($skinBytes); - }); - } catch (\Throwable $e) { - $resolver->reject($e); + self::downloadAndProcess($url, self::TYPE_SKIN, $onSuccess, $onError); + } catch (InvalidArgumentException $e) { + $onError($e); } - - return $resolver->getPromise(); } /** - * Downloads a cape from a URL and returns the cape bytes in a promise. - * - * @param string $url the URL of the PNG cape + * Download and process cape from URL asynchronously. * - * @return Promise a promise that resolves to the cape bytes + * @param string $url PNG image URL + * @param Closure $onSuccess Callback on success: function(string $capeBytes) + * @param Closure $onError Callback on error: function(Throwable $error) * - * @throws \InvalidArgumentException if the URL is invalid or not a PNG + * @throws InvalidArgumentException if URL format invalid */ - public static function capeFromURL(string $url) : Promise { - $resolver = new PromiseResolver(); - + public static function capeFromURL(string $url, Closure $onSuccess, Closure $onError) : void { try { - self::validateUrl($url); self::validatePngUrl($url); - - Utils::fetchAsync($url, function ($result) use ($resolver) : void { - if ($result === null) { - $resolver->reject(new \RuntimeException('Failed to download cape.')); - return; - } - - $capeData = $result->getBody(); - $filePath = self::saveSkinToFile($capeData); - - $capeBytes = self::capeFromFile($filePath); - $resolver->resolve($capeBytes); - }); - } catch (\Throwable $e) { - $resolver->reject($e); + self::downloadAndProcess($url, self::TYPE_CAPE, $onSuccess, $onError); + } catch (InvalidArgumentException $e) { + $onError($e); } - - return $resolver->getPromise(); } /** - * Processes a skin from a file path and returns the skin bytes. + * Process skin from local file. * - * @param string $filePath the file path of the PNG skin + * @param string $filePath Path to PNG file * - * @return string the skin bytes + * @return string RGBA bytes (64x64 or 64x32) * - * @throws \RuntimeException if the file is not a valid PNG skin + * @throws RuntimeException if file invalid or processing fails */ public static function skinFromFile(string $filePath) : string { - return self::processPngFile($filePath, self::SKIN); + return self::processPngFile($filePath, self::TYPE_SKIN); } /** - * Processes a cape from a file path and returns the cape bytes. + * Process cape from local file. * - * @param string $filePath the file path of the PNG cape + * @param string $filePath Path to PNG file * - * @return string the cape bytes + * @return string RGBA bytes (typically 64x32) * - * @throws \RuntimeException if the file is not a valid PNG cape + * @throws RuntimeException if file invalid or processing fails */ public static function capeFromFile(string $filePath) : string { - return self::processPngFile($filePath, self::CAPE); + return self::processPngFile($filePath, self::TYPE_CAPE); } - private static function processPngFile(string $filePath, string $type) : string { - $image = imagecreatefrompng($filePath); - if ($image === false) { - self::cleanupFile($filePath); - throw new \RuntimeException("The file is not a valid PNG {$type}."); - } - - if (!imageistruecolor($image)) { - imagepalettetotruecolor($image); - } + /** + * Download and process skin or cape from URL. + * + * @phpstan-param self::TYPE_* $type + */ + private static function downloadAndProcess(string $url, string $type, Closure $onSuccess, Closure $onError) : void { + Utils::fetchAsync($url, static function (?InternetRequestResult $result) use ($type, $onSuccess, $onError) : void { + if ($result === null) { + $onError(new RuntimeException('Failed to download image from URL')); + return; + } - $bytes = ($type === self::SKIN) ? self::extractSkinBytes($image) : self::extractCapeBytes($image); - imagedestroy($image); - self::cleanupFile($filePath); + try { + $imageData = $result->getBody(); + $tempPath = self::saveTempFile($imageData); - return $bytes; + $bytes = self::processPngFile($tempPath, $type); + $onSuccess($bytes); + } catch (Throwable $e) { + $onError($e); + } + }); } /** - * Validates that a URL is in the correct format. + * Process PNG file to RGBA bytes. * - * @param string $url the URL to validate + * @phpstan-param self::TYPE_* $type * - * @throws \InvalidArgumentException if the URL format is invalid + * @throws RuntimeException on invalid PNG or processing error */ - private static function validateUrl(string $url) : void { - if (!Utils::isValidUrl($url)) { - throw new \InvalidArgumentException('Invalid URL format.'); + private static function processPngFile(string $filePath, string $type) : string { + $image = @imagecreatefrompng($filePath); + + if ($image === false) { + self::cleanupFile($filePath); + throw new RuntimeException("Invalid PNG {$type} file"); } - } - /** - * Validates that a URL points to a PNG image. - * - * @param string $url the URL to validate - * - * @throws \InvalidArgumentException if the URL does not point to a PNG image - */ - private static function validatePngUrl(string $url) : void { - if (!Utils::isPngUrl($url)) { - throw new \InvalidArgumentException('URL does not point to a PNG image.'); + // Convert palette images to truecolor + if (!imageistruecolor($image)) { + imagepalettetotruecolor($image); } - } - /** - * Saves image data to a temporary file. - * - * @param string $data the image data to save - * - * @return string the file path where the image data was saved - * - * @throws \RuntimeException if there is an error saving the image data - */ - private static function saveSkinToFile(string $data) : string { - $filePath = Path::join(Smaccer::getInstance()->getDataFolder(), uniqid('skin_', true) . '.png'); try { - Filesystem::safeFilePutContents($filePath, $data); - return $filePath; - } catch (\RuntimeException $e) { - throw new \RuntimeException('An error occurred while saving the skin file: ' . $e->getMessage()); + $bytes = match ($type) { + self::TYPE_SKIN => self::extractSkinBytes($image), + self::TYPE_CAPE => self::extractCapeBytes($image), + default => throw new RuntimeException("Unknown type: {$type}") + }; + } finally { + imagedestroy($image); + self::cleanupFile($filePath); } + + return $bytes; } /** - * Extracts the bytes from a GD image resource for skin. + * Extract RGBA bytes from skin image. + * + * Format: 4 bytes per pixel (R, G, B, A), row-major order. * - * @param \GdImage $image the GD image resource + * @param GdImage $image Source image resource * - * @return string the extracted skin bytes + * @return string RGBA byte string */ - private static function extractSkinBytes(\GdImage $image) : string { + private static function extractSkinBytes(GdImage $image) : string { $bytes = ''; - for ($y = 0; $y < imagesy($image); ++$y) { - for ($x = 0; $x < imagesx($image); ++$x) { + $width = imagesx($image); + $height = imagesy($image); + + for ($y = 0; $y < $height; ++$y) { + for ($x = 0; $x < $width; ++$x) { $rgba = imagecolorat($image, $x, $y); - $a = ((~($rgba >> 24)) << 1) & 0xFF; + $r = ($rgba >> 16) & 0xFF; $g = ($rgba >> 8) & 0xFF; $b = $rgba & 0xFF; + $a = ((~($rgba >> 24)) << 1) & 0xFF; // Convert 7-bit alpha to 8-bit + $bytes .= chr($r) . chr($g) . chr($b) . chr($a); } } @@ -218,18 +191,27 @@ private static function extractSkinBytes(\GdImage $image) : string { } /** - * Extracts the bytes from a GD image resource for cape. + * Extract RGBA bytes from cape image. * - * @param \GdImage $image the GD image resource + * @param GdImage $image Source image resource * - * @return string the extracted cape bytes + * @return string RGBA byte string */ - private static function extractCapeBytes(\GdImage $image) : string { + private static function extractCapeBytes(GdImage $image) : string { $bytes = ''; - for ($y = 0; $y < imagesy($image); ++$y) { - for ($x = 0; $x < imagesx($image); ++$x) { + $width = imagesx($image); + $height = imagesy($image); + + for ($y = 0; $y < $height; ++$y) { + for ($x = 0; $x < $width; ++$x) { $argb = imagecolorat($image, $x, $y); - $bytes .= chr(($argb >> 16) & 0xFF) . chr(($argb >> 8) & 0xFF) . chr($argb & 0xFF) . chr(((~($argb >> 24)) << 1) & 0xFF); + + $r = ($argb >> 16) & 0xFF; + $g = ($argb >> 8) & 0xFF; + $b = $argb & 0xFF; + $a = ((~($argb >> 24)) << 1) & 0xFF; + + $bytes .= chr($r) . chr($g) . chr($b) . chr($a); } } @@ -237,13 +219,51 @@ private static function extractCapeBytes(\GdImage $image) : string { } /** - * Deletes a file if it exists. + * Validate URL format and PNG extension. + * + * @throws InvalidArgumentException if validation fails + */ + private static function validatePngUrl(string $url) : void { + if (!Utils::isValidUrl($url)) { + throw new InvalidArgumentException('Invalid URL format'); + } + + if (!Utils::isPngUrl($url)) { + throw new InvalidArgumentException('URL must point to PNG image'); + } + } + + /** + * Save image data to temporary file. + * + * @param string $data Image bytes + * + * @return string Path to saved file + * + * @throws RuntimeException on write failure + */ + private static function saveTempFile(string $data) : string { + $filePath = Path::join( + Smaccer::getInstance()->getDataFolder(), + uniqid('skin_', true) . '.png' + ); + + try { + Filesystem::safeFilePutContents($filePath, $data); + return $filePath; + } catch (RuntimeException $e) { + throw new RuntimeException('Failed to save temporary file: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Delete temporary file if exists. * - * @param string $filePath the path to the file to delete + * @param string $filePath Path to file */ private static function cleanupFile(string $filePath) : void { if (is_file($filePath)) { - unlink($filePath); + @unlink($filePath); } } } diff --git a/src/aiptu/smaccer/utils/Utils.php b/src/aiptu/smaccer/utils/Utils.php index 51f5cfad..08c6ae08 100644 --- a/src/aiptu/smaccer/utils/Utils.php +++ b/src/aiptu/smaccer/utils/Utils.php @@ -1,7 +1,7 @@ $results - */ - $bulkCurlTaskCallback = function (array $results) use ($callback) : void { - if (isset($results[0]) && !$results[0] instanceof InternetException) { - $callback($results[0]); - } else { - $callback(null); + /** + * Perform async HTTP GET request. + * + * @param string $url Target URL + * + * @phpstan-param Closure(InternetRequestResult|null): void $callback Result handler + */ + public static function fetchAsync(string $url, Closure $callback) : void { + $task = new BulkCurlTask( + [new BulkCurlTaskOperation($url)], + /** + * @phpstan-param list $results + */ + static function (array $results) use ($callback) : void { + $result = $results[0] ?? null; + $callback($result instanceof InternetException ? null : $result); } - }; - $task = new BulkCurlTask([ - new BulkCurlTaskOperation($url), - ], $bulkCurlTaskCallback); + ); + Server::getInstance()->getAsyncPool()->submitTask($task); } + /** + * Validate URL format (RFC 3986). + * + * @param string $url URL to validate + */ public static function isValidUrl(string $url) : bool { return filter_var($url, FILTER_VALIDATE_URL) !== false; } /** - * Validates the IP address or host name format. + * Check if URL points to PNG image (by extension). + * + * @param string $url URL to check + */ + public static function isPngUrl(string $url) : bool { + return preg_match('/^https?:\/\/.+\.png$/i', $url) === 1; + } + + /** + * Validate IP address or domain name. + * + * @param string $ipOrDomain IP or domain to validate */ public static function isValidIpOrDomain(string $ipOrDomain) : bool { return self::isValidIp($ipOrDomain) || self::isValidDomain($ipOrDomain); } /** - * Validates the IP address format. + * Validate IPv4 or IPv6 address. + * + * @param string $ip IP address to validate */ public static function isValidIp(string $ip) : bool { return filter_var($ip, FILTER_VALIDATE_IP) !== false; } /** - * Validates the host name format. + * Validate domain name format. + * + * @param string $host Domain name to validate */ public static function isValidDomain(string $host) : bool { return filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) !== false; } /** - * Validates the port number. + * Validate TCP/UDP port number (1-65535). + * + * @param int $port Port number to validate + * + * @phpstan-assert-if-true int<1, 65535> $port */ public static function isValidPort(int $port) : bool { - return $port > 0 && $port <= 65535; - } - - public static function isPngUrl(string $url) : bool { - return preg_match('/^https?:\/\/.+\.(png)$/i', $url) === 1; + return $port >= 1 && $port <= 65535; } } diff --git a/src/aiptu/smaccer/utils/promise/Promise.php b/src/aiptu/smaccer/utils/promise/Promise.php deleted file mode 100644 index 1528d45f..00000000 --- a/src/aiptu/smaccer/utils/promise/Promise.php +++ /dev/null @@ -1,113 +0,0 @@ - $shared - */ - public function __construct(private PromiseSharedData $shared) {} - - /** - * Provide callbacks to be called when the promise is resolved or rejected. - * - * @phpstan-param (Closure(TValue): void)|(Closure(): void) $onSuccess - * @phpstan-param (Closure(Throwable): void)|(Closure(): void) $onFailure - */ - public function onCompletion(Closure $onSuccess, Closure $onFailure) : void { - if ($this->shared->result !== null) { - $onSuccess($this->shared->result); - } elseif ($this->shared->error !== null) { - $onFailure($this->shared->error); - } else { - $this->shared->onSuccess[spl_object_id($onSuccess)] = $onSuccess; - $this->shared->onError[spl_object_id($onFailure)] = $onFailure; - } - } - - /** - * Returns true if the promise has been resolved or rejected. - */ - public function isResolved() : bool { - return $this->shared->result !== null || $this->shared->error !== null; - } - - /** - * Returns the result of the promise. - * - * @phpstan-return TValue|null - */ - public function getResult() : mixed { - return $this->shared->result; - } - - /** - * Returns the exception that was thrown when the promise was rejected. - */ - public function getError() : ?Throwable { - return $this->shared->error; - } - - /** - * Utility method to create a promise that resolves once all the given promises have resolved. - * - * @param Promise ...$promises All the promises to wait for. - * - * @phpstan-template UValue of mixed - * - * @phpstan-param Promise ...$promises - * - * Returns a {@see Promise} that resolves with an array of all the results of the given promises. The results in the - * array are in the same order in which the promises were given to the method. - * If any of the given promises is rejected, the returned promise is rejected with the same exception. - * - * @phpstan-return Promise> - */ - public static function all(self ...$promises) : self { - /** @phpstan-var PromiseResolver> $resolver */ - $resolver = new PromiseResolver(); - /** @phpstan-var non-empty-array $results */ - $results = []; - foreach ($promises as $key => $promise) { - $promise->onCompletion( - function (mixed $value) use ($promises, $key, &$results, $resolver) : void { - $results[$key] = $value; - if (count($results) === count($promises)) { - $resolver->resolveSilent($results); - } - }, - function (Throwable $error) use ($resolver) : void { - $resolver->rejectSilent($error); - } - ); - } - - return $resolver->getPromise(); - } -} diff --git a/src/aiptu/smaccer/utils/promise/PromiseResolver.php b/src/aiptu/smaccer/utils/promise/PromiseResolver.php deleted file mode 100644 index e50c86d9..00000000 --- a/src/aiptu/smaccer/utils/promise/PromiseResolver.php +++ /dev/null @@ -1,119 +0,0 @@ - */ - private PromiseSharedData $shared; - /** @phpstan-var Promise */ - private Promise $promise; - - public function __construct() { - $this->shared = new PromiseSharedData(); - $this->promise = new Promise($this->shared); - } - - /** - * Resolves the promise with the given value. - * - * @param mixed $value the value to resolve the promise with - * - * @phpstan-param TValue $value - * - * @throws LogicException when the promise has already been resolved or rejected - */ - public function resolve(mixed $value) : void { - if ($this->promise->isResolved()) { - throw new LogicException('Promise has already been ' . ($this->shared->result === null ? 'rejected' : 'resolved')); - } - - $this->shared->result = $value; - foreach ($this->shared->onSuccess as $closure) { - $closure($value); - } - - $this->shared->onSuccess = []; - $this->shared->onError = []; - } - - /** - * Resolves the promise with the given value. Unlike {@see PromiseResolver::resolve()}, this method does not throw - * an exception if the promise has already been resolved or rejected. - * - * @param mixed $value the value to resolve the promise with - * - * @phpstan-param TValue $value - * Returns true if the promise was successfully resolved, or false if it was already resolved or rejected. - */ - public function resolveSilent(mixed $value) : bool { - try { - $this->resolve($value); - } catch (LogicException) { - return false; - } - - return true; - } - - /** - * Rejects the promise with the given exception. - * - * @param Throwable $error the exception to reject the promise with - * - * @throws LogicException when the promise has already been resolved or rejected - */ - public function reject(Throwable $error) : void { - if ($this->promise->isResolved()) { - throw new LogicException('Promise has already been ' . ($this->shared->result === null ? 'rejected' : 'resolved')); - } - - $this->shared->error = $error; - foreach ($this->shared->onError as $closure) { - $closure($error); - } - - $this->shared->onSuccess = []; - $this->shared->onError = []; - } - - /** - * Rejects the promise with the given exception. Unlike {@see PromiseResolver::reject()}, this method does not throw - * an exception if the promise has already been resolved or rejected. - * - * @param Throwable $error The exception to reject the promise with. - * Returns true if the promise was successfully rejected, or false if it was already resolved or rejected. - */ - public function rejectSilent(Throwable $error) : bool { - try { - $this->reject($error); - } catch (LogicException) { - return false; - } - - return true; - } - - /** - * @phpstan-return Promise - */ - public function getPromise() : Promise { - return $this->promise; - } -} diff --git a/src/aiptu/smaccer/utils/promise/PromiseSharedData.php b/src/aiptu/smaccer/utils/promise/PromiseSharedData.php deleted file mode 100644 index 36a91f54..00000000 --- a/src/aiptu/smaccer/utils/promise/PromiseSharedData.php +++ /dev/null @@ -1,48 +0,0 @@ - - */ - public array $onSuccess = []; - /** - * An array of {@see Closure}s to call when the promise is rejected. - * - * @phpstan-var array - */ - public array $onError = []; - - /** - * The result of the promise. - * - * @phpstan-var TValue|null - */ - public mixed $result = null; - /** The exception that was thrown when the promise was rejected. */ - public ?Throwable $error = null; -}