mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-09-08 10:53:05 +00:00
Compare commits
153 Commits
Author | SHA1 | Date | |
---|---|---|---|
f2df702c67 | |||
481270e6aa | |||
e7bdaa8579 | |||
76749cbaa7 | |||
a897bdfaa0 | |||
09668a37d6 | |||
ea92a23d0d | |||
691e67018d | |||
fe2140a716 | |||
57330a7186 | |||
9ddac21de0 | |||
c91aa24daa | |||
6186fc0bfe | |||
ef40934d24 | |||
69b668355f | |||
0547383296 | |||
c7dff9ea40 | |||
043350753b | |||
5ad8016b99 | |||
2e5b2eed6e | |||
5a0cde49cc | |||
008a022ec1 | |||
5c85a7c306 | |||
599c4284f5 | |||
9499e2e595 | |||
a4fea1444a | |||
1ba47802a8 | |||
9d111e13f1 | |||
44bc4d8c7c | |||
d317347a9b | |||
077fac84bf | |||
fdb3a5b121 | |||
e3bc36ab5b | |||
283ff28aa9 | |||
c3ceeeace7 | |||
aac4f6c0e1 | |||
bb60a9057f | |||
3b893961e4 | |||
325ffec1be | |||
fa715a074a | |||
4caa2c7690 | |||
d04da9b1d8 | |||
02cf5ed388 | |||
6cad559dbe | |||
84a943bcec | |||
633e77a34c | |||
092d130c96 | |||
c09390d20f | |||
22f8623e17 | |||
f04151dbe6 | |||
5dcd8bf289 | |||
b70ff32548 | |||
73bf5d4b29 | |||
eb136e60c8 | |||
4228880509 | |||
709d874204 | |||
07dc10d6e6 | |||
7f6269c432 | |||
194714b448 | |||
023460db2c | |||
2910ffebf4 | |||
fea820a99e | |||
7c19f14cf5 | |||
5a54d09869 | |||
4def4d52d9 | |||
1d10107024 | |||
54ae4d0ea2 | |||
0d21e591d1 | |||
408616723c | |||
9bfcd39f2a | |||
8102616ff4 | |||
b162d688a3 | |||
eb130f2906 | |||
3b09c3a48a | |||
87781cff4d | |||
db0cf4bb5a | |||
eb4679fefd | |||
a4f2b99ed5 | |||
3ecc980bc4 | |||
107b56154b | |||
f86fde064d | |||
84a16ce69a | |||
d06d3bc871 | |||
11e34b3e5c | |||
f9318bf286 | |||
71b78b02d3 | |||
674b65f789 | |||
a77fc8109f | |||
6102740ee3 | |||
40168a457e | |||
d07acd0013 | |||
9561ae5af7 | |||
56fbd45dd5 | |||
b5dc72b0ee | |||
4ba57f2b03 | |||
84cb070d56 | |||
df0d72bf61 | |||
a534ac759a | |||
5ab954b7a0 | |||
6c6f686f8e | |||
bf7975da57 | |||
7aeedd8220 | |||
3dd1ce2d02 | |||
5a29d07021 | |||
692e1253c6 | |||
ab0c444823 | |||
e48a4aaa55 | |||
6fc4ce0f86 | |||
10243c7b2c | |||
8102586ee0 | |||
18b528f72d | |||
0336394098 | |||
7e92da126d | |||
1569bed37a | |||
ba62e0f9cb | |||
a6e79c5004 | |||
858d4a2ed2 | |||
87d8c1ea11 | |||
89deb0fe18 | |||
ad88490e84 | |||
3490e2b06a | |||
946c2fbacc | |||
1c0eed56f1 | |||
9e9b4db00f | |||
e667b5c7db | |||
76ebedff6a | |||
3ea8d27a3b | |||
d6c923b525 | |||
1683aa681d | |||
bf84caa02c | |||
734adec90d | |||
4724195791 | |||
f32a853bd4 | |||
61b0ad3e7f | |||
b2f755720d | |||
8ef2780dcd | |||
b19c7212ab | |||
73522d06ef | |||
a6a360d179 | |||
199ef7401f | |||
f63d349be4 | |||
02e11b5a60 | |||
4a770e5801 | |||
a2ff9649d5 | |||
821dd8885b | |||
bed218d1dd | |||
5e1f837a73 | |||
b49a9ae81d | |||
4c60e82110 | |||
1959d6dc9b | |||
9db7e5f0ca | |||
c1cef19f84 | |||
cebdb95265 |
4
.github/workflows/discord-release-notify.yml
vendored
4
.github/workflows/discord-release-notify.yml
vendored
@ -13,9 +13,9 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
php-version: 8.1
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
|
4
.github/workflows/draft-release.yml
vendored
4
.github/workflows/draft-release.yml
vendored
@ -18,9 +18,9 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
php-version: 8.1
|
||||
|
||||
- name: Restore Composer package cache
|
||||
uses: actions/cache@v3
|
||||
|
26
.github/workflows/main.yml
vendored
26
.github/workflows/main.yml
vendored
@ -13,11 +13,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.28, 8.1.16, 8.2.3]
|
||||
php: [8.1.19, 8.2.6]
|
||||
|
||||
steps:
|
||||
- name: Build and prepare PHP cache
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@c7fb29d83535320922068087c7285bdedbbfa3c2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
@ -32,13 +32,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.28, 8.1.16, 8.2.3]
|
||||
php: [8.1.19, 8.2.6]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@c7fb29d83535320922068087c7285bdedbbfa3c2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
@ -71,13 +71,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.28, 8.1.16, 8.2.3]
|
||||
php: [8.1.19, 8.2.6]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@c7fb29d83535320922068087c7285bdedbbfa3c2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
@ -110,7 +110,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.28, 8.1.16, 8.2.3]
|
||||
php: [8.1.19, 8.2.6]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -118,7 +118,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@c7fb29d83535320922068087c7285bdedbbfa3c2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
@ -151,13 +151,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image: [ubuntu-20.04]
|
||||
php: [8.0.28, 8.1.16, 8.2.3]
|
||||
php: [8.1.19, 8.2.6]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: pmmp/setup-php-action@6dd74c145250109942b08fc63cd5118edd2fd256
|
||||
uses: pmmp/setup-php-action@c7fb29d83535320922068087c7285bdedbbfa3c2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
install-path: "./bin"
|
||||
@ -203,10 +203,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PHP and tools
|
||||
uses: shivammathur/setup-php@2.24.0
|
||||
uses: shivammathur/setup-php@2.25.2
|
||||
with:
|
||||
php-version: 8.0
|
||||
tools: php-cs-fixer:3.11
|
||||
php-version: 8.1
|
||||
tools: php-cs-fixer:3.16
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
1
.github/workflows/update-php-versions.php
vendored
1
.github/workflows/update-php-versions.php
vendored
@ -22,7 +22,6 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
const VERSIONS = [
|
||||
"8.0",
|
||||
"8.1",
|
||||
"8.2"
|
||||
];
|
||||
|
10
BUILDING.md
10
BUILDING.md
@ -2,13 +2,13 @@
|
||||
## Pre-requisites
|
||||
- A bash shell (git bash is sufficient for Windows)
|
||||
- [`git`](https://git-scm.com) available in your shell
|
||||
- PHP 8.0 or newer available in your shell
|
||||
- PHP 8.1 or newer available in your shell
|
||||
- [`composer`](https://getcomposer.org) available in your shell
|
||||
|
||||
## Custom PHP binaries
|
||||
Because PocketMine-MP requires several non-standard PHP extensions and configuration, PMMP provides scripts to build custom binaries for running PocketMine-MP, as well as prebuilt binaries.
|
||||
|
||||
- [Prebuilt binaries](https://jenkins.pmmp.io/job/PHP-8.0-Aggregate)
|
||||
- [Prebuilt binaries](https://github.com/pmmp/PHP-Binaries/releases)
|
||||
- [Compile scripts](https://github.com/pmmp/php-build-scripts) are provided as a submodule in the path `build/php`
|
||||
|
||||
If you use a custom binary, you'll need to replace `composer` usages in this guide with `path/to/your/php path/to/your/composer.phar`.
|
||||
@ -29,11 +29,5 @@ Run `composer make-server` using your preferred PHP binary. It'll drop a `Pocket
|
||||
|
||||
You can also use the `--out` option to change the output filename.
|
||||
|
||||
There is a bug in PHP that might cause an error which looks like this:
|
||||
```
|
||||
Fatal error: Uncaught BadMethodCallException: unable to create temporary file in PocketMine-MP/build/server-phar.php:119
|
||||
```
|
||||
You can work around it by setting `ulimit -n` to some bigger number, e.g. `8192`, or by updating your PHP version to at least 8.0.3.
|
||||
|
||||
## Running PocketMine-MP from source code
|
||||
Run `src/PocketMine.php` using your preferred PHP binary.
|
||||
|
@ -18,6 +18,30 @@ Larger contributions like feature additions should be preceded by a [Change Prop
|
||||
## Other things you'll need
|
||||
- [git](https://git-scm.com/)
|
||||
|
||||
## List of `pocketmine` namespaces which are in other repos
|
||||
PocketMine-MP has several dependencies which are independent from the main server code. Most of them use the `pocketmine` namespace.
|
||||
Some of these add extra classes to packages which already exist in PocketMine-MP.
|
||||
|
||||
Take a look at the table below if you can't find the class or function you're looking for.
|
||||
|
||||
| Source URL | Namespace, class or function |
|
||||
|:----------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [pmmp/BedrockProtocol](https://github.com/pmmp/BedrockProtocol) | `pocketmine\network\mcpe\protocol` |
|
||||
| [pmmp/BinaryUtils](https://github.com/pmmp/BinaryUtils) | `pocketmine\utils\BinaryDataException`</br>`pocketmine\utils\BinaryStream`</br>`pocketmine\utils\Binary` |
|
||||
| [pmmp/ClassLoader](https://github.com/pmmp/`ClassLoader`) | `BaseClassLoader`</br>`ClassLoader`</br>`DynamicClassLoader` |
|
||||
| [pmmp/Color](https://github.com/pmmp/Color) | `pocketmine\color` |
|
||||
| [pmmp/ErrorHandler](https://github.com/pmmp/ErrorHandler) | `pocketmine\errorhandler` |
|
||||
| [pmmp/LogPthreads](https://github.com/pmmp/LogPthreads) | `ThreadedLoggerAttachment`</br>`ThreadedLogger`</br>`AttachableThreadedLogger` |
|
||||
| [pmmp/Log](https://github.com/pmmp/Log) | `AttachableLogger`</br>`BufferedLogger`</br>`GlobalLogger`</br>`LogLevel`</br>`Logger`</br>`PrefixedLogger`</br>`SimpleLogger` |
|
||||
| [pmmp/Math](https://github.com/pmmp/Math) | `pocketmine\math` |
|
||||
| [pmmp/NBT](https://github.com/pmmp/NBT) | `pocketmine\nbt` |
|
||||
| [pmmp/RakLibIpc](https://github.com/pmmp/RakLibIpc) | `raklib\server\ipc` |
|
||||
| [pmmp/RakLib](https://github.com/pmmp/RakLib) | `raklib` |
|
||||
| [pmmp/Snooze](https://github.com/pmmp/Snooze) | `pocketmine\snooze` |
|
||||
| [pmmp/ext-chunkutils2](https://github.com/pmmp/ext-chunkutils2) | `pocketmine\world\format\LightArray`</br>`pocketmine\world\format\PalettedBlockArray`</br>`pocketmine\world\format\io\SubChunkConverter` |
|
||||
| [pmmp/ext-morton](https://github.com/pmmp/ext-morton) | `morton2d_decode`</br>`morton2d_encode`</br>`morton3d_decode`</br>`morton3d_encode` |
|
||||
| [pmmp/ext-libdeflate](https://github.com/pmmp/ext-libdeflate) | `libdeflate_deflate_compress`</br>`libdeflate_gzip_compress`</br>`libdeflate_zlib_compress` |
|
||||
|
||||
## Choosing a target branch
|
||||
PocketMine-MP has three primary branches of development.
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
||||
* [Building and running from source](BUILDING.md)
|
||||
* [Developer documentation](https://devdoc.pmmp.io) - General documentation for PocketMine-MP plugin developers
|
||||
* [Latest release API documentation](https://apidoc.pmmp.io) - Doxygen API documentation generated for each release
|
||||
* [Latest bleeding-edge API documentation](https://apidoc-dev.pmmp.io) - Doxygen API documentation generated weekly from `next-major` branch
|
||||
* [Latest bleeding-edge API documentation](https://apidoc-dev.pmmp.io) - Doxygen API documentation generated weekly from `major-next` branch
|
||||
* [DevTools](https://github.com/pmmp/DevTools/) - Development tools plugin for creating plugins
|
||||
* [ExamplePlugin](https://github.com/pmmp/ExamplePlugin/) - Example plugin demonstrating some basic API features
|
||||
* [Contributing Guidelines](CONTRIBUTING.md)
|
||||
|
Submodule build/php updated: 9d8807be82...f2ece7b30d
111
changelogs/4.19.md
Normal file
111
changelogs/4.19.md
Normal file
@ -0,0 +1,111 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.70**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**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.
|
||||
|
||||
### Highlights
|
||||
This version introduces support for a new, more advanced version of Timings.
|
||||
This improved system provides more detail than the old system, and supports being displayed in a tree view, making it much easier to see which timers contribute to which other timers.
|
||||
|
||||
In addition, some minor performance improvements have been made, along with a couple of minor API additions.
|
||||
|
||||
# 4.19.0
|
||||
Released 11th April 2023.
|
||||
|
||||
## General
|
||||
- Updated the Timings system.
|
||||
- Timings records now include parent information, allowing them to be displayed in a tree view (e.g. https://timings.pmmp.io/?id=303556).
|
||||
- Timings records now include additional information, such as Peak (max time spent on any single tick), and Ticks (number of ticks the timer was active on).
|
||||
- New timings have been added for every event.
|
||||
- A new timer `Player Network Send - Pre-Spawn Game Data` has been added, and covers most of the time spent handling `ResourcePackClientResponsePacket`, giving a clearer picture of what's happening.
|
||||
- Improved performance of the plugin event system.
|
||||
- By introducing some caching, the event system now has 90% less overhead than in previous versions.
|
||||
- Improved performance of the random chunk ticking system.
|
||||
- The selection of ticked random chunks, and their validation for ticking, is now cached. This significantly reduces the overhead of chunk selection.
|
||||
- Factions servers and other game modes with big maps and sparsely populated areas will see the most benefit from this change.
|
||||
- Real-world performance benefit of this change is anywhere from 0-20%, depending on server type and configuration.
|
||||
- The `timings paste` command now logs a debug message with the server response on failure to paste a timings report.
|
||||
|
||||
## API
|
||||
### `pocketmine\entity\object`
|
||||
- The following API constants have been added:
|
||||
- `ExperienceOrb::DEFAULT_DESPAWN_DELAY` - the default delay in ticks before an experience orb despawns
|
||||
- `ExperienceOrb::NEVER_DESPAWN` - magic value for `setDespawnDelay()` to make an experience orb never despawn
|
||||
- `ExperienceOrb::MAX_DESPAWN_DELAY` - the maximum delay in ticks before an experience orb despawns
|
||||
- The following API methods have been added:
|
||||
- `public ExperienceOrb->getDespawnDelay() : int` - returns the delay in ticks before this experience orb despawns
|
||||
- `public ExperienceOrb->setDespawnDelay(int $despawnDelay) : void` - sets the delay in ticks before this experience orb despawns
|
||||
- The following properties have been deprecated
|
||||
- `ExperienceOrb->age` - superseded by despawn delay methods
|
||||
|
||||
### `pocketmine\event`
|
||||
- The following API methods have been added:
|
||||
- `public HandlerList->getListenerList() : list<RegisteredListener>` - returns an ordered list of handlers to be called for the event
|
||||
|
||||
### `pocketmine\player`
|
||||
- The following API methods have behavioural changes:
|
||||
- `ChunkSelector->selectChunks()` now yields the distance in chunks from the center as the key, instead of an incrementing integer.
|
||||
- The following classes have been deprecated:
|
||||
- `PlayerChunkLoader` (this was technically internal, but never marked as such)
|
||||
|
||||
### `pocketmine\timings`
|
||||
- The following API constants have been deprecated:
|
||||
- `Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX` - this is superseded by timings group support (see `Timings::GROUP_BREAKDOWN`)
|
||||
- The following API constants have been added:
|
||||
- `Timings::GROUP_BREAKDOWN` - this group makes a timer appear in the `Minecraft - Breakdown` section of a timings report
|
||||
- The following API methods have been added:
|
||||
- `public TimingsHandler->getGroup() : string` - returns the name of the table in which this timer will appear in a timings report
|
||||
- The following API methods have changed signatures:
|
||||
- `TimingsHandler->__construct()` now accepts an additional, optional `string $group` parameter, which defaults to `Minecraft`.
|
||||
|
||||
### `pocketmine\world`
|
||||
#### Highlights
|
||||
Ticking chunks is now done using the `ChunkTicker` system, which has a much more fine-grained API than the old `TickingChunkLoader` system, as well as better performance.
|
||||
It works similarly to the `ChunkLoader` system, in that chunks will be ticked as long as at least one `ChunkTicker` is registered for them.
|
||||
|
||||
#### API changes
|
||||
- The following classes have been deprecated:
|
||||
- `TickingChunkLoader` - this has been superseded by the more powerful and performant `ChunkTicker` APIs
|
||||
- The following classes have been added:
|
||||
- `ChunkTicker` - an opaque object used for `registerTickingChunk()` to instruct the `World` that we want a chunk to be ticked
|
||||
- The following API methods have been added:
|
||||
- `public World->registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void` - registers a chunk to be ticked by the given `ChunkTicker`
|
||||
- `public World->unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void` - unregisters a chunk from being ticked by the given `ChunkTicker`
|
||||
|
||||
# 4.19.1
|
||||
Released 14th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed inventory rollbacks when spreading items in ender chests.
|
||||
- Fixed inventory rollbacks when shift-clicking to craft and the outputs would have been split across multiple inventory slots.
|
||||
- Fixed incorrect spawn terrain generation for newly created worlds.
|
||||
- Fixed `chunk-ticking.tick-radius` not disabling chunk ticking when set to `0`.
|
||||
- Fixed chunks not being ticked if they previously left a player's simulation distance without leaving their view distance.
|
||||
- Fixed height of collision boxes for Grass Path and Farmland blocks.
|
||||
|
||||
# 4.19.2
|
||||
Released 14th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed player timings duplication leading to extremely large timings reports when timings runs for a long time with many players.
|
||||
- Packet timings are now indexed by class FQN instead of packet ID. This prevents erroneous timer reuse on packet ID reuse (e.g. multi version servers).
|
||||
- Fixed entity timings being shared by different classes with the same short name. This led to incorrect timings being reported for some entities when custom entities were used.
|
||||
|
||||
# 4.19.3
|
||||
Released 21st April 2023.
|
||||
|
||||
## General
|
||||
- Error IDs for `Packet processing error` disconnects are now split into 4-character chunks to make them easier to type (since they can't be copied from the disconnection screen of a client).
|
||||
|
||||
## Fixes
|
||||
- Fixed entity-block intersections being checked twice per tick. Besides wasting CPU time, this may have caused unexpected behaviour during entity-block interactions with blocks like water or cacti.
|
||||
- Fixed performance issue in network inventory synchronization due item NBT being prepared twice.
|
||||
- Fixed `tools/simulate-chunk-selector.php` argument parsing being completely broken (weird behaviour of PHP `getopt()`).
|
||||
|
||||
## Internals
|
||||
- `TimingsHandler->stopTiming()` now logs an error message if a subtimer wasn't stopped, rather than throwing an exception.
|
||||
- Due to interactions between `try...finally` and unexpected errors, throwing exceptions made it difficult for plugin developers to debug errors in their plugins, since it obscured the original error.
|
79
changelogs/4.20.md
Normal file
79
changelogs/4.20.md
Normal file
@ -0,0 +1,79 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.80**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace are compatible with any previous 4.x.y version will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**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.
|
||||
|
||||
### Interim releases
|
||||
If you're upgrading from 4.17.x directly to 4.20.x, please also read the following changelogs, as the interim releases contain important changes:
|
||||
|
||||
- [4.18.0](https://github.com/pmmp/PocketMine-MP/blob/4.20.0/changelogs/4.18.md#4180) - major performance improvements, internal network changes, minor API additions
|
||||
- [4.19.0](https://github.com/pmmp/PocketMine-MP/blob/4.20.0/changelogs/4.19.md#4190) - minor performance improvements, improved timings system, minor API additions
|
||||
|
||||
# 4.20.0
|
||||
Released 26th April 2023.
|
||||
|
||||
## General
|
||||
- Added support for Minecraft: Bedrock Edition 1.19.80.
|
||||
- Removed support for older versions.
|
||||
|
||||
## Fixes
|
||||
- Fixed packet processing error when attempting to use a stonecutter.
|
||||
- Fixed armor slots containing ghost items when cancelling right-click to equip armor.
|
||||
- Fixed crash in `HandlerList->getListenersByPriority()` when no listeners are registered at the given priority.
|
||||
|
||||
## API
|
||||
### `pocketmine\block`
|
||||
- The following API methods have been added:
|
||||
- `public BaseSign->getEditorEntityRuntimeId() : int` - returns the entity runtime ID of the player currently editing this sign, or `null` if none
|
||||
- `public BaseSign->setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : $this` - sets the entity runtime ID of the player currently editing this sign
|
||||
|
||||
### `pocketmine\player`
|
||||
- The following API methods have been added:
|
||||
- `public Player->openSignEditor(Vector3 $position) : void` - opens the client-side sign editor GUI for the given position
|
||||
|
||||
# 4.20.1
|
||||
Released 27th April 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash when firing a bow while holding arrows in the offhand slot.
|
||||
|
||||
## Internals
|
||||
- `ItemStackContainerIdTranslator::translate()` now requires an additional `int $slotId` parameter and returns `array{int, int}` (translated window ID, translated slot ID) to be used with `InventoryManager->locateWindowAndSlot()`.
|
||||
- `InventoryManager->locateWindowAndSlot()` now checks if the translated slot actually exists in the requested inventory, and returns `null` if not. Previously, it would return potentially invalid slot IDs without checking them, potentially leading to crashes.
|
||||
|
||||
# 4.20.2
|
||||
Released 4th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed all types of wooden logs appearing as oak in the inventory.
|
||||
- Fixed a performance issue in `BaseInventory->canAddItem()` (missing `continue` causing useless logic to run).
|
||||
|
||||
# 4.20.3
|
||||
Released 6th May 2023.
|
||||
|
||||
## Improvements
|
||||
- Reduced memory usage of `RuntimeBlockMapping` from 25 MB to 9 MB. Since every thread has its own copy of the block map, this saves a substantial amount of memory.
|
||||
|
||||
## Fixes
|
||||
- Fixed players falling through blocks in spectator mode.
|
||||
- Fixed timings reports getting bloated by prolific usage of `PluginManager->registerEvent()`.
|
||||
- This was caused by creating a new timings handler for each call, regardless of whether a timer already existed for the given event and callback.
|
||||
- Fixed `Full Server Tick` and other records being missing from timings reports.
|
||||
- This was caused by timings handler depth not getting reset when timings was disabled and later re-enabled.
|
||||
|
||||
# 4.20.4
|
||||
Released 6th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed players being forced into flight mode in every game mode.
|
||||
- Moral of the story: do not assume anything in Mojang internals does what its name suggests...
|
||||
|
||||
# 4.20.5
|
||||
Released 30th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash due to a bug in upstream dependency [`netresearch/jsonmapper`](https://github.com/cweiske/JsonMapper).
|
77
changelogs/4.21.md
Normal file
77
changelogs/4.21.md
Normal file
@ -0,0 +1,77 @@
|
||||
**For Minecraft: Bedrock Edition 1.19.80**
|
||||
|
||||
### Note about API versions
|
||||
Plugins which don't touch the `pocketmine\network\mcpe` namespace, and don't use reflection or any internal methods,
|
||||
will also run on these releases and do not need API bumps.
|
||||
Plugin developers should **only** update their required API to this version if you need the changes in this build.
|
||||
|
||||
**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.
|
||||
|
||||
# 4.21.0
|
||||
Released 17th May 2023.
|
||||
|
||||
## General
|
||||
- PHP 8.1 is now required. Most plugins should run without changes, but some might need to be updated due to language-level deprecations.
|
||||
- Ticking chunk count is now shown separately from loaded chunk count in the `/status` command, providing useful performance information.
|
||||
- Further improved performance of ticking chunk selection.
|
||||
- Improved performance of some inventory functions.
|
||||
- Reduced server memory footprint in most cases by ~9 MB per thread.
|
||||
- Due to large overhead, async network compression is now only used for packets larger than 10 KB by default.
|
||||
|
||||
## Configuration
|
||||
- Added the following new `pocketmine.yml` configuration options:
|
||||
- `network.async-compression-threshold` - minimum size of packet which will be compressed using `AsyncTask`
|
||||
- Default is 10 KB, which means that very few packets will use async compression in practice. This is because the overhead of compressing async is currently so high that it's not worth it for smaller packets.
|
||||
|
||||
## Timings
|
||||
- Timings reports no longer include the unused metadata fields `Entities` and `LivingEntities`.
|
||||
- Timings reports now correctly calculate the peak time of a timer.
|
||||
- Previously it was incorrectly recorded as the longest time spent in a single tick, rather than the longest time spent in a single activation.
|
||||
- Timings report version has been bumped to `2` to reflect this change.
|
||||
- All world-specific timers now have generic aggregate timings, making it much easier to locate performance patterns across all worlds.
|
||||
|
||||
## Gameplay
|
||||
- Players in spectator mode are no longer able to pick blocks, and now have finite resources similar to survival mode.
|
||||
|
||||
## API
|
||||
### `pocketmine\world`
|
||||
- The following API methods have been added:
|
||||
- `public World->getTickingChunks() : list<int>` - returns a list of chunk position hashes (a la `World::chunkHash()`) which are currently valid for random ticking
|
||||
|
||||
### `pocketmine\inventory`
|
||||
- The following API methods have been added:
|
||||
- `protected BaseInventory->getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int` - returns the number of items in the given stack if the content of the slot matches the test item, or zero otherwise
|
||||
- This should be overridden if directly extending `BaseInventory` to provide a performance-optimised version. A slow default implementation is provided, but it will be removed in the future.
|
||||
|
||||
## Internals
|
||||
### Entity
|
||||
- Unused `NameTag` tag is no longer saved for `Human` entities.
|
||||
|
||||
### Inventory
|
||||
- `BaseInventory` now uses a new internal method `getMatchingItemCount()` to locate items in the inventory without useless cloning. This improves performance of various API methods, such as `addItem()`, `contains()`, and more.
|
||||
- Specialization of `Inventory->isSlotEmpty()` in `BaseInventory` subclasses has been added to improve performance of some API methods.
|
||||
|
||||
### Network
|
||||
- `RuntimeBlockMapping` no longer keeps all block palette NBT data in memory.
|
||||
- This significantly reduces server idle memory footprint.
|
||||
- For multi-version implementations, this will have a significant impact on memory usage, since a different block palette is often required to support each version.
|
||||
- NBT will be lazy-loaded into memory and cached if `getBedrockKnownStates()` is called. However, this is not used by PocketMine-MP under normal circumstances.
|
||||
- Removed unnecessary usage of `Utils::validateCallableSignature()` from some internal network pathways, improving performance.
|
||||
|
||||
### Scheduler
|
||||
- `AsyncPool` no longer double-checks progress updates on completed tasks.
|
||||
|
||||
### World
|
||||
- Ticking chunks are now tracked in `World->validTickingChunks` and `World->recheckTickingChunks`.
|
||||
- This allows avoiding rechecking every ticking chunk for validity during ticking chunk selection, improving performance.
|
||||
- In some cases, this allows bypassing chunk selection entirely, reducing selection cost to zero.
|
||||
- Registered but ineligible ticking chunks are no longer rechecked every tick.
|
||||
- This was causing wasted cycles during async worker backlog.
|
||||
- The internal system must call `markTickingChunkForRecheck()` whenever a ticking chunk's eligibility for ticking has potentially changed, rather than just when it has changed from a yes to a no.
|
||||
|
||||
# 4.21.1
|
||||
Released 30th May 2023.
|
||||
|
||||
## Fixes
|
||||
- Fixed server crash due to a bug in upstream dependency [`netresearch/jsonmapper`](https://github.com/cweiske/JsonMapper).
|
@ -5,7 +5,7 @@
|
||||
"homepage": "https://pmmp.io",
|
||||
"license": "LGPL-3.0",
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": "^8.1",
|
||||
"php-64bit": "*",
|
||||
"ext-chunkutils2": "^0.3.1",
|
||||
"ext-crypto": "^0.3.1",
|
||||
@ -33,11 +33,11 @@
|
||||
"composer-runtime-api": "^2.0",
|
||||
"adhocore/json-comment": "^1.1",
|
||||
"fgrosse/phpasn1": "^2.3",
|
||||
"netresearch/jsonmapper": "^4.0",
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~1.1.1+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-data": "~2.1.1+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.1.0+bedrock-1.19.70",
|
||||
"pocketmine/bedrock-protocol": "~20.1.1+bedrock-1.19.70",
|
||||
"netresearch/jsonmapper": "dev-array-in-string-property-error as 4.2.0",
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~2.1.0+bedrock-1.19.80",
|
||||
"pocketmine/bedrock-data": "~2.2.0+bedrock-1.19.80",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.2.0+bedrock-1.19.80",
|
||||
"pocketmine/bedrock-protocol": "~21.0.0+bedrock-1.19.80",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/classloader": "^0.2.0",
|
||||
@ -56,10 +56,10 @@
|
||||
"webmozart/path-util": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "1.10.11",
|
||||
"phpstan/phpstan": "1.10.15",
|
||||
"phpstan/phpstan-phpunit": "^1.1.0",
|
||||
"phpstan/phpstan-strict-rules": "^1.2.0",
|
||||
"phpunit/phpunit": "^9.2"
|
||||
"phpunit/phpunit": "^10.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@ -77,7 +77,7 @@
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.0.0"
|
||||
"php": "8.1.0"
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
@ -93,5 +93,11 @@
|
||||
"update-translation-apis": [
|
||||
"@php build/generate-known-translation-apis.php"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/dktapps/JsonMapper.git"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
821
composer.lock
generated
821
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -85,8 +85,11 @@ network:
|
||||
batch-threshold: 256
|
||||
#Compression level used when sending batched packets. Higher = more CPU, less bandwidth usage
|
||||
compression-level: 6
|
||||
#Use AsyncTasks for compression. Adds half/one tick delay, less CPU load on main thread
|
||||
#Use AsyncTasks for compression during the main game session. Increases latency, but may reduce main thread load
|
||||
async-compression: false
|
||||
#Threshold for async compression, in bytes. Only packets larger than this will be compressed asynchronously
|
||||
#Due to large overhead of AsyncTask, async compression isn't worth it except for large packets
|
||||
async-compression-threshold: 10000
|
||||
#Experimental. Use UPnP to automatically port forward
|
||||
upnp-forwarding: false
|
||||
#Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be
|
||||
|
@ -50,7 +50,7 @@ namespace pocketmine {
|
||||
|
||||
require_once __DIR__ . '/VersionInfo.php';
|
||||
|
||||
const MIN_PHP_VERSION = "8.0.0";
|
||||
const MIN_PHP_VERSION = "8.1.0";
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
@ -265,9 +265,6 @@ JIT_WARNING
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if(extension_loaded('parallel')){
|
||||
\parallel\bootstrap(\pocketmine\COMPOSER_AUTOLOADER_PATH);
|
||||
}
|
||||
|
||||
ErrorToExceptionHandler::set();
|
||||
|
||||
|
@ -208,6 +208,8 @@ class Server{
|
||||
private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND;
|
||||
private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND;
|
||||
|
||||
private const DEFAULT_ASYNC_COMPRESSION_THRESHOLD = 10_000;
|
||||
|
||||
private static ?Server $instance = null;
|
||||
|
||||
private TimeTrackingSleeperHandler $tickSleeper;
|
||||
@ -266,6 +268,7 @@ class Server{
|
||||
|
||||
private Network $network;
|
||||
private bool $networkCompressionAsync = true;
|
||||
private int $networkCompressionAsyncThreshold = self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD;
|
||||
|
||||
private Language $language;
|
||||
private bool $forceLanguage = false;
|
||||
@ -908,6 +911,10 @@ class Server{
|
||||
ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
|
||||
|
||||
$this->networkCompressionAsync = $this->configGroup->getPropertyBool("network.async-compression", true);
|
||||
$this->networkCompressionAsyncThreshold = max(
|
||||
$this->configGroup->getPropertyInt("network.async-compression-threshold", self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD),
|
||||
$netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD
|
||||
);
|
||||
|
||||
EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool("network.enable-encryption", true);
|
||||
|
||||
@ -1375,7 +1382,7 @@ class Server{
|
||||
}
|
||||
|
||||
$promise = new CompressBatchPromise();
|
||||
if(!$sync){
|
||||
if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){
|
||||
$task = new CompressBatchTask($buffer, $promise, $compressor);
|
||||
$this->asyncPool->submitTask($task);
|
||||
}else{
|
||||
|
@ -31,7 +31,7 @@ use function str_repeat;
|
||||
|
||||
final class VersionInfo{
|
||||
public const NAME = "PocketMine-MP";
|
||||
public const BASE_VERSION = "4.18.4";
|
||||
public const BASE_VERSION = "4.21.1";
|
||||
public const IS_DEVELOPMENT_BUILD = false;
|
||||
public const BUILD_CHANNEL = "stable";
|
||||
|
||||
|
@ -97,6 +97,15 @@ abstract class BaseSign extends Transparent{
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
public function onPostPlace() : void{
|
||||
$player = $this->editorEntityRuntimeId !== null ?
|
||||
$this->position->getWorld()->getEntity($this->editorEntityRuntimeId) :
|
||||
null;
|
||||
if($player instanceof Player){
|
||||
$player->openSignEditor($this->position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing information about the sign text.
|
||||
*/
|
||||
@ -110,6 +119,19 @@ abstract class BaseSign extends Transparent{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the runtime entity ID of the player editing this sign. Only this player will be able to edit the sign.
|
||||
* This is used to prevent multiple players from editing the same sign at the same time, and to prevent players
|
||||
* from editing signs they didn't place.
|
||||
*/
|
||||
public function getEditorEntityRuntimeId() : ?int{ return $this->editorEntityRuntimeId; }
|
||||
|
||||
/** @return $this */
|
||||
public function setEditorEntityRuntimeId(?int $editorEntityRuntimeId) : self{
|
||||
$this->editorEntityRuntimeId = $editorEntityRuntimeId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the player controller (network session) to update the sign text, firing events as appropriate.
|
||||
*
|
||||
@ -133,6 +155,7 @@ abstract class BaseSign extends Transparent{
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$this->setText($ev->getNewText());
|
||||
$this->setEditorEntityRuntimeId(null);
|
||||
$this->position->getWorld()->setBlock($this->position, $this);
|
||||
return true;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ class Farmland extends Transparent{
|
||||
* @return AxisAlignedBB[]
|
||||
*/
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109)
|
||||
return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)];
|
||||
}
|
||||
|
||||
public function onNearbyBlockChange() : void{
|
||||
|
@ -33,7 +33,7 @@ class GrassPath extends Transparent{
|
||||
* @return AxisAlignedBB[]
|
||||
*/
|
||||
protected function recalculateCollisionBoxes() : array{
|
||||
return [AxisAlignedBB::one()]; //TODO: this should be trimmed at the top by 1/16, but MCPE currently treats them as a full block (https://bugs.mojang.com/browse/MCPE-12109)
|
||||
return [AxisAlignedBB::one()->trim(Facing::UP, 1 / 16)];
|
||||
}
|
||||
|
||||
public function onNearbyBlockChange() : void{
|
||||
|
@ -85,6 +85,20 @@ class DoubleChestInventory extends BaseInventory implements BlockInventory, Inve
|
||||
$this->right->setContents($rightContents);
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $slot < $leftSize ?
|
||||
$this->left->getMatchingItemCount($slot, $test, $checkDamage, $checkTags) :
|
||||
$this->right->getMatchingItemCount($slot - $leftSize, $test, $checkDamage, $checkTags);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
$leftSize = $this->left->getSize();
|
||||
return $index < $leftSize ?
|
||||
$this->left->isSlotEmpty($index) :
|
||||
$this->right->isSlotEmpty($index - $leftSize);
|
||||
}
|
||||
|
||||
protected function getOpenSound() : Sound{ return new ChestOpenSound(); }
|
||||
|
||||
protected function getCloseSound() : Sound{ return new ChestCloseSound(); }
|
||||
|
@ -45,12 +45,18 @@ class Sign extends Spawnable{
|
||||
public const TAG_TEXT_LINE = "Text%d"; //sprintf()able
|
||||
public const TAG_TEXT_COLOR = "SignTextColor";
|
||||
public const TAG_GLOWING_TEXT = "IgnoreLighting";
|
||||
public const TAG_PERSIST_FORMATTING = "PersistFormatting"; //TAG_Byte
|
||||
/**
|
||||
* This tag is set to indicate that MCPE-117835 has been addressed in whatever version this sign was created.
|
||||
* @see https://bugs.mojang.com/browse/MCPE-117835
|
||||
*/
|
||||
public const TAG_LEGACY_BUG_RESOLVE = "TextIgnoreLegacyBugResolved";
|
||||
|
||||
public const TAG_FRONT_TEXT = "FrontText"; //TAG_Compound
|
||||
public const TAG_BACK_TEXT = "BackText"; //TAG_Compound
|
||||
public const TAG_WAXED = "IsWaxed"; //TAG_Byte
|
||||
public const TAG_LOCKED_FOR_EDITING_BY = "LockedForEditingBy"; //TAG_Long
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@ -118,12 +124,22 @@ class Sign extends Spawnable{
|
||||
}
|
||||
|
||||
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
|
||||
$nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines()));
|
||||
|
||||
//the following are not yet used by the server, but needed to roll back any changes to glowing state or colour
|
||||
//if the client uses dye on the sign
|
||||
$nbt->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00));
|
||||
$nbt->setByte(self::TAG_GLOWING_TEXT, 0);
|
||||
$nbt->setByte(self::TAG_LEGACY_BUG_RESOLVE, 1);
|
||||
$textPolyfill = function(CompoundTag $textTag) : CompoundTag{
|
||||
//the following are not yet used by the server, but needed to roll back any changes to glowing state or colour
|
||||
//if the client uses dye on the sign
|
||||
return $textTag
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
|
||||
->setByte(self::TAG_GLOWING_TEXT, 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1); //TODO: not sure what this is used for
|
||||
};
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, $textPolyfill(CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines()))
|
||||
));
|
||||
//TODO: this is not yet used by the server, but needed to rollback any client-side changes to the back text
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, $textPolyfill(CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, "")
|
||||
));
|
||||
$nbt->setByte(self::TAG_WAXED, 0);
|
||||
$nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1);
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ abstract class Command{
|
||||
public function setLabel(string $name) : bool{
|
||||
$this->nextLabel = $name;
|
||||
if(!$this->isRegistered()){
|
||||
$this->timings = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Command: " . $name);
|
||||
$this->timings = new TimingsHandler("Command: " . $name, group: Timings::GROUP_BREAKDOWN);
|
||||
$this->label = $name;
|
||||
|
||||
return true;
|
||||
|
@ -110,7 +110,8 @@ class StatusCommand extends VanillaCommand{
|
||||
$worldName = $world->getFolderName() !== $world->getDisplayName() ? " (" . $world->getDisplayName() . ")" : "";
|
||||
$timeColor = $world->getTickRateTime() > 40 ? TextFormat::RED : TextFormat::YELLOW;
|
||||
$sender->sendMessage(TextFormat::GOLD . "World \"{$world->getFolderName()}\"$worldName: " .
|
||||
TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getLoadedChunks())) . TextFormat::GREEN . " loaded chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getTickingChunks())) . TextFormat::GREEN . " ticking chunks, " .
|
||||
TextFormat::RED . number_format(count($world->getEntities())) . TextFormat::GREEN . " entities. " .
|
||||
"Time $timeColor" . round($world->getTickRateTime(), 2) . "ms"
|
||||
);
|
||||
|
@ -167,6 +167,7 @@ class TimingsCommand extends VanillaCommand{
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
|
||||
"https://" . $host . "/?id=" . $response["id"]));
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ class CraftingManager{
|
||||
* @var FurnaceRecipeManager[]
|
||||
* @phpstan-var array<int, FurnaceRecipeManager>
|
||||
*/
|
||||
protected $furnaceRecipeManagers;
|
||||
protected $furnaceRecipeManagers = [];
|
||||
|
||||
/**
|
||||
* @var PotionTypeRecipe[][]
|
||||
|
@ -115,6 +115,8 @@ abstract class Entity{
|
||||
/** @var Block[]|null */
|
||||
protected $blocksAround;
|
||||
|
||||
private bool $checkBlockIntersectionsNextTick = true;
|
||||
|
||||
/** @var Location */
|
||||
protected $location;
|
||||
/** @var Location */
|
||||
@ -649,7 +651,10 @@ abstract class Entity{
|
||||
|
||||
$hasUpdate = false;
|
||||
|
||||
$this->checkBlockIntersections();
|
||||
if($this->checkBlockIntersectionsNextTick){
|
||||
$this->checkBlockIntersections();
|
||||
}
|
||||
$this->checkBlockIntersectionsNextTick = true;
|
||||
|
||||
if($this->location->y <= World::Y_MIN - 16 && $this->isAlive()){
|
||||
$ev = new EntityDamageEvent($this, EntityDamageEvent::CAUSE_VOID, 10);
|
||||
@ -1308,6 +1313,7 @@ abstract class Entity{
|
||||
}
|
||||
|
||||
protected function checkBlockIntersections() : void{
|
||||
$this->checkBlockIntersectionsNextTick = false;
|
||||
$vectors = [];
|
||||
|
||||
foreach($this->getBlocksAroundWithEntityInsideActions() as $block){
|
||||
@ -1319,10 +1325,12 @@ abstract class Entity{
|
||||
}
|
||||
}
|
||||
|
||||
$vector = Vector3::sum(...$vectors);
|
||||
if($vector->lengthSquared() > 0){
|
||||
$d = 0.014;
|
||||
$this->motion = $this->motion->addVector($vector->normalize()->multiply($d));
|
||||
if(count($vectors) > 0){
|
||||
$vector = Vector3::sum(...$vectors);
|
||||
if($vector->lengthSquared() > 0){
|
||||
$d = 0.014;
|
||||
$this->motion = $this->motion->addVector($vector->normalize()->multiply($d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
private const TAG_XP_PROGRESS = "XpP"; //TAG_Float
|
||||
private const TAG_LIFETIME_XP_TOTAL = "XpTotal"; //TAG_Int
|
||||
private const TAG_XP_SEED = "XpSeed"; //TAG_Int
|
||||
private const TAG_NAME_TAG = "NameTag"; //TAG_String
|
||||
private const TAG_SKIN = "Skin"; //TAG_Compound
|
||||
private const TAG_SKIN_NAME = "Name"; //TAG_String
|
||||
private const TAG_SKIN_DATA = "Data"; //TAG_ByteArray
|
||||
@ -245,10 +244,6 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
* For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT.
|
||||
*/
|
||||
protected function initHumanData(CompoundTag $nbt) : void{
|
||||
if(($nameTagTag = $nbt->getTag(self::TAG_NAME_TAG)) instanceof StringTag){
|
||||
$this->setNameTag($nameTagTag->getValue());
|
||||
}
|
||||
|
||||
//TODO: use of NIL UUID for namespace is a hack; we should provide a proper UUID for the namespace
|
||||
$this->uuid = Uuid::uuid3(Uuid::NIL, ((string) $this->getId()) . $this->skin->getSkinData() . $this->getNameTag());
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
|
||||
use pocketmine\player\Player;
|
||||
use function max;
|
||||
use function sqrt;
|
||||
|
||||
class ExperienceOrb extends Entity{
|
||||
@ -48,6 +49,10 @@ class ExperienceOrb extends Entity{
|
||||
/** Split sizes used for dropping experience orbs. */
|
||||
public const ORB_SPLIT_SIZES = [2477, 1237, 617, 307, 149, 73, 37, 17, 7, 3, 1]; //This is indexed biggest to smallest so that we can return as soon as we found the biggest value.
|
||||
|
||||
public const DEFAULT_DESPAWN_DELAY = 6000;
|
||||
public const NEVER_DESPAWN = -1;
|
||||
public const MAX_DESPAWN_DELAY = 32767 + self::DEFAULT_DESPAWN_DELAY; //max value storable by mojang NBT :(
|
||||
|
||||
/**
|
||||
* Returns the largest size of normal XP orb that will be spawned for the specified amount of XP. Used to split XP
|
||||
* up into multiple orbs when an amount of XP is dropped.
|
||||
@ -82,7 +87,10 @@ class ExperienceOrb extends Entity{
|
||||
public $gravity = 0.04;
|
||||
public $drag = 0.02;
|
||||
|
||||
/** @var int */
|
||||
/**
|
||||
* @var int
|
||||
* @deprecated
|
||||
*/
|
||||
protected $age = 0;
|
||||
|
||||
/**
|
||||
@ -100,6 +108,8 @@ class ExperienceOrb extends Entity{
|
||||
/** @var int */
|
||||
protected $xpValue;
|
||||
|
||||
private int $despawnDelay = self::DEFAULT_DESPAWN_DELAY;
|
||||
|
||||
public function __construct(Location $location, int $xpValue, ?CompoundTag $nbt = null){
|
||||
$this->xpValue = $xpValue;
|
||||
parent::__construct($location, $nbt);
|
||||
@ -111,12 +121,22 @@ class ExperienceOrb extends Entity{
|
||||
parent::initEntity($nbt);
|
||||
|
||||
$this->age = $nbt->getShort(self::TAG_AGE, 0);
|
||||
if($this->age === -32768){
|
||||
$this->despawnDelay = self::NEVER_DESPAWN;
|
||||
}else{
|
||||
$this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $this->age);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveNBT() : CompoundTag{
|
||||
$nbt = parent::saveNBT();
|
||||
|
||||
$nbt->setShort(self::TAG_AGE, $this->age);
|
||||
if($this->despawnDelay === self::NEVER_DESPAWN){
|
||||
$age = -32768;
|
||||
}else{
|
||||
$age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
|
||||
}
|
||||
$nbt->setShort(self::TAG_AGE, $age);
|
||||
|
||||
$nbt->setShort(self::TAG_VALUE_PC, $this->getXpValue());
|
||||
$nbt->setInt(self::TAG_VALUE_PE, $this->getXpValue());
|
||||
@ -124,6 +144,15 @@ class ExperienceOrb extends Entity{
|
||||
return $nbt;
|
||||
}
|
||||
|
||||
public function getDespawnDelay() : int{ return $this->despawnDelay; }
|
||||
|
||||
public function setDespawnDelay(int $despawnDelay) : void{
|
||||
if(($despawnDelay < 0 || $despawnDelay > self::MAX_DESPAWN_DELAY) && $despawnDelay !== self::NEVER_DESPAWN){
|
||||
throw new \InvalidArgumentException("Despawn ticker must be in range 0 ... " . self::MAX_DESPAWN_DELAY . " or " . self::NEVER_DESPAWN . ", got $despawnDelay");
|
||||
}
|
||||
$this->despawnDelay = $despawnDelay;
|
||||
}
|
||||
|
||||
public function getXpValue() : int{
|
||||
return $this->xpValue;
|
||||
}
|
||||
@ -161,7 +190,8 @@ class ExperienceOrb extends Entity{
|
||||
$hasUpdate = parent::entityBaseTick($tickDiff);
|
||||
|
||||
$this->age += $tickDiff;
|
||||
if($this->age > 6000){
|
||||
$this->despawnDelay -= $tickDiff;
|
||||
if($this->despawnDelay <= 0){
|
||||
$this->flagForDespawn();
|
||||
return true;
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\timings\Timings;
|
||||
use function get_class;
|
||||
|
||||
abstract class Event{
|
||||
@ -51,22 +52,19 @@ abstract class Event{
|
||||
throw new \RuntimeException("Recursive event call detected (reached max depth of " . self::MAX_EVENT_CALL_DEPTH . " calls)");
|
||||
}
|
||||
|
||||
$timings = Timings::getEventTimings($this);
|
||||
$timings->startTiming();
|
||||
|
||||
$handlerList = HandlerListManager::global()->getListFor(get_class($this));
|
||||
|
||||
++self::$eventCallDepth;
|
||||
try{
|
||||
foreach(EventPriority::ALL as $priority){
|
||||
$currentList = $handlerList;
|
||||
while($currentList !== null){
|
||||
foreach($currentList->getListenersByPriority($priority) as $registration){
|
||||
$registration->callEvent($this);
|
||||
}
|
||||
|
||||
$currentList = $currentList->getParent();
|
||||
}
|
||||
foreach($handlerList->getListenerList() as $registration){
|
||||
$registration->callEvent($this);
|
||||
}
|
||||
}finally{
|
||||
--self::$eventCallDepth;
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ use function mb_strtoupper;
|
||||
* LOWEST -> LOW -> NORMAL -> HIGH -> HIGHEST -> MONITOR
|
||||
*
|
||||
* MONITOR events should not change the event outcome or contents
|
||||
*
|
||||
* WARNING: If these values are changed, handler sorting in HandlerList::getListenerList() may need to be updated.
|
||||
*/
|
||||
final class EventPriority{
|
||||
|
||||
|
@ -24,13 +24,20 @@ declare(strict_types=1);
|
||||
namespace pocketmine\event;
|
||||
|
||||
use pocketmine\plugin\Plugin;
|
||||
use function array_fill_keys;
|
||||
use function array_merge;
|
||||
use function krsort;
|
||||
use function spl_object_id;
|
||||
use const SORT_NUMERIC;
|
||||
|
||||
class HandlerList{
|
||||
/** @var RegisteredListener[][] */
|
||||
private array $handlerSlots = [];
|
||||
|
||||
private RegisteredListenerCache $handlerCache;
|
||||
|
||||
/** @var RegisteredListenerCache[] */
|
||||
private array $affectedHandlerCaches = [];
|
||||
|
||||
/**
|
||||
* @phpstan-template TEvent of Event
|
||||
* @phpstan-param class-string<TEvent> $class
|
||||
@ -39,7 +46,10 @@ class HandlerList{
|
||||
private string $class,
|
||||
private ?HandlerList $parentList
|
||||
){
|
||||
$this->handlerSlots = array_fill_keys(EventPriority::ALL, []);
|
||||
$this->handlerCache = new RegisteredListenerCache();
|
||||
for($list = $this; $list !== null; $list = $list->parentList){
|
||||
$list->affectedHandlerCaches[spl_object_id($this->handlerCache)] = $this->handlerCache;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +60,7 @@ class HandlerList{
|
||||
throw new \InvalidArgumentException("This listener is already registered to priority {$listener->getPriority()} of event {$this->class}");
|
||||
}
|
||||
$this->handlerSlots[$listener->getPriority()][spl_object_id($listener)] = $listener;
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,6 +70,7 @@ class HandlerList{
|
||||
foreach($listeners as $listener){
|
||||
$this->register($listener);
|
||||
}
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,24 +88,60 @@ class HandlerList{
|
||||
}
|
||||
}
|
||||
}elseif($object instanceof RegisteredListener){
|
||||
if(isset($this->handlerSlots[$object->getPriority()][spl_object_id($object)])){
|
||||
unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]);
|
||||
}
|
||||
unset($this->handlerSlots[$object->getPriority()][spl_object_id($object)]);
|
||||
}
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
public function clear() : void{
|
||||
$this->handlerSlots = array_fill_keys(EventPriority::ALL, []);
|
||||
$this->handlerSlots = [];
|
||||
$this->invalidateAffectedCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
*/
|
||||
public function getListenersByPriority(int $priority) : array{
|
||||
return $this->handlerSlots[$priority];
|
||||
return $this->handlerSlots[$priority] ?? [];
|
||||
}
|
||||
|
||||
public function getParent() : ?HandlerList{
|
||||
return $this->parentList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates all known caches which might be affected by this list's contents.
|
||||
*/
|
||||
private function invalidateAffectedCaches() : void{
|
||||
foreach($this->affectedHandlerCaches as $cache){
|
||||
$cache->list = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RegisteredListener[]
|
||||
* @phpstan-return list<RegisteredListener>
|
||||
*/
|
||||
public function getListenerList() : array{
|
||||
if($this->handlerCache->list !== null){
|
||||
return $this->handlerCache->list;
|
||||
}
|
||||
|
||||
$handlerLists = [];
|
||||
for($currentList = $this; $currentList !== null; $currentList = $currentList->parentList){
|
||||
$handlerLists[] = $currentList;
|
||||
}
|
||||
|
||||
$listenersByPriority = [];
|
||||
foreach($handlerLists as $currentList){
|
||||
foreach($currentList->handlerSlots as $priority => $listeners){
|
||||
$listenersByPriority[$priority] = array_merge($listenersByPriority[$priority] ?? [], $listeners);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: why on earth do the priorities have higher values for lower priority?
|
||||
krsort($listenersByPriority, SORT_NUMERIC);
|
||||
|
||||
return $this->handlerCache->list = array_merge(...$listenersByPriority);
|
||||
}
|
||||
}
|
||||
|
@ -57,8 +57,11 @@ class RegisteredListener{
|
||||
return;
|
||||
}
|
||||
$this->timings->startTiming();
|
||||
($this->handler)($event);
|
||||
$this->timings->stopTiming();
|
||||
try{
|
||||
($this->handler)($event);
|
||||
}finally{
|
||||
$this->timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
public function isHandlingCancelled() : bool{
|
||||
|
38
src/event/RegisteredListenerCache.php
Normal file
38
src/event/RegisteredListenerCache.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
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>
|
||||
*/
|
||||
public ?array $list = null;
|
||||
}
|
@ -108,13 +108,23 @@ abstract class BaseInventory implements Inventory{
|
||||
$this->onContentChange($oldContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for utility functions which search the inventory.
|
||||
* TODO: make this abstract instead of providing a slow default implementation (BC break)
|
||||
*/
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$item = $this->getItem($slot);
|
||||
return $item->equals($test, $checkDamage, $checkTags) ? $item->getCount() : 0;
|
||||
}
|
||||
|
||||
public function contains(Item $item) : bool{
|
||||
$count = max(1, $item->getCount());
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
foreach($this->getContents() as $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$count -= $i->getCount();
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags);
|
||||
if($slotCount > 0){
|
||||
$count -= $slotCount;
|
||||
if($count <= 0){
|
||||
return true;
|
||||
}
|
||||
@ -128,9 +138,9 @@ abstract class BaseInventory implements Inventory{
|
||||
$slots = [];
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$slots[$index] = $i;
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){
|
||||
$slots[$i] = $this->getItem($i);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,18 +152,9 @@ abstract class BaseInventory implements Inventory{
|
||||
$checkDamage = $exact || !$item->hasAnyDamageValue();
|
||||
$checkTags = $exact || $item->hasNamedTag();
|
||||
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags) && ($i->getCount() === $count || (!$exact && $i->getCount() > $count))){
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public function firstEmpty() : int{
|
||||
foreach($this->getContents(true) as $i => $slot){
|
||||
if($slot->isNull()){
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, $checkDamage, $checkTags);
|
||||
if($slotCount > 0 && ($slotCount === $count || (!$exact && $slotCount > $count))){
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
@ -161,6 +162,20 @@ abstract class BaseInventory implements Inventory{
|
||||
return -1;
|
||||
}
|
||||
|
||||
public function firstEmpty() : int{
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->isSlotEmpty($i)){
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: make this abstract and force implementations to implement it properly (BC break)
|
||||
* This default implementation works, but is slow.
|
||||
*/
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->getItem($index)->isNull();
|
||||
}
|
||||
@ -171,14 +186,16 @@ abstract class BaseInventory implements Inventory{
|
||||
|
||||
public function getAddableItemQuantity(Item $item) : int{
|
||||
$count = $item->getCount();
|
||||
$maxStackSize = min($this->getMaxStackSize(), $item->getMaxStackSize());
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$slot = $this->getItem($i);
|
||||
if($item->canStackWith($slot)){
|
||||
if(($diff = min($slot->getMaxStackSize(), $item->getMaxStackSize()) - $slot->getCount()) > 0){
|
||||
if($this->isSlotEmpty($i)){
|
||||
$count -= $maxStackSize;
|
||||
}else{
|
||||
$slotCount = $this->getMatchingItemCount($i, $item, true, true);
|
||||
if($slotCount > 0 && ($diff = $maxStackSize - $slotCount) > 0){
|
||||
$count -= $diff;
|
||||
}
|
||||
}elseif($slot->isNull()){
|
||||
$count -= min($this->getMaxStackSize(), $item->getMaxStackSize());
|
||||
}
|
||||
|
||||
if($count <= 0){
|
||||
@ -212,22 +229,29 @@ abstract class BaseInventory implements Inventory{
|
||||
return $returnSlots;
|
||||
}
|
||||
|
||||
private function internalAddItem(Item $slot) : Item{
|
||||
private function internalAddItem(Item $newItem) : Item{
|
||||
$emptySlots = [];
|
||||
|
||||
$maxStackSize = min($this->getMaxStackSize(), $newItem->getMaxStackSize());
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$item = $this->getItem($i);
|
||||
if($item->isNull()){
|
||||
if($this->isSlotEmpty($i)){
|
||||
$emptySlots[] = $i;
|
||||
continue;
|
||||
}
|
||||
$slotCount = $this->getMatchingItemCount($i, $newItem, true, true);
|
||||
if($slotCount === 0){
|
||||
continue;
|
||||
}
|
||||
|
||||
if($slot->canStackWith($item) && $item->getCount() < $item->getMaxStackSize()){
|
||||
$amount = min($item->getMaxStackSize() - $item->getCount(), $slot->getCount(), $this->getMaxStackSize());
|
||||
if($slotCount < $maxStackSize){
|
||||
$amount = min($maxStackSize - $slotCount, $newItem->getCount());
|
||||
if($amount > 0){
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item->setCount($item->getCount() + $amount);
|
||||
$this->setItem($i, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
$newItem->setCount($newItem->getCount() - $amount);
|
||||
$slotItem = $this->getItem($i);
|
||||
$slotItem->setCount($slotItem->getCount() + $amount);
|
||||
$this->setItem($i, $slotItem);
|
||||
if($newItem->getCount() <= 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -236,65 +260,67 @@ abstract class BaseInventory implements Inventory{
|
||||
|
||||
if(count($emptySlots) > 0){
|
||||
foreach($emptySlots as $slotIndex){
|
||||
$amount = min($slot->getMaxStackSize(), $slot->getCount(), $this->getMaxStackSize());
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item = clone $slot;
|
||||
$item->setCount($amount);
|
||||
$this->setItem($slotIndex, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
$amount = min($maxStackSize, $newItem->getCount());
|
||||
$newItem->setCount($newItem->getCount() - $amount);
|
||||
$slotItem = clone $newItem;
|
||||
$slotItem->setCount($amount);
|
||||
$this->setItem($slotIndex, $slotItem);
|
||||
if($newItem->getCount() <= 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $slot;
|
||||
return $newItem;
|
||||
}
|
||||
|
||||
public function remove(Item $item) : void{
|
||||
$checkDamage = !$item->hasAnyDamageValue();
|
||||
$checkTags = $item->hasNamedTag();
|
||||
|
||||
foreach($this->getContents() as $index => $i){
|
||||
if($item->equals($i, $checkDamage, $checkTags)){
|
||||
$this->clear($index);
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; $i++){
|
||||
if($this->getMatchingItemCount($i, $item, $checkDamage, $checkTags) > 0){
|
||||
$this->clear($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeItem(Item ...$slots) : array{
|
||||
/** @var Item[] $itemSlots */
|
||||
/** @var Item[] $searchItems */
|
||||
/** @var Item[] $slots */
|
||||
$itemSlots = [];
|
||||
$searchItems = [];
|
||||
foreach($slots as $slot){
|
||||
if(!$slot->isNull()){
|
||||
$itemSlots[] = clone $slot;
|
||||
$searchItems[] = clone $slot;
|
||||
}
|
||||
}
|
||||
|
||||
for($i = 0, $size = $this->getSize(); $i < $size; ++$i){
|
||||
$item = $this->getItem($i);
|
||||
if($item->isNull()){
|
||||
if($this->isSlotEmpty($i)){
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach($itemSlots as $index => $slot){
|
||||
if($slot->equals($item, !$slot->hasAnyDamageValue(), $slot->hasNamedTag())){
|
||||
$amount = min($item->getCount(), $slot->getCount());
|
||||
$slot->setCount($slot->getCount() - $amount);
|
||||
$item->setCount($item->getCount() - $amount);
|
||||
$this->setItem($i, $item);
|
||||
if($slot->getCount() <= 0){
|
||||
unset($itemSlots[$index]);
|
||||
foreach($searchItems as $index => $search){
|
||||
$slotCount = $this->getMatchingItemCount($i, $search, !$search->hasAnyDamageValue(), $search->hasNamedTag());
|
||||
if($slotCount > 0){
|
||||
$amount = min($slotCount, $search->getCount());
|
||||
$search->setCount($search->getCount() - $amount);
|
||||
|
||||
$slotItem = $this->getItem($i);
|
||||
$slotItem->setCount($slotItem->getCount() - $amount);
|
||||
$this->setItem($i, $slotItem);
|
||||
if($search->getCount() <= 0){
|
||||
unset($searchItems[$index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(count($itemSlots) === 0){
|
||||
if(count($searchItems) === 0){
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $itemSlots;
|
||||
return $searchItems;
|
||||
}
|
||||
|
||||
public function clear(int $index) : void{
|
||||
|
@ -30,6 +30,7 @@ use pocketmine\item\Item;
|
||||
*/
|
||||
class DelegateInventory extends BaseInventory{
|
||||
private InventoryListener $inventoryListener;
|
||||
private bool $backingInventoryChanging = false;
|
||||
|
||||
public function __construct(
|
||||
private Inventory $backingInventory
|
||||
@ -39,12 +40,22 @@ class DelegateInventory extends BaseInventory{
|
||||
$this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener(
|
||||
static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->onSlotChange($slot, $oldItem);
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onSlotChange($slot, $oldItem);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
static function(Inventory $unused, array $oldContents) use ($weakThis) : void{
|
||||
if(($strongThis = $weakThis->get()) !== null){
|
||||
$strongThis->onContentChange($oldContents);
|
||||
$strongThis->backingInventoryChanging = true;
|
||||
try{
|
||||
$strongThis->onContentChange($oldContents);
|
||||
}finally{
|
||||
$strongThis->backingInventoryChanging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
@ -73,4 +84,20 @@ class DelegateInventory extends BaseInventory{
|
||||
protected function internalSetContents(array $items) : void{
|
||||
$this->backingInventory->setContents($items);
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->backingInventory->isSlotEmpty($index);
|
||||
}
|
||||
|
||||
protected function onSlotChange(int $index, Item $before) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onSlotChange($index, $before);
|
||||
}
|
||||
}
|
||||
|
||||
protected function onContentChange(array $itemsBefore) : void{
|
||||
if($this->backingInventoryChanging){
|
||||
parent::onContentChange($itemsBefore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,4 +83,13 @@ class SimpleInventory extends BaseInventory{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMatchingItemCount(int $slot, Item $test, bool $checkDamage, bool $checkTags) : int{
|
||||
$slotItem = $this->slots[$slot];
|
||||
return $slotItem !== null && $slotItem->equals($test, $checkDamage, $checkTags) ? $slotItem->getCount() : 0;
|
||||
}
|
||||
|
||||
public function isSlotEmpty(int $index) : bool{
|
||||
return $this->slots[$index] === null || $this->slots[$index]->isNull();
|
||||
}
|
||||
}
|
||||
|
@ -353,30 +353,35 @@ class Item implements \JsonSerializable{
|
||||
}
|
||||
|
||||
protected function serializeCompoundTag(CompoundTag $tag) : void{
|
||||
$display = $tag->getCompoundTag(self::TAG_DISPLAY) ?? new CompoundTag();
|
||||
$display = $tag->getCompoundTag(self::TAG_DISPLAY);
|
||||
|
||||
$this->hasCustomName() ?
|
||||
$display->setString(self::TAG_DISPLAY_NAME, $this->getCustomName()) :
|
||||
$display->removeTag(self::TAG_DISPLAY_NAME);
|
||||
if($this->customName !== ""){
|
||||
$display ??= new CompoundTag();
|
||||
$display->setString(self::TAG_DISPLAY_NAME, $this->customName);
|
||||
}else{
|
||||
$display?->removeTag(self::TAG_DISPLAY_NAME);
|
||||
}
|
||||
|
||||
if(count($this->lore) > 0){
|
||||
$loreTag = new ListTag();
|
||||
foreach($this->lore as $line){
|
||||
$loreTag->push(new StringTag($line));
|
||||
}
|
||||
$display ??= new CompoundTag();
|
||||
$display->setTag(self::TAG_DISPLAY_LORE, $loreTag);
|
||||
}else{
|
||||
$display->removeTag(self::TAG_DISPLAY_LORE);
|
||||
$display?->removeTag(self::TAG_DISPLAY_LORE);
|
||||
}
|
||||
$display->count() > 0 ?
|
||||
$display !== null && $display->count() > 0 ?
|
||||
$tag->setTag(self::TAG_DISPLAY, $display) :
|
||||
$tag->removeTag(self::TAG_DISPLAY);
|
||||
|
||||
if($this->hasEnchantments()){
|
||||
if(count($this->enchantments) > 0){
|
||||
$ench = new ListTag();
|
||||
foreach($this->getEnchantments() as $enchantmentInstance){
|
||||
$enchantmentIdMap = EnchantmentIdMap::getInstance();
|
||||
foreach($this->enchantments as $enchantmentInstance){
|
||||
$ench->push(CompoundTag::create()
|
||||
->setShort(self::TAG_ENCH_ID, EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType()))
|
||||
->setShort(self::TAG_ENCH_ID, $enchantmentIdMap->toId($enchantmentInstance->getType()))
|
||||
->setShort(self::TAG_ENCH_LVL, $enchantmentInstance->getLevel())
|
||||
);
|
||||
}
|
||||
@ -385,8 +390,8 @@ class Item implements \JsonSerializable{
|
||||
$tag->removeTag(self::TAG_ENCH);
|
||||
}
|
||||
|
||||
($blockData = $this->getCustomBlockData()) !== null ?
|
||||
$tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $blockData) :
|
||||
$this->blockEntityTag !== null ?
|
||||
$tag->setTag(self::TAG_BLOCK_ENTITY_TAG, clone $this->blockEntityTag) :
|
||||
$tag->removeTag(self::TAG_BLOCK_ENTITY_TAG);
|
||||
|
||||
if(count($this->canPlaceOn) > 0){
|
||||
|
@ -205,11 +205,13 @@ class InventoryManager{
|
||||
if($entry === null){
|
||||
return null;
|
||||
}
|
||||
$inventory = $entry->getInventory();
|
||||
$coreSlotId = $entry->mapNetToCore($netSlotId);
|
||||
return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null;
|
||||
return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
|
||||
}
|
||||
if(isset($this->networkIdToInventoryMap[$windowId])){
|
||||
return [$this->networkIdToInventoryMap[$windowId], $netSlotId];
|
||||
$inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
|
||||
if($inventory !== null && $inventory->slotExists($netSlotId)){
|
||||
return [$inventory, $netSlotId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ final class InventoryManagerEntry{
|
||||
public array $itemStackInfos = [];
|
||||
|
||||
/**
|
||||
* @var int[]
|
||||
* @var ItemStack[]
|
||||
* @phpstan-var array<int, ItemStack>
|
||||
*/
|
||||
public array $pendingSyncs = [];
|
||||
|
@ -60,6 +60,7 @@ use pocketmine\network\mcpe\protocol\DisconnectPacket;
|
||||
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
|
||||
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
|
||||
use pocketmine\network\mcpe\protocol\NetworkChunkPublisherUpdatePacket;
|
||||
use pocketmine\network\mcpe\protocol\OpenSignPacket;
|
||||
use pocketmine\network\mcpe\protocol\Packet;
|
||||
use pocketmine\network\mcpe\protocol\PacketDecodeException;
|
||||
use pocketmine\network\mcpe\protocol\PacketPool;
|
||||
@ -106,7 +107,6 @@ use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\Position;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
@ -886,14 +886,26 @@ class NetworkSession{
|
||||
AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER => false,
|
||||
];
|
||||
|
||||
$layers = [
|
||||
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
|
||||
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
|
||||
];
|
||||
if(!$for->hasBlockCollision()){
|
||||
//TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a
|
||||
//block. We can't seem to prevent this short of forcing the player to always fly when block collision is
|
||||
//disabled. Also, for some reason the client always reads flight state from this layer if present, even
|
||||
//though the player isn't in spectator mode.
|
||||
|
||||
$layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
|
||||
AbilitiesLayer::ABILITY_FLYING => true,
|
||||
], null, null);
|
||||
}
|
||||
|
||||
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
|
||||
$isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
|
||||
$isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
|
||||
$for->getId(),
|
||||
[
|
||||
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
|
||||
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
|
||||
]
|
||||
$layers
|
||||
)));
|
||||
}
|
||||
|
||||
@ -987,8 +999,6 @@ class NetworkSession{
|
||||
* @phpstan-param \Closure() : void $onCompletion
|
||||
*/
|
||||
public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{
|
||||
Utils::validateCallableSignature(function() : void{}, $onCompletion);
|
||||
|
||||
$world = $this->player->getLocation()->getWorld();
|
||||
ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve(
|
||||
|
||||
@ -1093,6 +1103,10 @@ class NetworkSession{
|
||||
$this->sendDataPacket(ToastRequestPacket::create($title, $body));
|
||||
}
|
||||
|
||||
public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{
|
||||
$this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
|
||||
}
|
||||
|
||||
public function tick() : void{
|
||||
if(!$this->isConnected()){
|
||||
$this->dispose();
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\compression;
|
||||
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_push;
|
||||
|
||||
class CompressBatchPromise{
|
||||
@ -42,9 +41,6 @@ class CompressBatchPromise{
|
||||
*/
|
||||
public function onResolve(\Closure ...$callbacks) : void{
|
||||
$this->checkCancelled();
|
||||
foreach($callbacks as $callback){
|
||||
Utils::validateCallableSignature(function(CompressBatchPromise $promise) : void{}, $callback);
|
||||
}
|
||||
if($this->result !== null){
|
||||
foreach($callbacks as $callback){
|
||||
$callback($this);
|
||||
|
@ -110,6 +110,12 @@ final class ItemTranslator{
|
||||
//new item without a fixed legacy ID - we can't handle this right now
|
||||
continue;
|
||||
}
|
||||
if(isset($complexMappings[$newId]) && $complexMappings[$newId][0] === $intId && $complexMappings[$newId][1] <= $meta){
|
||||
//TODO: HACK! Multiple legacy ID/meta pairs can be mapped to the same new ID (see minecraft:log)
|
||||
//Assume that the first one is the most relevant for now
|
||||
//However, this could catch fire in the future if this assumption is broken
|
||||
continue;
|
||||
}
|
||||
$complexMappings[$newId] = [$intId, (int) $meta];
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,12 @@ use pocketmine\block\Block;
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\tag\ByteTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\Filesystem;
|
||||
@ -43,8 +48,8 @@ final class RuntimeBlockMapping{
|
||||
private array $legacyToRuntimeMap = [];
|
||||
/** @var int[] */
|
||||
private array $runtimeToLegacyMap = [];
|
||||
/** @var CompoundTag[] */
|
||||
private array $bedrockKnownStates;
|
||||
/** @var CompoundTag[]|null */
|
||||
private ?array $bedrockKnownStates = null;
|
||||
|
||||
private static function make() : self{
|
||||
return new self(
|
||||
@ -53,22 +58,40 @@ final class RuntimeBlockMapping{
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){
|
||||
$stream = new BinaryStream(Filesystem::fileGetContents($canonicalBlockStatesFile));
|
||||
$list = [];
|
||||
$nbtReader = new NetworkNbtSerializer();
|
||||
while(!$stream->feof()){
|
||||
$offset = $stream->getOffset();
|
||||
$blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
|
||||
$stream->setOffset($offset);
|
||||
$list[] = $blockState;
|
||||
/**
|
||||
* @param string[] $keyIndex
|
||||
* @param (ByteTag|StringTag|IntTag)[][] $valueIndex
|
||||
* @phpstan-param array<string, string> $keyIndex
|
||||
* @phpstan-param array<int, array<int|string, ByteTag|IntTag|StringTag>> $valueIndex
|
||||
*/
|
||||
private static function deduplicateCompound(CompoundTag $tag, array &$keyIndex, array &$valueIndex) : CompoundTag{
|
||||
if($tag->count() === 0){
|
||||
return $tag;
|
||||
}
|
||||
$this->bedrockKnownStates = $list;
|
||||
|
||||
$this->setupLegacyMappings($r12ToCurrentBlockMapFile);
|
||||
$newTag = CompoundTag::create();
|
||||
foreach($tag as $key => $value){
|
||||
$key = $keyIndex[$key] ??= $key;
|
||||
|
||||
if($value instanceof CompoundTag){
|
||||
$value = $valueIndex[$value->getType()][(new LittleEndianNbtSerializer())->write(new TreeRoot($value))] ??= self::deduplicateCompound($value, $keyIndex, $valueIndex);
|
||||
}elseif($value instanceof ByteTag || $value instanceof IntTag || $value instanceof StringTag){
|
||||
$value = $valueIndex[$value->getType()][$value->getValue()] ??= $value;
|
||||
}
|
||||
|
||||
$newTag->setTag($key, $value);
|
||||
}
|
||||
|
||||
return $newTag;
|
||||
}
|
||||
|
||||
private function setupLegacyMappings(string $r12ToCurrentBlockMapFile) : void{
|
||||
public function __construct(
|
||||
private string $canonicalBlockStatesFile,
|
||||
string $r12ToCurrentBlockMapFile
|
||||
){
|
||||
//do not cache this - we only need it to set up mappings under normal circumstances
|
||||
$bedrockKnownStates = $this->loadBedrockKnownStates();
|
||||
|
||||
$legacyIdMap = LegacyBlockIdToStringIdMap::getInstance();
|
||||
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
|
||||
$legacyStateMap = [];
|
||||
@ -88,7 +111,7 @@ final class RuntimeBlockMapping{
|
||||
* @var int[][] $idToStatesMap string id -> int[] list of candidate state indices
|
||||
*/
|
||||
$idToStatesMap = [];
|
||||
foreach($this->bedrockKnownStates as $k => $state){
|
||||
foreach($bedrockKnownStates as $k => $state){
|
||||
$idToStatesMap[$state->getString("name")][] = $k;
|
||||
}
|
||||
foreach($legacyStateMap as $pair){
|
||||
@ -107,7 +130,7 @@ final class RuntimeBlockMapping{
|
||||
throw new \RuntimeException("Mapped new state does not appear in network table");
|
||||
}
|
||||
foreach($idToStatesMap[$mappedName] as $k){
|
||||
$networkState = $this->bedrockKnownStates[$k];
|
||||
$networkState = $bedrockKnownStates[$k];
|
||||
if($mappedState->equals($networkState)){
|
||||
$this->registerMapping($k, $id, $data);
|
||||
continue 2;
|
||||
@ -117,6 +140,25 @@ final class RuntimeBlockMapping{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CompoundTag[]
|
||||
*/
|
||||
private function loadBedrockKnownStates() : array{
|
||||
$stream = new BinaryStream(Filesystem::fileGetContents($this->canonicalBlockStatesFile));
|
||||
$list = [];
|
||||
$nbtReader = new NetworkNbtSerializer();
|
||||
|
||||
$keyIndex = [];
|
||||
$valueIndex = [];
|
||||
while(!$stream->feof()){
|
||||
$offset = $stream->getOffset();
|
||||
$blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
|
||||
$stream->setOffset($offset);
|
||||
$list[] = self::deduplicateCompound($blockState, $keyIndex, $valueIndex);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function toRuntimeId(int $internalStateId) : int{
|
||||
return $this->legacyToRuntimeMap[$internalStateId] ?? $this->legacyToRuntimeMap[BlockLegacyIds::INFO_UPDATE << Block::INTERNAL_METADATA_BITS];
|
||||
}
|
||||
@ -131,9 +173,14 @@ final class RuntimeBlockMapping{
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: This method may load the palette from disk, which is a slow operation.
|
||||
* Afterwards, it will cache the palette in memory, which requires (in some cases) tens of MB of memory.
|
||||
* Avoid using this where possible.
|
||||
*
|
||||
* @deprecated
|
||||
* @return CompoundTag[]
|
||||
*/
|
||||
public function getBedrockKnownStates() : array{
|
||||
return $this->bedrockKnownStates;
|
||||
return $this->bedrockKnownStates ??= $this->loadBedrockKnownStates();
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\item\Durable;
|
||||
use pocketmine\item\Item;
|
||||
@ -135,14 +136,16 @@ class TypeConverter{
|
||||
if($itemStack->isNull()){
|
||||
return ItemStack::null();
|
||||
}
|
||||
$nbt = null;
|
||||
if($itemStack->hasNamedTag()){
|
||||
$nbt = clone $itemStack->getNamedTag();
|
||||
$nbt = $itemStack->getNamedTag();
|
||||
if($nbt->count() === 0){
|
||||
$nbt = null;
|
||||
}else{
|
||||
$nbt = clone $nbt;
|
||||
}
|
||||
|
||||
$isBlockItem = $itemStack->getId() < 256;
|
||||
|
||||
$idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($itemStack->getId(), $itemStack->getMeta());
|
||||
$internalId = $itemStack->getId();
|
||||
$internalMeta = $itemStack->getMeta();
|
||||
$idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($internalId, $internalMeta);
|
||||
if($idMeta === null){
|
||||
//Display unmapped items as INFO_UPDATE, but stick something in their NBT to make sure they don't stack with
|
||||
//other unmapped items.
|
||||
@ -150,8 +153,8 @@ class TypeConverter{
|
||||
if($nbt === null){
|
||||
$nbt = new CompoundTag();
|
||||
}
|
||||
$nbt->setInt(self::PM_ID_TAG, $itemStack->getId());
|
||||
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
|
||||
$nbt->setInt(self::PM_ID_TAG, $internalId);
|
||||
$nbt->setInt(self::PM_META_TAG, $internalMeta);
|
||||
}else{
|
||||
[$id, $meta] = $idMeta;
|
||||
|
||||
@ -166,23 +169,15 @@ class TypeConverter{
|
||||
}
|
||||
$nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage());
|
||||
$meta = 0;
|
||||
}elseif($isBlockItem && $itemStack->getMeta() !== 0){
|
||||
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
|
||||
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
|
||||
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
|
||||
if($nbt === null){
|
||||
$nbt = new CompoundTag();
|
||||
}
|
||||
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
|
||||
$meta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
$blockRuntimeId = 0;
|
||||
if($isBlockItem){
|
||||
if($internalId < 256){
|
||||
$block = $itemStack->getBlock();
|
||||
if($block->getId() !== BlockLegacyIds::AIR){
|
||||
$blockRuntimeId = RuntimeBlockMapping::getInstance()->toRuntimeId($block->getFullId());
|
||||
$meta = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,6 +203,11 @@ class TypeConverter{
|
||||
$compound = $itemStack->getNbt();
|
||||
|
||||
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta());
|
||||
if($itemStack->getBlockRuntimeId() !== 0){
|
||||
//blockitem meta is zeroed out by the client, so we have to infer it from the block runtime ID
|
||||
$blockFullId = RuntimeBlockMapping::getInstance()->fromRuntimeId($itemStack->getBlockRuntimeId());
|
||||
$meta = $blockFullId & Block::INTERNAL_METADATA_MASK;
|
||||
}
|
||||
|
||||
if($compound !== null){
|
||||
$compound = clone $compound;
|
||||
@ -222,12 +222,6 @@ class TypeConverter{
|
||||
$compound->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION);
|
||||
$compound->setTag(self::DAMAGE_TAG, $conflicted);
|
||||
}
|
||||
}elseif(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){
|
||||
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
|
||||
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
|
||||
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
|
||||
$meta = $metaTag->getValue();
|
||||
$compound->removeTag(self::PM_META_TAG);
|
||||
}
|
||||
if($compound->count() === 0){
|
||||
$compound = null;
|
||||
|
@ -328,6 +328,9 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(count($packet->trData->getActions()) > 50){
|
||||
throw new PacketHandlingException("Too many actions in inventory transaction");
|
||||
}
|
||||
if(count($packet->requestChangedSlots) > 10){
|
||||
throw new PacketHandlingException("Too many slot sync requests in inventory transaction");
|
||||
}
|
||||
|
||||
$this->inventoryManager->setCurrentItemStackRequestId($packet->requestId);
|
||||
$this->inventoryManager->addRawPredictedSlotChanges($packet->trData->getActions());
|
||||
@ -347,6 +350,21 @@ class InGamePacketHandler extends PacketHandler{
|
||||
}
|
||||
|
||||
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
|
||||
|
||||
//requestChangedSlots asks the server to always send out the contents of the specified slots, even if they
|
||||
//haven't changed. Handling these is necessary to ensure the client inventory stays in sync if the server
|
||||
//rejects the transaction. The most common example of this is equipping armor by right-click, which doesn't send
|
||||
//a legacy prediction action for the destination armor slot.
|
||||
foreach($packet->requestChangedSlots as $containerInfo){
|
||||
foreach($containerInfo->getChangedSlotIndexes() as $netSlot){
|
||||
[$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot);
|
||||
$inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
|
||||
if($inventoryAndSlot !== null){ //trigger the normal slot sync logic
|
||||
$this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->inventoryManager->setCurrentItemStackRequestId(null);
|
||||
return $result;
|
||||
}
|
||||
@ -730,7 +748,7 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
|
||||
|
||||
if($block instanceof BaseSign){
|
||||
if(($textBlobTag = $nbt->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
|
||||
if(($textBlobTag = $nbt->getCompoundTag(Sign::TAG_FRONT_TEXT)?->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
|
||||
try{
|
||||
$text = SignText::fromBlob($textBlobTag->getValue());
|
||||
}catch(\InvalidArgumentException $e){
|
||||
|
@ -33,15 +33,21 @@ final class ItemStackContainerIdTranslator{
|
||||
//NOOP
|
||||
}
|
||||
|
||||
public static function translate(int $containerInterfaceId, int $currentWindowId) : int{
|
||||
/**
|
||||
* @return int[]
|
||||
* @phpstan-return array{int, int}
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
public static function translate(int $containerInterfaceId, int $currentWindowId, int $slotId) : array{
|
||||
return match($containerInterfaceId){
|
||||
ContainerUIIds::ARMOR => ContainerIds::ARMOR,
|
||||
ContainerUIIds::ARMOR => [ContainerIds::ARMOR, $slotId],
|
||||
|
||||
ContainerUIIds::HOTBAR,
|
||||
ContainerUIIds::INVENTORY,
|
||||
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => ContainerIds::INVENTORY,
|
||||
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => [ContainerIds::INVENTORY, $slotId],
|
||||
|
||||
ContainerUIIds::OFFHAND => ContainerIds::OFFHAND,
|
||||
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70 (though this doesn't really matter since the offhand has only 1 slot anyway)
|
||||
ContainerUIIds::OFFHAND => [ContainerIds::OFFHAND, 0],
|
||||
|
||||
ContainerUIIds::ANVIL_INPUT,
|
||||
ContainerUIIds::ANVIL_MATERIAL,
|
||||
@ -68,7 +74,7 @@ final class ItemStackContainerIdTranslator{
|
||||
ContainerUIIds::TRADE2_INGREDIENT1,
|
||||
ContainerUIIds::TRADE2_INGREDIENT2,
|
||||
ContainerUIIds::TRADE_INGREDIENT1,
|
||||
ContainerUIIds::TRADE_INGREDIENT2 => ContainerIds::UI,
|
||||
ContainerUIIds::TRADE_INGREDIENT2 => [ContainerIds::UI, $slotId],
|
||||
|
||||
ContainerUIIds::BARREL,
|
||||
ContainerUIIds::BLAST_FURNACE_INGREDIENT,
|
||||
@ -80,7 +86,7 @@ final class ItemStackContainerIdTranslator{
|
||||
ContainerUIIds::FURNACE_RESULT,
|
||||
ContainerUIIds::LEVEL_ENTITY, //chest
|
||||
ContainerUIIds::SHULKER_BOX,
|
||||
ContainerUIIds::SMOKER_INGREDIENT => $currentWindowId,
|
||||
ContainerUIIds::SMOKER_INGREDIENT => [$currentWindowId, $slotId],
|
||||
|
||||
//all preview slots are ignored, since the client shouldn't be modifying those directly
|
||||
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\handler;
|
||||
|
||||
use pocketmine\crafting\CraftingGrid;
|
||||
use pocketmine\inventory\CreativeInventory;
|
||||
use pocketmine\inventory\Inventory;
|
||||
use pocketmine\inventory\transaction\action\CreateItemAction;
|
||||
@ -112,12 +111,7 @@ class ItemStackRequestExecutor{
|
||||
* @throws ItemStackRequestProcessException
|
||||
*/
|
||||
protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
|
||||
$windowId = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId());
|
||||
$slotId = $info->getSlotId();
|
||||
if($info->getContainerId() === ContainerUIIds::OFFHAND && $slotId === 1){
|
||||
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70
|
||||
$slotId = 0;
|
||||
}
|
||||
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $info->getSlotId());
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerId() . ", slot ID: " . $info->getSlotId());
|
||||
@ -246,13 +240,11 @@ class ItemStackRequestExecutor{
|
||||
|
||||
$this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
|
||||
|
||||
$currentWindow = $this->player->getCurrentWindow();
|
||||
if($currentWindow !== null && !($currentWindow instanceof CraftingGrid)){
|
||||
throw new ItemStackRequestProcessException("Player's current window is not a crafting grid");
|
||||
}
|
||||
$craftingGrid = $currentWindow ?? $this->player->getCraftingGrid();
|
||||
|
||||
$craftingResults = $recipe->getResultsFor($craftingGrid);
|
||||
//TODO: Since the system assumes that crafting can only be done in the crafting grid, we have to give it a
|
||||
//crafting grid to make the API happy. No implementation of getResultsFor() actually uses the crafting grid
|
||||
//right now, so this will work, but this will become a problem in the future for things like shulker boxes and
|
||||
//custom crafting recipes.
|
||||
$craftingResults = $recipe->getResultsFor($this->player->getCraftingGrid());
|
||||
foreach($craftingResults as $k => $craftingResult){
|
||||
$craftingResult->setCount($craftingResult->getCount() * $repetitions);
|
||||
$this->craftingResults[$k] = $craftingResult;
|
||||
@ -284,12 +276,12 @@ class ItemStackRequestExecutor{
|
||||
}
|
||||
|
||||
$this->createdItemsTakenCount += $count;
|
||||
$createdItem = clone $createdItem;
|
||||
$createdItem->setCount($count);
|
||||
$takenItem = clone $createdItem;
|
||||
$takenItem->setCount($count);
|
||||
if(!$this->createdItemFromCreativeInventory && $this->createdItemsTakenCount >= $createdItem->getCount()){
|
||||
$this->setNextCreatedItem(null);
|
||||
}
|
||||
return $createdItem;
|
||||
return $takenItem;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,11 +53,7 @@ final class ItemStackResponseBuilder{
|
||||
* @phpstan-return array{Inventory, int}
|
||||
*/
|
||||
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
|
||||
if($containerInterfaceId === ContainerUIIds::OFFHAND && $slotId === 1){
|
||||
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70
|
||||
$slotId = 0;
|
||||
}
|
||||
$windowId = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId());
|
||||
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId(), $slotId);
|
||||
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
|
||||
if($windowAndSlot === null){
|
||||
return null;
|
||||
|
@ -44,6 +44,7 @@ use pocketmine\network\mcpe\protocol\types\PlayerMovementType;
|
||||
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\VersionInfo;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function sprintf;
|
||||
@ -60,89 +61,95 @@ class PreSpawnPacketHandler extends PacketHandler{
|
||||
){}
|
||||
|
||||
public function setUp() : void{
|
||||
$location = $this->player->getLocation();
|
||||
$world = $location->getWorld();
|
||||
Timings::$playerNetworkSendPreSpawnGameData->startTiming();
|
||||
try{
|
||||
$location = $this->player->getLocation();
|
||||
$world = $location->getWorld();
|
||||
|
||||
$this->session->getLogger()->debug("Preparing StartGamePacket");
|
||||
$levelSettings = new LevelSettings();
|
||||
$levelSettings->seed = -1;
|
||||
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
|
||||
$levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
|
||||
$levelSettings->difficulty = $world->getDifficulty();
|
||||
$levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation());
|
||||
$levelSettings->hasAchievementsDisabled = true;
|
||||
$levelSettings->time = $world->getTime();
|
||||
$levelSettings->eduEditionOffer = 0;
|
||||
$levelSettings->rainLevel = 0; //TODO: implement these properly
|
||||
$levelSettings->lightningLevel = 0;
|
||||
$levelSettings->commandsEnabled = true;
|
||||
$levelSettings->gameRules = [
|
||||
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
|
||||
];
|
||||
$levelSettings->experiments = new Experiments([], false);
|
||||
$this->session->getLogger()->debug("Preparing StartGamePacket");
|
||||
$levelSettings = new LevelSettings();
|
||||
$levelSettings->seed = -1;
|
||||
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
|
||||
$levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
|
||||
$levelSettings->difficulty = $world->getDifficulty();
|
||||
$levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation());
|
||||
$levelSettings->hasAchievementsDisabled = true;
|
||||
$levelSettings->time = $world->getTime();
|
||||
$levelSettings->eduEditionOffer = 0;
|
||||
$levelSettings->rainLevel = 0; //TODO: implement these properly
|
||||
$levelSettings->lightningLevel = 0;
|
||||
$levelSettings->commandsEnabled = true;
|
||||
$levelSettings->gameRules = [
|
||||
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
|
||||
];
|
||||
$levelSettings->experiments = new Experiments([], false);
|
||||
|
||||
$this->session->sendDataPacket(StartGamePacket::create(
|
||||
$this->player->getId(),
|
||||
$this->player->getId(),
|
||||
TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
|
||||
$this->player->getOffsetPosition($location),
|
||||
$location->pitch,
|
||||
$location->yaw,
|
||||
new CacheableNbt(CompoundTag::create()), //TODO: we don't care about this right now
|
||||
$levelSettings,
|
||||
"",
|
||||
$this->server->getMotd(),
|
||||
"",
|
||||
false,
|
||||
new PlayerMovementSettings(PlayerMovementType::SERVER_AUTHORITATIVE_V1, 0, false),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||
Uuid::fromString(Uuid::NIL),
|
||||
false,
|
||||
[],
|
||||
0,
|
||||
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
|
||||
));
|
||||
$this->session->sendDataPacket(StartGamePacket::create(
|
||||
$this->player->getId(),
|
||||
$this->player->getId(),
|
||||
TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
|
||||
$this->player->getOffsetPosition($location),
|
||||
$location->pitch,
|
||||
$location->yaw,
|
||||
new CacheableNbt(CompoundTag::create()), //TODO: we don't care about this right now
|
||||
$levelSettings,
|
||||
"",
|
||||
$this->server->getMotd(),
|
||||
"",
|
||||
false,
|
||||
new PlayerMovementSettings(PlayerMovementType::SERVER_AUTHORITATIVE_V1, 0, false),
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
|
||||
Uuid::fromString(Uuid::NIL),
|
||||
false,
|
||||
false,
|
||||
[],
|
||||
0,
|
||||
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
|
||||
));
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor identifiers");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
|
||||
$this->session->getLogger()->debug("Sending actor identifiers");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
|
||||
|
||||
$this->session->getLogger()->debug("Sending biome definitions");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
|
||||
$this->session->getLogger()->debug("Sending biome definitions");
|
||||
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
|
||||
|
||||
$this->session->getLogger()->debug("Sending attributes");
|
||||
$this->session->getEntityEventBroadcaster()->syncAttributes([$this->session], $this->player, $this->player->getAttributeMap()->getAll());
|
||||
$this->session->getLogger()->debug("Sending attributes");
|
||||
$this->session->getEntityEventBroadcaster()->syncAttributes([$this->session], $this->player, $this->player->getAttributeMap()->getAll());
|
||||
|
||||
$this->session->getLogger()->debug("Sending available commands");
|
||||
$this->session->syncAvailableCommands();
|
||||
$this->session->getLogger()->debug("Sending available commands");
|
||||
$this->session->syncAvailableCommands();
|
||||
|
||||
$this->session->getLogger()->debug("Sending abilities");
|
||||
$this->session->syncAbilities($this->player);
|
||||
$this->session->syncAdventureSettings();
|
||||
$this->session->getLogger()->debug("Sending abilities");
|
||||
$this->session->syncAbilities($this->player);
|
||||
$this->session->syncAdventureSettings();
|
||||
|
||||
$this->session->getLogger()->debug("Sending effects");
|
||||
foreach($this->player->getEffects()->all() as $effect){
|
||||
$this->session->getEntityEventBroadcaster()->onEntityEffectAdded([$this->session], $this->player, $effect, false);
|
||||
$this->session->getLogger()->debug("Sending effects");
|
||||
foreach($this->player->getEffects()->all() as $effect){
|
||||
$this->session->getEntityEventBroadcaster()->onEntityEffectAdded([$this->session], $this->player, $effect, false);
|
||||
}
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor metadata");
|
||||
$this->player->sendData([$this->player]);
|
||||
|
||||
$this->session->getLogger()->debug("Sending inventory");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->syncSelectedHotbarSlot();
|
||||
|
||||
$this->session->getLogger()->debug("Sending creative inventory data");
|
||||
$this->inventoryManager->syncCreative();
|
||||
|
||||
$this->session->getLogger()->debug("Sending crafting data");
|
||||
$this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager()));
|
||||
|
||||
$this->session->getLogger()->debug("Sending player list");
|
||||
$this->session->syncPlayerList($this->server->getOnlinePlayers());
|
||||
}finally{
|
||||
Timings::$playerNetworkSendPreSpawnGameData->stopTiming();
|
||||
}
|
||||
|
||||
$this->session->getLogger()->debug("Sending actor metadata");
|
||||
$this->player->sendData([$this->player]);
|
||||
|
||||
$this->session->getLogger()->debug("Sending inventory");
|
||||
$this->inventoryManager->syncAll();
|
||||
$this->inventoryManager->syncSelectedHotbarSlot();
|
||||
|
||||
$this->session->getLogger()->debug("Sending creative inventory data");
|
||||
$this->inventoryManager->syncCreative();
|
||||
|
||||
$this->session->getLogger()->debug("Sending crafting data");
|
||||
$this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager()));
|
||||
|
||||
$this->session->getLogger()->debug("Sending player list");
|
||||
$this->session->syncPlayerList($this->server->getOnlinePlayers());
|
||||
}
|
||||
|
||||
public function handleRequestChunkRadius(RequestChunkRadiusPacket $packet) : bool{
|
||||
|
@ -53,6 +53,7 @@ use function implode;
|
||||
use function mt_rand;
|
||||
use function random_bytes;
|
||||
use function rtrim;
|
||||
use function str_split;
|
||||
use function substr;
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
@ -196,7 +197,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
|
||||
try{
|
||||
$session->handleEncoded($buf);
|
||||
}catch(PacketHandlingException $e){
|
||||
$errorId = bin2hex(random_bytes(6));
|
||||
$errorId = implode("-", str_split(bin2hex(random_bytes(6)), 4));
|
||||
|
||||
$logger = $session->getLogger();
|
||||
$logger->error("Bad packet (error ID $errorId): " . $e->getMessage());
|
||||
|
@ -54,23 +54,23 @@ final class ChunkSelector{
|
||||
//If the chunk is in the radius, others at the same offsets in different quadrants are also guaranteed to be.
|
||||
|
||||
/* Top right quadrant */
|
||||
yield World::chunkHash($centerX + $x, $centerZ + $z);
|
||||
yield $subRadius => World::chunkHash($centerX + $x, $centerZ + $z);
|
||||
/* Top left quadrant */
|
||||
yield World::chunkHash($centerX - $x - 1, $centerZ + $z);
|
||||
yield $subRadius => World::chunkHash($centerX - $x - 1, $centerZ + $z);
|
||||
/* Bottom right quadrant */
|
||||
yield World::chunkHash($centerX + $x, $centerZ - $z - 1);
|
||||
yield $subRadius => World::chunkHash($centerX + $x, $centerZ - $z - 1);
|
||||
/* Bottom left quadrant */
|
||||
yield World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
|
||||
yield $subRadius => World::chunkHash($centerX - $x - 1, $centerZ - $z - 1);
|
||||
|
||||
if($x !== $z){
|
||||
/* Top right quadrant mirror */
|
||||
yield World::chunkHash($centerX + $z, $centerZ + $x);
|
||||
yield $subRadius => World::chunkHash($centerX + $z, $centerZ + $x);
|
||||
/* Top left quadrant mirror */
|
||||
yield World::chunkHash($centerX - $z - 1, $centerZ + $x);
|
||||
yield $subRadius => World::chunkHash($centerX - $z - 1, $centerZ + $x);
|
||||
/* Bottom right quadrant mirror */
|
||||
yield World::chunkHash($centerX + $z, $centerZ - $x - 1);
|
||||
yield $subRadius => World::chunkHash($centerX + $z, $centerZ - $x - 1);
|
||||
/* Bottom left quadrant mirror */
|
||||
yield World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
|
||||
yield $subRadius => World::chunkHash($centerX - $z - 1, $centerZ - $x - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\player;
|
||||
|
||||
use pocketmine\block\BaseSign;
|
||||
use pocketmine\block\Bed;
|
||||
use pocketmine\block\BlockLegacyIds;
|
||||
use pocketmine\block\UnknownBlock;
|
||||
@ -121,6 +122,8 @@ use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\world\ChunkListener;
|
||||
use pocketmine\world\ChunkListenerNoOpTrait;
|
||||
use pocketmine\world\ChunkLoader;
|
||||
use pocketmine\world\ChunkTicker;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\EntityAttackNoDamageSound;
|
||||
@ -237,12 +240,16 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
protected array $loadQueue = [];
|
||||
protected int $nextChunkOrderRun = 5;
|
||||
|
||||
/** @var true[] */
|
||||
private array $tickingChunks = [];
|
||||
|
||||
protected int $viewDistance = -1;
|
||||
protected int $spawnThreshold;
|
||||
protected int $spawnChunkLoadCount = 0;
|
||||
protected int $chunksPerTick;
|
||||
protected ChunkSelector $chunkSelector;
|
||||
protected PlayerChunkLoader $chunkLoader;
|
||||
protected ChunkLoader $chunkLoader;
|
||||
protected ChunkTicker $chunkTicker;
|
||||
|
||||
/** @var bool[] map: raw UUID (string) => bool */
|
||||
protected array $hiddenPlayers = [];
|
||||
@ -308,8 +315,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->spawnThreshold = (int) (($this->server->getConfigGroup()->getPropertyInt("chunk-sending.spawn-radius", 4) ** 2) * M_PI);
|
||||
$this->chunkSelector = new ChunkSelector();
|
||||
|
||||
$this->chunkLoader = new PlayerChunkLoader($spawnLocation);
|
||||
|
||||
$this->chunkLoader = new class implements ChunkLoader{};
|
||||
$this->chunkTicker = new ChunkTicker();
|
||||
$world = $spawnLocation->getWorld();
|
||||
//load the spawn chunk so we can see the terrain
|
||||
$xSpawnChunk = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
|
||||
@ -747,6 +754,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$world->unregisterChunkLoader($this->chunkLoader, $x, $z);
|
||||
$world->unregisterChunkListener($this, $x, $z);
|
||||
unset($this->loadQueue[$index]);
|
||||
$world->unregisterTickingChunk($this->chunkTicker, $x, $z);
|
||||
unset($this->tickingChunks[$index]);
|
||||
}
|
||||
|
||||
protected function spawnEntitiesOnAllChunks() : void{
|
||||
@ -798,6 +807,9 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
unset($this->loadQueue[$index]);
|
||||
$this->getWorld()->registerChunkLoader($this->chunkLoader, $X, $Z, true);
|
||||
$this->getWorld()->registerChunkListener($this, $X, $Z);
|
||||
if(isset($this->tickingChunks[$index])){
|
||||
$this->getWorld()->registerTickingChunk($this->chunkTicker, $X, $Z);
|
||||
}
|
||||
|
||||
$this->getWorld()->requestChunkPopulation($X, $Z, $this->chunkLoader)->onCompletion(
|
||||
function() use ($X, $Z, $index, $world) : void{
|
||||
@ -883,6 +895,31 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param true[] $oldTickingChunks
|
||||
* @param true[] $newTickingChunks
|
||||
*
|
||||
* @phpstan-param array<int, true> $oldTickingChunks
|
||||
* @phpstan-param array<int, true> $newTickingChunks
|
||||
*/
|
||||
private function updateTickingChunkRegistrations(array $oldTickingChunks, array $newTickingChunks) : void{
|
||||
$world = $this->getWorld();
|
||||
foreach($oldTickingChunks as $hash => $_){
|
||||
if(!isset($newTickingChunks[$hash]) && !isset($this->loadQueue[$hash])){
|
||||
//we are (probably) still using this chunk, but it's no longer within ticking range
|
||||
World::getXZ($hash, $tickingChunkX, $tickingChunkZ);
|
||||
$world->unregisterTickingChunk($this->chunkTicker, $tickingChunkX, $tickingChunkZ);
|
||||
}
|
||||
}
|
||||
foreach($newTickingChunks as $hash => $_){
|
||||
if(!isset($oldTickingChunks[$hash]) && !isset($this->loadQueue[$hash])){
|
||||
//we were already using this chunk, but it is now within ticking range
|
||||
World::getXZ($hash, $tickingChunkX, $tickingChunkZ);
|
||||
$world->registerTickingChunk($this->chunkTicker, $tickingChunkX, $tickingChunkZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates which new chunks this player needs to use, and which currently-used chunks it needs to stop using.
|
||||
* This is based on factors including the player's current render radius and current position.
|
||||
@ -895,16 +932,23 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
Timings::$playerChunkOrder->startTiming();
|
||||
|
||||
$newOrder = [];
|
||||
$tickingChunks = [];
|
||||
$unloadChunks = $this->usedChunks;
|
||||
|
||||
$world = $this->getWorld();
|
||||
$tickingChunkRadius = $world->getChunkTickRadius();
|
||||
|
||||
foreach($this->chunkSelector->selectChunks(
|
||||
$this->server->getAllowedViewDistance($this->viewDistance),
|
||||
$this->location->getFloorX() >> Chunk::COORD_BIT_SIZE,
|
||||
$this->location->getFloorZ() >> Chunk::COORD_BIT_SIZE
|
||||
) as $hash){
|
||||
) as $radius => $hash){
|
||||
if(!isset($this->usedChunks[$hash]) || $this->usedChunks[$hash]->equals(UsedChunkStatus::NEEDED())){
|
||||
$newOrder[$hash] = true;
|
||||
}
|
||||
if($radius < $tickingChunkRadius){
|
||||
$tickingChunks[$hash] = true;
|
||||
}
|
||||
unset($unloadChunks[$hash]);
|
||||
}
|
||||
|
||||
@ -914,8 +958,11 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
|
||||
$this->loadQueue = $newOrder;
|
||||
|
||||
$this->updateTickingChunkRegistrations($this->tickingChunks, $tickingChunks);
|
||||
$this->tickingChunks = $tickingChunks;
|
||||
|
||||
if(count($this->loadQueue) > 0 || count($unloadChunks) > 0){
|
||||
$this->chunkLoader->setCurrentLocation($this->location);
|
||||
$this->getNetworkSession()->syncViewAreaCenterPoint($this->location, $this->viewDistance);
|
||||
}
|
||||
|
||||
@ -1144,7 +1191,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
* TODO: make this a dynamic ability instead of being hardcoded
|
||||
*/
|
||||
public function hasFiniteResources() : bool{
|
||||
return $this->gamemode->equals(GameMode::SURVIVAL()) || $this->gamemode->equals(GameMode::ADVENTURE());
|
||||
return !$this->gamemode->equals(GameMode::CREATIVE());
|
||||
}
|
||||
|
||||
public function isFireProof() : bool{
|
||||
@ -1610,7 +1657,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
$ev = new PlayerBlockPickEvent($this, $block, $item);
|
||||
$existingSlot = $this->inventory->first($item);
|
||||
if($existingSlot === -1 && ($this->hasFiniteResources() || $this->isSpectator())){
|
||||
if($existingSlot === -1 && $this->hasFiniteResources()){
|
||||
$ev->cancel();
|
||||
}
|
||||
$ev->call();
|
||||
@ -2580,6 +2627,20 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
$this->permanentWindows = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the player's sign editor GUI for the sign at the given position.
|
||||
* TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations)
|
||||
*/
|
||||
public function openSignEditor(Vector3 $position) : void{
|
||||
$block = $this->getWorld()->getBlock($position);
|
||||
if($block instanceof BaseSign){
|
||||
$this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId()));
|
||||
$this->getNetworkSession()->onOpenSignEditor($position, true);
|
||||
}else{
|
||||
throw new \InvalidArgumentException("Block at this position is not a sign");
|
||||
}
|
||||
}
|
||||
|
||||
use ChunkListenerNoOpTrait {
|
||||
onChunkChanged as private;
|
||||
onChunkUnloaded as private;
|
||||
|
@ -26,6 +26,10 @@ namespace pocketmine\player;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\world\TickingChunkLoader;
|
||||
|
||||
/**
|
||||
* @deprecated This class was only needed to implement TickingChunkLoader, which is now deprecated.
|
||||
* ChunkTicker should be registered on ticking chunks to make them tick instead.
|
||||
*/
|
||||
final class PlayerChunkLoader implements TickingChunkLoader{
|
||||
public function __construct(private Vector3 $currentLocation){}
|
||||
|
||||
|
@ -37,7 +37,7 @@ use pocketmine\permission\DefaultPermissions;
|
||||
use pocketmine\permission\PermissionManager;
|
||||
use pocketmine\permission\PermissionParser;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\TimingsHandler;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Utils;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
@ -651,7 +651,7 @@ class PluginManager{
|
||||
throw new PluginException("Plugin attempted to register event handler " . $handlerName . "() to event " . $event . " while not enabled");
|
||||
}
|
||||
|
||||
$timings = new TimingsHandler("Plugin: " . $plugin->getDescription()->getFullName() . " Event: " . $handlerName . "(" . (new \ReflectionClass($event))->getShortName() . ")");
|
||||
$timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
|
||||
|
||||
$registeredListener = new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
|
||||
HandlerListManager::global()->getListFor($event)->register($registeredListener);
|
||||
|
@ -247,7 +247,6 @@ class AsyncPool{
|
||||
while(!$queue->isEmpty()){
|
||||
/** @var AsyncTask $task */
|
||||
$task = $queue->bottom();
|
||||
$task->checkProgressUpdates();
|
||||
if($task->isFinished()){ //make sure the task actually executed before trying to collect
|
||||
$queue->dequeue();
|
||||
|
||||
@ -268,6 +267,7 @@ class AsyncPool{
|
||||
$task->onCompletion();
|
||||
}
|
||||
}else{
|
||||
$task->checkProgressUpdates();
|
||||
$more = true;
|
||||
break; //current task is still running, skip to next worker
|
||||
}
|
||||
|
@ -25,13 +25,22 @@ namespace pocketmine\timings;
|
||||
|
||||
use pocketmine\block\tile\Tile;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Event;
|
||||
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\scheduler\TaskHandler;
|
||||
use function get_class;
|
||||
use function str_starts_with;
|
||||
|
||||
abstract class Timings{
|
||||
/**
|
||||
* @deprecated This was used by the old timings viewer to make a timer appear in the Breakdown section of a timings
|
||||
* report. Provide a group to your timer's constructor instead.
|
||||
* @see Timings::GROUP_BREAKDOWN
|
||||
*/
|
||||
public const INCLUDED_BY_OTHER_TIMINGS_PREFIX = "** ";
|
||||
public const GROUP_BREAKDOWN = "Minecraft - Breakdown";
|
||||
|
||||
private static bool $initialized = false;
|
||||
|
||||
@ -59,6 +68,7 @@ abstract class Timings{
|
||||
public static $playerNetworkSendEncrypt;
|
||||
|
||||
public static TimingsHandler $playerNetworkSendInventorySync;
|
||||
public static TimingsHandler $playerNetworkSendPreSpawnGameData;
|
||||
|
||||
/** @var TimingsHandler */
|
||||
public static $playerNetworkReceive;
|
||||
@ -77,14 +87,6 @@ abstract class Timings{
|
||||
/** @var TimingsHandler */
|
||||
public static $serverCommand;
|
||||
/** @var TimingsHandler */
|
||||
public static $worldLoad;
|
||||
/** @var TimingsHandler */
|
||||
public static $worldSave;
|
||||
/** @var TimingsHandler */
|
||||
public static $population;
|
||||
/** @var TimingsHandler */
|
||||
public static $generationCallback;
|
||||
/** @var TimingsHandler */
|
||||
public static $permissibleCalculation;
|
||||
/** @var TimingsHandler */
|
||||
public static $permissibleCalculationDiff;
|
||||
@ -101,10 +103,6 @@ abstract class Timings{
|
||||
|
||||
/** @var TimingsHandler */
|
||||
public static $playerCheckNearEntities;
|
||||
/** @var TimingsHandler */
|
||||
public static $tickEntity;
|
||||
/** @var TimingsHandler */
|
||||
public static $tickTileEntity;
|
||||
|
||||
/** @var TimingsHandler */
|
||||
public static $entityBaseTick;
|
||||
@ -154,6 +152,11 @@ abstract class Timings{
|
||||
|
||||
public static TimingsHandler $playerMove;
|
||||
|
||||
/** @var TimingsHandler[] */
|
||||
private static array $events = [];
|
||||
/** @var TimingsHandler[][] */
|
||||
private static array $eventHandlers = [];
|
||||
|
||||
public static function init() : void{
|
||||
if(self::$initialized){
|
||||
return;
|
||||
@ -161,8 +164,8 @@ abstract class Timings{
|
||||
self::$initialized = true;
|
||||
|
||||
self::$fullTick = new TimingsHandler("Full Server Tick");
|
||||
self::$serverTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Server Tick Update Cycle", self::$fullTick);
|
||||
self::$serverInterrupts = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Server Mid-Tick Processing", self::$fullTick);
|
||||
self::$serverTick = new TimingsHandler("Server Tick Update Cycle", self::$fullTick, group: self::GROUP_BREAKDOWN);
|
||||
self::$serverInterrupts = new TimingsHandler("Server Mid-Tick Processing", self::$fullTick, group: self::GROUP_BREAKDOWN);
|
||||
self::$memoryManager = new TimingsHandler("Memory Manager");
|
||||
self::$garbageCollector = new TimingsHandler("Garbage Collector", self::$memoryManager);
|
||||
self::$titleTick = new TimingsHandler("Console Title Tick");
|
||||
@ -170,58 +173,52 @@ abstract class Timings{
|
||||
self::$connection = new TimingsHandler("Connection Handler");
|
||||
|
||||
self::$playerNetworkSend = new TimingsHandler("Player Network Send", self::$connection);
|
||||
self::$playerNetworkSendCompress = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression", self::$playerNetworkSend);
|
||||
self::$playerNetworkSendCompressBroadcast = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression (Broadcast)", self::$playerNetworkSendCompress);
|
||||
self::$playerNetworkSendCompressSessionBuffer = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Compression (Session Buffer)", self::$playerNetworkSendCompress);
|
||||
self::$playerNetworkSendEncrypt = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Encryption", self::$playerNetworkSend);
|
||||
self::$playerNetworkSendInventorySync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Inventory Sync", self::$playerNetworkSend);
|
||||
self::$playerNetworkSendCompress = new TimingsHandler("Player Network Send - Compression", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkSendCompressBroadcast = new TimingsHandler("Player Network Send - Compression (Broadcast)", self::$playerNetworkSendCompress, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkSendCompressSessionBuffer = new TimingsHandler("Player Network Send - Compression (Session Buffer)", self::$playerNetworkSendCompress, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkSendEncrypt = new TimingsHandler("Player Network Send - Encryption", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkSendInventorySync = new TimingsHandler("Player Network Send - Inventory Sync", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkSendPreSpawnGameData = new TimingsHandler("Player Network Send - Pre-Spawn Game Data", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$playerNetworkReceive = new TimingsHandler("Player Network Receive", self::$connection);
|
||||
self::$playerNetworkReceiveDecompress = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Receive - Decompression", self::$playerNetworkReceive);
|
||||
self::$playerNetworkReceiveDecrypt = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Receive - Decryption", self::$playerNetworkReceive);
|
||||
self::$playerNetworkReceiveDecompress = new TimingsHandler("Player Network Receive - Decompression", self::$playerNetworkReceive, group: self::GROUP_BREAKDOWN);
|
||||
self::$playerNetworkReceiveDecrypt = new TimingsHandler("Player Network Receive - Decryption", self::$playerNetworkReceive, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$broadcastPackets = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Broadcast Packets", self::$playerNetworkSend);
|
||||
self::$broadcastPackets = new TimingsHandler("Broadcast Packets", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$playerMove = new TimingsHandler("Player Movement");
|
||||
self::$playerChunkOrder = new TimingsHandler("Player Order Chunks");
|
||||
self::$playerChunkSend = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Network Send - Chunks", self::$playerNetworkSend);
|
||||
self::$playerChunkSend = new TimingsHandler("Player Network Send - Chunks", self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
self::$scheduler = new TimingsHandler("Scheduler");
|
||||
self::$serverCommand = new TimingsHandler("Server Command");
|
||||
self::$worldLoad = new TimingsHandler("World Load");
|
||||
self::$worldSave = new TimingsHandler("World Save");
|
||||
self::$population = new TimingsHandler("World Population");
|
||||
self::$generationCallback = new TimingsHandler("World Generation Callback");
|
||||
self::$permissibleCalculation = new TimingsHandler("Permissible Calculation");
|
||||
self::$permissibleCalculationDiff = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Permissible Calculation - Diff", self::$permissibleCalculation);
|
||||
self::$permissibleCalculationCallback = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Permissible Calculation - Callbacks", self::$permissibleCalculation);
|
||||
self::$permissibleCalculationDiff = new TimingsHandler("Permissible Calculation - Diff", self::$permissibleCalculation, group: self::GROUP_BREAKDOWN);
|
||||
self::$permissibleCalculationCallback = new TimingsHandler("Permissible Calculation - Callbacks", self::$permissibleCalculation, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$syncPlayerDataLoad = new TimingsHandler("Player Data Load");
|
||||
self::$syncPlayerDataSave = new TimingsHandler("Player Data Save");
|
||||
|
||||
self::$entityMove = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Movement");
|
||||
self::$entityMoveCollision = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Movement - Collision Checks", self::$entityMove);
|
||||
self::$entityMove = new TimingsHandler("Entity Movement", group: self::GROUP_BREAKDOWN);
|
||||
self::$entityMoveCollision = new TimingsHandler("Entity Movement - Collision Checks", self::$entityMove, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$projectileMove = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Projectile Movement", self::$entityMove);
|
||||
self::$projectileMoveRayTrace = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Projectile Movement - Ray Tracing", self::$projectileMove);
|
||||
self::$projectileMove = new TimingsHandler("Projectile Movement", self::$entityMove, group: self::GROUP_BREAKDOWN);
|
||||
self::$projectileMoveRayTrace = new TimingsHandler("Projectile Movement - Ray Tracing", self::$projectileMove, group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$playerCheckNearEntities = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "checkNearEntities");
|
||||
self::$tickEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Tick");
|
||||
self::$tickTileEntity = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Block Entity Tick");
|
||||
self::$playerCheckNearEntities = new TimingsHandler("checkNearEntities", group: self::GROUP_BREAKDOWN);
|
||||
self::$entityBaseTick = new TimingsHandler("Entity Base Tick", group: self::GROUP_BREAKDOWN);
|
||||
self::$livingEntityBaseTick = new TimingsHandler("Entity Base Tick - Living", group: self::GROUP_BREAKDOWN);
|
||||
self::$itemEntityBaseTick = new TimingsHandler("Entity Base Tick - ItemEntity", group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$entityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick");
|
||||
self::$livingEntityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick - Living");
|
||||
self::$itemEntityBaseTick = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Base Tick - ItemEntity");
|
||||
self::$schedulerSync = new TimingsHandler("Scheduler - Sync Tasks", group: self::GROUP_BREAKDOWN);
|
||||
self::$schedulerAsync = new TimingsHandler("Scheduler - Async Tasks", group: self::GROUP_BREAKDOWN);
|
||||
|
||||
self::$schedulerSync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Scheduler - Sync Tasks");
|
||||
self::$schedulerAsync = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Scheduler - Async Tasks");
|
||||
|
||||
self::$playerCommand = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Player Command");
|
||||
self::$craftingDataCacheRebuild = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Build CraftingDataPacket Cache");
|
||||
self::$playerCommand = new TimingsHandler("Player Command", group: self::GROUP_BREAKDOWN);
|
||||
self::$craftingDataCacheRebuild = new TimingsHandler("Build CraftingDataPacket Cache", group: self::GROUP_BREAKDOWN);
|
||||
|
||||
}
|
||||
|
||||
public static function getScheduledTaskTimings(TaskHandler $task, int $period) : TimingsHandler{
|
||||
$name = "Task: " . $task->getOwnerName() . " Runnable: " . $task->getTaskName();
|
||||
$name = "Task: " . $task->getTaskName();
|
||||
|
||||
if($period > 0){
|
||||
$name .= "(interval:" . $period . ")";
|
||||
@ -230,75 +227,107 @@ abstract class Timings{
|
||||
}
|
||||
|
||||
if(!isset(self::$pluginTaskTimingMap[$name])){
|
||||
self::$pluginTaskTimingMap[$name] = new TimingsHandler($name, self::$schedulerSync);
|
||||
self::$pluginTaskTimingMap[$name] = new TimingsHandler($name, self::$schedulerSync, $task->getOwnerName());
|
||||
}
|
||||
|
||||
return self::$pluginTaskTimingMap[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-template T of object
|
||||
* @phpstan-param class-string<T> $class
|
||||
*/
|
||||
private static function shortenCoreClassName(string $class, string $prefix) : string{
|
||||
if(str_starts_with($class, $prefix)){
|
||||
return (new \ReflectionClass($class))->getShortName();
|
||||
}
|
||||
return $class;
|
||||
}
|
||||
|
||||
public static function getEntityTimings(Entity $entity) : TimingsHandler{
|
||||
$reflect = new \ReflectionClass($entity);
|
||||
$entityType = $reflect->getShortName();
|
||||
if(!isset(self::$entityTypeTimingMap[$entityType])){
|
||||
//the timings viewer calculates average player count by looking at this timer, so we need to ensure it has
|
||||
//a name it can identify. However, we also want to make it obvious if this is a custom Player class.
|
||||
if($entity instanceof Player && $reflect->getName() !== Player::class){
|
||||
$entityType = "Player (" . $reflect->getName() . ")";
|
||||
if(!isset(self::$entityTypeTimingMap[$entity::class])){
|
||||
if($entity instanceof Player){
|
||||
//the timings viewer calculates average player count by looking at this timer, so we need to ensure it has
|
||||
//a name it can identify. However, we also want to make it obvious if this is a custom Player class.
|
||||
$displayName = $entity::class !== Player::class ? "Player (" . $entity::class . ")" : "Player";
|
||||
}else{
|
||||
$displayName = self::shortenCoreClassName($entity::class, "pocketmine\\entity\\");
|
||||
}
|
||||
self::$entityTypeTimingMap[$entityType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Entity Tick - " . $entityType, self::$tickEntity);
|
||||
self::$entityTypeTimingMap[$entity::class] = new TimingsHandler("Entity Tick - " . $displayName, group: self::GROUP_BREAKDOWN);
|
||||
}
|
||||
|
||||
return self::$entityTypeTimingMap[$entityType];
|
||||
return self::$entityTypeTimingMap[$entity::class];
|
||||
}
|
||||
|
||||
public static function getTileEntityTimings(Tile $tile) : TimingsHandler{
|
||||
$tileType = (new \ReflectionClass($tile))->getShortName();
|
||||
if(!isset(self::$tileEntityTypeTimingMap[$tileType])){
|
||||
self::$tileEntityTypeTimingMap[$tileType] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Block Entity Tick - " . $tileType, self::$tickTileEntity);
|
||||
if(!isset(self::$tileEntityTypeTimingMap[$tile::class])){
|
||||
self::$tileEntityTypeTimingMap[$tile::class] = new TimingsHandler(
|
||||
"Block Entity Tick - " . self::shortenCoreClassName($tile::class, "pocketmine\\block\\tile\\"),
|
||||
group: self::GROUP_BREAKDOWN
|
||||
);
|
||||
}
|
||||
|
||||
return self::$tileEntityTypeTimingMap[$tileType];
|
||||
return self::$tileEntityTypeTimingMap[$tile::class];
|
||||
}
|
||||
|
||||
public static function getReceiveDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
|
||||
$pid = $pk->pid();
|
||||
if(!isset(self::$packetReceiveTimingMap[$pid])){
|
||||
self::$packetReceiveTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Receive - " . $pk->getName(), self::$playerNetworkReceive);
|
||||
if(!isset(self::$packetReceiveTimingMap[$pk::class])){
|
||||
self::$packetReceiveTimingMap[$pk::class] = new TimingsHandler("Receive - " . $pk->getName(), self::$playerNetworkReceive, group: self::GROUP_BREAKDOWN);
|
||||
}
|
||||
|
||||
return self::$packetReceiveTimingMap[$pid];
|
||||
return self::$packetReceiveTimingMap[$pk::class];
|
||||
}
|
||||
|
||||
public static function getDecodeDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
|
||||
$pid = $pk->pid();
|
||||
return self::$packetDecodeTimingMap[$pid] ??= new TimingsHandler(
|
||||
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Decode - " . $pk->getName(),
|
||||
self::getReceiveDataPacketTimings($pk)
|
||||
return self::$packetDecodeTimingMap[$pk::class] ??= new TimingsHandler(
|
||||
"Decode - " . $pk->getName(),
|
||||
self::getReceiveDataPacketTimings($pk),
|
||||
group: self::GROUP_BREAKDOWN
|
||||
);
|
||||
}
|
||||
|
||||
public static function getHandleDataPacketTimings(ServerboundPacket $pk) : TimingsHandler{
|
||||
$pid = $pk->pid();
|
||||
return self::$packetHandleTimingMap[$pid] ??= new TimingsHandler(
|
||||
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Handler - " . $pk->getName(),
|
||||
self::getReceiveDataPacketTimings($pk)
|
||||
return self::$packetHandleTimingMap[$pk::class] ??= new TimingsHandler(
|
||||
"Handler - " . $pk->getName(),
|
||||
self::getReceiveDataPacketTimings($pk),
|
||||
group: self::GROUP_BREAKDOWN
|
||||
);
|
||||
}
|
||||
|
||||
public static function getEncodeDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{
|
||||
$pid = $pk->pid();
|
||||
return self::$packetEncodeTimingMap[$pid] ??= new TimingsHandler(
|
||||
self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Encode - " . $pk->getName(),
|
||||
self::getSendDataPacketTimings($pk)
|
||||
return self::$packetEncodeTimingMap[$pk::class] ??= new TimingsHandler(
|
||||
"Encode - " . $pk->getName(),
|
||||
self::getSendDataPacketTimings($pk),
|
||||
group: self::GROUP_BREAKDOWN
|
||||
);
|
||||
}
|
||||
|
||||
public static function getSendDataPacketTimings(ClientboundPacket $pk) : TimingsHandler{
|
||||
$pid = $pk->pid();
|
||||
if(!isset(self::$packetSendTimingMap[$pid])){
|
||||
self::$packetSendTimingMap[$pid] = new TimingsHandler(self::INCLUDED_BY_OTHER_TIMINGS_PREFIX . "Send - " . $pk->getName(), self::$playerNetworkSend);
|
||||
if(!isset(self::$packetSendTimingMap[$pk::class])){
|
||||
self::$packetSendTimingMap[$pk::class] = new TimingsHandler("Send - " . $pk->getName(), self::$playerNetworkSend, group: self::GROUP_BREAKDOWN);
|
||||
}
|
||||
|
||||
return self::$packetSendTimingMap[$pid];
|
||||
return self::$packetSendTimingMap[$pk::class];
|
||||
}
|
||||
|
||||
public static function getEventTimings(Event $event) : TimingsHandler{
|
||||
$eventClass = get_class($event);
|
||||
if(!isset(self::$events[$eventClass])){
|
||||
self::$events[$eventClass] = new TimingsHandler(self::shortenCoreClassName($eventClass, "pocketmine\\event\\"), group: "Events");
|
||||
}
|
||||
|
||||
return self::$events[$eventClass];
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-template TEvent of Event
|
||||
* @phpstan-param class-string<TEvent> $event
|
||||
*/
|
||||
public static function getEventHandlerTimings(string $event, string $handlerName, string $group) : TimingsHandler{
|
||||
if(!isset(self::$eventHandlers[$event][$handlerName])){
|
||||
self::$eventHandlers[$event][$handlerName] = new TimingsHandler($handlerName . "(" . self::shortenCoreClassName($event, "pocketmine\\event\\") . ")", group: $group);
|
||||
}
|
||||
|
||||
return self::$eventHandlers[$event][$handlerName];
|
||||
}
|
||||
}
|
||||
|
@ -23,18 +23,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\timings;
|
||||
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\Server;
|
||||
use function count;
|
||||
use pocketmine\utils\Utils;
|
||||
use function hrtime;
|
||||
use function implode;
|
||||
use function spl_object_id;
|
||||
|
||||
class TimingsHandler{
|
||||
private const FORMAT_VERSION = 2; //peak timings fix
|
||||
|
||||
private static bool $enabled = false;
|
||||
private static int $timingStart = 0;
|
||||
|
||||
/** @return string[] */
|
||||
public static function printTimings() : array{
|
||||
$result = ["Minecraft"];
|
||||
$groups = [];
|
||||
|
||||
foreach(TimingsRecord::getAll() as $timings){
|
||||
$time = $timings->getTotalTime();
|
||||
@ -46,25 +49,33 @@ class TimingsHandler{
|
||||
|
||||
$avg = $time / $count;
|
||||
|
||||
$result[] = " " . $timings->getName() . " Time: $time Count: " . $count . " Avg: $avg Violations: " . $timings->getViolations();
|
||||
$group = $timings->getGroup();
|
||||
$groups[$group][] = implode(" ", [
|
||||
$timings->getName(),
|
||||
"Time: $time",
|
||||
"Count: $count",
|
||||
"Avg: $avg",
|
||||
"Violations: " . $timings->getViolations(),
|
||||
"RecordId: " . $timings->getId(),
|
||||
"ParentRecordId: " . ($timings->getParentId() ?? "none"),
|
||||
"TimerId: " . $timings->getTimerId(),
|
||||
"Ticks: " . $timings->getTicksActive(),
|
||||
"Peak: " . $timings->getPeakTime(),
|
||||
]);
|
||||
}
|
||||
$result = [];
|
||||
|
||||
foreach(Utils::stringifyKeys($groups) as $groupName => $lines){
|
||||
$result[] = $groupName;
|
||||
foreach($lines as $line){
|
||||
$result[] = " $line";
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = "# Version " . Server::getInstance()->getVersion();
|
||||
$result[] = "# " . Server::getInstance()->getName() . " " . Server::getInstance()->getPocketMineVersion();
|
||||
|
||||
$entities = 0;
|
||||
$livingEntities = 0;
|
||||
foreach(Server::getInstance()->getWorldManager()->getWorlds() as $world){
|
||||
$entities += count($world->getEntities());
|
||||
foreach($world->getEntities() as $e){
|
||||
if($e instanceof Living){
|
||||
++$livingEntities;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = "# Entities " . $entities;
|
||||
$result[] = "# LivingEntities " . $livingEntities;
|
||||
$result[] = "# FormatVersion " . self::FORMAT_VERSION;
|
||||
|
||||
$sampleTime = hrtime(true) - self::$timingStart;
|
||||
$result[] = "Sample time $sampleTime (" . ($sampleTime / 1000000000) . "s)";
|
||||
@ -85,7 +96,7 @@ class TimingsHandler{
|
||||
}
|
||||
|
||||
public static function reload() : void{
|
||||
TimingsRecord::clearRecords();
|
||||
TimingsRecord::reset();
|
||||
if(self::$enabled){
|
||||
self::$timingStart = hrtime(true);
|
||||
}
|
||||
@ -97,16 +108,25 @@ class TimingsHandler{
|
||||
}
|
||||
}
|
||||
|
||||
private ?TimingsRecord $record = null;
|
||||
private ?TimingsRecord $rootRecord = null;
|
||||
private int $timingDepth = 0;
|
||||
|
||||
/**
|
||||
* @var TimingsRecord[]
|
||||
* @phpstan-var array<int, TimingsRecord>
|
||||
*/
|
||||
private array $recordsByParent = [];
|
||||
|
||||
public function __construct(
|
||||
private string $name,
|
||||
private ?TimingsHandler $parent = null
|
||||
private ?TimingsHandler $parent = null,
|
||||
private string $group = "Minecraft"
|
||||
){}
|
||||
|
||||
public function getName() : string{ return $this->name; }
|
||||
|
||||
public function getGroup() : string{ return $this->group; }
|
||||
|
||||
public function startTiming() : void{
|
||||
if(self::$enabled){
|
||||
$this->internalStartTiming(hrtime(true));
|
||||
@ -115,13 +135,24 @@ class TimingsHandler{
|
||||
|
||||
private function internalStartTiming(int $now) : void{
|
||||
if(++$this->timingDepth === 1){
|
||||
if($this->record === null){
|
||||
$this->record = new TimingsRecord($this);
|
||||
}
|
||||
$this->record->startTiming($now);
|
||||
if($this->parent !== null){
|
||||
$this->parent->internalStartTiming($now);
|
||||
}
|
||||
|
||||
$current = TimingsRecord::getCurrentRecord();
|
||||
if($current !== null){
|
||||
$record = $this->recordsByParent[spl_object_id($current)] ?? null;
|
||||
if($record === null){
|
||||
$record = new TimingsRecord($this, $current);
|
||||
$this->recordsByParent[spl_object_id($current)] = $record;
|
||||
}
|
||||
}else{
|
||||
if($this->rootRecord === null){
|
||||
$this->rootRecord = new TimingsRecord($this, null);
|
||||
}
|
||||
$record = $this->rootRecord;
|
||||
}
|
||||
$record->startTiming($now);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,10 +173,13 @@ class TimingsHandler{
|
||||
return;
|
||||
}
|
||||
|
||||
if($this->record !== null){
|
||||
//this might be null if a timings reset occurred while the timer was running
|
||||
$this->record->stopTiming($now);
|
||||
$record = TimingsRecord::getCurrentRecord();
|
||||
$timerId = spl_object_id($this);
|
||||
for(; $record !== null && $record->getTimerId() !== $timerId; $record = TimingsRecord::getCurrentRecord()){
|
||||
\GlobalLogger::get()->error("Timer \"" . $record->getName() . "\" should have been stopped before stopping timer \"" . $this->name . "\"");
|
||||
$record->stopTiming($now);
|
||||
}
|
||||
$record?->stopTiming($now);
|
||||
if($this->parent !== null){
|
||||
$this->parent->internalStopTiming($now);
|
||||
}
|
||||
@ -170,7 +204,9 @@ class TimingsHandler{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function destroyCycles() : void{
|
||||
$this->record = null;
|
||||
public function reset() : void{
|
||||
$this->rootRecord = null;
|
||||
$this->recordsByParent = [];
|
||||
$this->timingDepth = 0;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace pocketmine\timings;
|
||||
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function floor;
|
||||
use function spl_object_id;
|
||||
|
||||
@ -39,11 +40,17 @@ final class TimingsRecord{
|
||||
*/
|
||||
private static array $records = [];
|
||||
|
||||
public static function clearRecords() : void{
|
||||
private static ?self $currentRecord = null;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function reset() : void{
|
||||
foreach(self::$records as $record){
|
||||
$record->handler->destroyCycles();
|
||||
$record->handler->reset();
|
||||
}
|
||||
self::$records = [];
|
||||
self::$currentRecord = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,11 +62,14 @@ final class TimingsRecord{
|
||||
public static function tick(bool $measure = true) : void{
|
||||
if($measure){
|
||||
foreach(self::$records as $record){
|
||||
if($record->curTickTotal > Server::TARGET_NANOSECONDS_PER_TICK){
|
||||
$record->violations += (int) floor($record->curTickTotal / Server::TARGET_NANOSECONDS_PER_TICK);
|
||||
if($record->curCount > 0){
|
||||
if($record->curTickTotal > Server::TARGET_NANOSECONDS_PER_TICK){
|
||||
$record->violations += (int) floor($record->curTickTotal / Server::TARGET_NANOSECONDS_PER_TICK);
|
||||
}
|
||||
$record->curTickTotal = 0;
|
||||
$record->curCount = 0;
|
||||
$record->ticksActive++;
|
||||
}
|
||||
$record->curTickTotal = 0;
|
||||
$record->curCount = 0;
|
||||
}
|
||||
}else{
|
||||
foreach(self::$records as $record){
|
||||
@ -78,16 +88,27 @@ final class TimingsRecord{
|
||||
private int $totalTime = 0;
|
||||
private int $curTickTotal = 0;
|
||||
private int $violations = 0;
|
||||
private int $ticksActive = 0;
|
||||
private int $peakTime = 0;
|
||||
|
||||
public function __construct(
|
||||
//I'm not the biggest fan of this cycle, but it seems to be the most effective way to avoid leaking anything.
|
||||
private TimingsHandler $handler
|
||||
private TimingsHandler $handler,
|
||||
private ?TimingsRecord $parentRecord
|
||||
){
|
||||
self::$records[spl_object_id($this)] = $this;
|
||||
}
|
||||
|
||||
public function getId() : int{ return spl_object_id($this); }
|
||||
|
||||
public function getParentId() : ?int{ return $this->parentRecord?->getId(); }
|
||||
|
||||
public function getTimerId() : int{ return spl_object_id($this->handler); }
|
||||
|
||||
public function getName() : string{ return $this->handler->getName(); }
|
||||
|
||||
public function getGroup() : string{ return $this->handler->getGroup(); }
|
||||
|
||||
public function getCount() : int{ return $this->count; }
|
||||
|
||||
public function getCurCount() : int{ return $this->curCount; }
|
||||
@ -100,19 +121,40 @@ final class TimingsRecord{
|
||||
|
||||
public function getViolations() : int{ return $this->violations; }
|
||||
|
||||
public function getTicksActive() : int{ return $this->ticksActive; }
|
||||
|
||||
public function getPeakTime() : int{ return $this->peakTime; }
|
||||
|
||||
public function startTiming(int $now) : void{
|
||||
$this->start = $now;
|
||||
self::$currentRecord = $this;
|
||||
}
|
||||
|
||||
public function stopTiming(int $now) : void{
|
||||
if($this->start == 0){
|
||||
return;
|
||||
}
|
||||
if(self::$currentRecord !== $this){
|
||||
if(self::$currentRecord === null){
|
||||
//timings may have been stopped while this timer was running
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AssumptionFailedError("stopTiming() called on a non-current timer");
|
||||
}
|
||||
self::$currentRecord = $this->parentRecord;
|
||||
$diff = $now - $this->start;
|
||||
$this->totalTime += $diff;
|
||||
$this->curTickTotal += $diff;
|
||||
++$this->curCount;
|
||||
++$this->count;
|
||||
$this->start = 0;
|
||||
if($diff > $this->peakTime){
|
||||
$this->peakTime = $diff;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCurrentRecord() : ?self{
|
||||
return self::$currentRecord;
|
||||
}
|
||||
}
|
||||
|
34
src/world/ChunkTicker.php
Normal file
34
src/world/ChunkTicker.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?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\world;
|
||||
|
||||
/**
|
||||
* Used to signal to the World that a chunk should be ticked.
|
||||
*
|
||||
* @see World::registerTickingChunk()
|
||||
* @see World::unregisterTickingChunk()
|
||||
*/
|
||||
final class ChunkTicker{
|
||||
|
||||
}
|
@ -27,6 +27,10 @@ namespace pocketmine\world;
|
||||
* TickingChunkLoader includes all of the same functionality as ChunkLoader (it can be used in the same way).
|
||||
* However, using this version will also cause chunks around the loader's reported coordinates to get random block
|
||||
* updates.
|
||||
*
|
||||
* @deprecated
|
||||
* @see World::registerTickingChunk()
|
||||
* @see World::unregisterTickingChunk()
|
||||
*/
|
||||
interface TickingChunkLoader extends ChunkLoader{
|
||||
|
||||
|
@ -77,7 +77,6 @@ use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\scheduler\AsyncPool;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\ServerConfigGroup;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Limits;
|
||||
use pocketmine\utils\ReversePriorityQueue;
|
||||
@ -104,6 +103,7 @@ use pocketmine\world\utils\SubChunkExplorer;
|
||||
use function abs;
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_sum;
|
||||
@ -210,13 +210,39 @@ class World implements ChunkManager{
|
||||
/**
|
||||
* @var TickingChunkLoader[] spl_object_id => TickingChunkLoader
|
||||
* @phpstan-var array<int, TickingChunkLoader>
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
private array $tickingLoaders = [];
|
||||
/**
|
||||
* @var int[] spl_object_id => number of chunks
|
||||
* @phpstan-var array<int, int>
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
private array $tickingLoaderCounter = [];
|
||||
|
||||
/**
|
||||
* @var ChunkTicker[][] chunkHash => [spl_object_id => ChunkTicker]
|
||||
* @phpstan-var array<ChunkPosHash, array<int, ChunkTicker>>
|
||||
*/
|
||||
private array $registeredTickingChunks = [];
|
||||
|
||||
/**
|
||||
* Set of chunks which are definitely ready for ticking.
|
||||
*
|
||||
* @var int[]
|
||||
* @phpstan-var array<ChunkPosHash, ChunkPosHash>
|
||||
*/
|
||||
private array $validTickingChunks = [];
|
||||
|
||||
/**
|
||||
* Set of chunks which might be ready for ticking. These will be checked at the next tick.
|
||||
* @var int[]
|
||||
* @phpstan-var array<ChunkPosHash, ChunkPosHash>
|
||||
*/
|
||||
private array $recheckTickingChunks = [];
|
||||
|
||||
/**
|
||||
* @var ChunkLoader[][] chunkHash => [spl_object_id => ChunkLoader]
|
||||
* @phpstan-var array<ChunkPosHash, array<int, ChunkLoader>>
|
||||
@ -496,7 +522,7 @@ class World implements ChunkManager{
|
||||
$this->time = $this->provider->getWorldData()->getTime();
|
||||
|
||||
$cfg = $this->server->getConfigGroup();
|
||||
$this->chunkTickRadius = min($this->server->getViewDistance(), max(1, $cfg->getPropertyInt("chunk-ticking.tick-radius", 4)));
|
||||
$this->chunkTickRadius = min($this->server->getViewDistance(), max(0, $cfg->getPropertyInt("chunk-ticking.tick-radius", 4)));
|
||||
if($cfg->getPropertyInt("chunk-ticking.per-tick", 40) <= 0){
|
||||
//TODO: this needs l10n
|
||||
$this->logger->warning("\"chunk-ticking.per-tick\" setting is deprecated, but you've used it to disable chunk ticking. Set \"chunk-ticking.tick-radius\" to 0 in \"pocketmine.yml\" instead.");
|
||||
@ -972,7 +998,6 @@ class World implements ChunkManager{
|
||||
|
||||
$this->timings->entityTick->startTiming();
|
||||
//Update entities that need update
|
||||
Timings::$tickEntity->startTiming();
|
||||
foreach($this->updateEntities as $id => $entity){
|
||||
if($entity->isClosed() || $entity->isFlaggedForDespawn() || !$entity->onUpdate($currentTick)){
|
||||
unset($this->updateEntities[$id]);
|
||||
@ -981,7 +1006,6 @@ class World implements ChunkManager{
|
||||
$entity->close();
|
||||
}
|
||||
}
|
||||
Timings::$tickEntity->stopTiming();
|
||||
$this->timings->entityTick->stopTiming();
|
||||
|
||||
$this->timings->randomChunkUpdates->startTiming();
|
||||
@ -1129,32 +1153,73 @@ class World implements ChunkManager{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the radius of chunks to be ticked around each ticking chunk loader (usually players). This is referred to
|
||||
* as "simulation distance" in the Minecraft: Bedrock world options screen.
|
||||
* Returns the radius of chunks to be ticked around each player. This is referred to as "simulation distance" in the
|
||||
* Minecraft: Bedrock world options screen.
|
||||
*/
|
||||
public function getChunkTickRadius() : int{
|
||||
return $this->chunkTickRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the radius of chunks ticked around each ticking chunk loader (usually players).
|
||||
* Sets the radius of chunks ticked around each player. This may not take effect immediately, since each player
|
||||
* needs to recalculate their tick radius.
|
||||
*/
|
||||
public function setChunkTickRadius(int $radius) : void{
|
||||
$this->chunkTickRadius = $radius;
|
||||
}
|
||||
|
||||
private function tickChunks() : void{
|
||||
if($this->chunkTickRadius <= 0 || count($this->tickingLoaders) === 0){
|
||||
return;
|
||||
/**
|
||||
* Returns a list of chunk position hashes (as returned by World::chunkHash()) which are currently valid for
|
||||
* ticking.
|
||||
*
|
||||
* @return int[]
|
||||
* @phpstan-return list<ChunkPosHash>
|
||||
*/
|
||||
public function getTickingChunks() : array{
|
||||
return array_keys($this->validTickingChunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructs the World to tick the specified chunk, for as long as this chunk ticker (or any other chunk ticker) is
|
||||
* registered to it.
|
||||
*/
|
||||
public function registerTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
|
||||
$chunkPosHash = World::chunkHash($chunkX, $chunkZ);
|
||||
$this->registeredTickingChunks[$chunkPosHash][spl_object_id($ticker)] = $ticker;
|
||||
$this->recheckTickingChunks[$chunkPosHash] = $chunkPosHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the given chunk ticker from the specified chunk. If there are other tickers still registered to the
|
||||
* chunk, it will continue to be ticked.
|
||||
*/
|
||||
public function unregisterTickingChunk(ChunkTicker $ticker, int $chunkX, int $chunkZ) : void{
|
||||
$chunkHash = World::chunkHash($chunkX, $chunkZ);
|
||||
$tickerId = spl_object_id($ticker);
|
||||
if(isset($this->registeredTickingChunks[$chunkHash][$tickerId])){
|
||||
unset($this->registeredTickingChunks[$chunkHash][$tickerId]);
|
||||
if(count($this->registeredTickingChunks[$chunkHash]) === 0){
|
||||
unset(
|
||||
$this->registeredTickingChunks[$chunkHash],
|
||||
$this->recheckTickingChunks[$chunkHash],
|
||||
$this->validTickingChunks[$chunkHash]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->timings->randomChunkUpdatesChunkSelection->startTiming();
|
||||
|
||||
/** @var bool[] $chunkTickList chunkhash => dummy */
|
||||
$chunkTickList = [];
|
||||
|
||||
$chunkTickableCache = [];
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* @param true[] $chunkTickList
|
||||
* @param bool[] $chunkTickableCache
|
||||
*
|
||||
* @phpstan-param array<int, true> $chunkTickList
|
||||
* @phpstan-param array<int, bool> $chunkTickableCache
|
||||
* @phpstan-param-out array<int, true> $chunkTickList
|
||||
* @phpstan-param-out array<int, bool> $chunkTickableCache
|
||||
*/
|
||||
private function selectTickableChunksLegacy(array &$chunkTickList, array &$chunkTickableCache) : void{
|
||||
$centerChunks = [];
|
||||
|
||||
$selector = new ChunkSelector();
|
||||
@ -1179,8 +1244,40 @@ class World implements ChunkManager{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->timings->randomChunkUpdatesChunkSelection->stopTiming();
|
||||
private function tickChunks() : void{
|
||||
if($this->chunkTickRadius <= 0 || (count($this->registeredTickingChunks) === 0 && count($this->tickingLoaders) === 0)){
|
||||
return;
|
||||
}
|
||||
|
||||
if(count($this->recheckTickingChunks) > 0 || count($this->tickingLoaders) > 0){
|
||||
$this->timings->randomChunkUpdatesChunkSelection->startTiming();
|
||||
|
||||
$chunkTickableCache = [];
|
||||
|
||||
foreach($this->recheckTickingChunks as $hash => $_){
|
||||
World::getXZ($hash, $chunkX, $chunkZ);
|
||||
if($this->isChunkTickable($chunkX, $chunkZ, $chunkTickableCache)){
|
||||
$this->validTickingChunks[$hash] = $hash;
|
||||
}
|
||||
}
|
||||
$this->recheckTickingChunks = [];
|
||||
|
||||
//TODO: REMOVE THIS - we need a local var to add extra chunks to if we have legacy ticking loaders
|
||||
//this is copy-on-write, so it won't have any performance impact if there are no legacy ticking loaders
|
||||
$chunkTickList = $this->validTickingChunks;
|
||||
|
||||
//TODO: REMOVE THIS
|
||||
//backwards compatibility for TickingChunkLoader, although I'm not sure this is really necessary in practice
|
||||
if(count($this->tickingLoaders) !== 0){
|
||||
$this->selectTickableChunksLegacy($chunkTickList, $chunkTickableCache);
|
||||
}
|
||||
|
||||
$this->timings->randomChunkUpdatesChunkSelection->stopTiming();
|
||||
}else{
|
||||
$chunkTickList = $this->validTickingChunks;
|
||||
}
|
||||
|
||||
foreach($chunkTickList as $index => $_){
|
||||
World::getXZ($index, $chunkX, $chunkZ);
|
||||
@ -1230,11 +1327,35 @@ class World implements ChunkManager{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the 3x3 square of chunks centered on the specified chunk for chunk ticking eligibility recheck.
|
||||
*
|
||||
* This should be used whenever the chunk's eligibility to be ticked is changed. This includes:
|
||||
* - Loading/unloading the chunk (the chunk may be registered for ticking before it is loaded)
|
||||
* - Locking/unlocking the chunk (e.g. world population)
|
||||
* - Light populated state change (i.e. scheduled for light population, or light population completed)
|
||||
* - Arbitrary chunk replacement (i.e. setChunk() or similar)
|
||||
*/
|
||||
private function markTickingChunkForRecheck(int $chunkX, int $chunkZ) : void{
|
||||
for($cx = -1; $cx <= 1; ++$cx){
|
||||
for($cz = -1; $cz <= 1; ++$cz){
|
||||
$chunkHash = World::chunkHash($chunkX + $cx, $chunkZ + $cz);
|
||||
unset($this->validTickingChunks[$chunkHash]);
|
||||
if(isset($this->registeredTickingChunks[$chunkHash])){
|
||||
$this->recheckTickingChunks[$chunkHash] = $chunkHash;
|
||||
}else{
|
||||
unset($this->recheckTickingChunks[$chunkHash]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function orderLightPopulation(int $chunkX, int $chunkZ) : void{
|
||||
$chunkHash = World::chunkHash($chunkX, $chunkZ);
|
||||
$lightPopulatedState = $this->chunks[$chunkHash]->isLightPopulated();
|
||||
if($lightPopulatedState === false){
|
||||
$this->chunks[$chunkHash]->setLightPopulated(null);
|
||||
$this->markTickingChunkForRecheck($chunkX, $chunkZ);
|
||||
|
||||
$this->workerPool->submitTask(new LightPopulationTask(
|
||||
$this->chunks[$chunkHash],
|
||||
@ -1258,6 +1379,7 @@ class World implements ChunkManager{
|
||||
$chunk->getSubChunk($y)->setBlockSkyLightArray($lightArray);
|
||||
}
|
||||
$chunk->setLightPopulated(true);
|
||||
$this->markTickingChunkForRecheck($chunkX, $chunkZ);
|
||||
}
|
||||
));
|
||||
}
|
||||
@ -1314,10 +1436,15 @@ class World implements ChunkManager{
|
||||
|
||||
(new WorldSaveEvent($this))->call();
|
||||
|
||||
$timings = $this->timings->syncDataSave;
|
||||
$timings->startTiming();
|
||||
|
||||
$this->provider->getWorldData()->setTime($this->time);
|
||||
$this->saveChunks();
|
||||
$this->provider->getWorldData()->save();
|
||||
|
||||
$timings->stopTiming();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2302,6 +2429,7 @@ class World implements ChunkManager{
|
||||
throw new \InvalidArgumentException("Chunk $chunkX $chunkZ is already locked");
|
||||
}
|
||||
$this->chunkLock[$chunkHash] = $lockId;
|
||||
$this->markTickingChunkForRecheck($chunkX, $chunkZ);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2316,6 +2444,7 @@ class World implements ChunkManager{
|
||||
$chunkHash = World::chunkHash($chunkX, $chunkZ);
|
||||
if(isset($this->chunkLock[$chunkHash]) && ($lockId === null || $this->chunkLock[$chunkHash] === $lockId)){
|
||||
unset($this->chunkLock[$chunkHash]);
|
||||
$this->markTickingChunkForRecheck($chunkX, $chunkZ);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -2367,6 +2496,7 @@ class World implements ChunkManager{
|
||||
unset($this->blockCache[$chunkHash]);
|
||||
unset($this->changedBlocks[$chunkHash]);
|
||||
$chunk->setTerrainDirty();
|
||||
$this->markTickingChunkForRecheck($chunkX, $chunkZ); //this replacement chunk may not meet the conditions for ticking
|
||||
|
||||
if(!$this->isChunkInUse($chunkX, $chunkZ)){
|
||||
$this->unloadChunkRequest($chunkX, $chunkZ);
|
||||
@ -2648,6 +2778,7 @@ class World implements ChunkManager{
|
||||
foreach($this->getChunkListeners($x, $z) as $listener){
|
||||
$listener->onChunkLoaded($x, $z, $this->chunks[$chunkHash]);
|
||||
}
|
||||
$this->markTickingChunkForRecheck($x, $z); //tickers may have been registered before the chunk was loaded
|
||||
|
||||
$this->timings->syncChunkLoad->stopTiming();
|
||||
|
||||
@ -2809,6 +2940,8 @@ class World implements ChunkManager{
|
||||
unset($this->chunks[$chunkHash]);
|
||||
unset($this->blockCache[$chunkHash]);
|
||||
unset($this->changedBlocks[$chunkHash]);
|
||||
unset($this->registeredTickingChunks[$chunkHash]);
|
||||
$this->markTickingChunkForRecheck($x, $z);
|
||||
|
||||
if(array_key_exists($chunkHash, $this->chunkPopulationRequestMap)){
|
||||
$this->logger->debug("Rejecting population promise for chunk $x $z");
|
||||
@ -3117,7 +3250,8 @@ class World implements ChunkManager{
|
||||
private function internalOrderChunkPopulation(int $chunkX, int $chunkZ, ?ChunkLoader $associatedChunkLoader, ?PromiseResolver $resolver) : Promise{
|
||||
$chunkHash = World::chunkHash($chunkX, $chunkZ);
|
||||
|
||||
Timings::$population->startTiming();
|
||||
$timings = $this->timings->chunkPopulationOrder;
|
||||
$timings->startTiming();
|
||||
|
||||
try{
|
||||
for($xx = -1; $xx <= 1; ++$xx){
|
||||
@ -3174,7 +3308,7 @@ class World implements ChunkManager{
|
||||
|
||||
return $resolver->getPromise();
|
||||
}finally{
|
||||
Timings::$population->stopTiming();
|
||||
$timings->stopTiming();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3183,7 +3317,8 @@ class World implements ChunkManager{
|
||||
* @phpstan-param array<int, Chunk> $adjacentChunks
|
||||
*/
|
||||
private function generateChunkCallback(ChunkLockId $chunkLockId, int $x, int $z, Chunk $chunk, array $adjacentChunks, ChunkLoader $temporaryChunkLoader) : void{
|
||||
Timings::$generationCallback->startTiming();
|
||||
$timings = $this->timings->chunkPopulationCompletion;
|
||||
$timings->startTiming();
|
||||
|
||||
$dirtyChunks = 0;
|
||||
for($xx = -1; $xx <= 1; ++$xx){
|
||||
@ -3252,7 +3387,7 @@ class World implements ChunkManager{
|
||||
|
||||
$this->drainPopulationRequestQueue();
|
||||
}
|
||||
Timings::$generationCallback->stopTiming();
|
||||
$timings->stopTiming();
|
||||
}
|
||||
|
||||
public function doChunkGarbageCollection() : void{
|
||||
|
@ -30,7 +30,6 @@ use pocketmine\event\world\WorldUnloadEvent;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\player\ChunkSelector;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\exception\CorruptedWorldException;
|
||||
use pocketmine\world\format\io\exception\UnsupportedWorldFormatException;
|
||||
@ -286,7 +285,7 @@ class WorldManager{
|
||||
$centerX = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
|
||||
$centerZ = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
|
||||
|
||||
$selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ));
|
||||
$selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ), preserve_keys: false);
|
||||
$done = 0;
|
||||
$total = count($selected);
|
||||
foreach($selected as $index){
|
||||
@ -391,7 +390,6 @@ class WorldManager{
|
||||
}
|
||||
|
||||
private function doAutoSave() : void{
|
||||
Timings::$worldSave->startTiming();
|
||||
foreach($this->worlds as $world){
|
||||
foreach($world->getPlayers() as $player){
|
||||
if($player->spawned){
|
||||
@ -400,6 +398,5 @@ class WorldManager{
|
||||
}
|
||||
$world->save(false);
|
||||
}
|
||||
Timings::$worldSave->stopTiming();
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ class WorldTimings{
|
||||
public TimingsHandler $randomChunkUpdatesChunkSelection;
|
||||
public TimingsHandler $doChunkGC;
|
||||
public TimingsHandler $entityTick;
|
||||
public TimingsHandler $tileTick;
|
||||
public TimingsHandler $doTick;
|
||||
|
||||
public TimingsHandler $syncChunkSend;
|
||||
@ -48,33 +49,54 @@ class WorldTimings{
|
||||
public TimingsHandler $syncChunkLoadFixInvalidBlocks;
|
||||
public TimingsHandler $syncChunkLoadEntities;
|
||||
public TimingsHandler $syncChunkLoadTileEntities;
|
||||
|
||||
public TimingsHandler $syncDataSave;
|
||||
public TimingsHandler $syncChunkSave;
|
||||
|
||||
public TimingsHandler $chunkPopulationOrder;
|
||||
public TimingsHandler $chunkPopulationCompletion;
|
||||
|
||||
/**
|
||||
* @var TimingsHandler[]
|
||||
* @phpstan-var array<string, TimingsHandler>
|
||||
*/
|
||||
private static array $aggregators = [];
|
||||
|
||||
private static function newTimer(string $worldName, string $timerName) : TimingsHandler{
|
||||
$aggregator = self::$aggregators[$timerName] ??= new TimingsHandler("Worlds - $timerName"); //displayed in Minecraft primary table
|
||||
|
||||
return new TimingsHandler("$worldName - $timerName", $aggregator, Timings::GROUP_BREAKDOWN);
|
||||
}
|
||||
|
||||
public function __construct(World $world){
|
||||
$name = $world->getFolderName() . " - ";
|
||||
$name = $world->getFolderName();
|
||||
|
||||
$this->setBlock = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "setBlock");
|
||||
$this->doBlockLightUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Block Light Updates");
|
||||
$this->doBlockSkyLightUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Sky Light Updates");
|
||||
$this->setBlock = self::newTimer($name, "Set Blocks");
|
||||
$this->doBlockLightUpdates = self::newTimer($name, "Block Light Updates");
|
||||
$this->doBlockSkyLightUpdates = self::newTimer($name, "Sky Light Updates");
|
||||
|
||||
$this->doChunkUnload = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Unload Chunks");
|
||||
$this->scheduledBlockUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Scheduled Block Updates");
|
||||
$this->randomChunkUpdates = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Random Chunk Updates");
|
||||
$this->randomChunkUpdatesChunkSelection = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Random Chunk Updates - Chunk Selection");
|
||||
$this->doChunkGC = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Garbage Collection");
|
||||
$this->entityTick = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Tick Entities");
|
||||
$this->doChunkUnload = self::newTimer($name, "Unload Chunks");
|
||||
$this->scheduledBlockUpdates = self::newTimer($name, "Scheduled Block Updates");
|
||||
$this->randomChunkUpdates = self::newTimer($name, "Random Chunk Updates");
|
||||
$this->randomChunkUpdatesChunkSelection = self::newTimer($name, "Random Chunk Updates - Chunk Selection");
|
||||
$this->doChunkGC = self::newTimer($name, "Garbage Collection");
|
||||
$this->entityTick = self::newTimer($name, "Entity Tick");
|
||||
$this->tileTick = self::newTimer($name, "Block Entity Tick");
|
||||
$this->doTick = self::newTimer($name, "World Tick");
|
||||
|
||||
Timings::init(); //make sure the timers we want are available
|
||||
$this->syncChunkSend = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Player Send Chunks", Timings::$playerChunkSend);
|
||||
$this->syncChunkSendPrepare = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Player Send Chunk Prepare", Timings::$playerChunkSend);
|
||||
$this->syncChunkSend = self::newTimer($name, "Player Send Chunks");
|
||||
$this->syncChunkSendPrepare = self::newTimer($name, "Player Send Chunk Prepare");
|
||||
|
||||
$this->syncChunkLoad = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load", Timings::$worldLoad);
|
||||
$this->syncChunkLoadData = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Data");
|
||||
$this->syncChunkLoadFixInvalidBlocks = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Fix Invalid Blocks");
|
||||
$this->syncChunkLoadEntities = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - Entities");
|
||||
$this->syncChunkLoadTileEntities = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Load - TileEntities");
|
||||
$this->syncChunkSave = new TimingsHandler(Timings::INCLUDED_BY_OTHER_TIMINGS_PREFIX . $name . "Chunk Save", Timings::$worldSave);
|
||||
$this->syncChunkLoad = self::newTimer($name, "Chunk Load");
|
||||
$this->syncChunkLoadData = self::newTimer($name, "Chunk Load - Data");
|
||||
$this->syncChunkLoadFixInvalidBlocks = self::newTimer($name, "Chunk Load - Fix Invalid Blocks");
|
||||
$this->syncChunkLoadEntities = self::newTimer($name, "Chunk Load - Entities");
|
||||
$this->syncChunkLoadTileEntities = self::newTimer($name, "Chunk Load - Block Entities");
|
||||
|
||||
$this->doTick = new TimingsHandler($name . "World Tick");
|
||||
$this->syncDataSave = self::newTimer($name, "Data Save");
|
||||
$this->syncChunkSave = self::newTimer($name, "Chunk Save");
|
||||
|
||||
$this->chunkPopulationOrder = self::newTimer($name, "Chunk Population - Order");
|
||||
$this->chunkPopulationCompletion = self::newTimer($name, "Chunk Population - Completion");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,88 @@
|
||||
<?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 pmmp\TesterPlugin;
|
||||
|
||||
use pmmp\TesterPlugin\event\ChildEvent;
|
||||
use pmmp\TesterPlugin\event\GrandchildEvent;
|
||||
use pmmp\TesterPlugin\event\ParentEvent;
|
||||
use pocketmine\event\EventPriority;
|
||||
use function implode;
|
||||
|
||||
final class EventHandlerInheritanceTest extends Test{
|
||||
|
||||
private const EXPECTED_ORDER = [
|
||||
GrandchildEvent::class,
|
||||
ChildEvent::class,
|
||||
ParentEvent::class
|
||||
];
|
||||
|
||||
/** @var string[] */
|
||||
private array $callOrder = [];
|
||||
|
||||
public function getName() : string{
|
||||
return "Event Handler Inheritance Test";
|
||||
}
|
||||
|
||||
public function getDescription() : string{
|
||||
return "Tests that child events are correctly passed to parent event handlers";
|
||||
}
|
||||
|
||||
public function run() : void{
|
||||
$plugin = $this->getPlugin();
|
||||
$plugin->getServer()->getPluginManager()->registerEvent(
|
||||
ParentEvent::class,
|
||||
function(ParentEvent $event) : void{
|
||||
$this->callOrder[] = ParentEvent::class;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$plugin
|
||||
);
|
||||
$plugin->getServer()->getPluginManager()->registerEvent(
|
||||
ChildEvent::class,
|
||||
function(ChildEvent $event) : void{
|
||||
$this->callOrder[] = ChildEvent::class;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$plugin
|
||||
);
|
||||
$plugin->getServer()->getPluginManager()->registerEvent(
|
||||
GrandchildEvent::class,
|
||||
function(GrandchildEvent $event) : void{
|
||||
$this->callOrder[] = GrandchildEvent::class;
|
||||
},
|
||||
EventPriority::NORMAL,
|
||||
$plugin
|
||||
);
|
||||
|
||||
$event = new GrandchildEvent();
|
||||
$event->call();
|
||||
|
||||
if($this->callOrder === self::EXPECTED_ORDER){
|
||||
$this->setResult(Test::RESULT_OK);
|
||||
}else{
|
||||
$plugin->getLogger()->error("Expected order: " . implode(", ", self::EXPECTED_ORDER) . ", got: " . implode(", ", $this->callOrder));
|
||||
$this->setResult(Test::RESULT_FAILED);
|
||||
}
|
||||
}
|
||||
}
|
@ -56,7 +56,9 @@ class Main extends PluginBase implements Listener{
|
||||
}
|
||||
}), 10);
|
||||
|
||||
$this->waitingTests = [];
|
||||
$this->waitingTests = [
|
||||
new EventHandlerInheritanceTest($this),
|
||||
];
|
||||
}
|
||||
|
||||
public function onServerCommand(CommandEvent $event) : void{
|
||||
|
28
tests/plugins/TesterPlugin/src/event/ChildEvent.php
Normal file
28
tests/plugins/TesterPlugin/src/event/ChildEvent.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 pmmp\TesterPlugin\event;
|
||||
|
||||
class ChildEvent extends ParentEvent{
|
||||
|
||||
}
|
28
tests/plugins/TesterPlugin/src/event/GrandchildEvent.php
Normal file
28
tests/plugins/TesterPlugin/src/event/GrandchildEvent.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 pmmp\TesterPlugin\event;
|
||||
|
||||
class GrandchildEvent extends ChildEvent{
|
||||
|
||||
}
|
28
tests/plugins/TesterPlugin/src/event/ParentEvent.php
Normal file
28
tests/plugins/TesterPlugin/src/event/ParentEvent.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 pmmp\TesterPlugin\event;
|
||||
|
||||
class ParentEvent extends \pocketmine\event\Event{
|
||||
|
||||
}
|
@ -24,12 +24,10 @@ declare(strict_types=1);
|
||||
namespace pocketmine\tools\simulate_chunk_selector;
|
||||
|
||||
use pocketmine\player\ChunkSelector;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\World;
|
||||
use Symfony\Component\Filesystem\Path;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function dirname;
|
||||
use function fwrite;
|
||||
@ -128,7 +126,12 @@ if(count(getopt("", ["help"])) !== 0){
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach(Utils::stringifyKeys(getopt("", ["radius:", "baseX:", "baseZ:", "scale:", "chunksPerStep:"])) as $name => $value){
|
||||
$opts = getopt("", ["radius:", "baseX:", "baseZ:", "scale:", "chunksPerStep:", "output:"]);
|
||||
foreach(["radius", "baseX", "baseZ", "scale", "chunksPerStep"] as $name){
|
||||
$value = $opts[$name] ?? null;
|
||||
if($value === null){
|
||||
continue;
|
||||
}
|
||||
if(!is_string($value) || (string) ((int) $value) !== $value){
|
||||
fwrite(STDERR, "Value for --$name must be an integer\n");
|
||||
exit(1);
|
||||
@ -139,8 +142,7 @@ foreach(Utils::stringifyKeys(getopt("", ["radius:", "baseX:", "baseZ:", "scale:"
|
||||
"baseX" => ($baseX = $value),
|
||||
"baseZ" => ($baseZ = $value),
|
||||
"scale" => ($scale = $value),
|
||||
"chunksPerStep" => ($nChunksPerStep = $value),
|
||||
default => throw new AssumptionFailedError("getopt() returned unknown option")
|
||||
"chunksPerStep" => ($nChunksPerStep = $value)
|
||||
};
|
||||
}
|
||||
if($radius === null){
|
||||
@ -149,10 +151,10 @@ if($radius === null){
|
||||
}
|
||||
|
||||
$outputDirectory = null;
|
||||
foreach(Utils::stringifyKeys(getopt("", ["output:"])) as $name => $value){
|
||||
assert($name === "output");
|
||||
if(isset($opts["output"])){
|
||||
$value = $opts["output"];
|
||||
if(!is_string($value)){
|
||||
fwrite(STDERR, "Value for --$name must be a string\n");
|
||||
fwrite(STDERR, "Value for --output be a string\n");
|
||||
exit(1);
|
||||
}
|
||||
if(!@mkdir($value) && !is_dir($value)){
|
||||
|
Reference in New Issue
Block a user