mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-14 05:15:11 +00:00
Compare commits
91 Commits
translatab
...
feat/async
Author | SHA1 | Date | |
---|---|---|---|
39c9387efe | |||
31275ba681 | |||
d41f1b2889 | |||
051671df50 | |||
4047cbaafe | |||
0977f0db7d | |||
a1d74b5710 | |||
50e15db9ac | |||
55777d20c3 | |||
f1f6e796a4 | |||
6e861afd9e | |||
7ea0f2ff43 | |||
6dbd4282cb | |||
3176e7549e | |||
92c3ce7f02 | |||
3a5432b316 | |||
45e350ddf7 | |||
40a3ee68dd | |||
7082522509 | |||
e415518435 | |||
cb508f4382 | |||
177fa76434 | |||
2a97b4294d | |||
258923cc78 | |||
04494e845c | |||
0e511ff783 | |||
6826420876 | |||
f0161c84b9 | |||
3643d3aeb8 | |||
a662510cca | |||
670d3fb997 | |||
8843b1b568 | |||
9c71f4fc1c | |||
bd39caccb0 | |||
95b4db5169 | |||
a0d69a9fb8 | |||
e8ec81d123 | |||
866d473553 | |||
d9080f182c | |||
d9f5634262 | |||
406e2c6c57 | |||
4451770ca3 | |||
d2d663b1c9 | |||
117026cb83 | |||
a7a1077676 | |||
0a56cf877b | |||
11fdf79a7c | |||
edae9f26e4 | |||
667656b1c6 | |||
972a9fb201 | |||
ac1cf73f8e | |||
96989d1dc4 | |||
8aed5d6b27 | |||
fa796535ff | |||
32b1d6c0c2 | |||
6f40c6fc1d | |||
a6a44bde90 | |||
409066c8f5 | |||
cb2fadeb26 | |||
a14afb4bb5 | |||
db88e543fa | |||
c426677841 | |||
17ae932d31 | |||
8f48fe4856 | |||
48d2430064 | |||
b82d47dd32 | |||
86fb041a65 | |||
b276133003 | |||
c1e3903934 | |||
eb9814197b | |||
d6b7a9ed45 | |||
64bbff6286 | |||
f82c422f64 | |||
aaa37baf2e | |||
243a3035ba | |||
823d4ead6a | |||
ca95b2f28d | |||
cc6e8ef232 | |||
5beaa3ce4e | |||
7e87fbbb7a | |||
ed739cff4f | |||
dc85bba995 | |||
1176b7090c | |||
2b2fa9ddf1 | |||
58155a77fb | |||
c250bb0da7 | |||
b78ff00418 | |||
9b2b92ac1f | |||
7a4b9a0367 | |||
a84fc2b901 | |||
5fe57a8f6f |
47
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
47
.github/workflows/copilot-setup-steps.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: "Copilot Agent environment setup"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
jobs:
|
||||
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@3.2.0
|
||||
with:
|
||||
php-version: 8.3
|
||||
install-path: "./bin"
|
||||
pm-version-major: 5
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/composer/files
|
||||
~/.cache/composer/vcs
|
||||
key: "composer-v2-cache-8.3-${{ hashFiles('./composer.lock') }}"
|
||||
restore-keys: |
|
||||
composer-v2-cache-
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: composer install --prefer-dist --no-interaction
|
||||
|
||||
- name: Clone extension stubs
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: pmmp/phpstorm-stubs
|
||||
path: extension-stubs
|
2
.github/workflows/discord-release-notify.yml
vendored
2
.github/workflows/discord-release-notify.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.33.0
|
||||
uses: shivammathur/setup-php@2.34.1
|
||||
with:
|
||||
php-version: 8.2
|
||||
|
||||
|
2
.github/workflows/draft-release-pr-check.yml
vendored
2
.github/workflows/draft-release-pr-check.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.33.0
|
||||
uses: shivammathur/setup-php@2.34.1
|
||||
with:
|
||||
php-version: 8.2
|
||||
|
||||
|
6
.github/workflows/draft-release.yml
vendored
6
.github/workflows/draft-release.yml
vendored
@ -87,7 +87,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.33.0
|
||||
uses: shivammathur/setup-php@2.34.1
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
|
||||
@ -165,7 +165,7 @@ jobs:
|
||||
${{ github.workspace }}/core-permissions.rst
|
||||
|
||||
- name: Create draft release
|
||||
uses: ncipollo/release-action@v1.16.0
|
||||
uses: ncipollo/release-action@v1.18.0
|
||||
id: create-draft
|
||||
with:
|
||||
artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst
|
||||
@ -188,4 +188,4 @@ jobs:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: "[Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."
|
||||
message: "${{ vars.DRAFT_RELEASE_NOTIFICATION_MENTION }} [Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."
|
||||
|
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.33.0
|
||||
uses: shivammathur/setup-php@2.34.1
|
||||
with:
|
||||
php-version: 8.3
|
||||
tools: php-cs-fixer:3.75
|
||||
|
@ -56,6 +56,7 @@ PocketMine-MP has three primary branches of development.
|
||||
| Deprecating API classes, methods or constants | ❌ | ✔️ | ✔️ |
|
||||
| Adding optional parameters to an API method | ❌ | ✔️ | ✔️ |
|
||||
| Changing API behaviour | ❌ | 🟡 Only if backwards-compatible | ✔️ |
|
||||
| Changing an event from sync to async or vice versa | ❌ | ❌ | ✔️ |
|
||||
| Removal of API | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible API change (e.g. renaming a method) | ❌ | ❌ | ✔️ |
|
||||
| Backwards-incompatible internals change (e.g. changing things in `pocketmine\network\mcpe`) | ❌ | ✔️ | ✔️ |
|
||||
|
Submodule build/php updated: 1549433797...ce1b095a9c
25
changelogs/5.29.md
Normal file
25
changelogs/5.29.md
Normal file
@ -0,0 +1,25 @@
|
||||
# 5.29.0
|
||||
Released 18th June 2025.
|
||||
|
||||
This is a support release for Minecraft: Bedrock Edition 1.21.90.
|
||||
|
||||
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
|
||||
Do not update plugin minimum API versions unless you need new features added in this release.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.21.90.
|
||||
- Removed support for earlier versions.
|
||||
|
||||
## Fixes
|
||||
- Fixed thread crashes sometimes not reporting proper cause information in crashdumps.
|
||||
- Fixed crash when a plugin replaced a player's held tool with a different tool with a damage exceeding the old tool's max damage during an action.
|
||||
- Fixed performance issue of `PlayerAuthInputPacket` input flags handling (broken change detection).
|
||||
- Fixed `BaseInventory->addItem()` triggering updates on empty slots when no items were added.
|
||||
- Fixed slow check in `SubChunk` block layer garbage collection.
|
||||
|
||||
## Internals
|
||||
- `LoginPacketHandler->processLogin()` signature has changed. This will break any plugins overriding `LoginPacketHandler`. As noted above, this is _not_ covered by the API version guarantee.
|
||||
- Automated branch sync for `minor-next` and `major-next` is now triggered by `repository_dispatch` from a cron job in this repository instead of `RestrictedActions`. The `RestrictedActions` cron job was getting automatically disabled by GitHub due to repo inactivity.
|
73
changelogs/5.30.md
Normal file
73
changelogs/5.30.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 5.30.0
|
||||
Released 18th June 2025.
|
||||
|
||||
This is a minor feature release containing API additions, internals cleanup and user experience improvements.
|
||||
|
||||
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
|
||||
Do not update plugin minimum API versions unless you need new features added in this release.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
## General
|
||||
- Significantly reduced log spam when unknown blocks, tiles and entities are found in saved worlds.
|
||||
- The file name structure for crashdumps has been changed to improve sorting order in file browsers.
|
||||
- Buffering is now skipped on the RakLib layer. In theory this could reduce player network latency by 10 ms (YMMV).
|
||||
|
||||
## Gameplay
|
||||
### Blocks
|
||||
- Many blocks have had their hardness and blast resistance updated to match vanilla.
|
||||
- Implemented Respawn Anchor.
|
||||
- Melon Stem and Pumpkin Stem drop amounts should now match vanilla (using binomial distribution).
|
||||
|
||||
## API
|
||||
## General
|
||||
- Verification of save registration has been added for blocks, entities and tiles. This is intended to make it easier to find mistakes when registering custom things, which previously would produce obscure core crashes.
|
||||
|
||||
### `pocketmine\event\block`
|
||||
- The following classes have been added:
|
||||
- `BlockPreExplodeEvent` - called before a block tries to explode
|
||||
- `BlockExplodeEvent` - called when after a block's explosion calculation has been done, but before any changes are applied
|
||||
|
||||
### `pocketmine\event\entity`
|
||||
- The following classes have been added:
|
||||
- `EntityExtinguishEvent` - called when a burning entity is extinguished by water or other sources
|
||||
- `EntityFrostWalkerEvent` - called every tick upon which an entity wearing Frost Walker boots moves; this can be used to customise or cancel the behaviour of the Frost Walker enchantment
|
||||
|
||||
### `pocketmine\event\player`
|
||||
- The following classes have been added:
|
||||
- `PlayerRespawnAnchorUseEvent` - called when a player interacts with a charged respawn anchor
|
||||
|
||||
### `pocketmine\entity`
|
||||
- The following methods have been added:
|
||||
- `public Entity->getStepHeight() : float`
|
||||
- `public Entity->setStepHeight(float $stepHeight) : void`
|
||||
|
||||
### `pocketmine\world\generator`
|
||||
- Generator execution has been decoupled from `PopulationTask` and async tasks in general. The following classes have been added:
|
||||
- `executor\GeneratorExecutor`
|
||||
- `executor\SyncGeneratorExecutor` - runs a generator on the main thread (used for flat world generation, which doesn't need threads)
|
||||
- `executor\AsyncGeneratorExecutor` - runs a generator inside an async task, as before
|
||||
- `PopulationUtils` - contains population business logic previously baked into `PopulationTask` - this permits the reuse of that logic outside async tasks
|
||||
- The following methods have signature changes:
|
||||
- `GeneratorManager->addGenerator()` now accepts an optional `bool $fast` parameter, defaulting to `false`; setting this to `true` will cause your generator to run on the main thread
|
||||
- The following methods have been added:
|
||||
- `public GeneratorManagerEntry->isFast() : bool` - returns whether this generator should run on the main thread
|
||||
- `PopulationTask` has been marked as `@internal`. In the next major version, it will move to the `generator\executor` namespace; however, for now it stays put because plugins currently have no other way to regenerate chunks.
|
||||
|
||||
## Internals
|
||||
- World data version numbers have been consolidated in `pocketmine\data\bedrock\WorldDataVersions`. This removes the need to modify several different files to support new world versions, and reduces the chances of things getting missed.
|
||||
- Block hardness and blast resistance is now unit-tested against `block_properties_table.json` in `BedrockData`. This file comes from vanilla BDS, so we can use it to verify compliance.
|
||||
- Protocol-layer "server auth block breaking" has been enabled. Functionally, this is no different from the previous system, it just works differently on the network layer.
|
||||
- Various internal classes in the `pocketmine\world\generator` namespace have been moved to the `generator\executor` namespace.
|
||||
- Removed `World->registerGenerator()` and `World->unregisterGenerator()`.
|
||||
- Removed redundant calls to `curl_close()` (obsolete since PHP 8.0).
|
||||
|
||||
# 5.30.1
|
||||
Released 23rd June 2025.
|
||||
|
||||
## Fixes
|
||||
- Fixed accidental break of backwards compatibility in `EntityExplodeEvent` introduced in the previous release.
|
||||
- Fixed placement of player holding block when exploding respawn anchor.
|
||||
- Updated BedrockProtocol to fix incorrect encoding of `ServerScriptDebugDrawerPacket`.
|
||||
- Disabled client-side locator bar, allowing plugins to write their own implementations.
|
14
changelogs/5.31.md
Normal file
14
changelogs/5.31.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 5.31.0
|
||||
Released 8th July 2025.
|
||||
|
||||
This is a support release for Minecraft: Bedrock Edition 1.21.93.
|
||||
|
||||
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
|
||||
Do not update plugin minimum API versions unless you need new features added in this release.
|
||||
|
||||
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
|
||||
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.21.93.
|
||||
- Removed support for earlier versions.
|
@ -34,9 +34,9 @@
|
||||
"adhocore/json-comment": "~1.2.0",
|
||||
"netresearch/jsonmapper": "~v5.0.0",
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
|
||||
"pocketmine/bedrock-data": "~5.0.0+bedrock-1.21.80",
|
||||
"pocketmine/bedrock-data": "~5.2.0+bedrock-1.21.93",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50",
|
||||
"pocketmine/bedrock-protocol": "~38.1.0+bedrock-1.21.80",
|
||||
"pocketmine/bedrock-protocol": "~39.1.0+bedrock-1.21.93",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/color": "^0.3.0",
|
||||
|
50
composer.lock
generated
50
composer.lock
generated
@ -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": "7c3052613e98e566d8b00ae3c9119057",
|
||||
"content-hash": "679ab8fc31e55b5170daa34258dc0fd4",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@ -204,16 +204,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-data",
|
||||
"version": "5.0.0+bedrock-1.21.80",
|
||||
"version": "5.2.0+bedrock-1.21.93",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BedrockData.git",
|
||||
"reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080"
|
||||
"reference": "740e18e490c6a102b774518ff2224a06762bcaf8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/e38d5ea19f794ec5216e5f96742237e8c4e7f080",
|
||||
"reference": "e38d5ea19f794ec5216e5f96742237e8c4e7f080",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/740e18e490c6a102b774518ff2224a06762bcaf8",
|
||||
"reference": "740e18e490c6a102b774518ff2224a06762bcaf8",
|
||||
"shasum": ""
|
||||
},
|
||||
"type": "library",
|
||||
@ -224,9 +224,9 @@
|
||||
"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.80"
|
||||
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.93"
|
||||
},
|
||||
"time": "2025-05-09T14:15:18+00:00"
|
||||
"time": "2025-07-08T12:30:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-item-upgrade-schema",
|
||||
@ -256,16 +256,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-protocol",
|
||||
"version": "38.1.0+bedrock-1.21.80",
|
||||
"version": "39.1.0+bedrock-1.21.93",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BedrockProtocol.git",
|
||||
"reference": "a1fa215563517050045309bb779a67f75843b867"
|
||||
"reference": "e9bc5fb691d18dab229a158462c13f0c6fea79c8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/a1fa215563517050045309bb779a67f75843b867",
|
||||
"reference": "a1fa215563517050045309bb779a67f75843b867",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/e9bc5fb691d18dab229a158462c13f0c6fea79c8",
|
||||
"reference": "e9bc5fb691d18dab229a158462c13f0c6fea79c8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -296,9 +296,9 @@
|
||||
"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/38.1.0+bedrock-1.21.80"
|
||||
"source": "https://github.com/pmmp/BedrockProtocol/tree/39.1.0+bedrock-1.21.93"
|
||||
},
|
||||
"time": "2025-05-28T22:19:59+00:00"
|
||||
"time": "2025-07-08T12:31:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
@ -1312,16 +1312,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a"
|
||||
"reference": "1f1358da2f8e1317478c63c21beb9918c9821f6f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/3e139cbe67fafa3588e1dbe27ca50f31fdb6236a",
|
||||
"reference": "3e139cbe67fafa3588e1dbe27ca50f31fdb6236a",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/1f1358da2f8e1317478c63c21beb9918c9821f6f",
|
||||
"reference": "1f1358da2f8e1317478c63c21beb9918c9821f6f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1354,9 +1354,9 @@
|
||||
"description": "Extra strict and opinionated rules for PHPStan",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.4"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.5"
|
||||
},
|
||||
"time": "2025-03-18T11:42:40+00:00"
|
||||
"time": "2025-07-17T12:01:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
@ -1681,16 +1681,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "10.5.46",
|
||||
"version": "10.5.47",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "8080be387a5be380dda48c6f41cee4a13aadab3d"
|
||||
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d",
|
||||
"reference": "8080be387a5be380dda48c6f41cee4a13aadab3d",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
|
||||
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1762,7 +1762,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.47"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -1786,7 +1786,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-05-02T06:46:24+00:00"
|
||||
"time": "2025-06-20T11:29:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
@ -39,6 +39,7 @@ use pocketmine\crash\CrashDumpRenderer;
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\entity\EntityDataHelper;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\event\AsyncHandlerListManager;
|
||||
use pocketmine\event\HandlerListManager;
|
||||
use pocketmine\event\player\PlayerCreationEvent;
|
||||
use pocketmine\event\player\PlayerDataSaveEvent;
|
||||
@ -1527,6 +1528,7 @@ class Server{
|
||||
|
||||
$this->logger->debug("Removing event handlers");
|
||||
HandlerListManager::global()->unregisterAll();
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
if(isset($this->asyncPool)){
|
||||
$this->logger->debug("Shutting down async task worker pool");
|
||||
|
@ -31,7 +31,7 @@ use function str_repeat;
|
||||
|
||||
final class VersionInfo{
|
||||
public const NAME = "PocketMine-MP";
|
||||
public const BASE_VERSION = "5.28.3";
|
||||
public const BASE_VERSION = "5.31.1";
|
||||
public const IS_DEVELOPMENT_BUILD = true;
|
||||
public const BUILD_CHANNEL = "stable";
|
||||
|
||||
|
@ -85,7 +85,7 @@ final class RespawnAnchor extends Opaque{
|
||||
switch($ev->getAction()){
|
||||
case PlayerRespawnAnchorUseEvent::ACTION_EXPLODE:
|
||||
$this->explode($player);
|
||||
return false;
|
||||
return true;
|
||||
|
||||
case PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN:
|
||||
if($player->getSpawn() !== null && $player->getSpawn()->equals($this->position)){
|
||||
|
@ -123,7 +123,7 @@ abstract class Command{
|
||||
}
|
||||
|
||||
if($this->permissionMessage === null){
|
||||
$target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->baseTextFormat(TextFormat::RED));
|
||||
$target->sendMessage(KnownTranslationFactory::pocketmine_command_error_permission($this->name)->prefix(TextFormat::RED));
|
||||
}elseif($this->permissionMessage !== ""){
|
||||
$target->sendMessage(str_replace("<permission>", $permission ?? implode(";", $this->permission), $this->permissionMessage));
|
||||
}
|
||||
@ -237,7 +237,7 @@ abstract class Command{
|
||||
public static function broadcastCommandMessage(CommandSender $source, Translatable|string $message, bool $sendToSource = true) : void{
|
||||
$users = $source->getServer()->getBroadcastChannelSubscribers(Server::BROADCAST_CHANNEL_ADMINISTRATIVE);
|
||||
$result = KnownTranslationFactory::chat_type_admin($source->getName(), $message);
|
||||
$colored = $result->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC);
|
||||
$colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC);
|
||||
|
||||
if($sendToSource){
|
||||
$source->sendMessage($message);
|
||||
|
@ -107,7 +107,7 @@ class FormattedCommandAlias extends Command{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}else{
|
||||
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->baseTextFormat(TextFormat::RED)));
|
||||
$sender->sendMessage($sender->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_notFound($commandLabel, "/help")->prefix(TextFormat::RED)));
|
||||
|
||||
//to match the behaviour of SimpleCommandMap::dispatch()
|
||||
//this shouldn't normally happen, but might happen if the command was unregistered or modified after
|
||||
|
@ -226,7 +226,7 @@ class SimpleCommandMap implements CommandMap{
|
||||
return true;
|
||||
}
|
||||
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($sentCommandLabel ?? "", "/help")->prefix(TextFormat::RED));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ class ClearCommand extends VanillaCommand{
|
||||
}
|
||||
}catch(LegacyStringToItemParserException $e){
|
||||
//vanilla checks this at argument parsing layer, can't come up with a better alternative
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,7 @@ class ClearCommand extends VanillaCommand{
|
||||
if($count > 0){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_clear_testing($target->getName(), (string) $count));
|
||||
}else{
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED));
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -132,7 +132,7 @@ class ClearCommand extends VanillaCommand{
|
||||
if($clearedCount > 0){
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_clear_success($target->getName(), (string) $clearedCount));
|
||||
}else{
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_clear_failure_no_items($target->getName())->prefix(TextFormat::RED));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -68,7 +68,7 @@ class EffectCommand extends VanillaCommand{
|
||||
|
||||
$effect = StringToEffectParser::getInstance()->parse($args[1]);
|
||||
if($effect === null){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_effect_notFound($args[1])->prefix(TextFormat::RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class GiveCommand extends VanillaCommand{
|
||||
try{
|
||||
$item = StringToItemParser::getInstance()->parse($args[1]) ?? LegacyStringToItemParser::getInstance()->parse($args[1]);
|
||||
}catch(LegacyStringToItemParserException $e){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_give_item_notFound($args[1])->prefix(TextFormat::RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ class GiveCommand extends VanillaCommand{
|
||||
$player->getInventory()->addItem($item);
|
||||
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_give_success(
|
||||
$item->getName() . TextFormat::RESET . " (" . $args[1] . ")",
|
||||
$item->getName() . " (" . $args[1] . ")",
|
||||
(string) $item->getCount(),
|
||||
$player->getName()
|
||||
));
|
||||
|
@ -105,22 +105,22 @@ class HelpCommand extends VanillaCommand{
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getLabel())
|
||||
->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------"));
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString)
|
||||
->baseTextFormat(TextFormat::GOLD));
|
||||
->prefix(TextFormat::GOLD));
|
||||
|
||||
$usage = $cmd->getUsage();
|
||||
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage;
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX)))
|
||||
->baseTextFormat(TextFormat::GOLD));
|
||||
->prefix(TextFormat::GOLD));
|
||||
|
||||
$aliases = $cmd->getAliases();
|
||||
sort($aliases, SORT_NATURAL);
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases))
|
||||
->baseTextFormat(TextFormat::GOLD));
|
||||
->prefix(TextFormat::GOLD));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_notFound($commandName, "/help")->prefix(TextFormat::RED));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ class ParticleCommand extends VanillaCommand{
|
||||
$particle = $this->getParticle($name, $data);
|
||||
|
||||
if($particle === null){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_particle_notFound($name)->prefix(TextFormat::RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ class SayCommand extends VanillaCommand{
|
||||
$sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_announcement(
|
||||
$sender instanceof Player ? $sender->getDisplayName() : ($sender instanceof ConsoleCommandSender ? "Server" : $sender->getName()),
|
||||
implode(" ", $args)
|
||||
)->baseTextFormat(TextFormat::LIGHT_PURPLE));
|
||||
)->prefix(TextFormat::LIGHT_PURPLE));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -60,9 +60,9 @@ class TellCommand extends VanillaCommand{
|
||||
|
||||
if($player instanceof Player){
|
||||
$message = implode(" ", $args);
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC));
|
||||
$name = $sender instanceof Player ? $sender->getDisplayName() : $sender->getName();
|
||||
$player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->baseTextFormat(TextFormat::GRAY . TextFormat::ITALIC));
|
||||
$player->sendMessage(KnownTranslationFactory::commands_message_display_incoming($name, $message)->prefix(TextFormat::GRAY . TextFormat::ITALIC));
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_message_display_outgoing($player->getDisplayName(), $message), false);
|
||||
}else{
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound());
|
||||
|
@ -99,11 +99,11 @@ abstract class VanillaCommand extends Command{
|
||||
|
||||
$v = (int) $input;
|
||||
if($v > $max){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooBig($input, (string) $max)->prefix(TextFormat::RED));
|
||||
return null;
|
||||
}
|
||||
if($v < $min){
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->baseTextFormat(TextFormat::RED));
|
||||
$sender->sendMessage(KnownTranslationFactory::commands_generic_num_tooSmall($input, (string) $min)->prefix(TextFormat::RED));
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -54,12 +54,12 @@ final class WorldDataVersions{
|
||||
* This may be lower than the current protocol version if PocketMine-MP does not yet support features of the newer
|
||||
* version. This allows the protocol to be updated independently of world format support.
|
||||
*/
|
||||
public const NETWORK = 800;
|
||||
public const NETWORK = 818;
|
||||
|
||||
public const LAST_OPENED_IN = [
|
||||
1, //major
|
||||
21, //minor
|
||||
80, //patch
|
||||
90, //patch
|
||||
3, //revision
|
||||
0 //is beta
|
||||
];
|
||||
|
@ -372,6 +372,7 @@ final class ItemTypeNames{
|
||||
public const MUSIC_DISC_CREATOR = "minecraft:music_disc_creator";
|
||||
public const MUSIC_DISC_CREATOR_MUSIC_BOX = "minecraft:music_disc_creator_music_box";
|
||||
public const MUSIC_DISC_FAR = "minecraft:music_disc_far";
|
||||
public const MUSIC_DISC_LAVA_CHICKEN = "minecraft:music_disc_lava_chicken";
|
||||
public const MUSIC_DISC_MALL = "minecraft:music_disc_mall";
|
||||
public const MUSIC_DISC_MELLOHI = "minecraft:music_disc_mellohi";
|
||||
public const MUSIC_DISC_OTHERSIDE = "minecraft:music_disc_otherside";
|
||||
@ -380,6 +381,7 @@ final class ItemTypeNames{
|
||||
public const MUSIC_DISC_RELIC = "minecraft:music_disc_relic";
|
||||
public const MUSIC_DISC_STAL = "minecraft:music_disc_stal";
|
||||
public const MUSIC_DISC_STRAD = "minecraft:music_disc_strad";
|
||||
public const MUSIC_DISC_TEARS = "minecraft:music_disc_tears";
|
||||
public const MUSIC_DISC_WAIT = "minecraft:music_disc_wait";
|
||||
public const MUSIC_DISC_WARD = "minecraft:music_disc_ward";
|
||||
public const MUTTON = "minecraft:mutton";
|
||||
|
134
src/event/AsyncEvent.php
Normal file
134
src/event/AsyncEvent.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\Utils;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* This class is used to permit asynchronous event handling.
|
||||
*
|
||||
* When an event is called asynchronously, the event handlers are called by priority level.
|
||||
* When all the promises of a priority level have been resolved, the next priority level is called.
|
||||
*/
|
||||
abstract class AsyncEvent{
|
||||
/** @var array<int, int> $handlersCallState */
|
||||
private static array $handlersCallState = [];
|
||||
private const MAX_CONCURRENT_CALLS = 1000; //max number of concurrent calls to a single handler
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<static>
|
||||
*/
|
||||
final public function call() : Promise{
|
||||
$timings = Timings::getAsyncEventTimings($this);
|
||||
$timings->startTiming();
|
||||
|
||||
try{
|
||||
/** @phpstan-var PromiseResolver<static> $globalResolver */
|
||||
$globalResolver = new PromiseResolver();
|
||||
|
||||
$handlers = AsyncHandlerListManager::global()->getHandlersFor(static::class);
|
||||
if(count($handlers) > 0){
|
||||
$this->processRemainingHandlers($handlers, fn() => $globalResolver->resolve($this), $globalResolver->reject(...));
|
||||
}else{
|
||||
$globalResolver->resolve($this);
|
||||
}
|
||||
|
||||
return $globalResolver->getPromise();
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AsyncRegisteredListener[] $handlers
|
||||
* @phpstan-param list<AsyncRegisteredListener> $handlers
|
||||
* @phpstan-param \Closure() : void $resolve
|
||||
* @phpstan-param \Closure() : void $reject
|
||||
*/
|
||||
private function processRemainingHandlers(array $handlers, \Closure $resolve, \Closure $reject) : void{
|
||||
$currentPriority = null;
|
||||
$awaitPromises = [];
|
||||
foreach($handlers as $k => $handler){
|
||||
$priority = $handler->getPriority();
|
||||
if(count($awaitPromises) > 0 && $currentPriority !== null && $currentPriority !== $priority){
|
||||
//wait for concurrent promises from previous priority to complete
|
||||
break;
|
||||
}
|
||||
|
||||
$currentPriority = $priority;
|
||||
$handlerId = spl_object_id($handler) << 3 | $priority;
|
||||
if(!isset(self::$handlersCallState[$handlerId])){
|
||||
self::$handlersCallState[$handlerId] = 0;
|
||||
}
|
||||
if(self::$handlersCallState[$handlerId] >= self::MAX_CONCURRENT_CALLS){
|
||||
throw new \RuntimeException("Concurrent call limit reached for handler " .
|
||||
Utils::getNiceClosureName($handler->getHandler()) . "(" . Utils::getNiceClassName($this) . ")" .
|
||||
" (max: " . self::MAX_CONCURRENT_CALLS . ")");
|
||||
}
|
||||
$removeCallback = static fn() => --self::$handlersCallState[$handlerId];
|
||||
if($handler->canBeCalledConcurrently()){
|
||||
unset($handlers[$k]);
|
||||
++self::$handlersCallState[$handlerId];
|
||||
$promise = $handler->callAsync($this);
|
||||
if($promise !== null){
|
||||
$promise->onCompletion($removeCallback, $removeCallback);
|
||||
$awaitPromises[] = $promise;
|
||||
}else{
|
||||
$removeCallback();
|
||||
}
|
||||
}else{
|
||||
if(count($awaitPromises) > 0){
|
||||
//wait for concurrent promises to complete
|
||||
break;
|
||||
}
|
||||
|
||||
unset($handlers[$k]);
|
||||
++self::$handlersCallState[$handlerId];
|
||||
$promise = $handler->callAsync($this);
|
||||
if($promise !== null){
|
||||
$promise->onCompletion($removeCallback, $removeCallback);
|
||||
$promise->onCompletion(
|
||||
onSuccess: fn() => $this->processRemainingHandlers($handlers, $resolve, $reject),
|
||||
onFailure: $reject
|
||||
);
|
||||
return;
|
||||
}
|
||||
$removeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
if(count($awaitPromises) > 0){
|
||||
Promise::all($awaitPromises)->onCompletion(
|
||||
onSuccess: fn() => $this->processRemainingHandlers($handlers, $resolve, $reject),
|
||||
onFailure: $reject
|
||||
);
|
||||
}else{
|
||||
$resolve();
|
||||
}
|
||||
}
|
||||
}
|
58
src/event/AsyncHandlerListManager.php
Normal file
58
src/event/AsyncHandlerListManager.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use function uasort;
|
||||
|
||||
/**
|
||||
* @phpstan-extends BaseHandlerListManager<AsyncEvent, AsyncRegisteredListener>
|
||||
*/
|
||||
final class AsyncHandlerListManager extends BaseHandlerListManager{
|
||||
private static ?self $globalInstance = null;
|
||||
|
||||
public static function global() : self{
|
||||
return self::$globalInstance ?? (self::$globalInstance = new self());
|
||||
}
|
||||
|
||||
protected function getBaseEventClass() : string{
|
||||
return AsyncEvent::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array<int, AsyncRegisteredListener> $listeners
|
||||
* @phpstan-return array<int, AsyncRegisteredListener>
|
||||
*/
|
||||
private static function sortSamePriorityHandlers(array $listeners) : array{
|
||||
uasort($listeners, function(AsyncRegisteredListener $left, AsyncRegisteredListener $right) : int{
|
||||
//Promise::all() can be used more efficiently if concurrent handlers are grouped together.
|
||||
//It's not important whether they are grouped before or after exclusive handlers.
|
||||
return $left->canBeCalledConcurrently() <=> $right->canBeCalledConcurrently();
|
||||
});
|
||||
return $listeners;
|
||||
}
|
||||
|
||||
protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList{
|
||||
return new HandlerList($event, $parentList, $handlerCache, self::sortSamePriorityHandlers(...));
|
||||
}
|
||||
}
|
60
src/event/AsyncRegisteredListener.php
Normal file
60
src/event/AsyncRegisteredListener.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
|
||||
class AsyncRegisteredListener extends BaseRegisteredListener{
|
||||
public function __construct(
|
||||
\Closure $handler,
|
||||
int $priority,
|
||||
Plugin $plugin,
|
||||
bool $handleCancelled,
|
||||
private bool $exclusiveCall,
|
||||
TimingsHandler $timings
|
||||
){
|
||||
parent::__construct($handler, $priority, $plugin, $handleCancelled, $timings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<null>|null
|
||||
*/
|
||||
public function callAsync(AsyncEvent $event) : ?Promise{
|
||||
if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){
|
||||
return null;
|
||||
}
|
||||
$this->timings->startTiming();
|
||||
try{
|
||||
return ($this->handler)($event);
|
||||
}finally{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function canBeCalledConcurrently() : bool{
|
||||
return !$this->exclusiveCall;
|
||||
}
|
||||
}
|
156
src/event/BaseHandlerListManager.php
Normal file
156
src/event/BaseHandlerListManager.php
Normal file
@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
/**
|
||||
* @phpstan-template TEvent of Event|AsyncEvent
|
||||
* @phpstan-template TRegisteredListener of BaseRegisteredListener
|
||||
*
|
||||
* @phpstan-type THandlerList HandlerList<TRegisteredListener>
|
||||
*/
|
||||
abstract class BaseHandlerListManager{
|
||||
/**
|
||||
* @var HandlerList[] classname => HandlerList
|
||||
* @phpstan-var array<class-string<covariant TEvent>, THandlerList>
|
||||
*/
|
||||
private array $allLists = [];
|
||||
/**
|
||||
* @var RegisteredListenerCache[] event class name => cache
|
||||
* @phpstan-var array<class-string<TEvent>, RegisteredListenerCache<TRegisteredListener>>
|
||||
*/
|
||||
private array $handlerCaches = [];
|
||||
|
||||
/**
|
||||
* Unregisters all the listeners
|
||||
* If a Plugin or Listener is passed, all the listeners with that object will be removed
|
||||
*
|
||||
* @phpstan-param TRegisteredListener|Plugin|Listener|null $object
|
||||
*/
|
||||
public function unregisterAll(BaseRegisteredListener|Plugin|Listener|null $object = null) : void{
|
||||
if($object !== null){
|
||||
foreach($this->allLists as $h){
|
||||
$h->unregister($object);
|
||||
}
|
||||
}else{
|
||||
foreach($this->allLists as $h){
|
||||
$h->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<TEvent> $class
|
||||
*/
|
||||
private static function isValidClass(\ReflectionClass $class) : bool{
|
||||
$tags = Utils::parseDocComment((string) $class->getDocComment());
|
||||
return !$class->isAbstract() || isset($tags["allowHandle"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<TEvent> $class
|
||||
*
|
||||
* @phpstan-return \ReflectionClass<TEvent>|null
|
||||
*/
|
||||
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
|
||||
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
|
||||
if(self::isValidClass($parent)){
|
||||
return $parent;
|
||||
}
|
||||
//NOOP
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return class-string<TEvent>
|
||||
*/
|
||||
abstract protected function getBaseEventClass() : string;
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
* @phpstan-param HandlerList<TRegisteredListener>|null $parentList
|
||||
* @phpstan-param RegisteredListenerCache<TRegisteredListener> $handlerCache
|
||||
*
|
||||
* @phpstan-return THandlerList
|
||||
*/
|
||||
abstract protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList;
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for listeners that explicitly handle this event.
|
||||
*
|
||||
* Calling this method also lazily initializes the $classMap inheritance tree of handler lists.
|
||||
*
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
* @phpstan-return THandlerList
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getListFor(string $event) : HandlerList{
|
||||
if(isset($this->allLists[$event])){
|
||||
return $this->allLists[$event];
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass($event);
|
||||
if(!$class->isSubclassOf($this->getBaseEventClass())){
|
||||
throw new \InvalidArgumentException("Cannot get sync handler list for async event");
|
||||
}
|
||||
if(!self::isValidClass($class)){
|
||||
throw new \InvalidArgumentException("Event must be non-abstract or have the @allowHandle annotation");
|
||||
}
|
||||
|
||||
$parent = self::resolveNearestHandleableParent($class);
|
||||
/** @phpstan-var RegisteredListenerCache<TRegisteredListener> $cache */
|
||||
$cache = new RegisteredListenerCache();
|
||||
$this->handlerCaches[$event] = $cache;
|
||||
return $this->allLists[$event] = $this->createHandlerList(
|
||||
$event,
|
||||
parentList: $parent !== null ? $this->getListFor($parent->getName()) : null,
|
||||
handlerCache: $cache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant TEvent> $event
|
||||
*
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<TRegisteredListener>
|
||||
*/
|
||||
public function getHandlersFor(string $event) : array{
|
||||
$cache = $this->handlerCaches[$event] ?? null;
|
||||
//getListFor() will populate the cache for the next call
|
||||
return $cache?->list ?? $this->getListFor($event)->getListenerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HandlerList[]
|
||||
* @phpstan-return array<class-string<covariant TEvent>, THandlerList>
|
||||
*/
|
||||
public function getAll() : array{
|
||||
return $this->allLists;
|
||||
}
|
||||
}
|
58
src/event/BaseRegisteredListener.php
Normal file
58
src/event/BaseRegisteredListener.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use function in_array;
|
||||
|
||||
abstract class BaseRegisteredListener{
|
||||
public function __construct(
|
||||
protected \Closure $handler,
|
||||
private int $priority,
|
||||
private Plugin $plugin,
|
||||
private bool $handleCancelled,
|
||||
protected TimingsHandler $timings
|
||||
){
|
||||
if(!in_array($priority, EventPriority::ALL, true)){
|
||||
throw new \InvalidArgumentException("Invalid priority number $priority");
|
||||
}
|
||||
}
|
||||
|
||||
public function getHandler() : \Closure{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function getPlugin() : Plugin{
|
||||
return $this->plugin;
|
||||
}
|
||||
|
||||
public function getPriority() : int{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
return $this->handleCancelled;
|
||||
}
|
||||
}
|
@ -29,23 +29,33 @@ use function krsort;
|
||||
use function spl_object_id;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
/**
|
||||
* @phpstan-template TListener of BaseRegisteredListener
|
||||
*/
|
||||
class HandlerList{
|
||||
/**
|
||||
* @var RegisteredListener[][]
|
||||
* @phpstan-var array<int, array<int, RegisteredListener>>
|
||||
* @var BaseRegisteredListener[][]
|
||||
* @phpstan-var array<int, array<int, TListener>>
|
||||
*/
|
||||
private array $handlerSlots = [];
|
||||
|
||||
/** @var RegisteredListenerCache[] */
|
||||
/**
|
||||
* @var RegisteredListenerCache[]
|
||||
* @phpstan-var array<int, RegisteredListenerCache<TListener>>
|
||||
*/
|
||||
private array $affectedHandlerCaches = [];
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $class
|
||||
* @phpstan-param class-string $class
|
||||
* @phpstan-param ?static<TListener> $parentList
|
||||
* @phpstan-param RegisteredListenerCache<TListener> $handlerCache
|
||||
* @phpstan-param ?\Closure(array<int, TListener>) : array<int, TListener> $sortSamePriorityHandlers
|
||||
*/
|
||||
public function __construct(
|
||||
private string $class,
|
||||
private ?HandlerList $parentList,
|
||||
private RegisteredListenerCache $handlerCache = new RegisteredListenerCache()
|
||||
private RegisteredListenerCache $handlerCache = new RegisteredListenerCache(),
|
||||
private ?\Closure $sortSamePriorityHandlers = null
|
||||
){
|
||||
for($list = $this; $list !== null; $list = $list->parentList){
|
||||
$list->affectedHandlerCaches[spl_object_id($this->handlerCache)] = $this->handlerCache;
|
||||
@ -53,9 +63,9 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @phpstan-param TListener $listener
|
||||
*/
|
||||
public function register(RegisteredListener $listener) : void{
|
||||
public function register(BaseRegisteredListener $listener) : void{
|
||||
if(isset($this->handlerSlots[$listener->getPriority()][spl_object_id($listener)])){
|
||||
throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
|
||||
}
|
||||
@ -64,7 +74,8 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RegisteredListener[] $listeners
|
||||
* @param BaseRegisteredListener[] $listeners
|
||||
* @phpstan-param array<TListener> $listeners
|
||||
*/
|
||||
public function registerAll(array $listeners) : void{
|
||||
foreach($listeners as $listener){
|
||||
@ -73,7 +84,10 @@ class HandlerList{
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
public function unregister(RegisteredListener|Plugin|Listener $object) : void{
|
||||
/**
|
||||
* @phpstan-param TListener|Plugin|Listener $object
|
||||
*/
|
||||
public function unregister(BaseRegisteredListener|Plugin|Listener $object) : void{
|
||||
if($object instanceof Plugin || $object instanceof Listener){
|
||||
foreach($this->handlerSlots as $priority => $list){
|
||||
foreach($list as $hash => $listener){
|
||||
@ -96,12 +110,16 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @return BaseRegisteredListener[]
|
||||
* @phpstan-return array<int, TListener>
|
||||
*/
|
||||
public function getListenersByPriority(int $priority) : array{
|
||||
return $this->handlerSlots[$priority] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return static<TListener>
|
||||
*/
|
||||
public function getParent() : ?HandlerList{
|
||||
return $this->parentList;
|
||||
}
|
||||
@ -116,8 +134,8 @@ class HandlerList{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<RegisteredListener>
|
||||
* @return BaseRegisteredListener[]
|
||||
* @phpstan-return list<TListener>
|
||||
*/
|
||||
public function getListenerList() : array{
|
||||
if($this->handlerCache->list !== null){
|
||||
@ -132,7 +150,12 @@ class HandlerList{
|
||||
$listenersByPriority = [];
|
||||
foreach($handlerLists as $currentList){
|
||||
foreach($currentList->handlerSlots as $priority => $listeners){
|
||||
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
|
||||
$listenersByPriority[$priority] = array_merge(
|
||||
$listenersByPriority[$priority] ?? [],
|
||||
$this->sortSamePriorityHandlers !== null ?
|
||||
($this->sortSamePriorityHandlers)($listeners) :
|
||||
$listeners
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,109 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
class HandlerListManager{
|
||||
|
||||
/**
|
||||
* @phpstan-extends BaseHandlerListManager<Event, RegisteredListener>
|
||||
*/
|
||||
class HandlerListManager extends BaseHandlerListManager{
|
||||
private static ?self $globalInstance = null;
|
||||
|
||||
public static function global() : self{
|
||||
return self::$globalInstance ?? (self::$globalInstance = new self());
|
||||
}
|
||||
|
||||
/** @var HandlerList[] classname => HandlerList */
|
||||
private array $allLists = [];
|
||||
/**
|
||||
* @var RegisteredListenerCache[] event class name => cache
|
||||
* @phpstan-var array<class-string<Event>, RegisteredListenerCache>
|
||||
*/
|
||||
private array $handlerCaches = [];
|
||||
|
||||
/**
|
||||
* Unregisters all the listeners
|
||||
* If a Plugin or Listener is passed, all the listeners with that object will be removed
|
||||
*/
|
||||
public function unregisterAll(RegisteredListener|Plugin|Listener|null $object = null) : void{
|
||||
if($object instanceof Listener || $object instanceof Plugin || $object instanceof RegisteredListener){
|
||||
foreach($this->allLists as $h){
|
||||
$h->unregister($object);
|
||||
}
|
||||
}else{
|
||||
foreach($this->allLists as $h){
|
||||
$h->clear();
|
||||
}
|
||||
}
|
||||
protected function getBaseEventClass() : string{
|
||||
return Event::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<Event> $class
|
||||
*/
|
||||
private static function isValidClass(\ReflectionClass $class) : bool{
|
||||
$tags = Utils::parseDocComment((string) $class->getDocComment());
|
||||
return !$class->isAbstract() || isset($tags["allowHandle"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param \ReflectionClass<Event> $class
|
||||
*
|
||||
* @phpstan-return \ReflectionClass<Event>|null
|
||||
*/
|
||||
private static function resolveNearestHandleableParent(\ReflectionClass $class) : ?\ReflectionClass{
|
||||
for($parent = $class->getParentClass(); $parent !== false; $parent = $parent->getParentClass()){
|
||||
if(self::isValidClass($parent)){
|
||||
return $parent;
|
||||
}
|
||||
//NOOP
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HandlerList for listeners that explicitly handle this event.
|
||||
*
|
||||
* Calling this method also lazily initializes the $classMap inheritance tree of handler lists.
|
||||
*
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getListFor(string $event) : HandlerList{
|
||||
if(isset($this->allLists[$event])){
|
||||
return $this->allLists[$event];
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass($event);
|
||||
if(!self::isValidClass($class)){
|
||||
throw new \InvalidArgumentException("Event must be non-abstract or have the @allowHandle annotation");
|
||||
}
|
||||
|
||||
$parent = self::resolveNearestHandleableParent($class);
|
||||
$cache = new RegisteredListenerCache();
|
||||
$this->handlerCaches[$event] = $cache;
|
||||
return $this->allLists[$event] = new HandlerList(
|
||||
$event,
|
||||
parentList: $parent !== null ? $this->getListFor($parent->getName()) : null,
|
||||
handlerCache: $cache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
*
|
||||
* @return RegisteredListener[]
|
||||
*/
|
||||
public function getHandlersFor(string $event) : array{
|
||||
$cache = $this->handlerCaches[$event] ?? null;
|
||||
//getListFor() will populate the cache for the next call
|
||||
return $cache->list ?? $this->getListFor($event)->getListenerList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HandlerList[]
|
||||
*/
|
||||
public function getAll() : array{
|
||||
return $this->allLists;
|
||||
protected function createHandlerList(string $event, ?HandlerList $parentList, RegisteredListenerCache $handlerCache) : HandlerList{
|
||||
return new HandlerList($event, $parentList, $handlerCache);
|
||||
}
|
||||
}
|
||||
|
@ -31,4 +31,5 @@ final class ListenerMethodTags{
|
||||
public const HANDLE_CANCELLED = "handleCancelled";
|
||||
public const NOT_HANDLER = "notHandler";
|
||||
public const PRIORITY = "priority";
|
||||
public const EXCLUSIVE_CALL = "exclusiveCall";
|
||||
}
|
||||
|
@ -23,34 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use function in_array;
|
||||
|
||||
class RegisteredListener{
|
||||
public function __construct(
|
||||
private \Closure $handler,
|
||||
private int $priority,
|
||||
private Plugin $plugin,
|
||||
private bool $handleCancelled,
|
||||
private TimingsHandler $timings
|
||||
){
|
||||
if(!in_array($priority, EventPriority::ALL, true)){
|
||||
throw new \InvalidArgumentException("Invalid priority number $priority");
|
||||
}
|
||||
}
|
||||
|
||||
public function getHandler() : \Closure{
|
||||
return $this->handler;
|
||||
}
|
||||
|
||||
public function getPlugin() : Plugin{
|
||||
return $this->plugin;
|
||||
}
|
||||
|
||||
public function getPriority() : int{
|
||||
return $this->priority;
|
||||
}
|
||||
class RegisteredListener extends BaseRegisteredListener{
|
||||
|
||||
public function callEvent(Event $event) : void{
|
||||
if($event instanceof Cancellable && $event->isCancelled() && !$this->isHandlingCancelled()){
|
||||
@ -63,8 +36,4 @@ class RegisteredListener{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
return $this->handleCancelled;
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,14 @@ namespace pocketmine\event;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @phpstan-template TListener
|
||||
*/
|
||||
final class RegisteredListenerCache{
|
||||
|
||||
/**
|
||||
* List of all handlers that will be called for a particular event, ordered by execution order.
|
||||
*
|
||||
* @var RegisteredListener[]
|
||||
* @phpstan-var list<RegisteredListener>
|
||||
* @phpstan-var list<TListener>
|
||||
*/
|
||||
public ?array $list = null;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
|
||||
protected Position $position,
|
||||
protected array $blocks,
|
||||
protected float $yield,
|
||||
private array $ignitions
|
||||
private array $ignitions = []
|
||||
){
|
||||
$this->entity = $entity;
|
||||
if($yield < 0.0 || $yield > 100.0){
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\lang;
|
||||
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function array_filter;
|
||||
@ -142,26 +141,18 @@ class Language{
|
||||
/**
|
||||
* @param (float|int|string|Translatable)[] $params
|
||||
*/
|
||||
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null, string $baseFormat = "") : string{
|
||||
if($onlyPrefix !== null && !str_starts_with($str, $onlyPrefix)){
|
||||
//plain key for client-side translation
|
||||
//% is added here if we add base format since this will turn into an embedded key
|
||||
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, "%" . $str) : $str;
|
||||
}
|
||||
$baseText = $this->internalGet($str);
|
||||
if($baseText === null){ //key not found, embedded inside format string with %, or doesn't match prefix
|
||||
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null) : string{
|
||||
$baseText = ($onlyPrefix === null || str_starts_with($str, $onlyPrefix)) ? $this->internalGet($str) : null;
|
||||
if($baseText === null){ //key not found, embedded inside format string, or doesn't match prefix
|
||||
$baseText = $this->parseTranslation($str, $onlyPrefix);
|
||||
}
|
||||
|
||||
foreach(Utils::promoteKeys($params) as $i => $p){
|
||||
$replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p;
|
||||
if($baseFormat !== ""){
|
||||
$replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET;
|
||||
}
|
||||
$baseText = str_replace("{%$i}", $replacement, $baseText);
|
||||
}
|
||||
|
||||
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText;
|
||||
return $baseText;
|
||||
}
|
||||
|
||||
public function translate(Translatable $c) : string{
|
||||
@ -170,17 +161,12 @@ class Language{
|
||||
$baseText = $this->parseTranslation($c->getText());
|
||||
}
|
||||
|
||||
$baseFormat = $c->getBaseFormat();
|
||||
|
||||
foreach(Utils::promoteKeys($c->getParameters()) as $i => $p){
|
||||
$replacement = $p instanceof Translatable ? $this->translate($p) : $p;
|
||||
if($baseFormat !== ""){
|
||||
$replacement = TextFormat::addBase($baseFormat, $replacement) . TextFormat::RESET;
|
||||
}
|
||||
$baseText = str_replace("{%$i}", $replacement, $baseText);
|
||||
}
|
||||
|
||||
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $baseText) : $baseText;
|
||||
return $baseText;
|
||||
}
|
||||
|
||||
protected function internalGet(string $id) : ?string{
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\lang;
|
||||
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
|
||||
final class Translatable{
|
||||
@ -35,8 +34,7 @@ final class Translatable{
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $text,
|
||||
array $params = [],
|
||||
private string $baseFormat = ""
|
||||
array $params = []
|
||||
){
|
||||
foreach(Utils::promoteKeys($params) as $k => $param){
|
||||
if(!($param instanceof Translatable)){
|
||||
@ -62,8 +60,6 @@ final class Translatable{
|
||||
return $this->params[$i] ?? null;
|
||||
}
|
||||
|
||||
public function getBaseFormat() : string{ return $this->baseFormat; }
|
||||
|
||||
public function format(string $before, string $after) : self{
|
||||
return new self("$before%$this->text$after", $this->params);
|
||||
}
|
||||
@ -75,12 +71,4 @@ final class Translatable{
|
||||
public function postfix(string $postfix) : self{
|
||||
return new self("%$this->text" . $postfix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base format to be applied to the translation result by {@link TextFormat::addBase()}.
|
||||
* Any existing base format is overwritten.
|
||||
*/
|
||||
public function baseTextFormat(string $baseFormat) : self{
|
||||
return new self($this->text, $this->params, $baseFormat);
|
||||
}
|
||||
}
|
||||
|
@ -767,7 +767,7 @@ class NetworkSession{
|
||||
$errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
|
||||
|
||||
$this->disconnect(
|
||||
reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->baseTextFormat(TextFormat::RED),
|
||||
reason: KnownTranslationFactory::pocketmine_disconnect_error($reason, $errorId)->prefix(TextFormat::RED),
|
||||
disconnectScreenMessage: KnownTranslationFactory::pocketmine_disconnect_error($disconnectScreenMessage ?? $reason, $errorId),
|
||||
);
|
||||
}
|
||||
@ -1130,12 +1130,8 @@ class NetworkSession{
|
||||
public function prepareClientTranslatableMessage(Translatable $message) : array{
|
||||
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
|
||||
$language = $this->player->getLanguage();
|
||||
$baseFormat = $message->getBaseFormat();
|
||||
$parameters = array_map(function(string|Translatable $p) use ($baseFormat, $language){
|
||||
$string = $p instanceof Translatable ? $language->translate($p) : $p;
|
||||
return $baseFormat !== "" ? TextFormat::addBase($baseFormat, $string) . TextFormat::RESET : $string;
|
||||
}, $message->getParameters());
|
||||
return [$language->translateString($message->getText(), $parameters, "pocketmine.", $baseFormat), $parameters];
|
||||
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
|
||||
return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters];
|
||||
}
|
||||
|
||||
public function onChatMessage(Translatable|string $message) : void{
|
||||
|
@ -33,6 +33,8 @@ use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\LoginPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
|
||||
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
|
||||
@ -42,7 +44,11 @@ use pocketmine\player\PlayerInfo;
|
||||
use pocketmine\player\XboxLivePlayerInfo;
|
||||
use pocketmine\Server;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function gettype;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function json_decode;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
* Handles the initial login phase of the session. This handler is used as the initial state.
|
||||
@ -60,7 +66,9 @@ class LoginPacketHandler extends PacketHandler{
|
||||
){}
|
||||
|
||||
public function handleLogin(LoginPacket $packet) : bool{
|
||||
$extraData = $this->fetchAuthData($packet->chainDataJwt);
|
||||
$authInfo = $this->parseAuthInfo($packet->authInfoJson);
|
||||
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
|
||||
$extraData = $this->fetchAuthData($jwtChain);
|
||||
|
||||
if(!Player::isValidUserName($extraData->displayName)){
|
||||
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
|
||||
@ -139,11 +147,61 @@ class LoginPacketHandler extends PacketHandler{
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->processLogin($packet, $ev->isAuthRequired());
|
||||
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
protected function parseAuthInfo(string $authInfo) : AuthenticationInfo{
|
||||
try{
|
||||
$authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!is_object($authInfoJson)){
|
||||
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
try{
|
||||
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
return $clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
|
||||
try{
|
||||
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!is_object($jwtChainJson)){
|
||||
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
try{
|
||||
$clientData = $mapper->map($jwtChainJson, new JwtChain());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
return $clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
@ -211,10 +269,15 @@ class LoginPacketHandler extends PacketHandler{
|
||||
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication.
|
||||
* In the future this won't be necessary.
|
||||
*
|
||||
* @param null|string[] $legacyCertificate
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function processLogin(LoginPacket $packet, bool $authRequired) : void{
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback));
|
||||
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{
|
||||
if($legacyCertificate === null){
|
||||
throw new PacketHandlingException("Legacy certificate cannot be null");
|
||||
}
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
|
||||
$this->session->setHandler(null); //drop packets received during login verification
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ use pocketmine\network\mcpe\protocol\types\Experiments;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSettings;
|
||||
use pocketmine\network\mcpe\protocol\types\NetworkPermissions;
|
||||
use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings;
|
||||
use pocketmine\network\mcpe\protocol\types\ServerAuthMovementMode;
|
||||
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\Server;
|
||||
@ -82,7 +81,8 @@ class PreSpawnPacketHandler extends PacketHandler{
|
||||
$levelSettings->lightningLevel = 0;
|
||||
$levelSettings->commandsEnabled = true;
|
||||
$levelSettings->gameRules = [
|
||||
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
|
||||
"naturalregeneration" => new BoolGameRule(false, false), //Hack for client side regeneration
|
||||
"locatorbar" => new BoolGameRule(false, false) //Disable client-side tracking of nearby players
|
||||
];
|
||||
$levelSettings->experiments = new Experiments([], false);
|
||||
|
||||
@ -99,7 +99,7 @@ class PreSpawnPacketHandler extends PacketHandler{
|
||||
$this->server->getMotd(),
|
||||
"",
|
||||
false,
|
||||
new PlayerMovementSettings(ServerAuthMovementMode::SERVER_AUTHORITATIVE_V3, 0, true),
|
||||
new PlayerMovementSettings(0, true),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
|
@ -120,7 +120,8 @@ class ResourcePacksPacketHandler extends PacketHandler{
|
||||
hasAddons: false,
|
||||
hasScripts: false,
|
||||
worldTemplateId: Uuid::fromString(Uuid::NIL),
|
||||
worldTemplateVersion: ""
|
||||
worldTemplateVersion: "",
|
||||
forceDisableVibrantVisuals: true,
|
||||
));
|
||||
$this->session->getLogger()->debug("Waiting for client to accept resource packs");
|
||||
}
|
||||
|
@ -411,7 +411,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
public function getLeaveMessage() : Translatable|string{
|
||||
if($this->spawned){
|
||||
return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW);
|
||||
return KnownTranslationFactory::multiplayer_player_left($this->getDisplayName())->prefix(TextFormat::YELLOW);
|
||||
}
|
||||
|
||||
return "";
|
||||
@ -946,7 +946,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
});
|
||||
|
||||
$ev = new PlayerJoinEvent($this,
|
||||
KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->baseTextFormat(TextFormat::YELLOW)
|
||||
KnownTranslationFactory::multiplayer_player_joined($this->getDisplayName())->prefix(TextFormat::YELLOW)
|
||||
);
|
||||
$ev->call();
|
||||
if($ev->getJoinMessage() !== ""){
|
||||
|
@ -23,6 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\plugin;
|
||||
|
||||
use pocketmine\event\AsyncEvent;
|
||||
use pocketmine\event\AsyncHandlerListManager;
|
||||
use pocketmine\event\AsyncRegisteredListener;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\event\EventPriority;
|
||||
@ -36,6 +39,7 @@ use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\permission\DefaultPermissions;
|
||||
use pocketmine\permission\PermissionManager;
|
||||
use pocketmine\permission\PermissionParser;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
@ -529,6 +533,7 @@ class PluginManager{
|
||||
$plugin->onEnableStateChange(false);
|
||||
$plugin->getScheduler()->shutdown();
|
||||
HandlerListManager::global()->unregisterAll($plugin);
|
||||
AsyncHandlerListManager::global()->unregisterAll($plugin);
|
||||
}
|
||||
}
|
||||
|
||||
@ -582,7 +587,7 @@ class PluginManager{
|
||||
/** @phpstan-var class-string $paramClass */
|
||||
$paramClass = $paramType->getName();
|
||||
$eventClass = new \ReflectionClass($paramClass);
|
||||
if(!$eventClass->isSubclassOf(Event::class)){
|
||||
if(!$eventClass->isSubclassOf(Event::class) && !$eventClass->isSubclassOf(AsyncEvent::class)){
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -636,8 +641,36 @@ class PluginManager{
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED . " value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] . "\"");
|
||||
}
|
||||
}
|
||||
$exclusiveCall = false;
|
||||
if(isset($tags[ListenerMethodTags::EXCLUSIVE_CALL])){
|
||||
if(!is_a($eventClass, AsyncEvent::class, true)){
|
||||
throw new PluginException(sprintf(
|
||||
"Event handler %s() declares @%s for non-async event of type %s",
|
||||
Utils::getNiceClosureName($handlerClosure),
|
||||
ListenerMethodTags::EXCLUSIVE_CALL,
|
||||
$eventClass
|
||||
));
|
||||
}
|
||||
switch(strtolower($tags[ListenerMethodTags::EXCLUSIVE_CALL])){
|
||||
case "true":
|
||||
case "":
|
||||
$exclusiveCall = true;
|
||||
break;
|
||||
case "false":
|
||||
break;
|
||||
default:
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . "() declares invalid @" . ListenerMethodTags::EXCLUSIVE_CALL . " value \"" . $tags[ListenerMethodTags::EXCLUSIVE_CALL] . "\"");
|
||||
}
|
||||
}
|
||||
|
||||
$this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
|
||||
if(is_subclass_of($eventClass, AsyncEvent::class)){
|
||||
if(!$this->canHandleAsyncEvent($handlerClosure)){
|
||||
throw new PluginException("Event handler " . Utils::getNiceClosureName($handlerClosure) . " must return null|Promise<null> to be able to handle async events");
|
||||
}
|
||||
$this->registerAsyncEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled, $exclusiveCall);
|
||||
}else{
|
||||
$this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -672,4 +705,46 @@ class PluginManager{
|
||||
HandlerListManager::global()->getListFor($event)->register($registeredListener);
|
||||
return $registeredListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $event Class name that extends Event and AsyncEvent
|
||||
*
|
||||
* @phpstan-template TEvent of AsyncEvent
|
||||
* @phpstan-param class-string<TEvent> $event
|
||||
* @phpstan-param \Closure(TEvent) : ?Promise<null> $handler
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function registerAsyncEvent(string $event, \Closure $handler, int $priority, Plugin $plugin, bool $handleCancelled = false, bool $exclusiveCall = false) : AsyncRegisteredListener{
|
||||
//TODO: Not loving the code duplication here
|
||||
if(!is_subclass_of($event, AsyncEvent::class)){
|
||||
throw new PluginException($event . " is not an AsyncEvent");
|
||||
}
|
||||
|
||||
$handlerName = Utils::getNiceClosureName($handler);
|
||||
|
||||
if(!$plugin->isEnabled()){
|
||||
throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled");
|
||||
}
|
||||
|
||||
$timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
|
||||
|
||||
$registeredListener = new AsyncRegisteredListener($handler, $priority, $plugin, $handleCancelled, $exclusiveCall, $timings);
|
||||
AsyncHandlerListManager::global()->getListFor($event)->register($registeredListener);
|
||||
return $registeredListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given handler return type is async-compatible (equal to Promise)
|
||||
*
|
||||
* @phpstan-param \Closure(AsyncEvent) : Promise<null> $handler
|
||||
*
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
private function canHandleAsyncEvent(\Closure $handler) : bool{
|
||||
$reflection = new \ReflectionFunction($handler);
|
||||
$return = $reflection->getReturnType();
|
||||
|
||||
return $return instanceof \ReflectionNamedType && $return->getName() === Promise::class;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ namespace pocketmine\timings;
|
||||
|
||||
use pocketmine\block\tile\Tile;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\AsyncEvent;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
||||
@ -116,6 +117,8 @@ abstract class Timings{
|
||||
|
||||
/** @var TimingsHandler[] */
|
||||
private static array $events = [];
|
||||
/** @var TimingsHandler[] */
|
||||
private static array $asyncEvents = [];
|
||||
/** @var TimingsHandler[][] */
|
||||
private static array $eventHandlers = [];
|
||||
|
||||
@ -313,8 +316,18 @@ abstract class Timings{
|
||||
return self::$events[$eventClass];
|
||||
}
|
||||
|
||||
public static function getAsyncEventTimings(AsyncEvent $event) : TimingsHandler{
|
||||
$eventClass = get_class($event);
|
||||
if(!isset(self::$asyncEvents[$eventClass])){
|
||||
self::$asyncEvents[$eventClass] = new TimingsHandler(self::shortenCoreClassName($eventClass, "pocketmine\\event\\"), group: "Events");
|
||||
}
|
||||
|
||||
return self::$asyncEvents[$eventClass];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant Event> $event
|
||||
* @phpstan-template TEvent of Event|AsyncEvent
|
||||
* @phpstan-param class-string<TEvent> $event
|
||||
*/
|
||||
public static function getEventHandlerTimings(string $event, string $handlerName, string $group) : TimingsHandler{
|
||||
if(!isset(self::$eventHandlers[$event][$handlerName])){
|
||||
|
@ -190,10 +190,8 @@ abstract class TextFormat{
|
||||
* - Base format "§c" (red) + "Hello" (no format) = "§r§cHello"
|
||||
* - Base format "§c" + "Hello §rWorld" = "§r§cHello §r§cWorld"
|
||||
*
|
||||
* Note: Adding base formatting to the output string a second time won't override conflicting formatting from the
|
||||
* earlier call (e.g. adding base format BLUE to a string which already has YELLOW base formatting will
|
||||
* still result in yellow text after any RESET code). However, complementary codes (e.g. italic, bold) will combine
|
||||
* with the existing codes (e.g. adding ITALIC to a string with base format YELLOW will give yellow & italic text).
|
||||
* Note: Adding base formatting to the output string a second time will result in a combination of formats from both
|
||||
* calls. This is not by design, but simply a consequence of the way the function is implemented.
|
||||
*/
|
||||
public static function addBase(string $baseFormat, string $string) : string{
|
||||
$baseFormatParts = self::tokenize($baseFormat);
|
||||
|
@ -289,6 +289,12 @@ class World implements ChunkManager{
|
||||
*/
|
||||
private array $chunks = [];
|
||||
|
||||
/**
|
||||
* @var true[]
|
||||
* @phpstan-var array<ChunkPosHash, true>
|
||||
*/
|
||||
private array $knownUngeneratedChunks = [];
|
||||
|
||||
/**
|
||||
* @var Vector3[][] chunkHash => [relativeBlockHash => Vector3]
|
||||
* @phpstan-var array<ChunkPosHash, array<ChunkBlockPosHash, Vector3>>
|
||||
@ -625,6 +631,7 @@ class World implements ChunkManager{
|
||||
self::getXZ($chunkHash, $chunkX, $chunkZ);
|
||||
$this->unloadChunk($chunkX, $chunkZ, false);
|
||||
}
|
||||
$this->knownUngeneratedChunks = [];
|
||||
foreach($this->entitiesByChunk as $chunkHash => $entities){
|
||||
self::getXZ($chunkHash, $chunkX, $chunkZ);
|
||||
|
||||
@ -2625,6 +2632,16 @@ class World implements ChunkManager{
|
||||
}
|
||||
|
||||
public function setChunk(int $chunkX, int $chunkZ, Chunk $chunk) : void{
|
||||
foreach($chunk->getSubChunks() as $subChunk){
|
||||
foreach($subChunk->getBlockLayers() as $blockLayer){
|
||||
foreach($blockLayer->getPalette() as $blockStateId){
|
||||
if(!$this->blockStateRegistry->hasStateId($blockStateId)){
|
||||
throw new \InvalidArgumentException("Provided chunk contains unknown/unregistered blocks (found unknown state ID $blockStateId)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$chunkHash = World::chunkHash($chunkX, $chunkZ);
|
||||
$oldChunk = $this->loadChunk($chunkX, $chunkZ);
|
||||
if($oldChunk !== null && $oldChunk !== $chunk){
|
||||
@ -2657,6 +2674,7 @@ class World implements ChunkManager{
|
||||
}
|
||||
|
||||
$this->chunks[$chunkHash] = $chunk;
|
||||
unset($this->knownUngeneratedChunks[$chunkHash]);
|
||||
|
||||
$this->blockCacheSize -= count($this->blockCache[$chunkHash] ?? []);
|
||||
unset($this->blockCache[$chunkHash]);
|
||||
@ -2921,6 +2939,9 @@ class World implements ChunkManager{
|
||||
if(isset($this->chunks[$chunkHash = World::chunkHash($x, $z)])){
|
||||
return $this->chunks[$chunkHash];
|
||||
}
|
||||
if(isset($this->knownUngeneratedChunks[$chunkHash])){
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->timings->syncChunkLoad->startTiming();
|
||||
|
||||
@ -2940,6 +2961,7 @@ class World implements ChunkManager{
|
||||
|
||||
if($loadedChunkData === null){
|
||||
$this->timings->syncChunkLoad->stopTiming();
|
||||
$this->knownUngeneratedChunks[$chunkHash] = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -134,11 +134,8 @@ class SubChunk{
|
||||
foreach($this->blockLayers as $layer){
|
||||
$layer->collectGarbage();
|
||||
|
||||
foreach($layer->getPalette() as $p){
|
||||
if($p !== $this->emptyBlockId){
|
||||
$cleanedLayers[] = $layer;
|
||||
continue 2;
|
||||
}
|
||||
if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){
|
||||
$cleanedLayers[] = $layer;
|
||||
}
|
||||
}
|
||||
$this->blockLayers = $cleanedLayers;
|
||||
|
@ -4,11 +4,17 @@ parameters:
|
||||
message: '#^Method pocketmine\\event\\RegisteredListener\:\:__construct\(\) has parameter \$handler with no signature specified for Closure\.$#'
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/RegisteredListener.php
|
||||
path: ../../../src/event/AsyncRegisteredListener.php
|
||||
|
||||
-
|
||||
message: '#^Method pocketmine\\event\\RegisteredListener\:\:getHandler\(\) return type has no signature specified for Closure\.$#'
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/RegisteredListener.php
|
||||
path: ../../../src/event/BaseRegisteredListener.php
|
||||
|
||||
-
|
||||
message: "#^Method pocketmine\\\\event\\\\BaseRegisteredListener\\:\\:getHandler\\(\\) return type has no signature specified for Closure\\.$#"
|
||||
identifier: missingType.callable
|
||||
count: 1
|
||||
path: ../../../src/event/BaseRegisteredListener.php
|
||||
|
||||
|
124
tests/phpunit/event/AsyncEventConcurrencyTest.php
Normal file
124
tests/phpunit/event/AsyncEventConcurrencyTest.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use pocketmine\event\fixtures\TestGrandchildAsyncEvent;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\plugin\PluginManager;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\Server;
|
||||
use function count;
|
||||
|
||||
final class AsyncEventConcurrencyTest extends TestCase{
|
||||
|
||||
private Plugin $mockPlugin;
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
//this one gets its own class because it requires a bunch of context variables
|
||||
|
||||
/**
|
||||
* @var PromiseResolver[]
|
||||
* @phpstan-var list<PromiseResolver<null>>
|
||||
*/
|
||||
private array $resolvers = [];
|
||||
|
||||
private bool $activeExclusiveHandler = false;
|
||||
private bool $activeConcurrentHandler = false;
|
||||
|
||||
private int $done = 0;
|
||||
|
||||
protected function setUp() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
//TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field
|
||||
//we really need to make it possible to register events without a Plugin or Server context
|
||||
$mockServer = $this->createMock(Server::class);
|
||||
$this->mockPlugin = self::createStub(Plugin::class);
|
||||
$this->mockPlugin->method('isEnabled')->willReturn(true);
|
||||
$this->pluginManager = new PluginManager($mockServer, null);
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<null>
|
||||
*/
|
||||
private function handler(bool &$flag, string $label) : Promise{
|
||||
$flag = true;
|
||||
$resolver = new PromiseResolver();
|
||||
$this->resolvers[] = $resolver;
|
||||
$resolver->getPromise()->onCompletion(
|
||||
function() use (&$flag) : void{
|
||||
$flag = false;
|
||||
$this->done++;
|
||||
},
|
||||
fn() => self::fail("Not expecting this to be rejected for $label")
|
||||
);
|
||||
return $resolver->getPromise();
|
||||
}
|
||||
|
||||
public function testConcurrency() : void{
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) : Promise{
|
||||
self::assertFalse($this->activeExclusiveHandler, "Concurrent handler can't run while exclusive handlers are waiting to complete");
|
||||
|
||||
return $this->handler($this->activeConcurrentHandler, "concurrent");
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin,
|
||||
//non-exclusive - this must be completed before any exclusive handlers are run (or run after them)
|
||||
);
|
||||
for($i = 0; $i < 2; $i++){
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use ($i) : Promise{
|
||||
self::assertFalse($this->activeExclusiveHandler, "Exclusive handler $i can't run alongside other exclusive handlers");
|
||||
self::assertFalse($this->activeConcurrentHandler, "Exclusive handler $i can't run alongside concurrent handler");
|
||||
|
||||
return $this->handler($this->activeExclusiveHandler, "exclusive $i");
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin,
|
||||
exclusiveCall: true
|
||||
);
|
||||
}
|
||||
|
||||
(new TestGrandchildAsyncEvent())->call();
|
||||
|
||||
while(count($this->resolvers) > 0 && $this->done < 3){
|
||||
foreach($this->resolvers as $k => $resolver){
|
||||
unset($this->resolvers[$k]);
|
||||
//don't clear the array here - resolving this will trigger adding the next resolver
|
||||
$resolver->resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
self::assertSame(3, $this->done, "Expected feedback from exactly 3 handlers");
|
||||
}
|
||||
}
|
129
tests/phpunit/event/AsyncEventTest.php
Normal file
129
tests/phpunit/event/AsyncEventTest.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use pocketmine\event\fixtures\TestChildAsyncEvent;
|
||||
use pocketmine\event\fixtures\TestGrandchildAsyncEvent;
|
||||
use pocketmine\event\fixtures\TestParentAsyncEvent;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\plugin\PluginManager;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\Server;
|
||||
use function shuffle;
|
||||
|
||||
final class AsyncEventTest extends TestCase{
|
||||
private Plugin $mockPlugin;
|
||||
private PluginManager $pluginManager;
|
||||
|
||||
protected function setUp() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
|
||||
//TODO: this is a really bad hack and could break any time if PluginManager decides to access its Server field
|
||||
//we really need to make it possible to register events without a Plugin or Server context
|
||||
$mockServer = $this->createMock(Server::class);
|
||||
$this->mockPlugin = self::createStub(Plugin::class);
|
||||
$this->mockPlugin->method('isEnabled')->willReturn(true);
|
||||
$this->pluginManager = new PluginManager($mockServer, null);
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() : void{
|
||||
AsyncHandlerListManager::global()->unregisterAll();
|
||||
}
|
||||
|
||||
public function testHandlerInheritance() : void{
|
||||
$expectedOrder = [
|
||||
TestGrandchildAsyncEvent::class,
|
||||
TestChildAsyncEvent::class,
|
||||
TestParentAsyncEvent::class
|
||||
];
|
||||
$classes = $expectedOrder;
|
||||
$actualOrder = [];
|
||||
shuffle($classes);
|
||||
foreach($classes as $class){
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
$class,
|
||||
function(AsyncEvent $event) use (&$actualOrder, $class) : ?Promise{
|
||||
$actualOrder[] = $class;
|
||||
return null;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin
|
||||
);
|
||||
}
|
||||
|
||||
$event = new TestGrandchildAsyncEvent();
|
||||
$promise = $event->call();
|
||||
|
||||
$resolved = false;
|
||||
$promise->onCompletion(
|
||||
function() use ($expectedOrder, $actualOrder, &$resolved){
|
||||
self::assertSame($expectedOrder, $actualOrder, "Expected event handlers to be called from most specific to least specific");
|
||||
$resolved = true;
|
||||
},
|
||||
fn() => self::fail("Not expecting this to be rejected")
|
||||
);
|
||||
|
||||
self::assertTrue($resolved, "No promises were used, expected this promise to resolve immediately");
|
||||
}
|
||||
|
||||
public function testPriorityLock() : void{
|
||||
$resolver = null;
|
||||
$firstCompleted = false;
|
||||
$run = 0;
|
||||
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use (&$resolver, &$firstCompleted, &$run) : Promise{
|
||||
$run++;
|
||||
$resolver = new PromiseResolver();
|
||||
|
||||
$resolver->getPromise()->onCompletion(
|
||||
function() use (&$firstCompleted) : void{ $firstCompleted = true; },
|
||||
fn() => self::fail("Not expecting this to be rejected")
|
||||
);
|
||||
|
||||
return $resolver->getPromise();
|
||||
},
|
||||
EventPriority::LOW, //anything below NORMAL is fine
|
||||
$this->mockPlugin
|
||||
);
|
||||
$this->pluginManager->registerAsyncEvent(
|
||||
TestGrandchildAsyncEvent::class,
|
||||
function(TestGrandchildAsyncEvent $event) use (&$firstCompleted, &$run) : ?Promise{
|
||||
$run++;
|
||||
self::assertTrue($firstCompleted, "This shouldn't run until the previous priority is done");
|
||||
return null;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$this->mockPlugin
|
||||
);
|
||||
|
||||
(new TestGrandchildAsyncEvent())->call();
|
||||
self::assertNotNull($resolver, "First handler didn't provide a resolver");
|
||||
$resolver->resolve(null);
|
||||
self::assertSame(2, $run, "Expected feedback from 2 handlers");
|
||||
}
|
||||
}
|
28
tests/phpunit/event/fixtures/TestChildAsyncEvent.php
Normal file
28
tests/phpunit/event/fixtures/TestChildAsyncEvent.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\fixtures;
|
||||
|
||||
class TestChildAsyncEvent extends TestParentAsyncEvent{
|
||||
|
||||
}
|
28
tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php
Normal file
28
tests/phpunit/event/fixtures/TestGrandchildAsyncEvent.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\fixtures;
|
||||
|
||||
class TestGrandchildAsyncEvent extends TestChildAsyncEvent{
|
||||
|
||||
}
|
30
tests/phpunit/event/fixtures/TestParentAsyncEvent.php
Normal file
30
tests/phpunit/event/fixtures/TestParentAsyncEvent.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\fixtures;
|
||||
|
||||
use pocketmine\event\AsyncEvent;
|
||||
|
||||
class TestParentAsyncEvent extends AsyncEvent{
|
||||
|
||||
}
|
Reference in New Issue
Block a user