Compare commits

..

155 Commits

Author SHA1 Message Date
173b685b02 Bedrock 1.21.100 (#6760)
---------

Co-authored-by: Dylan T. <dktapps@pmmp.io>
2025-08-06 16:41:44 +01:00
89d18f929f RakLibServer: fixed deadlock on thread crash
synchronized block -> getCrashInfo -> join -> synchronized on the same
context on the child thread -> deadlock

instead we check for isTerminated and then get the crash info outside
of the synchronized block.
2025-08-06 15:49:52 +01:00
a83c62a4a2 readme: Stop showing random PR statuses on CI badge 2025-07-30 19:08:29 +01:00
40ea6dd30d Bump phpstan/phpstan-strict-rules in the development-patch-updates group (#6758) 2025-07-29 09:26:25 +00:00
866df55edf Bump ramsey/uuid from 4.8.1 to 4.9.0 (#6748) 2025-07-29 09:25:19 +00:00
4047cbaafe Bump phpstan/phpstan-strict-rules in the development-patch-updates group (#6756) 2025-07-23 12:07:07 +00:00
a1d74b5710 5.31.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/16143550499
2025-07-08 12:43:11 +00:00
50e15db9ac Prepare 5.31.0 release (#6752) 2025-07-08 13:41:59 +01:00
f1f6e796a4 Bump the github-actions group with 2 updates (#6749) 2025-07-01 10:28:28 +00:00
7ea0f2ff43 copilot-setup-steps: also add extension stubs 2025-06-26 00:14:34 +01:00
6dbd4282cb fix cache key 2025-06-26 00:12:12 +01:00
3176e7549e woops 2025-06-26 00:11:26 +01:00
92c3ce7f02 Create copilot-setup-steps.yml 2025-06-26 00:10:46 +01:00
3a5432b316 Bump build/php from 1549433 to ce1b095 (#6741) 2025-06-25 00:16:12 +00:00
40a3ee68dd fix typo in changelog (#6745) 2025-06-24 13:35:32 +01:00
e415518435 5.30.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15834488047
2025-06-23 20:31:58 +00:00
cb508f4382 Release 5.30.1 (#6744) 2025-06-23 23:30:48 +03:00
177fa76434 Disable client-side locator bar (#6743)
Without a propper server-side implementation, it just a mess of white dots of nearby players
2025-06-23 14:57:33 -05:00
2a97b4294d Fixed held block placement after respawn anchor explosion (#6742) 2025-06-23 16:59:40 +01:00
04494e845c EntityExplodeEvent: Fixed accidental BC break introduced by #6646
thanks @Yexeed
2025-06-20 15:26:42 +01:00
0e511ff783 smh 2025-06-18 21:55:53 +01:00
6826420876 5.30.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15743323722
2025-06-18 20:49:31 +00:00
f0161c84b9 Merge pull request #6737 from pmmp/r5.30.0
5.30.0
2025-06-18 21:48:30 +01:00
3643d3aeb8 Ready 5.30.0 release 2025-06-18 20:42:10 +01:00
a662510cca Merge remote-tracking branch 'origin/stable' into minor-next 2025-06-18 19:56:02 +01:00
670d3fb997 Mention developer team in draft release notification 2025-06-18 19:29:28 +01:00
8843b1b568 5.29.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15720776704
2025-06-18 00:16:13 +00:00
9c71f4fc1c Assemble 1.21.90 (#6736) 2025-06-18 01:15:00 +01:00
bd39caccb0 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/15598290427
2025-06-12 00:02:56 +00:00
95b4db5169 Fix slow SubChunk garbage collection check, closes #6574 (#6731) 2025-06-11 21:29:03 +01:00
48b80ecf78 Change crashdump file name format
this has bothered me for ages since it sorts into some absurd order by default due to the name starting with the day of the week.

this way it'll ensure that the files are always alphanumerically ordered, which means the most recent crashdump should always be
at the bottom.
2025-06-08 19:01:11 +01:00
4c3a2ef46e Update dependencies (minor-next) 2025-06-08 18:44:37 +01:00
d053e9e168 Merge branch 'stable' into minor-next 2025-06-08 18:43:31 +01:00
5ebbcd5d33 Move to newer systems for movement and block break handling (#6718)
MS is due to remove the non-server-auth versions of all of this stuff.

Fortunately v3 server auth movement works just fine without any changes,
although we will need to start sending player tick in some packets if
someone wants to actually use the rewind stuff.
2025-06-02 15:24:25 +01:00
a4ac28592c Updated dependencies 2025-06-02 15:17:00 +01:00
e99665fb12 Bump docker/build-push-action in the github-actions group (#6719) 2025-06-02 14:08:14 +00:00
b4b6bbe29f BaseInventory: fixed internalAddItem() setting air slots to air
this bug was introduced in #4237, but it was unnoticed due to having no adverse
effects other than noisy debugs and network traffic.
2025-05-29 17:18:45 +01:00
0910a219d4 Fixed improper pre-checking of PlayerAuthInputPacket flags 2025-05-28 23:29:37 +01:00
56da492e48 World: make less noise about deleted tile entities
no need to repeat the same message 100 times
2025-05-28 22:10:20 +01:00
035d2dec23 LevelDB: make unknown block errors way less annoying
these would previously generate a new line for every error.
since errors are often repeated for different offsets (e.g. different states of the same block),
we can save a lot of spam by deduplicating them and telling which offsets the errors occurred in.
2025-05-28 22:03:29 +01:00
b40b99fe72 Player: fixed crash on action item return
this can happen if the old item had a lower max damage than the new one, and the new
one has a damage higher than the old one's max damage.

it can also happen if the damage was overridden to some illegal value by a custom item
as seen in https://crash.pmmp.io/view/12754811
2025-05-28 21:32:48 +01:00
baafaed362 Stem drops seeds according to binomial distribution
fixes #6709

we really need a better way to reverse-engineer the chance parameter for these
as the wiki just gives a probability table, which is quite tiresome to extract
patterns from.
2025-05-28 21:00:40 +01:00
bf33a625c9 Implemented Respawn Anchor (#6646)
PlayerRespawnAnchorUseEvent is also added with options SET_SPAWN and EXPLODE, which allows plugins to customise the outcome of using the anchor in PM, which currently doesn't support dimensions. The event is also cancellable.
2025-05-27 21:57:28 +01:00
059f4ee7bf Extract GeneratorExecutor system from World, v2 (#6682)
- `AsyncGeneratorExecutor` class added that encapsulates the logic of generating chunks using async tasks as previously
- `GeneratorExecutor` interface added that can be implemented to provide chunks in other ways
- `SyncGeneratorExecutor` which invokes the generator directly on the main thread, useful for simple generators like `Flat` where async tasks are not needed
- Some redundant APIs were removed from `World` (these will probably come back as deprecated stubs for the remainder of 5.x, but I was having too much fun deleting code)
- Removed internal `World->registerGeneratorToWorker()` (no longer useful)
- `World` now invokes generator executor instead of posting AsyncTasks directly
- Some internal classes moved to `pocketmine\world\generator\executor` (PopulationTask excluded because plugins use it in lieu of being able to regenerate chunks
- Generators can opt into main-thread execution by setting the `$fast` parameter to `true` in `GeneratorManager::register()`
2025-05-27 21:51:10 +01:00
dd17adeaaf Reintroduce step height additions for minor-next
Revert "Revert change that can't go on stable"

This reverts commit a554d2acf5.
2025-05-25 11:33:47 +01:00
98f28f8b6d Merge branch 'stable' into minor-next 2025-05-25 11:33:32 +01:00
a554d2acf5 Revert change that can't go on stable
API additions need to wait for the next minor release

Revert "Entity: make stepHeight accessable (#6702)"

This reverts commit 5527a0c6bf.
2025-05-25 11:32:40 +01:00
5527a0c6bf Entity: make stepHeight accessable (#6702) 2025-05-25 10:07:41 +02:00
eee2e62d81 Add EntityFrostWalkerEvent (#6673) 2025-05-25 10:01:46 +02:00
4d5c27a734 Unit test block hardness & blast resistance values (#6629) 2025-05-24 21:01:36 +01:00
e1af2a4af1 Update language dependency 2025-05-24 16:19:48 +01:00
f656d7d3d7 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/15221216334
2025-05-24 00:02:49 +00:00
3636173d75 ... 2025-05-23 23:28:15 +01:00
9606c0e0bb Remove stale labels as well as Waiting on Author labels
actions/stale is far too slow to do this itself since it processes lots of irrelevant crap on every run
2025-05-23 22:16:57 +01:00
55123b36e1 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/15090310700
2025-05-18 00:03:33 +00:00
94fb5d95b9 CommonThreadPartsTrait: fixed thread crashes sometimes missing cause info
closes #6669

this happens because isTerminated returns true before the thread's shutdown handler runs,
so we join with the thread to make sure that shutdown handlers are done before returning.

... hopefully we don't get servers randomly deadlocking in shutdown handlers ???
2025-05-17 19:09:54 +01:00
b5f236c019 Apparently we're supposed to use replace for this, not provide 2025-05-17 18:09:14 +01:00
7169f8e553 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/15087062558
2025-05-17 16:31:32 +00:00
657e6c8130 Added trigger cron workflow for RestrictedActions branch sync
we're having problems with the restricted action getting disabled due to repo inactivity,
so it's best we trigger it from here, since this repo's activity is what it's
interested in anyway.
2025-05-17 17:29:35 +01:00
647c2587a8 5.28.3 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15086729525
2025-05-17 15:45:22 +00:00
81d3017ad5 Murphy's Law (#6698) 2025-05-17 16:44:19 +01:00
a37353c060 composer: fixed borked version constraints
bruhhhhhhhhhhhh
2025-05-17 16:37:22 +01:00
280911ec59 5.28.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/15085916916
2025-05-17 14:01:49 +00:00
abb004fbc5 Ready 5.28.1 (#6696) 2025-05-17 15:00:53 +01:00
e0864e7ee8 composer: also axe unnecessary ctype polyfill 2025-05-17 14:54:26 +01:00
dca37d5842 Hack: forcibly remove symfony/polyfill-mbstring
we don't need this dependency anyway because mbstring is already provided.
2025-05-17 14:11:57 +01:00
67f3bb9c52 Update composer dependencies
and fix an error found by new PHPStan update
2025-05-17 13:46:33 +01:00
acf4341d71 always the CS... 2025-05-17 13:35:55 +01:00
84bb9d2ab4 Consolidate Bedrock data version info
this ensures we don't have to go into a bunch of randomly scattered files to update version numbers.
2025-05-17 13:33:42 +01:00
50f3fe2578 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14951134662
2025-05-11 01:49:34 +00:00
04de72e85e Fix changelog typo (#6690) 2025-05-10 14:34:37 +01:00
4bcef443f7 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14940598161
2025-05-10 01:40:09 +00:00
d90fc3415c fixed wrong version info (#6689) 2025-05-09 16:33:55 +01:00
e2e16a4ec5 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14931383427
2025-05-09 14:38:28 +00:00
134c7309c5 5.28.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14931216524
2025-05-09 14:30:04 +00:00
5e830c7320 Protocol changes for 1.21.80 (#6687)
* Bedrock 1.21.80 support

* Update bedrock-data

* Add required tags to models

* Fixed biome data loading

* Support newest world format
apparently I messed up the blockstate data version last time around... it hasn't changed since 1.21.60

* always CS has to complain...

* Sync with release versions

* Ready 5.28.0 release

* this might help...

---------

Co-authored-by: Dylan T. <dktapps@pmmp.io>
2025-05-09 15:29:05 +01:00
c1cee1fc24 Merge branch 'stable' into minor-next 2025-05-08 02:27:51 +01:00
d789c75c00 Improve PHPStan error reporting for unsafe foreaches
these are actually two separate concerns: one for dodgy PHPStan type suppression on implicit keys, and the other for arrays being casted to strings by PHP.
2025-05-08 02:26:09 +01:00
6196b9c995 Merge branch 'stable' into minor-next 2025-05-04 17:20:27 +01:00
f2e7473629 Update PHP-CS-Fixer 2025-05-04 17:19:15 +01:00
6bf9a305de Rename confusing PHPStan rule name
it never occurred to me that this was misleading until I read some Devin documentation,
noticed that Devin misunderstood was the class was for, and then realized actually
Devin understood correctly, and it was the name of the class that was wrong. Funny
how that happens...
2025-05-03 19:24:21 +01:00
f8abcd5102 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14786989960
2025-05-02 01:42:51 +00:00
6f3506360e Bump the github-actions group with 3 updates (#6683) 2025-05-01 08:30:26 +00:00
efaf9311b3 Extract population business logic from PopulationTask 2025-04-30 17:38:16 +01:00
fe70b31881 Fix crash when a player is added to the world 2025-04-26 22:11:03 +01:00
cfafb584a8 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14565559872
2025-04-21 01:45:17 +00:00
ad6f7dfedb World: verify saveability of blocks, entities and tiles at entry points
I want to do the same for items, but items are going to be a pain in the ass.
For items there are multiple possible entry points and all of them will need to be checked:
- dropped items
- inventory contents
- lecterns
- item frames

I don't see a good way to deal with all these. We can't check for registration in the constructor
because we need to fully construct the item in order to register it.

Blocks are also a potential issue in other areas, but setBlock() is definitely the biggest offender.
2025-04-20 19:48:28 +01:00
1ea5c060fd bruh 2025-04-20 18:16:54 +01:00
4a5c1e7540 Entity: truncate fire ticks instead of throwing exceptions
as written in the comments, it's not reasonable to propagate this limitation, since it
ultimately comes from a shortfall in the Mojang save format, not a limitation of PM's
capability. It's also not obvious how this would be propagated to the likes of setOnFire(),
as this would translate into a max time of 1638 seconds, a value no one is going to
remember.

There's a case to be made for truncating this on save rather than on initial set, but
this is at least better than having Fire Aspect level 1000 cause crashes and whatever
other gameplay logic that would have to work around this stupid limitation.
2025-04-20 16:57:44 +01:00
2548422973 AvailableEnchantmentRegistry: reject non-string tags
fixes https://crash.pmmp.io/view/12627328
2025-04-20 16:44:23 +01:00
028815490e Add EntityExtinguishEvent (#6671) 2025-04-18 11:19:46 +02:00
a74168953c Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14482870321
2025-04-16 01:41:52 +00:00
f661443ec7 Update Ubuntu base image for GitHub Actions 2025-04-15 16:48:13 +01:00
1073f372f8 Merge branch 'stable' into minor-next 2025-04-06 20:00:45 +01:00
835c383d4e Update Composer dependencies 2025-04-06 20:00:21 +01:00
d3f6c22996 5.27.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14288755593
2025-04-06 03:53:08 +00:00
6f3851be80 5.27.1 (#6670) 2025-04-06 04:52:15 +01:00
071c15d7de NetworkSession: immediate-send now causes a buffer flush when the packet is ready
instead of skipping queues and forcing sync compression as previously seen.

this maintains proper packet order and allows immediate-flush to be used to reduce latency in-game.

Small servers won't notice any difference, but for larger ones it may make a difference, since the buffer time effectively depends on the amount of load RakLib is under.
closes #3325
2025-04-05 17:40:48 +01:00
673b39e2a1 Internet: remove curl_close() call (#6667)
curl_close() has no effect as of php8: https://www.php.net/manual/en/function.curl-close.php
2025-04-02 22:24:50 +01:00
17faa19743 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14120132647
2025-03-28 01:37:48 +00:00
e88b81a4cb 5.27.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14110940403
2025-03-27 15:49:33 +00:00
687112f4cd 5.27.0, Bedrock 1.21.70 support (#6665)
Co-authored-by: Dylan K. Taylor <dktapps@pmmp.io>
2025-03-27 15:48:19 +00:00
644c04bee4 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14049692123
2025-03-25 01:38:29 +00:00
c9e85603b0 Bump phpstan/phpstan-strict-rules in the development-patch-updates group (#6664) 2025-03-24 14:43:29 +00:00
d954772a20 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/14014323991
2025-03-23 01:42:15 +00:00
c80a4d5b55 5.26.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/14011298411
2025-03-22 18:33:19 +00:00
f416cb8902 Merge pull request #6658 from pmmp/minor-next-release 2025-03-22 18:32:15 +00:00
f123df5e0d changelog: use more accurate terminology 2025-03-22 18:16:33 +00:00
de26ebd124 Prepare 5.26.0 release 2025-03-22 17:59:20 +00:00
6b04bb504f Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13913481960
2025-03-18 01:37:03 +00:00
1c6a4bde86 Bump pocketmine/locale-data in the production-patch-updates group (#6656) 2025-03-17 13:20:59 +00:00
c2f8e9365b BlockStateToObjectDeserializer: check that the returned state is actually registered
if not, this will cause random crashes in core code, which assumes that state IDs found on runtime chunk memory are valid and registered.

this problem exists in other places too, and probably requires a rethink of how we're dealing with this, but for now, this will do as a band-aid.
2025-03-15 20:53:49 +00:00
4ef21fabab Merge branch 'stable' into minor-next 2025-03-15 20:37:28 +00:00
4407e585e4 Update composer dependencies (minor-next) 2025-03-15 20:36:39 +00:00
463be36b72 Update composer dependencies 2025-03-15 20:33:47 +00:00
8b57e9007a 👺 2025-03-15 01:33:29 +00:00
e03c586c86 GarbageCollectorManager: promote debug message to info
this has such a big impact on performance that I think this is warranted. Should also make it more obvious what the GC is doing without needing to enable ALL debug info.
2025-03-15 01:29:49 +00:00
802e373bf3 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13868087288
2025-03-15 01:26:47 +00:00
09acbfab4c dependabot: ignore phpstan/phpstan updates
these are noisy and cause conflicts. Since they also usually cause new errors to be reported, we often can't directly update it anyway. Better to test & update this locally.
2025-03-15 00:03:09 +00:00
7cfaf04b87 stfu 2025-03-14 16:10:56 +00:00
d9e0e51e14 Reduce code duplication in copper block serialization handling 2025-03-14 16:08:06 +00:00
069ecf007f Merge branch 'stable' into minor-next 2025-03-14 15:41:46 +00:00
341c7a03a9 CopperMaterial: fixed missing @return $this docs 2025-03-14 15:40:27 +00:00
7ae90dda5d Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13825107599
2025-03-13 01:36:47 +00:00
73a4b076d6 actions: tidy support response message 2025-03-12 16:19:11 +00:00
00df508727 Update bug-report.yml 2025-03-12 13:06:57 +00:00
a6553097f4 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13754675430
2025-03-10 01:14:10 +00:00
ac7b5b3b13 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13743358576
2025-03-09 01:16:28 +00:00
95284bc9de change error identifier 2025-03-09 00:54:39 +00:00
2291546610 phpstan: added rule to ban new $class
see #6635 for rationale on why we want to get rid of this

for now, this rule will prevent this anti-feature from being used in new code
2025-03-09 00:51:12 +00:00
09f0ce458c Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13666455727
2025-03-05 01:27:20 +00:00
9382e6e5b3 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13610829991
2025-03-02 01:38:07 +00:00
afac178cf4 Merge branch 'stable' into minor-next 2025-02-26 17:31:55 +00:00
e2f5e3e73c Update composer dependencies 2025-02-26 17:31:26 +00:00
19425e35ea Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13488997706
2025-02-24 01:26:36 +00:00
a08b06d322 also sort table by ID 2025-02-18 15:34:20 +00:00
c876253f76 tools/blockstate-upgrade-schema-utils: added dump-table command
this command dumps a human-readable version of pmmp/mapping palette mapping files to a .txt file.
may be useful for debugging issues with the schema generator or the upgrade process.
2025-02-18 15:30:39 +00:00
67272f8f2b Merge branch 'stable' of github.com:pmmp/PocketMine-MP into minor-next 2025-02-18 01:25:46 +00:00
9744bd7073 CraftingManager: make a less dumb hash function
fixes #4415
2025-02-17 15:35:18 +00:00
51cf6817b1 World: fixed overflow checks for getCollisionBlocks(), closes #6630 2025-02-16 23:24:39 +00:00
fd04894a7b Merge branch 'stable' of github.com:pmmp/PocketMine-MP into minor-next 2025-02-16 23:18:34 +00:00
d2d6a59c72 ItemDeserializer: fix doc comment typo 2025-02-16 22:52:11 +00:00
788ee9a008 Allow overriding deserializers for block and item IDs
there's no technical reason not to support this, since it doesn't violate any assumptions and the type returned is a base anyway.

this enables implementing stuff like snow cauldrons in a plugin, which previously would require reflection due to the minecraft:cauldron deserializer being registered already.
it also enables overriding IDs to map to custom blocks, which might be useful for overriding some functionality (although this is inadvisable - and won't alter the usage of stuff like VanillaBlocks::WHATEVER()).

we do *not* allow overriding serializers, since type IDs are expected to be paired to block implementations, and allowing them to be reassigned could lead to crashes if the new class was incorrect. So the correct approach for overriding nether portals would be to create a custom type ID as if you were adding a fully custom item. This will also allow other plugins to distinguish between your implementation and the built-in one.
2025-02-16 22:49:40 +00:00
246c1776df InventoryAction: avoid throwaway Item clones 2025-02-16 21:47:35 +00:00
2670e81668 Merge branch 'stable' of github.com:pmmp/PocketMine-MP into minor-next 2025-02-12 02:14:51 +00:00
e29aa2f337 Added resin material color (#6622) 2025-02-11 16:15:42 +00:00
9b3b45258a Optimise collision box checks by caching some basic info (#6606)
This PR significantly improves performance of entity movement calculation.

Previous attempts to optimise this were ineffective, as they used a cache to mitigate the cost of recomputing AABBs. Entities tend to move around randomly, so the non-cached pathway really needed to be optimized.

This change improves performance on multiple fronts:
1) avoiding Block allocations for blocks with 1x1x1 AABBs and with no AABBs (the most common)
2) avoiding Block allocations and overlapping intersection checks unless a stateID is specifically known to potentially exceed its cell boundaries (like fences)
3) avoiding overlapping AABB checks when overlaps can't make any difference anyway (cubes)

Together, these changes improve the performance of World->getBlockCollisionBoxes() by a factor of 5. In real-world terms, this shows up as a major performance improvement in situations with lots of entities moving in random directions. Testing with item entities showed an increase from 400 to 1200 moving items with the same CPU usage.

This change is built on the assumption that `Block->recalculateCollisionBoxes()` and its overrides don't interact with any world. This is technically possible due to the crappy design of the `Block` architecture, but should be avoided. As a world is not available during `RuntimeBlockStateRegistry` initialization, attempting to interact with a world during `recalculateCollisionBoxes()` will now cause a crash.

This turned out to be a problem for `ChorusPlant`, which was fixed by 70fb9bbdfd. The correct solution in this case was to use dynamic states similar to how we currently deal with fence connections.
2025-02-06 15:42:10 +00:00
ca4debbf08 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13126338048
2025-02-04 01:22:15 +00:00
03f98ee0a9 New version of NBT for performance improvements 2025-02-02 19:47:47 +00:00
70368ea653 Merge branch 'stable' into minor-next 2025-02-02 19:47:03 +00:00
21ccd90147 ChunkCache: parameterize dimension ID
(cc @Muqsit)
2025-02-02 19:43:04 +00:00
0a9a45a126 Improve block break progress
closes #6500

This fixes break time animations for mining fatigue and haste, and
improves the underwater and on-ground behaviour.

on-ground is still not quite right for reasons not related to this PR
(see #6547).
I'm also not quite sure the underwater logic is correct (in water vs
underwater?) but it's definitely better than what we have currently.
2025-02-02 19:34:14 +00:00
88937f1785 Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13094301661
2025-02-02 01:26:08 +00:00
e5a783cb9e Merge 'stable' into 'minor-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/13001707314
2025-01-28 01:21:43 +00:00
70fb9bbdfd ChorusPlant: fixed recalculateCollisionBoxes() depending on the world 2025-01-27 21:28:26 +00:00
128 changed files with 3463 additions and 1344 deletions

View File

@ -57,6 +57,9 @@ body:
attributes:
value: |
## Version, OS and game info
> [!WARNING]
> "Latest" is not a valid version.
> Failure to fill these fields with valid information may result in your issue being closed.
- type: input
attributes:

View File

@ -12,6 +12,10 @@ updates:
update-types:
- "version-update:semver-major"
- "version-update:semver-minor"
#since we lock this to exact versions, it causes conflicts with minor-next & major-next in composer.lock
#better to just test updates to this locally anyway since almost every version breaks something
- dependency-name: phpstan/phpstan
groups:
production-patch-updates:
dependency-type: production

View File

@ -0,0 +1,32 @@
#Since GitHub automatically disables cron actions after 60 days of repo inactivity, we need the active repo (PM)
#to trigger the branch merge workflow explicitly. This avoids the need for TOS-violating actions which we previously
#used to keep the restricted action active, as the workflow depends on the activity of this repo anyway.
name: Trigger branch sync
on:
schedule:
- cron: "0 0 * * *" #once per day so we don't spam merge commits on busy days
workflow_dispatch: #for testing
jobs:
trigger:
name: Trigger branch sync RestrictedActions workflow
runs-on: ubuntu-22.04
steps:
- name: Generate access token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }}
private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }}
owner: ${{ github.repository_owner }}
repositories: RestrictedActions
- name: Dispatch branch sync restricted action
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.generate-token.outputs.token }}
repository: ${{ github.repository_owner }}/RestrictedActions
event-type: pocketmine_mp_branch_sync

View File

@ -8,7 +8,7 @@ on:
jobs:
build:
name: Update Docker Hub images
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Set up Docker Buildx
@ -53,7 +53,7 @@ jobs:
run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT
- name: Build image for tag
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.18.0
with:
push: true
context: ./pocketmine-mp
@ -66,7 +66,7 @@ jobs:
- name: Build image for major tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.18.0
with:
push: true
context: ./pocketmine-mp
@ -79,7 +79,7 @@ jobs:
- name: Build image for minor tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.18.0
with:
push: true
context: ./pocketmine-mp
@ -92,7 +92,7 @@ jobs:
- name: Build image for latest tag
if: steps.channel.outputs.CHANNEL == 'stable'
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v6.18.0
with:
push: true
context: ./pocketmine-mp

View File

@ -0,0 +1,47 @@
name: "Copilot Agent environment setup"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
# The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot.
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: pmmp/setup-php-action@3.2.0
with:
php-version: 8.3
install-path: "./bin"
pm-version-major: 5
- name: Restore Composer package cache
uses: actions/cache@v4
with:
path: |
~/.cache/composer/files
~/.cache/composer/vcs
key: "composer-v2-cache-8.3-${{ hashFiles('./composer.lock') }}"
restore-keys: |
composer-v2-cache-
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction
- name: Clone extension stubs
uses: actions/checkout@v4
with:
repository: pmmp/phpstorm-stubs
path: extension-stubs

View File

@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup PHP and tools
uses: shivammathur/setup-php@2.32.0
uses: shivammathur/setup-php@2.34.1
with:
php-version: 8.2

View File

@ -24,7 +24,7 @@ permissions:
jobs:
check-intent:
name: Check release trigger
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
valid: ${{ steps.validate.outputs.DEV_BUILD == 'false' }}
@ -43,13 +43,13 @@ jobs:
#don't do these checks if this isn't a release - we don't want to generate unnecessary failed statuses
if: needs.check-intent.outputs.valid == 'true'
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@2.32.0
uses: shivammathur/setup-php@2.34.1
with:
php-version: 8.2

View File

@ -23,7 +23,7 @@ env:
jobs:
skip:
name: Check whether to ignore this tag
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
skip: ${{ steps.exists.outputs.exists == 'true' }}
@ -54,12 +54,12 @@ jobs:
needs: [check]
if: needs.check.outputs.valid == 'true' && github.ref_type != 'tag' #can't do post-commit for a tag
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Generate access token
id: generate-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }}
private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }}
@ -79,7 +79,7 @@ jobs:
needs: [check]
if: needs.check.outputs.valid == 'true' || github.ref_type == 'tag' #ignore validity check for tags
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
@ -87,7 +87,7 @@ jobs:
submodules: true
- name: Setup PHP
uses: shivammathur/setup-php@2.32.0
uses: shivammathur/setup-php@2.34.1
with:
php-version: ${{ env.PHP_VERSION }}
@ -165,7 +165,7 @@ jobs:
${{ github.workspace }}/core-permissions.rst
- name: Create draft release
uses: ncipollo/release-action@v1.16.0
uses: ncipollo/release-action@v1.18.0
id: create-draft
with:
artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst
@ -188,4 +188,4 @@ jobs:
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v3
with:
message: "[Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."
message: "${{ vars.DRAFT_RELEASE_NOTIFICATION_MENTION }} [Draft release ${{ steps.get-pm-version.outputs.PM_VERSION }}](${{ steps.create-draft.outputs.html_url }}) has been created for commit ${{ github.sha }}. Please review and publish it."

View File

@ -15,7 +15,7 @@ on:
type: number
image:
description: 'Runner image to use'
default: 'ubuntu-20.04'
default: 'ubuntu-22.04'
type: string
jobs:

View File

@ -20,7 +20,7 @@ jobs:
codestyle:
name: Code Style checks
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
@ -28,10 +28,10 @@ jobs:
- uses: actions/checkout@v4
- name: Setup PHP and tools
uses: shivammathur/setup-php@2.32.0
uses: shivammathur/setup-php@2.34.1
with:
php-version: 8.2
tools: php-cs-fixer:3.49
php-version: 8.3
tools: php-cs-fixer:3.75
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -40,7 +40,7 @@ jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false

View File

@ -15,19 +15,23 @@ jobs:
with:
github-token: ${{ github.token }}
script: |
const [owner, repo] = context.payload.repository.full_name.split('/');
try {
await github.rest.issues.removeLabel({
owner: owner,
repo: repo,
issue_number: context.payload.number,
name: "Status: Waiting on Author",
});
} catch (error) {
if (error.status === 404) {
//probably label wasn't set on the issue
console.log('Failed to remove label (probably label isn\'t on the PR): ' + error.message);
} else {
throw error;
async function removeLabel(owner, repo, issue_number, name) {
try {
await github.rest.issues.removeLabel({
owner: owner,
repo: repo,
issue_number: issue_number,
name: name,
});
} catch (error) {
if (error.status === 404) {
//probably label wasn't set on the issue
console.log('Failed to remove label ' + name + ' (probably label isn\'t on the PR): ' + error.message);
} else {
throw error;
}
}
}
const [owner, repo] = context.payload.repository.full_name.split('/');
removeLabel(owner, repo, context.payload.number, "Status: Waiting on Author");
removeLabel(owner, repo, context.payload.number, "Stale");

View File

@ -20,10 +20,7 @@ jobs:
- Check our [Documentation](https://doc.pmmp.io) to see if you can find answers there
- Ask the community on our [Discord server](https://discord.gg/bmSAZBG) or our [Forums](https://forums.pmmp.io)
[Docs](https://pmmp.rtfd.io) | [Discord](https://discord.gg/bmSAZBG) | [Forums](https://forums.pmmp.io)
- Ask the community on our [Discord server](https://discord.gg/bmSAZBG)
close-issue: true
lock-issue: false

View File

@ -22,7 +22,7 @@ jobs:
steps:
- name: Generate access token
id: generate-token
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }}
private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }}

View File

@ -6,6 +6,12 @@ $finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/tests')
->in(__DIR__ . '/tools')
->notPath('plugins/DevTools')
//JsonMapper will break if the FQNs in the doc comments for these are shortened :(
->notPath('crafting/json')
->notPath('inventory/json')
->notPath('data/bedrock/block/upgrade/model')
->notPath('data/bedrock/item/upgrade/model')
->notName('PocketMine.php');
return (new PhpCsFixer\Config)

View File

@ -12,7 +12,7 @@
</p>
<p align="center">
<a href="https://github.com/pmmp/PocketMine-MP/actions/workflows/main.yml"><img src="https://github.com/pmmp/PocketMine-MP/workflows/CI/badge.svg" alt="CI" /></a>
<a href="https://github.com/pmmp/PocketMine-MP/actions/workflows/main.yml"><img src="https://github.com/pmmp/PocketMine-MP/actions/workflows/main.yml/badge.svg" alt="CI" /></a>
<a href="https://github.com/pmmp/PocketMine-MP/releases/latest"><img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/pmmp/PocketMine-MP?label=release&sort=semver"></a>
<a href="https://discord.gg/bmSAZBG"><img src="https://img.shields.io/discord/373199722573201408?label=discord&color=7289DA&logo=discord" alt="Discord" /></a>
<br>

View File

@ -31,8 +31,8 @@ require dirname(__DIR__) . '/vendor/autoload.php';
*/
/**
* @var string[]|\Closure[] $options
* @phpstan-var array<string, string|\Closure() : string> $options
* @var string[]|Closure[] $options
* @phpstan-var array<string, string|Closure() : string> $options
*/
$options = [
"base_version" => VersionInfo::BASE_VERSION,

71
changelogs/5.26.md Normal file
View File

@ -0,0 +1,71 @@
# 5.26.0
Released 22nd March 2025.
This is a minor feature release focused on performance improvements.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## Performance
- Significantly improved performance of entity movement. Load testing with item entities showed a 3x increase in the number of entities supported without lag.
- Significantly improved performance of on-ground checks for player movement. This still needs further work, but optimisations implemented in this version should improve performance substantially.
- Updated `pocketmine/nbt` dependency with performance improvements to `TAG_Compound` and `TAG_List` comparison. This should improve performance of inventory-related actions.
- `InventoryTransaction` now avoids useless item clones when processing transactions, which should improve performance of inventory-related actions.
## Dependencies
- `pocketmine/bedrock-protocol` has been updated to `36.2.0`, which adds new functions to access some packet fields.
- `pocketmine/nbt` has been updated to `1.1.0`, which improves performance when comparing NBT object trees.
## Gameplay
- Block breaking animation speed now takes into account the following: jumping, being in water, haste, mining fatigue
## Tools
- `blockstate-upgrade-schema-utils.php` now has a new `dump-table` command, which turns a `.bin` palette table file into human-readable text for debugging.
## API
### `pocketmine\block`
- The following methods have been added:
- `public RuntimeBlockStateRegistry->hasStateId(int $stateId) : bool` - checks whether the given state ID is registered
### `pocketmine\crafting`
- The following methods have been deprecated:
- `CraftingManager::sort()` - this was implicitly internal anyway
### `pocketmine\utils`
- The following constants have been added:
- `TextFormat::MATERIAL_RESIN`
- The following static properties have been added:
- `Terminal::$COLOR_MATERIAL_RESIN`
### `pocketmine\data\bedrock\block`
- `BlockStateToObjectDeserializer` now permits overriding **deserializers** for Bedrock IDs. This may be useful to implement custom state handling, or to implement missing block variants (such as snow cauldron).
- This was originally prohibited since 5.0.0. However, there is no technical reason to disallow overriding **deserializers**.
- Overriding **serializers** is still **not permitted**. Reusing type IDs doesn't make any sense and would break internal design contracts.
- If you want to make a custom version of a vanilla block, create a custom type ID for it, exactly as you would for a regular custom block.
- The following methods have been added:
- `public BlockStateToObjectDeserializer->getDeserializerForId(string $id) : ?(\Closure(BlockStateReader) : Block)`
### `pocketmine\data\bedrock\item`
- `ItemDeserializer` now permits overriding **deserializers** for Bedrock IDs. As above, this may be useful to implement custom data handling, or to implement missing variants of existing items.
- This was originally prohibited since 5.0.0. However, there is no technical reason to disallow overriding **deserializers**.
- Overriding **serializers** is still **not permitted**. Reusing type IDs doesn't make any sense and would break internal design contracts.
- As above, if you want to make a custom version of a vanilla item, create a custom type ID for it, exactly as you would for a regular custom item.
- The following methods have been added:
- `public ItemDeserializer->getDeserializerForId(string $id) : ?(\Closure(SavedItemData) : Item)`
## Internals
- `new $class` is now banned on new internals code by a PHPStan rule. Closures or factory objects should be used instead for greater flexibility and better static analysis.
- `CraftingManager` now uses a more stable hash function for recipe output filtering.
- `ChunkCache` now accepts `int $dimensionId` in the constructor. This may be useful for plugins which implement the nether.
- `RuntimeBlockStateRegistry` now precomputes basic collision info about known states for fast paths.
- This permits specialization for common shapes like cubes and collisionless blocks, which allows skipping complex logic in entity movement calculation. This vastly improves performance.
- Any block whose class overrides `readStateFromWorld()` or `getModelPositionOffset()` will *not* be optimised.
- `Block->recalculateCollisionBoxes()` now has a hard requirement not to depend on anything other than available properties. It must not use `World` or its position.
- This change was problematic for `ChorusPlant`, which used nearby blocks to calculate its collision boxes.
- Blocks which need nearby blocks should override `readStateFromWorld()` and set dynamic state properties, similar to fences.
- This design flaw will be corrected with a major change to `Block` internals currently in planning for a future major version.
- `Block->getCollisionBoxes()` may not be called at all during gameplay for blocks with shapes determined to be simple, like cubes and collisionless blocks.
- `BlockStateToObjectDeserializer` now checks if the returned blockstate is registered in `RuntimeBlockStateRegistry` to promote earlier error detection (instead of crashing in random code paths).

24
changelogs/5.27.md Normal file
View File

@ -0,0 +1,24 @@
# 5.27.0
Released 27th March 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.70.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## Interim releases
If you're upgrading from 5.25.x directly to 5.27.0, please also read the following changelogs, as the interim releases contain important changes:
- [5.26.0](https://github.com/pmmp/PocketMine-MP/blob/5.26.0/changelogs/5.26.md#5260) - Performance improvements and other internal improvements
## General
- Aded support for Minecraft: Bedrock Edition 1.21.70.
- Removed support for earlier versions.
# 5.27.1
Released 6th April 2025.
## Fixes
- Updated RakLib to get ping timestamp handling fixes.

34
changelogs/5.28.md Normal file
View File

@ -0,0 +1,34 @@
# 5.28.0
Released 9th May 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.80.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- Added support for Minecraft: Bedrock Edition 1.21.80.
- Removed support for earlier versions.
## Fixes
- `AvailableEnchantmentRegistry` now requires provided tags to always be `string`. Previously, this wasn't enforced, leading to random crashes in core code related to enchanting.
- `Entity->setFireTicks()` and `Entity->setOnFire()` now truncate the fire time to the max value instead of throwing exceptions.
## Internals
- Improved PHPStan error reporting for unsafe foreaches. Foreach on an array with implicit keys now generates different errors than foreach on an array with string keys.
# 5.28.1
Released 17th May 2025.
## Fixes
- Fixed errors when PlayStation players attempt to join due to null `TitleID`.
# 5.28.2
Released 17th May 2025.
## Fixes
- Fixed version constraints which were incorrectly updated during the 1.21.80 update. This led to an unnoticed failure to update BedrockProtocol in the previous patch release.
- Actually fixed PlayStation issues this time

25
changelogs/5.29.md Normal file
View File

@ -0,0 +1,25 @@
# 5.29.0
Released 18th June 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.90.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- Added support for Minecraft: Bedrock Edition 1.21.90.
- Removed support for earlier versions.
## Fixes
- Fixed thread crashes sometimes not reporting proper cause information in crashdumps.
- Fixed crash when a plugin replaced a player's held tool with a different tool with a damage exceeding the old tool's max damage during an action.
- Fixed performance issue of `PlayerAuthInputPacket` input flags handling (broken change detection).
- Fixed `BaseInventory->addItem()` triggering updates on empty slots when no items were added.
- Fixed slow check in `SubChunk` block layer garbage collection.
## Internals
- `LoginPacketHandler->processLogin()` signature has changed. This will break any plugins overriding `LoginPacketHandler`. As noted above, this is _not_ covered by the API version guarantee.
- Automated branch sync for `minor-next` and `major-next` is now triggered by `repository_dispatch` from a cron job in this repository instead of `RestrictedActions`. The `RestrictedActions` cron job was getting automatically disabled by GitHub due to repo inactivity.

73
changelogs/5.30.md Normal file
View File

@ -0,0 +1,73 @@
# 5.30.0
Released 18th June 2025.
This is a minor feature release containing API additions, internals cleanup and user experience improvements.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- Significantly reduced log spam when unknown blocks, tiles and entities are found in saved worlds.
- The file name structure for crashdumps has been changed to improve sorting order in file browsers.
- Buffering is now skipped on the RakLib layer. In theory this could reduce player network latency by 10 ms (YMMV).
## Gameplay
### Blocks
- Many blocks have had their hardness and blast resistance updated to match vanilla.
- Implemented Respawn Anchor.
- Melon Stem and Pumpkin Stem drop amounts should now match vanilla (using binomial distribution).
## API
## General
- Verification of save registration has been added for blocks, entities and tiles. This is intended to make it easier to find mistakes when registering custom things, which previously would produce obscure core crashes.
### `pocketmine\event\block`
- The following classes have been added:
- `BlockPreExplodeEvent` - called before a block tries to explode
- `BlockExplodeEvent` - called when after a block's explosion calculation has been done, but before any changes are applied
### `pocketmine\event\entity`
- The following classes have been added:
- `EntityExtinguishEvent` - called when a burning entity is extinguished by water or other sources
- `EntityFrostWalkerEvent` - called every tick upon which an entity wearing Frost Walker boots moves; this can be used to customise or cancel the behaviour of the Frost Walker enchantment
### `pocketmine\event\player`
- The following classes have been added:
- `PlayerRespawnAnchorUseEvent` - called when a player interacts with a charged respawn anchor
### `pocketmine\entity`
- The following methods have been added:
- `public Entity->getStepHeight() : float`
- `public Entity->setStepHeight(float $stepHeight) : void`
### `pocketmine\world\generator`
- Generator execution has been decoupled from `PopulationTask` and async tasks in general. The following classes have been added:
- `executor\GeneratorExecutor`
- `executor\SyncGeneratorExecutor` - runs a generator on the main thread (used for flat world generation, which doesn't need threads)
- `executor\AsyncGeneratorExecutor` - runs a generator inside an async task, as before
- `PopulationUtils` - contains population business logic previously baked into `PopulationTask` - this permits the reuse of that logic outside async tasks
- The following methods have signature changes:
- `GeneratorManager->addGenerator()` now accepts an optional `bool $fast` parameter, defaulting to `false`; setting this to `true` will cause your generator to run on the main thread
- The following methods have been added:
- `public GeneratorManagerEntry->isFast() : bool` - returns whether this generator should run on the main thread
- `PopulationTask` has been marked as `@internal`. In the next major version, it will move to the `generator\executor` namespace; however, for now it stays put because plugins currently have no other way to regenerate chunks.
## Internals
- World data version numbers have been consolidated in `pocketmine\data\bedrock\WorldDataVersions`. This removes the need to modify several different files to support new world versions, and reduces the chances of things getting missed.
- Block hardness and blast resistance is now unit-tested against `block_properties_table.json` in `BedrockData`. This file comes from vanilla BDS, so we can use it to verify compliance.
- Protocol-layer "server auth block breaking" has been enabled. Functionally, this is no different from the previous system, it just works differently on the network layer.
- Various internal classes in the `pocketmine\world\generator` namespace have been moved to the `generator\executor` namespace.
- Removed `World->registerGenerator()` and `World->unregisterGenerator()`.
- Removed redundant calls to `curl_close()` (obsolete since PHP 8.0).
# 5.30.1
Released 23rd June 2025.
## Fixes
- Fixed accidental break of backwards compatibility in `EntityExplodeEvent` introduced in the previous release.
- Fixed placement of player holding block when exploding respawn anchor.
- Updated BedrockProtocol to fix incorrect encoding of `ServerScriptDebugDrawerPacket`.
- Disabled client-side locator bar, allowing plugins to write their own implementations.

14
changelogs/5.31.md Normal file
View File

@ -0,0 +1,14 @@
# 5.31.0
Released 8th July 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.93.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- Added support for Minecraft: Bedrock Edition 1.21.93.
- Removed support for earlier versions.

17
changelogs/5.32.md Normal file
View File

@ -0,0 +1,17 @@
# 5.32.0
Released 6th August 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.100.
**Plugin compatibility:** Plugins for previous 5.x versions will run unchanged on this release, unless they use internal APIs, reflection, or packages like the `pocketmine\network\mcpe` or `pocketmine\data` namespace.
Do not update plugin minimum API versions unless you need new features added in this release.
**WARNING: If your plugin uses the `pocketmine\network\mcpe` namespace, you're not shielded by API change constraints.**
Consider using the `mcpe-protocol` directive in `plugin.yml` as a constraint if you're using packets directly.
## General
- Added support for Minecraft: Bedrock Edition 1.21.100.
- Removed support for earlier versions.
## Fixes
- Fixed deadlock on RakLib thread crash (e.g. due to port binding failure).

View File

@ -34,29 +34,33 @@
"adhocore/json-comment": "~1.2.0",
"netresearch/jsonmapper": "~v5.0.0",
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~4.0.0+bedrock-1.21.60",
"pocketmine/bedrock-item-upgrade-schema": "~1.14.0+bedrock-1.21.50",
"pocketmine/bedrock-protocol": "~36.0.0+bedrock-1.21.60",
"pocketmine/binaryutils": "dev-experimental/read-ops-accounting as 0.2.6",
"pocketmine/bedrock-data": "~5.3.0+bedrock-1.21.100",
"pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~40.0.0+bedrock-1.21.100",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0",
"pocketmine/errorhandler": "^0.7.0",
"pocketmine/locale-data": "~2.24.0",
"pocketmine/locale-data": "~2.25.0",
"pocketmine/log": "^0.4.0",
"pocketmine/math": "~1.0.0",
"pocketmine/nbt": "~1.0.0",
"pocketmine/raklib": "~1.1.0",
"pocketmine/nbt": "~1.1.0",
"pocketmine/raklib": "~1.2.0",
"pocketmine/raklib-ipc": "~1.0.0",
"pocketmine/snooze": "^0.5.0",
"ramsey/uuid": "~4.7.0",
"ramsey/uuid": "~4.9.0",
"symfony/filesystem": "~6.4.0"
},
"require-dev": {
"phpstan/phpstan": "2.1.6",
"phpstan/phpstan": "2.1.17",
"phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.24"
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-mbstring": "*"
},
"autoload": {
"psr-4": {
"pocketmine\\": "src/"

475
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "415cb668882a32039066467bb308421f",
"content-hash": "402ad5667b1e636a8ec6acf2f1b5f055",
"packages": [
{
"name": "adhocore/json-comment",
@ -67,16 +67,16 @@
},
{
"name": "brick/math",
"version": "0.12.2",
"version": "0.13.1",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40"
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40",
"url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04",
"reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04",
"shasum": ""
},
"require": {
@ -115,7 +115,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.12.2"
"source": "https://github.com/brick/math/tree/0.13.1"
},
"funding": [
{
@ -123,7 +123,7 @@
"type": "github"
}
],
"time": "2025-02-26T10:21:45+00:00"
"time": "2025-03-29T13:50:30+00:00"
},
{
"name": "netresearch/jsonmapper",
@ -204,16 +204,16 @@
},
{
"name": "pocketmine/bedrock-data",
"version": "4.0.0+bedrock-1.21.60",
"version": "5.3.0+bedrock-1.21.100",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e"
"reference": "5279e76261df158d5af187cfaafc1618c1da9e3f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/2e5f16ec2facac653f3f894f22eb831d880ba98e",
"reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/5279e76261df158d5af187cfaafc1618c1da9e3f",
"reference": "5279e76261df158d5af187cfaafc1618c1da9e3f",
"shasum": ""
},
"type": "library",
@ -224,22 +224,22 @@
"description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.60"
"source": "https://github.com/pmmp/BedrockData/tree/5.3.0+bedrock-1.21.100"
},
"time": "2025-02-16T15:56:56+00:00"
"time": "2025-07-30T22:07:56+00:00"
},
{
"name": "pocketmine/bedrock-item-upgrade-schema",
"version": "1.14.0",
"version": "1.15.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockItemUpgradeSchema.git",
"reference": "9fc7c9bbb558a017395c1cb7dd819c033ee971bb"
"reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/9fc7c9bbb558a017395c1cb7dd819c033ee971bb",
"reference": "9fc7c9bbb558a017395c1cb7dd819c033ee971bb",
"url": "https://api.github.com/repos/pmmp/BedrockItemUpgradeSchema/zipball/09e0dbe9743f21a76b1fe04b2b4136785775f52b",
"reference": "09e0dbe9743f21a76b1fe04b2b4136785775f52b",
"shasum": ""
},
"type": "library",
@ -250,22 +250,22 @@
"description": "JSON schemas for upgrading items found in older Minecraft: Bedrock world saves",
"support": {
"issues": "https://github.com/pmmp/BedrockItemUpgradeSchema/issues",
"source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.14.0"
"source": "https://github.com/pmmp/BedrockItemUpgradeSchema/tree/1.15.0"
},
"time": "2024-12-04T12:22:49+00:00"
"time": "2025-08-06T15:08:48+00:00"
},
{
"name": "pocketmine/bedrock-protocol",
"version": "36.0.0+bedrock-1.21.60",
"version": "40.0.0+bedrock-1.21.100",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "2057de319c5c551001c2a544e08d1bc7727d9963"
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2057de319c5c551001c2a544e08d1bc7727d9963",
"reference": "2057de319c5c551001c2a544e08d1bc7727d9963",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"shasum": ""
},
"require": {
@ -296,22 +296,22 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/36.0.0+bedrock-1.21.60"
"source": "https://github.com/pmmp/BedrockProtocol/tree/40.0.0+bedrock-1.21.100"
},
"time": "2025-02-16T15:59:08+00:00"
"time": "2025-08-06T15:13:45+00:00"
},
{
"name": "pocketmine/binaryutils",
"version": "dev-experimental/read-ops-accounting",
"version": "0.2.6",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BinaryUtils.git",
"reference": "8cfa34c9d5aae11886a4142c172cff05f1e87ee2"
"reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/8cfa34c9d5aae11886a4142c172cff05f1e87ee2",
"reference": "8cfa34c9d5aae11886a4142c172cff05f1e87ee2",
"url": "https://api.github.com/repos/pmmp/BinaryUtils/zipball/ccfc1899b859d45814ea3592e20ebec4cb731c84",
"reference": "ccfc1899b859d45814ea3592e20ebec4cb731c84",
"shasum": ""
},
"require": {
@ -320,9 +320,9 @@
},
"require-dev": {
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "2.1.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpstan/phpstan": "~1.10.3",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0.0",
"phpunit/phpunit": "^9.5 || ^10.0 || ^11.0"
},
"type": "library",
@ -338,9 +338,9 @@
"description": "Classes and methods for conveniently handling binary data",
"support": {
"issues": "https://github.com/pmmp/BinaryUtils/issues",
"source": "https://github.com/pmmp/BinaryUtils/tree/experimental/read-ops-accounting"
"source": "https://github.com/pmmp/BinaryUtils/tree/0.2.6"
},
"time": "2025-03-11T11:50:46+00:00"
"time": "2024-03-04T15:04:17+00:00"
},
{
"name": "pocketmine/callback-validator",
@ -471,16 +471,16 @@
},
{
"name": "pocketmine/locale-data",
"version": "2.24.0",
"version": "2.25.1",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Language.git",
"reference": "6ec5e92c77a2102b2692763733e4763012facae9"
"reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/Language/zipball/6ec5e92c77a2102b2692763733e4763012facae9",
"reference": "6ec5e92c77a2102b2692763733e4763012facae9",
"url": "https://api.github.com/repos/pmmp/Language/zipball/8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd",
"reference": "8e6514f5a9638e69cdc2219c775fc7d3bb4c9fdd",
"shasum": ""
},
"type": "library",
@ -488,9 +488,9 @@
"description": "Language resources used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/Language/issues",
"source": "https://github.com/pmmp/Language/tree/2.24.0"
"source": "https://github.com/pmmp/Language/tree/2.25.1"
},
"time": "2025-02-16T20:46:34+00:00"
"time": "2025-04-16T11:15:32+00:00"
},
{
"name": "pocketmine/log",
@ -576,16 +576,16 @@
},
{
"name": "pocketmine/nbt",
"version": "1.0.1",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/pmmp/NBT.git",
"reference": "53db37487bc5ddbfbd84247966e1a073bdcfdb7d"
"reference": "c3c7b0a7295daeaf7873d90fed5c5d10381d12e1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/NBT/zipball/53db37487bc5ddbfbd84247966e1a073bdcfdb7d",
"reference": "53db37487bc5ddbfbd84247966e1a073bdcfdb7d",
"url": "https://api.github.com/repos/pmmp/NBT/zipball/c3c7b0a7295daeaf7873d90fed5c5d10381d12e1",
"reference": "c3c7b0a7295daeaf7873d90fed5c5d10381d12e1",
"shasum": ""
},
"require": {
@ -612,22 +612,22 @@
"description": "PHP library for working with Named Binary Tags",
"support": {
"issues": "https://github.com/pmmp/NBT/issues",
"source": "https://github.com/pmmp/NBT/tree/1.0.1"
"source": "https://github.com/pmmp/NBT/tree/1.1.1"
},
"time": "2025-01-07T22:47:46+00:00"
"time": "2025-03-09T01:46:03+00:00"
},
{
"name": "pocketmine/raklib",
"version": "1.1.1",
"version": "1.2.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/RakLib.git",
"reference": "be2783be516bf6e2872ff5c81fb9048596617b97"
"reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/RakLib/zipball/be2783be516bf6e2872ff5c81fb9048596617b97",
"reference": "be2783be516bf6e2872ff5c81fb9048596617b97",
"url": "https://api.github.com/repos/pmmp/RakLib/zipball/a28d05216d34dbd00e8aed827a58df6b4c11510b",
"reference": "a28d05216d34dbd00e8aed827a58df6b4c11510b",
"shasum": ""
},
"require": {
@ -639,8 +639,8 @@
"pocketmine/log": "^0.3.0 || ^0.4.0"
},
"require-dev": {
"phpstan/phpstan": "1.10.1",
"phpstan/phpstan-strict-rules": "^1.0"
"phpstan/phpstan": "2.1.0",
"phpstan/phpstan-strict-rules": "^2.0"
},
"type": "library",
"autoload": {
@ -655,9 +655,9 @@
"description": "A RakNet server implementation written in PHP",
"support": {
"issues": "https://github.com/pmmp/RakLib/issues",
"source": "https://github.com/pmmp/RakLib/tree/1.1.1"
"source": "https://github.com/pmmp/RakLib/tree/1.2.0"
},
"time": "2024-03-04T14:02:14+00:00"
"time": "2025-06-08T17:36:06+00:00"
},
{
"name": "pocketmine/raklib-ipc",
@ -742,16 +742,16 @@
},
{
"name": "ramsey/collection",
"version": "2.0.0",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/collection.git",
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5"
"reference": "344572933ad0181accbf4ba763e85a0306a8c5e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5",
"url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2",
"reference": "344572933ad0181accbf4ba763e85a0306a8c5e2",
"shasum": ""
},
"require": {
@ -759,25 +759,22 @@
},
"require-dev": {
"captainhook/plugin-composer": "^5.3",
"ergebnis/composer-normalize": "^2.28.3",
"fakerphp/faker": "^1.21",
"ergebnis/composer-normalize": "^2.45",
"fakerphp/faker": "^1.24",
"hamcrest/hamcrest-php": "^2.0",
"jangregor/phpstan-prophecy": "^1.0",
"mockery/mockery": "^1.5",
"jangregor/phpstan-prophecy": "^2.1",
"mockery/mockery": "^1.6",
"php-parallel-lint/php-console-highlighter": "^1.0",
"php-parallel-lint/php-parallel-lint": "^1.3",
"phpcsstandards/phpcsutils": "^1.0.0-rc1",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.2",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^1.3",
"phpunit/phpunit": "^9.5",
"psalm/plugin-mockery": "^1.1",
"psalm/plugin-phpunit": "^0.18.4",
"ramsey/coding-standard": "^2.0.3",
"ramsey/conventional-commits": "^1.3",
"vimeo/psalm": "^5.4"
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpspec/prophecy-phpunit": "^2.3",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-mockery": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^10.5",
"ramsey/coding-standard": "^2.3",
"ramsey/conventional-commits": "^1.6",
"roave/security-advisories": "dev-latest"
},
"type": "library",
"extra": {
@ -815,37 +812,26 @@
],
"support": {
"issues": "https://github.com/ramsey/collection/issues",
"source": "https://github.com/ramsey/collection/tree/2.0.0"
"source": "https://github.com/ramsey/collection/tree/2.1.1"
},
"funding": [
{
"url": "https://github.com/ramsey",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/ramsey/collection",
"type": "tidelift"
}
],
"time": "2022-12-31T21:50:55+00:00"
"time": "2025-03-22T05:38:12+00:00"
},
{
"name": "ramsey/uuid",
"version": "4.7.6",
"version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088"
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088",
"reference": "91039bc1faa45ba123c4328958e620d382ec7088",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12",
"ext-json": "*",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@ -853,26 +839,23 @@
"rhumsaa/uuid": "self.version"
},
"require-dev": {
"captainhook/captainhook": "^5.10",
"captainhook/captainhook": "^5.25",
"captainhook/plugin-composer": "^5.3",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
"doctrine/annotations": "^1.8",
"ergebnis/composer-normalize": "^2.15",
"mockery/mockery": "^1.3",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"ergebnis/composer-normalize": "^2.47",
"mockery/mockery": "^1.6",
"paragonie/random-lib": "^2",
"php-mock/php-mock": "^2.2",
"php-mock/php-mock-mockery": "^1.3",
"php-parallel-lint/php-parallel-lint": "^1.1",
"phpbench/phpbench": "^1.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9",
"ramsey/composer-repl": "^1.4",
"slevomat/coding-standard": "^8.4",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.9"
"php-mock/php-mock": "^2.6",
"php-mock/php-mock-mockery": "^1.5",
"php-parallel-lint/php-parallel-lint": "^1.4.0",
"phpbench/phpbench": "^1.2.14",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-mockery": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"slevomat/coding-standard": "^8.18",
"squizlabs/php_codesniffer": "^3.13"
},
"suggest": {
"ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
@ -907,19 +890,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.7.6"
"source": "https://github.com/ramsey/uuid/tree/4.9.0"
},
"funding": [
{
"url": "https://github.com/ramsey",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/ramsey/uuid",
"type": "tidelift"
}
],
"time": "2024-04-27T21:32:50+00:00"
"time": "2025-06-25T14:20:11+00:00"
},
{
"name": "symfony/filesystem",
@ -986,180 +959,21 @@
}
],
"time": "2024-10-25T15:07:50+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
}
],
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.13.0",
"version": "1.13.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414"
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
"shasum": ""
},
"require": {
@ -1198,7 +1012,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
},
"funding": [
{
@ -1206,20 +1020,20 @@
"type": "tidelift"
}
],
"time": "2025-02-12T12:17:51+00:00"
"time": "2025-04-29T12:36:36+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.4.0",
"version": "v5.5.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9",
"reference": "ae59794362fe85e051a58ad36b289443f57be7a9",
"shasum": ""
},
"require": {
@ -1262,9 +1076,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0"
},
"time": "2024-12-30T11:07:19+00:00"
"time": "2025-05-31T08:24:38+00:00"
},
{
"name": "phar-io/manifest",
@ -1386,16 +1200,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.6",
"version": "2.1.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c"
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
"reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": ""
},
"require": {
@ -1440,20 +1254,20 @@
"type": "github"
}
],
"time": "2025-02-19T15:46:42+00:00"
"time": "2025-05-21T20:55:28+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
"version": "2.0.4",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "d09e152f403c843998d7a52b5d87040c937525dd"
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d09e152f403c843998d7a52b5d87040c937525dd",
"reference": "d09e152f403c843998d7a52b5d87040c937525dd",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6b92469f8a7995e626da3aa487099617b8dfa260",
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260",
"shasum": ""
},
"require": {
@ -1464,7 +1278,9 @@
"phpunit/phpunit": "<7.0"
},
"require-dev": {
"nikic/php-parser": "^5",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6"
},
@ -1489,22 +1305,22 @@
"description": "PHPUnit extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.4"
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.6"
},
"time": "2025-01-22T13:07:38+00:00"
"time": "2025-03-26T12:47:06+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.3",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba"
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/8b88b5f818bfa301e0c99154ab622dace071c3ba",
"reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094",
"reference": "f9f77efa9de31992a832ff77ea52eb42d675b094",
"shasum": ""
},
"require": {
@ -1537,9 +1353,9 @@
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.3"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6"
},
"time": "2025-01-21T10:52:14+00:00"
"time": "2025-07-21T12:19:29+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -1864,16 +1680,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.45",
"version": "10.5.47",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8"
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8",
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
"reference": "3637b3e50d32ab3a0d1a33b3b6177169ec3d95a3",
"shasum": ""
},
"require": {
@ -1883,7 +1699,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.1",
"myclabs/deep-copy": "^1.13.1",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@ -1945,7 +1761,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.47"
},
"funding": [
{
@ -1956,12 +1772,20 @@
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
"time": "2025-02-06T16:08:12+00:00"
"time": "2025-06-20T11:29:11+00:00"
},
{
"name": "sebastian/cli-parser",
@ -2930,18 +2754,9 @@
"time": "2024-03-03T12:36:25+00:00"
}
],
"aliases": [
{
"package": "pocketmine/binaryutils",
"version": "dev-experimental/read-ops-accounting",
"alias": "0.2.6",
"alias_normalized": "0.2.6.0"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"pocketmine/binaryutils": 20
},
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@ -2972,7 +2787,7 @@
"ext-zlib": ">=1.2.11",
"composer-runtime-api": "^2.0"
},
"platform-dev": [],
"platform-dev": {},
"platform-overrides": {
"php": "8.1.0"
},

View File

@ -11,10 +11,11 @@ includes:
rules:
- pocketmine\phpstan\rules\DeprecatedLegacyEnumAccessRule
- pocketmine\phpstan\rules\DisallowDynamicNewRule
- pocketmine\phpstan\rules\DisallowEnumComparisonRule
- pocketmine\phpstan\rules\DisallowForeachByReferenceRule
- pocketmine\phpstan\rules\ExplodeLimitRule
- pocketmine\phpstan\rules\UnsafeForeachArrayOfStringRule
- pocketmine\phpstan\rules\UnsafeForeachRule
# - pocketmine\phpstan\rules\ThreadedSupportedTypesRule
parameters:

View File

@ -83,29 +83,6 @@ network:
#DO NOT DISABLE THIS unless you understand the risks involved.
enable-encryption: true
#EXPERIMENTAL! Limit packet read operations per network session.
#This is intended to stop exploitation of packets with arrays in them.
#Note that enabling this system may cause players to be unexpectedly kicked, or it may fail to stop attackers.
#As of March 2025, the system is still in development and subject to change.
packet-read-ops-limit:
#What to do when a session's read ops budget is depleted.
#Supported actions are "none", "warn" and "kick".
deplete-action: warn
#How many backlog ticks to budget for. 200 allows for a 10-second network lag spike, or a small number of complex
#packets.
session-budget-ticks: 200
#How much to top up each session's read operations budget per tick. Recommended to set this to about 2x the
#average number of read operations per tick per session.
#Exceeding this value won't cause any action to be taken. However, consistently exceeding it will cause the
#session's budget to be depleted, resulting in action being taken.
session-budget-per-tick: 100
#Whether to collect stats for debugging. This might impact performance.
#See NetworkSession::dumpDecodeCostStats() if you want to visualize the stats yourself.
collect-stats: false
debug:
#If > 1, it will show debug messages in the console
level: 1

View File

@ -31,6 +31,7 @@ use function hrtime;
use function max;
use function min;
use function number_format;
use function sprintf;
/**
* Allows threads to manually trigger the cyclic garbage collector using a threshold like PHP's own garbage collector,
@ -48,6 +49,7 @@ final class GarbageCollectorManager{
private int $threshold = self::GC_THRESHOLD_DEFAULT;
private int $collectionTimeTotalNs = 0;
private int $runs = 0;
private \Logger $logger;
private TimingsHandler $timings;
@ -96,7 +98,16 @@ final class GarbageCollectorManager{
$time = $end - $start;
$this->collectionTimeTotalNs += $time;
$this->logger->debug("gc_collect_cycles: " . number_format($time) . " ns ($rootsBefore -> $rootsAfter roots, $cycles cycles collected) - total GC time: " . number_format($this->collectionTimeTotalNs) . " ns");
$this->runs++;
$this->logger->info(sprintf(
"Run #%d took %s ms (%s -> %s roots, %s cycles collected) - cumulative GC time: %s ms",
$this->runs,
number_format($time / 1_000_000, 2),
$rootsBefore,
$rootsAfter,
$cycles,
number_format($this->collectionTimeTotalNs / 1_000_000, 2)
));
return $cycles;
}

View File

@ -1618,7 +1618,7 @@ class Server{
if(!is_dir($crashFolder)){
mkdir($crashFolder);
}
$crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
$crashDumpPath = Path::join($crashFolder, date("Y-m-d_H.i.s_T", (int) $dump->getData()->time) . ".log");
$fp = @fopen($crashDumpPath, "wb");
if(!is_resource($fp)){

View File

@ -31,8 +31,8 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.25.3";
public const IS_DEVELOPMENT_BUILD = true;
public const BASE_VERSION = "5.32.0";
public const IS_DEVELOPMENT_BUILD = false;
public const BUILD_CHANNEL = "stable";
/**

View File

@ -90,11 +90,6 @@ final class YmlServerProperties{
public const NETWORK_COMPRESSION_LEVEL = 'network.compression-level';
public const NETWORK_ENABLE_ENCRYPTION = 'network.enable-encryption';
public const NETWORK_MAX_MTU_SIZE = 'network.max-mtu-size';
public const NETWORK_PACKET_READ_OPS_LIMIT = 'network.packet-read-ops-limit';
public const NETWORK_PACKET_READ_OPS_LIMIT_COLLECT_STATS = 'network.packet-read-ops-limit.collect-stats';
public const NETWORK_PACKET_READ_OPS_LIMIT_DEPLETE_ACTION = 'network.packet-read-ops-limit.deplete-action';
public const NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_PER_TICK = 'network.packet-read-ops-limit.session-budget-per-tick';
public const NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_TICKS = 'network.packet-read-ops-limit.session-budget-ticks';
public const NETWORK_UPNP_FORWARDING = 'network.upnp-forwarding';
public const PLAYER = 'player';
public const PLAYER_SAVE_PLAYER_DATA = 'player.save-player-data';

View File

@ -73,7 +73,7 @@ class BlockBreakInfo{
return new self(0.0, $toolType, $toolHarvestLevel, 0.0);
}
public static function indestructible(float $blastResistance = 18000000.0) : self{
public static function indestructible(float $blastResistance = 18000003.75) : self{
return new self(-1.0, BlockToolType::NONE, 0, $blastResistance);
}

View File

@ -786,8 +786,9 @@ final class BlockTypeIds{
public const RESIN_BRICKS = 10756;
public const RESIN_CLUMP = 10757;
public const CHISELED_RESIN_BRICKS = 10758;
public const RESPAWN_ANCHOR = 10759;
public const FIRST_UNUSED_BLOCK_ID = 10759;
public const FIRST_UNUSED_BLOCK_ID = 10760;
private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID;

View File

@ -34,11 +34,16 @@ use function mt_rand;
final class ChorusPlant extends Flowable{
use StaticSupportTrait;
/**
* @var true[]
* @phpstan-var array<int, true>
*/
protected array $connections = [];
protected function recalculateCollisionBoxes() : array{
$bb = AxisAlignedBB::one();
foreach($this->getAllSides() as $facing => $block){
$id = $block->getTypeId();
if($id !== BlockTypeIds::END_STONE && $id !== BlockTypeIds::CHORUS_FLOWER && !$block->hasSameTypeId($this)){
foreach(Facing::ALL as $facing){
if(!isset($this->connections[$facing])){
$bb->trim($facing, 2 / 16);
}
}
@ -46,6 +51,26 @@ final class ChorusPlant extends Flowable{
return [$bb];
}
public function readStateFromWorld() : Block{
parent::readStateFromWorld();
$this->collisionBoxes = null;
foreach(Facing::ALL as $facing){
$block = $this->getSide($facing);
if(match($block->getTypeId()){
BlockTypeIds::END_STONE, BlockTypeIds::CHORUS_FLOWER, $this->getTypeId() => true,
default => false
}){
$this->connections[$facing] = true;
}else{
unset($this->connections[$facing]);
}
}
return $this;
}
private function canBeSupportedBy(Block $block) : bool{
return $block->hasSameTypeId($this) || $block->getTypeId() === BlockTypeIds::END_STONE;
}

123
src/block/RespawnAnchor.php Normal file
View File

@ -0,0 +1,123 @@
<?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\block;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\event\block\BlockPreExplodeEvent;
use pocketmine\event\player\PlayerRespawnAnchorUseEvent;
use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use pocketmine\world\Explosion;
use pocketmine\world\Position;
use pocketmine\world\sound\RespawnAnchorChargeSound;
use pocketmine\world\sound\RespawnAnchorSetSpawnSound;
final class RespawnAnchor extends Opaque{
private const MIN_CHARGES = 0;
private const MAX_CHARGES = 4;
private int $charges = self::MIN_CHARGES;
protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
$w->boundedIntAuto(self::MIN_CHARGES, self::MAX_CHARGES, $this->charges);
}
public function getCharges() : int{
return $this->charges;
}
/** @return $this */
public function setCharges(int $charges) : self{
if($charges < self::MIN_CHARGES || $charges > self::MAX_CHARGES){
throw new \InvalidArgumentException("Charges must be between " . self::MIN_CHARGES . " and " . self::MAX_CHARGES . ", given: $charges");
}
$this->charges = $charges;
return $this;
}
public function getLightLevel() : int{
return $this->charges > 0 ? ($this->charges * 4) - 1 : 0;
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($item->getTypeId() === ItemTypeIds::fromBlockTypeId(BlockTypeIds::GLOWSTONE) && $this->charges < self::MAX_CHARGES){
$this->position->getWorld()->setBlock($this->position, $this->setCharges($this->charges + 1));
$this->position->getWorld()->addSound($this->position, new RespawnAnchorChargeSound());
return true;
}
if($this->charges > self::MIN_CHARGES){
if($player === null){
return false;
}
$ev = new PlayerRespawnAnchorUseEvent($player, $this, PlayerRespawnAnchorUseEvent::ACTION_EXPLODE);
$ev->call();
if($ev->isCancelled()){
return false;
}
switch($ev->getAction()){
case PlayerRespawnAnchorUseEvent::ACTION_EXPLODE:
$this->explode($player);
return true;
case PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN:
if($player->getSpawn() !== null && $player->getSpawn()->equals($this->position)){
return true;
}
$player->setSpawn($this->position);
$this->position->getWorld()->addSound($this->position, new RespawnAnchorSetSpawnSound());
$player->sendMessage(KnownTranslationFactory::tile_respawn_anchor_respawnSet()->prefix(TextFormat::GRAY));
return true;
}
}
return false;
}
private function explode(?Player $player) : void{
$ev = new BlockPreExplodeEvent($this, 5, $player);
$ev->setIncendiary(true);
$ev->call();
if($ev->isCancelled()){
return;
}
$this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR());
$explosion = new Explosion(Position::fromObject($this->position->add(0.5, 0.5, 0.5), $this->position->getWorld()), $ev->getRadius(), $this);
$explosion->setFireChance($ev->getFireChance());
if($ev->isBlockBreaking()){
$explosion->explodeA();
}
$explosion->explodeB();
}
}

View File

@ -28,6 +28,7 @@ use pocketmine\block\BlockIdentifier as BID;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use pocketmine\world\light\LightUpdate;
use function count;
use function min;
/**
@ -40,6 +41,11 @@ use function min;
class RuntimeBlockStateRegistry{
use SingletonTrait;
public const COLLISION_CUSTOM = 0;
public const COLLISION_CUBE = 1;
public const COLLISION_NONE = 2;
public const COLLISION_MAY_OVERFLOW = 3;
/**
* @var Block[]
* @phpstan-var array<int, Block>
@ -74,6 +80,13 @@ class RuntimeBlockStateRegistry{
*/
public array $blastResistance = [];
/**
* Map of state ID -> useful AABB info to avoid unnecessary block allocations
* @var int[]
* @phpstan-var array<int, int>
*/
public array $collisionInfo = [];
public function __construct(){
foreach(VanillaBlocks::getAll() as $block){
$this->register($block);
@ -100,6 +113,70 @@ class RuntimeBlockStateRegistry{
}
}
/**
* Checks if the given class method overrides a method in Block.
* Used to determine if a block might need to disable fast path optimizations.
*
* @phpstan-param anyClosure $closure
*/
private static function overridesBlockMethod(\Closure $closure) : bool{
$declarer = (new \ReflectionFunction($closure))->getClosureScopeClass();
return $declarer !== null && $declarer->getName() !== Block::class;
}
/**
* A big ugly hack to set up fast paths for handling collisions on blocks with common shapes.
* The information returned here is stored in RuntimeBlockStateRegistry->collisionInfo, and is used during entity
* collision box calculations to avoid complex logic and unnecessary block object allocations.
* This hack allows significant performance improvements.
*
* TODO: We'll want to redesign block collision box handling and block shapes in the future, but that's a job for a
* major version. For now, this hack nets major performance wins.
*/
private static function calculateCollisionInfo(Block $block) : int{
if(
self::overridesBlockMethod($block->getModelPositionOffset(...)) ||
self::overridesBlockMethod($block->readStateFromWorld(...))
){
//getModelPositionOffset() might cause AABBs to shift outside the cell
//readStateFromWorld() might cause overflow in ways we can't predict just by looking at known states
//TODO: excluding overriders of readStateFromWorld() also excludes blocks with tiles that don't do anything
//weird with their AABBs, but for now this is the best we can do.
return self::COLLISION_MAY_OVERFLOW;
}
//TODO: this could blow up if any recalculateCollisionBoxes() uses the world
//it shouldn't, but that doesn't mean that custom blocks won't...
$boxes = $block->getCollisionBoxes();
if(count($boxes) === 0){
return self::COLLISION_NONE;
}
if(
count($boxes) === 1 &&
$boxes[0]->minX === 0.0 &&
$boxes[0]->minY === 0.0 &&
$boxes[0]->minZ === 0.0 &&
$boxes[0]->maxX === 1.0 &&
$boxes[0]->maxY === 1.0 &&
$boxes[0]->maxZ === 1.0
){
return self::COLLISION_CUBE;
}
foreach($boxes as $box){
if(
$box->minX < 0 || $box->maxX > 1 ||
$box->minY < 0 || $box->maxY > 1 ||
$box->minZ < 0 || $box->maxZ > 1
){
return self::COLLISION_MAY_OVERFLOW;
}
}
return self::COLLISION_CUSTOM;
}
private function fillStaticArrays(int $index, Block $block) : void{
$fullId = $block->getStateId();
if($index !== $fullId){
@ -112,6 +189,8 @@ class RuntimeBlockStateRegistry{
if($block->blocksDirectSkyLight()){
$this->blocksDirectSkyLight[$index] = true;
}
$this->collisionInfo[$index] = self::calculateCollisionInfo($block);
}
}
@ -130,6 +209,10 @@ class RuntimeBlockStateRegistry{
return $block;
}
public function hasStateId(int $stateId) : bool{
return isset($this->fullList[$stateId]);
}
/**
* @return Block[]
* @phpstan-return array<int, Block>

View File

@ -25,11 +25,12 @@ namespace pocketmine\block;
use pocketmine\block\utils\BlockEventHelper;
use pocketmine\block\utils\CropGrowthHelper;
use pocketmine\block\utils\FortuneDropHelper;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\math\Facing;
use function array_rand;
use function mt_rand;
abstract class Stem extends Crops{
protected int $facing = Facing::UP;
@ -90,8 +91,10 @@ abstract class Stem extends Crops{
}
public function getDropsForCompatibleTool(Item $item) : array{
//TODO: bit annoying we have to pass an Item instance here
//this should not be affected by Fortune, but still follows a binomial distribution
return [
$this->asItem()->setCount(mt_rand(0, 2))
$this->asItem()->setCount(FortuneDropHelper::binomial(VanillaItems::AIR(), 0, chance: ($this->age + 1) / 15))
];
}
}

View File

@ -694,6 +694,7 @@ use function strtolower;
* @method static Stair RESIN_BRICK_STAIRS()
* @method static Wall RESIN_BRICK_WALL()
* @method static ResinClump RESIN_CLUMP()
* @method static RespawnAnchor RESPAWN_ANCHOR()
* @method static DoublePlant ROSE_BUSH()
* @method static Sand SAND()
* @method static Opaque SANDSTONE()
@ -859,7 +860,7 @@ final class VanillaBlocks{
$railBreakInfo = new Info(new BreakInfo(0.7));
self::register("activator_rail", fn(BID $id) => new ActivatorRail($id, "Activator Rail", $railBreakInfo));
self::register("anvil", fn(BID $id) => new Anvil($id, "Anvil", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 6000.0))));
self::register("bamboo", fn(BID $id) => new Bamboo($id, "Bamboo", new Info(new class(2.0 /* 1.0 in PC */, ToolType::AXE) extends BreakInfo{
self::register("bamboo", fn(BID $id) => new Bamboo($id, "Bamboo", new Info(new class(1.0, ToolType::AXE) extends BreakInfo{
public function getBreakTime(Item $item) : float{
if($item->getBlockToolType() === ToolType::SWORD){
return 0.0;
@ -867,7 +868,7 @@ final class VanillaBlocks{
return parent::getBreakTime($item);
}
}, [Tags::POTTABLE_PLANTS])));
self::register("bamboo_sapling", fn(BID $id) => new BambooSapling($id, "Bamboo Sapling", new Info(BreakInfo::instant())));
self::register("bamboo_sapling", fn(BID $id) => new BambooSapling($id, "Bamboo Sapling", new Info(new BreakInfo(1.0))));
$bannerBreakInfo = new Info(BreakInfo::axe(1.0));
self::register("banner", fn(BID $id) => new FloorBanner($id, "Banner", $bannerBreakInfo), TileBanner::class);
@ -876,7 +877,7 @@ final class VanillaBlocks{
self::register("barrier", fn(BID $id) => new Transparent($id, "Barrier", new Info(BreakInfo::indestructible())));
self::register("beacon", fn(BID $id) => new Beacon($id, "Beacon", new Info(new BreakInfo(3.0))), TileBeacon::class);
self::register("bed", fn(BID $id) => new Bed($id, "Bed Block", new Info(new BreakInfo(0.2))), TileBed::class);
self::register("bedrock", fn(BID $id) => new Bedrock($id, "Bedrock", new Info(BreakInfo::indestructible())));
self::register("bedrock", fn(BID $id) => new Bedrock($id, "Bedrock", new Info(BreakInfo::indestructible(18000000.0))));
self::register("beetroots", fn(BID $id) => new Beetroot($id, "Beetroot Block", new Info(BreakInfo::instant())));
self::register("bell", fn(BID $id) => new Bell($id, "Bell", new Info(BreakInfo::pickaxe(5.0))), TileBell::class);
@ -913,7 +914,7 @@ final class VanillaBlocks{
self::register("cobweb", fn(BID $id) => new Cobweb($id, "Cobweb", new Info(new BreakInfo(4.0, ToolType::SWORD | ToolType::SHEARS, 1))));
self::register("cocoa_pod", fn(BID $id) => new CocoaBlock($id, "Cocoa Block", new Info(BreakInfo::axe(0.2, null, 15.0))));
self::register("coral_block", fn(BID $id) => new CoralBlock($id, "Coral Block", new Info(BreakInfo::pickaxe(7.0, ToolTier::WOOD))));
self::register("coral_block", fn(BID $id) => new CoralBlock($id, "Coral Block", new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0))));
self::register("daylight_sensor", fn(BID $id) => new DaylightSensor($id, "Daylight Sensor", new Info(BreakInfo::axe(0.2))), TileDaylightSensor::class);
self::register("dead_bush", fn(BID $id) => new DeadBush($id, "Dead Bush", new Info(BreakInfo::instant(ToolType::SHEARS, 1), [Tags::POTTABLE_PLANTS])));
self::register("detector_rail", fn(BID $id) => new DetectorRail($id, "Detector Rail", $railBreakInfo));
@ -930,15 +931,15 @@ final class VanillaBlocks{
self::register("pitcher_plant", fn(BID $id) => new DoublePlant($id, "Pitcher Plant", new Info(BreakInfo::instant())));
self::register("pitcher_crop", fn(BID $id) => new PitcherCrop($id, "Pitcher Crop", new Info(BreakInfo::instant())));
self::register("double_pitcher_crop", fn(BID $id) => new DoublePitcherCrop($id, "Double Pitcher Crop", new Info(BreakInfo::instant())));
self::register("dragon_egg", fn(BID $id) => new DragonEgg($id, "Dragon Egg", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD))));
self::register("dragon_egg", fn(BID $id) => new DragonEgg($id, "Dragon Egg", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, blastResistance: 45.0))));
self::register("dried_kelp", fn(BID $id) => new DriedKelp($id, "Dried Kelp Block", new Info(new BreakInfo(0.5, ToolType::NONE, 0, 12.5))));
self::register("emerald", fn(BID $id) => new Opaque($id, "Emerald Block", new Info(BreakInfo::pickaxe(5.0, ToolTier::IRON, 30.0))));
self::register("enchanting_table", fn(BID $id) => new EnchantingTable($id, "Enchanting Table", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 6000.0))), TileEnchantingTable::class);
self::register("end_portal_frame", fn(BID $id) => new EndPortalFrame($id, "End Portal Frame", new Info(BreakInfo::indestructible())));
self::register("end_portal_frame", fn(BID $id) => new EndPortalFrame($id, "End Portal Frame", new Info(BreakInfo::indestructible(18000000.0))));
self::register("end_rod", fn(BID $id) => new EndRod($id, "End Rod", new Info(BreakInfo::instant())));
self::register("end_stone", fn(BID $id) => new Opaque($id, "End Stone", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0))));
$endBrickBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD, 4.0));
$endBrickBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0));
self::register("end_stone_bricks", fn(BID $id) => new Opaque($id, "End Stone Bricks", $endBrickBreakInfo));
self::register("end_stone_brick_stairs", fn(BID $id) => new Stair($id, "End Stone Brick Stairs", $endBrickBreakInfo));
@ -962,7 +963,7 @@ final class VanillaBlocks{
self::register("torchflower", fn(BID $id) => new Flower($id, "Torchflower", $flowerTypeInfo));
self::register("torchflower_crop", fn(BID $id) => new TorchflowerCrop($id, "Torchflower Crop", new Info(BreakInfo::instant())));
self::register("flower_pot", fn(BID $id) => new FlowerPot($id, "Flower Pot", new Info(BreakInfo::instant())), TileFlowerPot::class);
self::register("frosted_ice", fn(BID $id) => new FrostedIce($id, "Frosted Ice", new Info(BreakInfo::pickaxe(2.5))));
self::register("frosted_ice", fn(BID $id) => new FrostedIce($id, "Frosted Ice", new Info(BreakInfo::pickaxe(0.5))));
self::register("furnace", fn(BID $id) => new Furnace($id, "Furnace", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::FURNACE), TileNormalFurnace::class);
self::register("blast_furnace", fn(BID $id) => new Furnace($id, "Blast Furnace", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::BLAST_FURNACE), TileBlastFurnace::class);
self::register("smoker", fn(BID $id) => new Furnace($id, "Smoker", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD)), FurnaceType::SMOKER), TileSmoker::class);
@ -970,30 +971,28 @@ final class VanillaBlocks{
$glassBreakInfo = new Info(new BreakInfo(0.3));
self::register("glass", fn(BID $id) => new Glass($id, "Glass", $glassBreakInfo));
self::register("glass_pane", fn(BID $id) => new GlassPane($id, "Glass Pane", $glassBreakInfo));
self::register("glowing_obsidian", fn(BID $id) => new GlowingObsidian($id, "Glowing Obsidian", new Info(BreakInfo::pickaxe(10.0, ToolTier::DIAMOND, 50.0))));
self::register("glowing_obsidian", fn(BID $id) => new GlowingObsidian($id, "Glowing Obsidian", new Info(BreakInfo::pickaxe(35.0, ToolTier::DIAMOND, 6000.0))));
self::register("glowstone", fn(BID $id) => new Glowstone($id, "Glowstone", new Info(BreakInfo::pickaxe(0.3))));
self::register("glow_lichen", fn(BID $id) => new GlowLichen($id, "Glow Lichen", new Info(BreakInfo::axe(0.2, null, 0.2))));
self::register("glow_lichen", fn(BID $id) => new GlowLichen($id, "Glow Lichen", new Info(BreakInfo::axe(0.2))));
self::register("gold", fn(BID $id) => new Opaque($id, "Gold Block", new Info(BreakInfo::pickaxe(3.0, ToolTier::IRON, 30.0))));
$grassBreakInfo = BreakInfo::shovel(0.6);
self::register("grass", fn(BID $id) => new Grass($id, "Grass", new Info($grassBreakInfo, [Tags::DIRT])));
self::register("grass_path", fn(BID $id) => new GrassPath($id, "Grass Path", new Info($grassBreakInfo)));
self::register("grass", fn(BID $id) => new Grass($id, "Grass", new Info(BreakInfo::shovel(0.6), [Tags::DIRT])));
self::register("grass_path", fn(BID $id) => new GrassPath($id, "Grass Path", new Info(BreakInfo::shovel(0.65))));
self::register("gravel", fn(BID $id) => new Gravel($id, "Gravel", new Info(BreakInfo::shovel(0.6))));
$hardenedClayBreakInfo = new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 21.0));
self::register("hardened_clay", fn(BID $id) => new HardenedClay($id, "Hardened Clay", $hardenedClayBreakInfo));
self::register("hardened_clay", fn(BID $id) => new HardenedClay($id, "Hardened Clay", new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 21.0))));
$hardenedGlassBreakInfo = new Info(new BreakInfo(10.0));
self::register("hardened_glass", fn(BID $id) => new HardenedGlass($id, "Hardened Glass", $hardenedGlassBreakInfo));
self::register("hardened_glass_pane", fn(BID $id) => new HardenedGlassPane($id, "Hardened Glass Pane", $hardenedGlassBreakInfo));
self::register("hay_bale", fn(BID $id) => new HayBale($id, "Hay Bale", new Info(new BreakInfo(0.5))));
self::register("hopper", fn(BID $id) => new Hopper($id, "Hopper", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 15.0))), TileHopper::class);
self::register("hopper", fn(BID $id) => new Hopper($id, "Hopper", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 24.0))), TileHopper::class);
self::register("ice", fn(BID $id) => new Ice($id, "Ice", new Info(BreakInfo::pickaxe(0.5))));
$updateBlockBreakInfo = new Info(new BreakInfo(1.0));
self::register("info_update", fn(BID $id) => new Opaque($id, "update!", $updateBlockBreakInfo));
self::register("info_update2", fn(BID $id) => new Opaque($id, "ate!upd", $updateBlockBreakInfo));
self::register("invisible_bedrock", fn(BID $id) => new Transparent($id, "Invisible Bedrock", new Info(BreakInfo::indestructible())));
self::register("invisible_bedrock", fn(BID $id) => new Transparent($id, "Invisible Bedrock", new Info(BreakInfo::indestructible(18000000.0))));
$ironBreakInfo = new Info(BreakInfo::pickaxe(5.0, ToolTier::STONE, 30.0));
self::register("iron", fn(BID $id) => new Opaque($id, "Iron Block", $ironBreakInfo));
@ -1006,16 +1005,16 @@ final class VanillaBlocks{
self::register("item_frame", fn(BID $id) => new ItemFrame($id, "Item Frame", $itemFrameInfo), TileItemFrame::class);
self::register("glowing_item_frame", fn(BID $id) => new ItemFrame($id, "Glow Item Frame", $itemFrameInfo), TileGlowingItemFrame::class);
self::register("jukebox", fn(BID $id) => new Jukebox($id, "Jukebox", new Info(BreakInfo::axe(0.8))), TileJukebox::class); //TODO: in PC the hardness is 2.0, not 0.8, unsure if this is a MCPE bug or not
self::register("jukebox", fn(BID $id) => new Jukebox($id, "Jukebox", new Info(BreakInfo::axe(2.0, blastResistance: 30.0))), TileJukebox::class);
self::register("ladder", fn(BID $id) => new Ladder($id, "Ladder", new Info(BreakInfo::axe(0.4))));
$lanternBreakInfo = new Info(BreakInfo::pickaxe(5.0));
$lanternBreakInfo = new Info(BreakInfo::pickaxe(3.5));
self::register("lantern", fn(BID $id) => new Lantern($id, "Lantern", $lanternBreakInfo, 15));
self::register("soul_lantern", fn(BID $id) => new Lantern($id, "Soul Lantern", $lanternBreakInfo, 10));
self::register("lapis_lazuli", fn(BID $id) => new Opaque($id, "Lapis Lazuli Block", new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE))));
self::register("lava", fn(BID $id) => new Lava($id, "Lava", new Info(BreakInfo::indestructible(500.0))));
self::register("lectern", fn(BID $id) => new Lectern($id, "Lectern", new Info(BreakInfo::axe(2.0))), TileLectern::class);
self::register("lectern", fn(BID $id) => new Lectern($id, "Lectern", new Info(BreakInfo::axe(2.5))), TileLectern::class);
self::register("lever", fn(BID $id) => new Lever($id, "Lever", new Info(new BreakInfo(0.5))));
self::register("magma", fn(BID $id) => new Magma($id, "Magma Block", new Info(BreakInfo::pickaxe(0.5, ToolTier::WOOD))));
self::register("melon", fn(BID $id) => new Melon($id, "Melon Block", new Info(BreakInfo::axe(1.0))));
@ -1065,14 +1064,15 @@ final class VanillaBlocks{
self::register("purpur_stairs", fn(BID $id) => new Stair($id, "Purpur Stairs", $purpurBreakInfo));
$quartzBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD));
$smoothQuartzBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("quartz", fn(BID $id) => new Opaque($id, "Quartz Block", $quartzBreakInfo));
self::register("chiseled_quartz", fn(BID $id) => new SimplePillar($id, "Chiseled Quartz Block", $quartzBreakInfo));
self::register("quartz_pillar", fn(BID $id) => new SimplePillar($id, "Quartz Pillar", $quartzBreakInfo));
self::register("smooth_quartz", fn(BID $id) => new Opaque($id, "Smooth Quartz Block", $quartzBreakInfo));
self::register("smooth_quartz", fn(BID $id) => new Opaque($id, "Smooth Quartz Block", $smoothQuartzBreakInfo));
self::register("quartz_bricks", fn(BID $id) => new Opaque($id, "Quartz Bricks", $quartzBreakInfo));
self::register("quartz_stairs", fn(BID $id) => new Stair($id, "Quartz Stairs", $quartzBreakInfo));
self::register("smooth_quartz_stairs", fn(BID $id) => new Stair($id, "Smooth Quartz Stairs", $quartzBreakInfo));
self::register("smooth_quartz_stairs", fn(BID $id) => new Stair($id, "Smooth Quartz Stairs", $smoothQuartzBreakInfo));
self::register("rail", fn(BID $id) => new Rail($id, "Rail", $railBreakInfo));
self::register("red_mushroom", fn(BID $id) => new RedMushroom($id, "Red Mushroom", new Info(BreakInfo::instant(), [Tags::POTTABLE_PLANTS])));
@ -1127,13 +1127,13 @@ final class VanillaBlocks{
$infestedStoneBreakInfo = new Info(BreakInfo::pickaxe(0.75));
self::register("infested_stone", fn(BID $id) => new InfestedStone($id, "Infested Stone", $infestedStoneBreakInfo, $stone));
self::register("infested_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Stone Brick", $infestedStoneBreakInfo, $stoneBrick));
self::register("infested_cobblestone", fn(BID $id) => new InfestedStone($id, "Infested Cobblestone", $infestedStoneBreakInfo, $cobblestone));
self::register("infested_cobblestone", fn(BID $id) => new InfestedStone($id, "Infested Cobblestone", new Info(BreakInfo::pickaxe(1.0, blastResistance: 3.75)), $cobblestone));
self::register("infested_mossy_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Mossy Stone Brick", $infestedStoneBreakInfo, $mossyStoneBrick));
self::register("infested_cracked_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Cracked Stone Brick", $infestedStoneBreakInfo, $crackedStoneBrick));
self::register("infested_chiseled_stone_brick", fn(BID $id) => new InfestedStone($id, "Infested Chiseled Stone Brick", $infestedStoneBreakInfo, $chiseledStoneBrick));
self::register("stone_stairs", fn(BID $id) => new Stair($id, "Stone Stairs", $stoneBreakInfo));
self::register("smooth_stone", fn(BID $id) => new Opaque($id, "Smooth Stone", $stoneBreakInfo));
self::register("smooth_stone", fn(BID $id) => new Opaque($id, "Smooth Stone", new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0))));
self::register("andesite_stairs", fn(BID $id) => new Stair($id, "Andesite Stairs", $stoneBreakInfo));
self::register("diorite_stairs", fn(BID $id) => new Stair($id, "Diorite Stairs", $stoneBreakInfo));
self::register("granite_stairs", fn(BID $id) => new Stair($id, "Granite Stairs", $stoneBreakInfo));
@ -1146,7 +1146,6 @@ final class VanillaBlocks{
self::register("stonecutter", fn(BID $id) => new Stonecutter($id, "Stonecutter", new Info(BreakInfo::pickaxe(3.5))));
self::register("stone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, "Stone Pressure Plate", new Info(BreakInfo::pickaxe(0.5))));
//TODO: in the future this won't be the same for all the types
$stoneSlabBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("brick_slab", fn(BID $id) => new Slab($id, "Brick", $stoneSlabBreakInfo));
@ -1157,28 +1156,31 @@ final class VanillaBlocks{
self::register("sandstone_slab", fn(BID $id) => new Slab($id, "Sandstone", $stoneSlabBreakInfo));
self::register("smooth_stone_slab", fn(BID $id) => new Slab($id, "Smooth Stone", $stoneSlabBreakInfo));
self::register("stone_brick_slab", fn(BID $id) => new Slab($id, "Stone Brick", $stoneSlabBreakInfo));
self::register("dark_prismarine_slab", fn(BID $id) => new Slab($id, "Dark Prismarine", $stoneSlabBreakInfo));
self::register("mossy_cobblestone_slab", fn(BID $id) => new Slab($id, "Mossy Cobblestone", $stoneSlabBreakInfo));
self::register("prismarine_slab", fn(BID $id) => new Slab($id, "Prismarine", $stoneSlabBreakInfo));
self::register("prismarine_bricks_slab", fn(BID $id) => new Slab($id, "Prismarine Bricks", $stoneSlabBreakInfo));
self::register("purpur_slab", fn(BID $id) => new Slab($id, "Purpur", $stoneSlabBreakInfo));
self::register("red_nether_brick_slab", fn(BID $id) => new Slab($id, "Red Nether Brick", $stoneSlabBreakInfo));
self::register("red_sandstone_slab", fn(BID $id) => new Slab($id, "Red Sandstone", $stoneSlabBreakInfo));
self::register("smooth_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Sandstone", $stoneSlabBreakInfo));
self::register("andesite_slab", fn(BID $id) => new Slab($id, "Andesite", $stoneSlabBreakInfo));
self::register("diorite_slab", fn(BID $id) => new Slab($id, "Diorite", $stoneSlabBreakInfo));
self::register("end_stone_brick_slab", fn(BID $id) => new Slab($id, "End Stone Brick", $stoneSlabBreakInfo));
self::register("granite_slab", fn(BID $id) => new Slab($id, "Granite", $stoneSlabBreakInfo));
self::register("polished_andesite_slab", fn(BID $id) => new Slab($id, "Polished Andesite", $stoneSlabBreakInfo));
self::register("polished_diorite_slab", fn(BID $id) => new Slab($id, "Polished Diorite", $stoneSlabBreakInfo));
self::register("polished_granite_slab", fn(BID $id) => new Slab($id, "Polished Granite", $stoneSlabBreakInfo));
self::register("smooth_red_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Red Sandstone", $stoneSlabBreakInfo));
self::register("cut_red_sandstone_slab", fn(BID $id) => new Slab($id, "Cut Red Sandstone", $stoneSlabBreakInfo));
self::register("cut_sandstone_slab", fn(BID $id) => new Slab($id, "Cut Sandstone", $stoneSlabBreakInfo));
self::register("mossy_stone_brick_slab", fn(BID $id) => new Slab($id, "Mossy Stone Brick", $stoneSlabBreakInfo));
self::register("mossy_cobblestone_slab", fn(BID $id) => new Slab($id, "Mossy Cobblestone", $stoneSlabBreakInfo));
self::register("purpur_slab", fn(BID $id) => new Slab($id, "Purpur", $stoneSlabBreakInfo));
self::register("smooth_red_sandstone_slab", fn(BID $id) => new Slab($id, "Smooth Red Sandstone", $stoneSlabBreakInfo));
self::register("smooth_quartz_slab", fn(BID $id) => new Slab($id, "Smooth Quartz", $stoneSlabBreakInfo));
self::register("stone_slab", fn(BID $id) => new Slab($id, "Stone", $stoneSlabBreakInfo));
self::register("end_stone_brick_slab", fn(BID $id) => new Slab($id, "End Stone Brick", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 30.0))));
$lightStoneSlabBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0));
self::register("dark_prismarine_slab", fn(BID $id) => new Slab($id, "Dark Prismarine", $lightStoneSlabBreakInfo));
self::register("prismarine_slab", fn(BID $id) => new Slab($id, "Prismarine", $lightStoneSlabBreakInfo));
self::register("prismarine_bricks_slab", fn(BID $id) => new Slab($id, "Prismarine Bricks", $lightStoneSlabBreakInfo));
self::register("andesite_slab", fn(BID $id) => new Slab($id, "Andesite", $lightStoneSlabBreakInfo));
self::register("diorite_slab", fn(BID $id) => new Slab($id, "Diorite", $lightStoneSlabBreakInfo));
self::register("granite_slab", fn(BID $id) => new Slab($id, "Granite", $lightStoneSlabBreakInfo));
self::register("polished_andesite_slab", fn(BID $id) => new Slab($id, "Polished Andesite", $lightStoneSlabBreakInfo));
self::register("polished_diorite_slab", fn(BID $id) => new Slab($id, "Polished Diorite", $lightStoneSlabBreakInfo));
self::register("polished_granite_slab", fn(BID $id) => new Slab($id, "Polished Granite", $lightStoneSlabBreakInfo));
self::register("mossy_stone_brick_slab", fn(BID $id) => new Slab($id, "Mossy Stone Brick", $lightStoneSlabBreakInfo));
self::register("legacy_stonecutter", fn(BID $id) => new Opaque($id, "Legacy Stonecutter", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD))));
self::register("sugarcane", fn(BID $id) => new Sugarcane($id, "Sugarcane", new Info(BreakInfo::instant())));
self::register("sweet_berry_bush", fn(BID $id) => new SweetBerryBush($id, "Sweet Berry Bush", new Info(BreakInfo::instant())));
@ -1237,25 +1239,26 @@ final class VanillaBlocks{
}
$sandstoneBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD));
$smoothSandstoneBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("red_sandstone_stairs", fn(BID $id) => new Stair($id, "Red Sandstone Stairs", $sandstoneBreakInfo));
self::register("smooth_red_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Red Sandstone Stairs", $sandstoneBreakInfo));
self::register("smooth_red_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Red Sandstone Stairs", $smoothSandstoneBreakInfo));
self::register("red_sandstone", fn(BID $id) => new Opaque($id, "Red Sandstone", $sandstoneBreakInfo));
self::register("chiseled_red_sandstone", fn(BID $id) => new Opaque($id, "Chiseled Red Sandstone", $sandstoneBreakInfo));
self::register("cut_red_sandstone", fn(BID $id) => new Opaque($id, "Cut Red Sandstone", $sandstoneBreakInfo));
self::register("smooth_red_sandstone", fn(BID $id) => new Opaque($id, "Smooth Red Sandstone", $sandstoneBreakInfo));
self::register("smooth_red_sandstone", fn(BID $id) => new Opaque($id, "Smooth Red Sandstone", $smoothSandstoneBreakInfo));
self::register("sandstone_stairs", fn(BID $id) => new Stair($id, "Sandstone Stairs", $sandstoneBreakInfo));
self::register("smooth_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Sandstone Stairs", $sandstoneBreakInfo));
self::register("smooth_sandstone_stairs", fn(BID $id) => new Stair($id, "Smooth Sandstone Stairs", $smoothSandstoneBreakInfo));
self::register("sandstone", fn(BID $id) => new Opaque($id, "Sandstone", $sandstoneBreakInfo));
self::register("chiseled_sandstone", fn(BID $id) => new Opaque($id, "Chiseled Sandstone", $sandstoneBreakInfo));
self::register("cut_sandstone", fn(BID $id) => new Opaque($id, "Cut Sandstone", $sandstoneBreakInfo));
self::register("smooth_sandstone", fn(BID $id) => new Opaque($id, "Smooth Sandstone", $sandstoneBreakInfo));
self::register("smooth_sandstone", fn(BID $id) => new Opaque($id, "Smooth Sandstone", $smoothSandstoneBreakInfo));
self::register("glazed_terracotta", fn(BID $id) => new GlazedTerracotta($id, "Glazed Terracotta", new Info(BreakInfo::pickaxe(1.4, ToolTier::WOOD))));
self::register("dyed_shulker_box", fn(BID $id) => new DyedShulkerBox($id, "Dyed Shulker Box", $shulkerBoxBreakInfo), TileShulkerBox::class);
self::register("stained_glass", fn(BID $id) => new StainedGlass($id, "Stained Glass", $glassBreakInfo));
self::register("stained_glass_pane", fn(BID $id) => new StainedGlassPane($id, "Stained Glass Pane", $glassBreakInfo));
self::register("stained_clay", fn(BID $id) => new StainedHardenedClay($id, "Stained Clay", $hardenedClayBreakInfo));
self::register("stained_clay", fn(BID $id) => new StainedHardenedClay($id, "Stained Clay", new Info(BreakInfo::pickaxe(1.25, ToolTier::WOOD, 6.25))));
self::register("stained_hardened_glass", fn(BID $id) => new StainedHardenedGlass($id, "Stained Hardened Glass", $hardenedGlassBreakInfo));
self::register("stained_hardened_glass_pane", fn(BID $id) => new StainedHardenedGlassPane($id, "Stained Hardened Glass Pane", $hardenedGlassBreakInfo));
self::register("carpet", fn(BID $id) => new Carpet($id, "Carpet", new Info(new BreakInfo(0.1))));
@ -1272,22 +1275,26 @@ final class VanillaBlocks{
}
})));
//TODO: in the future these won't all have the same hardness; they only do now because of the old metadata crap
$wallBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("cobblestone_wall", fn(BID $id) => new Wall($id, "Cobblestone Wall", $wallBreakInfo));
self::register("andesite_wall", fn(BID $id) => new Wall($id, "Andesite Wall", $wallBreakInfo));
self::register("brick_wall", fn(BID $id) => new Wall($id, "Brick Wall", $wallBreakInfo));
self::register("diorite_wall", fn(BID $id) => new Wall($id, "Diorite Wall", $wallBreakInfo));
self::register("end_stone_brick_wall", fn(BID $id) => new Wall($id, "End Stone Brick Wall", $wallBreakInfo));
self::register("granite_wall", fn(BID $id) => new Wall($id, "Granite Wall", $wallBreakInfo));
self::register("mossy_stone_brick_wall", fn(BID $id) => new Wall($id, "Mossy Stone Brick Wall", $wallBreakInfo));
self::register("mossy_cobblestone_wall", fn(BID $id) => new Wall($id, "Mossy Cobblestone Wall", $wallBreakInfo));
self::register("nether_brick_wall", fn(BID $id) => new Wall($id, "Nether Brick Wall", $wallBreakInfo));
self::register("prismarine_wall", fn(BID $id) => new Wall($id, "Prismarine Wall", $wallBreakInfo));
self::register("red_nether_brick_wall", fn(BID $id) => new Wall($id, "Red Nether Brick Wall", $wallBreakInfo));
self::register("red_sandstone_wall", fn(BID $id) => new Wall($id, "Red Sandstone Wall", $wallBreakInfo));
self::register("sandstone_wall", fn(BID $id) => new Wall($id, "Sandstone Wall", $wallBreakInfo));
self::register("stone_brick_wall", fn(BID $id) => new Wall($id, "Stone Brick Wall", $wallBreakInfo));
self::register("end_stone_brick_wall", fn(BID $id) => new Wall($id, "End Stone Brick Wall", new Info(BreakInfo::pickaxe(3.0, ToolTier::WOOD, 45.0))));
$brickWallBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("cobblestone_wall", fn(BID $id) => new Wall($id, "Cobblestone Wall", $brickWallBreakInfo));
self::register("brick_wall", fn(BID $id) => new Wall($id, "Brick Wall", $brickWallBreakInfo));
self::register("mossy_cobblestone_wall", fn(BID $id) => new Wall($id, "Mossy Cobblestone Wall", $brickWallBreakInfo));
self::register("nether_brick_wall", fn(BID $id) => new Wall($id, "Nether Brick Wall", $brickWallBreakInfo));
self::register("red_nether_brick_wall", fn(BID $id) => new Wall($id, "Red Nether Brick Wall", $brickWallBreakInfo));
$stoneWallBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0));
self::register("stone_brick_wall", fn(BID $id) => new Wall($id, "Stone Brick Wall", $stoneWallBreakInfo));
self::register("mossy_stone_brick_wall", fn(BID $id) => new Wall($id, "Mossy Stone Brick Wall", $stoneWallBreakInfo));
self::register("granite_wall", fn(BID $id) => new Wall($id, "Granite Wall", $stoneWallBreakInfo));
self::register("diorite_wall", fn(BID $id) => new Wall($id, "Diorite Wall", $stoneWallBreakInfo));
self::register("andesite_wall", fn(BID $id) => new Wall($id, "Andesite Wall", $stoneWallBreakInfo));
self::register("prismarine_wall", fn(BID $id) => new Wall($id, "Prismarine Wall", $stoneWallBreakInfo));
$sandstoneWallBreakInfo = new Info(BreakInfo::pickaxe(0.8, ToolTier::WOOD, 4.0));
self::register("red_sandstone_wall", fn(BID $id) => new Wall($id, "Red Sandstone Wall", $sandstoneWallBreakInfo));
self::register("sandstone_wall", fn(BID $id) => new Wall($id, "Sandstone Wall", $sandstoneWallBreakInfo));
self::registerElements();
@ -1320,8 +1327,8 @@ final class VanillaBlocks{
self::register("mangrove_roots", fn(BID $id) => new MangroveRoots($id, "Mangrove Roots", new Info(BreakInfo::axe(0.7))));
self::register("muddy_mangrove_roots", fn(BID $id) => new SimplePillar($id, "Muddy Mangrove Roots", new Info(BreakInfo::shovel(0.7), [Tags::MUD])));
self::register("froglight", fn(BID $id) => new Froglight($id, "Froglight", new Info(new BreakInfo(0.3))));
self::register("sculk", fn(BID $id) => new Sculk($id, "Sculk", new Info(new BreakInfo(0.6, ToolType::HOE))));
self::register("reinforced_deepslate", fn(BID $id) => new class($id, "Reinforced Deepslate", new Info(new BreakInfo(55.0, ToolType::NONE, 0, 3600.0))) extends Opaque{
self::register("sculk", fn(BID $id) => new Sculk($id, "Sculk", new Info(new BreakInfo(0.2, ToolType::HOE))));
self::register("reinforced_deepslate", fn(BID $id) => new class($id, "Reinforced Deepslate", new Info(new BreakInfo(55.0, ToolType::NONE, 0, 6000.0))) extends Opaque{
public function getDropsForCompatibleTool(Item $item) : array{
return [];
}
@ -1537,7 +1544,7 @@ final class VanillaBlocks{
self::register("lapis_lazuli_ore", fn(BID $id) => new LapisOre($id, "Lapis Lazuli Ore", $stoneOreBreakInfo(ToolTier::STONE)));
self::register("redstone_ore", fn(BID $id) => new RedstoneOre($id, "Redstone Ore", $stoneOreBreakInfo(ToolTier::IRON)));
$deepslateOreBreakInfo = fn(ToolTier $toolTier) => new Info(BreakInfo::pickaxe(4.5, $toolTier));
$deepslateOreBreakInfo = fn(ToolTier $toolTier) => new Info(BreakInfo::pickaxe(4.5, $toolTier, 15.0));
self::register("deepslate_coal_ore", fn(BID $id) => new CoalOre($id, "Deepslate Coal Ore", $deepslateOreBreakInfo(ToolTier::WOOD)));
self::register("deepslate_copper_ore", fn(BID $id) => new CopperOre($id, "Deepslate Copper Ore", $deepslateOreBreakInfo(ToolTier::STONE)));
self::register("deepslate_diamond_ore", fn(BID $id) => new DiamondOre($id, "Deepslate Diamond Ore", $deepslateOreBreakInfo(ToolTier::IRON)));
@ -1581,10 +1588,10 @@ final class VanillaBlocks{
//for some reason, slabs have weird hardness like the legacy ones
$slabBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
self::register("ancient_debris", fn(BID $id) => new class($id, "Ancient Debris", new Info(BreakInfo::pickaxe(30, ToolTier::DIAMOND, 3600.0))) extends Opaque{
self::register("ancient_debris", fn(BID $id) => new class($id, "Ancient Debris", new Info(BreakInfo::pickaxe(30, ToolTier::DIAMOND, 6000.0))) extends Opaque{
public function isFireProofAsItem() : bool{ return true; }
});
$netheriteBreakInfo = new Info(BreakInfo::pickaxe(50, ToolTier::DIAMOND, 3600.0));
$netheriteBreakInfo = new Info(BreakInfo::pickaxe(50, ToolTier::DIAMOND, 6000.0));
self::register("netherite", fn(BID $id) => new class($id, "Netherite Block", $netheriteBreakInfo) extends Opaque{
public function isFireProofAsItem() : bool{ return true; }
});
@ -1602,14 +1609,14 @@ final class VanillaBlocks{
self::register("gilded_blackstone", fn(BID $id) => new GildedBlackstone($id, "Gilded Blackstone", $blackstoneBreakInfo));
//TODO: polished blackstone ought to have 2.0 hardness (as per java) but it's 1.5 in Bedrock (probably parity bug)
$polishedBlackstoneBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
$prefix = fn(string $thing) => "Polished Blackstone" . ($thing !== "" ? " $thing" : "");
self::register("polished_blackstone", fn(BID $id) => new Opaque($id, $prefix(""), $blackstoneBreakInfo));
self::register("polished_blackstone", fn(BID $id) => new Opaque($id, $prefix(""), $polishedBlackstoneBreakInfo));
self::register("polished_blackstone_button", fn(BID $id) => new StoneButton($id, $prefix("Button"), new Info(BreakInfo::pickaxe(0.5))));
self::register("polished_blackstone_pressure_plate", fn(BID $id) => new StonePressurePlate($id, $prefix("Pressure Plate"), new Info(BreakInfo::pickaxe(0.5)), 20));
self::register("polished_blackstone_slab", fn(BID $id) => new Slab($id, $prefix(""), $slabBreakInfo));
self::register("polished_blackstone_stairs", fn(BID $id) => new Stair($id, $prefix("Stairs"), $blackstoneBreakInfo));
self::register("polished_blackstone_wall", fn(BID $id) => new Wall($id, $prefix("Wall"), $blackstoneBreakInfo));
self::register("polished_blackstone_stairs", fn(BID $id) => new Stair($id, $prefix("Stairs"), $polishedBlackstoneBreakInfo));
self::register("polished_blackstone_wall", fn(BID $id) => new Wall($id, $prefix("Wall"), $polishedBlackstoneBreakInfo));
self::register("chiseled_polished_blackstone", fn(BID $id) => new Opaque($id, "Chiseled Polished Blackstone", $blackstoneBreakInfo));
$prefix = fn(string $thing) => "Polished Blackstone Brick" . ($thing !== "" ? " $thing" : "");
@ -1622,8 +1629,7 @@ final class VanillaBlocks{
self::register("soul_torch", fn(BID $id) => new Torch($id, "Soul Torch", new Info(BreakInfo::instant())));
self::register("soul_fire", fn(BID $id) => new SoulFire($id, "Soul Fire", new Info(BreakInfo::instant(), [Tags::FIRE])));
//TODO: soul soul ought to have 0.5 hardness (as per java) but it's 1.0 in Bedrock (probably parity bug)
self::register("soul_soil", fn(BID $id) => new Opaque($id, "Soul Soil", new Info(BreakInfo::shovel(1.0))));
self::register("soul_soil", fn(BID $id) => new Opaque($id, "Soul Soil", new Info(BreakInfo::shovel(0.5))));
self::register("shroomlight", fn(BID $id) => new class($id, "Shroomlight", new Info(new BreakInfo(1.0, ToolType::HOE))) extends Opaque{
public function getLightLevel() : int{ return 15; }
@ -1641,7 +1647,9 @@ final class VanillaBlocks{
self::register("crimson_roots", fn(BID $id) => new NetherRoots($id, "Crimson Roots", $netherRootsInfo));
self::register("warped_roots", fn(BID $id) => new NetherRoots($id, "Warped Roots", $netherRootsInfo));
self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD))));
self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 30.0))));
self::register("respawn_anchor", fn(BID $id) => new RespawnAnchor($id, "Respawn Anchor", new Info(BreakInfo::pickaxe(50.0, ToolTier::DIAMOND, 6000.0))));
}
private static function registerBlocksR17() : void{
@ -1659,7 +1667,7 @@ final class VanillaBlocks{
self::register("raw_gold", fn(BID $id) => new Opaque($id, "Raw Gold Block", new Info(BreakInfo::pickaxe(5, ToolTier::IRON, 30.0))));
self::register("raw_iron", fn(BID $id) => new Opaque($id, "Raw Iron Block", new Info(BreakInfo::pickaxe(5, ToolTier::STONE, 30.0))));
$deepslateBreakInfo = new Info(BreakInfo::pickaxe(3, ToolTier::WOOD, 18.0));
$deepslateBreakInfo = new Info(BreakInfo::pickaxe(3, ToolTier::WOOD, 30.0));
self::register("deepslate", fn(BID $id) => new class($id, "Deepslate", $deepslateBreakInfo) extends SimplePillar{
public function getDropsForCompatibleTool(Item $item) : array{
return [VanillaBlocks::COBBLED_DEEPSLATE()->asItem()];
@ -1671,29 +1679,29 @@ final class VanillaBlocks{
});
//TODO: parity issue here - in Java this has a hardness of 3.0, but in bedrock it's 3.5
self::register("chiseled_deepslate", fn(BID $id) => new Opaque($id, "Chiseled Deepslate", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0))));
self::register("chiseled_deepslate", fn(BID $id) => new Opaque($id, "Chiseled Deepslate", new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0))));
$deepslateBrickBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0));
$deepslateBrickBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0));
self::register("deepslate_bricks", fn(BID $id) => new Opaque($id, "Deepslate Bricks", $deepslateBrickBreakInfo));
self::register("deepslate_brick_slab", fn(BID $id) => new Slab($id, "Deepslate Brick", $deepslateBrickBreakInfo));
self::register("deepslate_brick_stairs", fn(BID $id) => new Stair($id, "Deepslate Brick Stairs", $deepslateBrickBreakInfo));
self::register("deepslate_brick_wall", fn(BID $id) => new Wall($id, "Deepslate Brick Wall", $deepslateBrickBreakInfo));
self::register("cracked_deepslate_bricks", fn(BID $id) => new Opaque($id, "Cracked Deepslate Bricks", $deepslateBrickBreakInfo));
$deepslateTilesBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0));
$deepslateTilesBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0));
self::register("deepslate_tiles", fn(BID $id) => new Opaque($id, "Deepslate Tiles", $deepslateTilesBreakInfo));
self::register("deepslate_tile_slab", fn(BID $id) => new Slab($id, "Deepslate Tile", $deepslateTilesBreakInfo));
self::register("deepslate_tile_stairs", fn(BID $id) => new Stair($id, "Deepslate Tile Stairs", $deepslateTilesBreakInfo));
self::register("deepslate_tile_wall", fn(BID $id) => new Wall($id, "Deepslate Tile Wall", $deepslateTilesBreakInfo));
self::register("cracked_deepslate_tiles", fn(BID $id) => new Opaque($id, "Cracked Deepslate Tiles", $deepslateTilesBreakInfo));
$cobbledDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0));
$cobbledDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0));
self::register("cobbled_deepslate", fn(BID $id) => new Opaque($id, "Cobbled Deepslate", $cobbledDeepslateBreakInfo));
self::register("cobbled_deepslate_slab", fn(BID $id) => new Slab($id, "Cobbled Deepslate", $cobbledDeepslateBreakInfo));
self::register("cobbled_deepslate_stairs", fn(BID $id) => new Stair($id, "Cobbled Deepslate Stairs", $cobbledDeepslateBreakInfo));
self::register("cobbled_deepslate_wall", fn(BID $id) => new Wall($id, "Cobbled Deepslate Wall", $cobbledDeepslateBreakInfo));
$polishedDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 18.0));
$polishedDeepslateBreakInfo = new Info(BreakInfo::pickaxe(3.5, ToolTier::WOOD, 30.0));
self::register("polished_deepslate", fn(BID $id) => new Opaque($id, "Polished Deepslate", $polishedDeepslateBreakInfo));
self::register("polished_deepslate_slab", fn(BID $id) => new Slab($id, "Polished Deepslate", $polishedDeepslateBreakInfo));
self::register("polished_deepslate_stairs", fn(BID $id) => new Stair($id, "Polished Deepslate Stairs", $polishedDeepslateBreakInfo));
@ -1702,7 +1710,7 @@ final class VanillaBlocks{
self::register("tinted_glass", fn(BID $id) => new TintedGlass($id, "Tinted Glass", new Info(new BreakInfo(0.3))));
//blast resistance should be 30 if we were matched with java :(
$copperBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 18.0));
$copperBreakInfo = new Info(BreakInfo::pickaxe(3.0, ToolTier::STONE, 30.0));
self::register("lightning_rod", fn(BID $id) => new LightningRod($id, "Lightning Rod", $copperBreakInfo));
self::register("copper", fn(BID $id) => new Copper($id, "Copper Block", $copperBreakInfo));
@ -1730,8 +1738,8 @@ final class VanillaBlocks{
self::register("cave_vines", fn(BID $id) => new CaveVines($id, "Cave Vines", new Info(BreakInfo::instant())));
self::register("small_dripleaf", fn(BID $id) => new SmallDripleaf($id, "Small Dripleaf", new Info(BreakInfo::instant(ToolType::SHEARS, toolHarvestLevel: 1))));
self::register("big_dripleaf_head", fn(BID $id) => new BigDripleafHead($id, "Big Dripleaf", new Info(BreakInfo::instant())));
self::register("big_dripleaf_stem", fn(BID $id) => new BigDripleafStem($id, "Big Dripleaf Stem", new Info(BreakInfo::instant())));
self::register("big_dripleaf_head", fn(BID $id) => new BigDripleafHead($id, "Big Dripleaf", new Info(new BreakInfo(0.1))));
self::register("big_dripleaf_stem", fn(BID $id) => new BigDripleafStem($id, "Big Dripleaf Stem", new Info(new BreakInfo(0.1))));
}
private static function registerBlocksR18() : void{
@ -1742,7 +1750,7 @@ final class VanillaBlocks{
self::register("mud", fn(BID $id) => new Opaque($id, "Mud", new Info(BreakInfo::shovel(0.5), [Tags::MUD])));
self::register("packed_mud", fn(BID $id) => new Opaque($id, "Packed Mud", new Info(BreakInfo::pickaxe(1.0, null, 15.0))));
$mudBricksBreakInfo = new Info(BreakInfo::pickaxe(2.0, ToolTier::WOOD, 30.0));
$mudBricksBreakInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 15.0));
self::register("mud_bricks", fn(BID $id) => new Opaque($id, "Mud Bricks", $mudBricksBreakInfo));
self::register("mud_brick_slab", fn(BID $id) => new Slab($id, "Mud Brick", $mudBricksBreakInfo));
@ -1754,7 +1762,7 @@ final class VanillaBlocks{
self::register("resin", fn(BID $id) => new Opaque($id, "Block of Resin", new Info(BreakInfo::instant())));
self::register("resin_clump", fn(BID $id) => new ResinClump($id, "Resin Clump", new Info(BreakInfo::instant())));
$resinBricksInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD));
$resinBricksInfo = new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0));
self::register("resin_brick_slab", fn(BID $id) => new Slab($id, "Resin Brick", $resinBricksInfo));
self::register("resin_brick_stairs", fn(BID $id) => new Stair($id, "Resin Brick Stairs", $resinBricksInfo));
self::register("resin_brick_wall", fn(BID $id) => new Wall($id, "Resin Brick Wall", $resinBricksInfo));

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\entity\Entity;
use pocketmine\event\entity\EntityExtinguishEvent;
use pocketmine\world\sound\BucketEmptyWaterSound;
use pocketmine\world\sound\BucketFillWaterSound;
use pocketmine\world\sound\Sound;
@ -53,7 +54,7 @@ class Water extends Liquid{
public function onEntityInside(Entity $entity) : bool{
$entity->resetFallDistance();
if($entity->isOnFire()){
$entity->extinguish();
$entity->extinguish(EntityExtinguishEvent::CAUSE_WATER);
}
return true;
}

View File

@ -27,6 +27,7 @@ use pocketmine\block\tile\Cauldron as TileCauldron;
use pocketmine\block\utils\DyeColor;
use pocketmine\color\Color;
use pocketmine\entity\Entity;
use pocketmine\event\entity\EntityExtinguishEvent;
use pocketmine\item\Armor;
use pocketmine\item\Banner;
use pocketmine\item\Dye;
@ -183,7 +184,7 @@ final class WaterCauldron extends FillableCauldron{
public function onEntityInside(Entity $entity) : bool{
if($entity->isOnFire()){
$entity->extinguish();
$entity->extinguish(EntityExtinguishEvent::CAUSE_WATER_CAULDRON);
//TODO: particles
$this->position->getWorld()->setBlock($this->position, $this->withFillLevel($this->getFillLevel() - self::ENTITY_EXTINGUISH_USE_AMOUNT));

View File

@ -32,7 +32,7 @@ use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use function get_class;
abstract class Spawnable extends Tile{
/** @phpstan-var CacheableNbt<\pocketmine\nbt\tag\CompoundTag>|null */
/** @phpstan-var CacheableNbt<CompoundTag>|null */
private ?CacheableNbt $spawnCompoundCache = null;
/**
@ -73,7 +73,7 @@ abstract class Spawnable extends Tile{
* Returns encoded NBT (varint, little-endian) used to spawn this tile to clients. Uses cache where possible,
* populates cache if it is null.
*
* @phpstan-return CacheableNbt<\pocketmine\nbt\tag\CompoundTag>
* @phpstan-return CacheableNbt<CompoundTag>
*/
final public function getSerializedSpawnCompound() : CacheableNbt{
if($this->spawnCompoundCache === null){

View File

@ -114,6 +114,13 @@ final class TileFactory{
$this->saveNames[$className] = reset($saveNames);
}
/**
* @phpstan-param class-string<Tile> $class
*/
public function isRegistered(string $class) : bool{
return isset($this->saveNames[$class]);
}
/**
* @internal
* @throws SavedDataLoadingException

View File

@ -30,9 +30,15 @@ interface CopperMaterial{
public function getOxidation() : CopperOxidation;
/**
* @return $this
*/
public function setOxidation(CopperOxidation $oxidation) : CopperMaterial;
public function isWaxed() : bool;
/**
* @return $this
*/
public function setWaxed(bool $waxed) : CopperMaterial;
}

View File

@ -29,8 +29,12 @@ use pocketmine\nbt\TreeRoot;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\DestructorCallbackTrait;
use pocketmine\utils\ObjectSet;
use function array_shift;
use function count;
use function implode;
use function ksort;
use function spl_object_id;
use function usort;
use const SORT_STRING;
class CraftingManager{
use DestructorCallbackTrait;
@ -100,6 +104,7 @@ class CraftingManager{
/**
* Function used to arrange Shapeless Recipe ingredient lists into a consistent order.
* @deprecated
*/
public static function sort(Item $i1, Item $i2) : int{
//Use spaceship operator to compare each property, then try the next one if they are equivalent.
@ -108,45 +113,30 @@ class CraftingManager{
return $retval;
}
/**
* @param Item[] $items
*
* @return Item[]
* @phpstan-return list<Item>
*/
private static function pack(array $items) : array{
$result = [];
private static function hashOutput(Item $output) : string{
$write = new BinaryStream();
$write->putVarInt($output->getStateId());
$write->put((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
foreach($items as $item){
foreach($result as $otherItem){
if($item->canStackWith($otherItem)){
$otherItem->setCount($otherItem->getCount() + $item->getCount());
continue 2;
}
}
//No matching item found
$result[] = clone $item;
}
return $result;
return $write->getBuffer();
}
/**
* @param Item[] $outputs
*/
private static function hashOutputs(array $outputs) : string{
$outputs = self::pack($outputs);
usort($outputs, [self::class, "sort"]);
$result = new BinaryStream();
if(count($outputs) === 1){
return self::hashOutput(array_shift($outputs));
}
$unique = [];
foreach($outputs as $o){
//count is not written because the outputs might be from multiple repetitions of a single recipe
//this reduces the accuracy of the hash, but it won't matter in most cases.
$result->putVarInt($o->getStateId());
$result->put((new LittleEndianNbtSerializer())->write(new TreeRoot($o->getNamedTag())));
$hash = self::hashOutput($o);
$unique[$hash] = $hash;
}
return $result->getBuffer();
ksort($unique, SORT_STRING);
return implode("", $unique);
}
/**

View File

@ -31,8 +31,7 @@ final class BedrockDataFiles{
}
public const BANNER_PATTERNS_JSON = BEDROCK_DATA_PATH . '/banner_patterns.json';
public const BIOME_DEFINITIONS_NBT = BEDROCK_DATA_PATH . '/biome_definitions.nbt';
public const BIOME_DEFINITIONS_FULL_NBT = BEDROCK_DATA_PATH . '/biome_definitions_full.nbt';
public const BIOME_DEFINITIONS_JSON = BEDROCK_DATA_PATH . '/biome_definitions.json';
public const BIOME_ID_MAP_JSON = BEDROCK_DATA_PATH . '/biome_id_map.json';
public const BLOCK_ID_TO_ITEM_ID_MAP_JSON = BEDROCK_DATA_PATH . '/block_id_to_item_id_map.json';
public const BLOCK_PROPERTIES_TABLE_JSON = BEDROCK_DATA_PATH . '/block_properties_table.json';

View File

@ -0,0 +1,66 @@
<?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\data\bedrock;
use pocketmine\world\format\io\leveldb\ChunkVersion;
use pocketmine\world\format\io\leveldb\SubChunkVersion;
/**
* All version infos related to current Minecraft data version support
* These are mostly related to world storage but may also influence network stuff
*/
final class WorldDataVersions{
/**
* Bedrock version of the most recent backwards-incompatible change to blockstates.
*
* This is *NOT* the same as current game version. It should match the numbers in the
* newest blockstate upgrade schema used in BedrockBlockUpgradeSchema.
*/
public const BLOCK_STATES =
(1 << 24) | //major
(21 << 16) | //minor
(60 << 8) | //patch
(33); //revision
public const CHUNK = ChunkVersion::v1_21_40;
public const SUBCHUNK = SubChunkVersion::PALETTED_MULTI;
public const STORAGE = 10;
/**
* Highest NetworkVersion of Bedrock worlds currently supported by PocketMine-MP.
*
* This may be lower than the current protocol version if PocketMine-MP does not yet support features of the newer
* version. This allows the protocol to be updated independently of world format support.
*/
public const NETWORK = 827;
public const LAST_OPENED_IN = [
1, //major
21, //minor
100, //patch
23, //revision
0 //is beta
];
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock\block;
use pocketmine\data\bedrock\WorldDataVersions;
use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\Tag;
@ -36,17 +37,7 @@ use function implode;
* Contains the common information found in a serialized blockstate.
*/
final class BlockStateData{
/**
* Bedrock version of the most recent backwards-incompatible change to blockstates.
*
* This is *not* the same as current game version. It should match the numbers in the
* newest blockstate upgrade schema used in BedrockBlockUpgradeSchema.
*/
public const CURRENT_VERSION =
(1 << 24) | //major
(21 << 16) | //minor
(60 << 8) | //patch
(33); //revision
public const CURRENT_VERSION = WorldDataVersions::BLOCK_STATES;
public const TAG_NAME = "name";
public const TAG_STATES = "states";

View File

@ -113,6 +113,7 @@ final class BlockStateNames{
public const RAIL_DATA_BIT = "rail_data_bit";
public const RAIL_DIRECTION = "rail_direction";
public const REDSTONE_SIGNAL = "redstone_signal";
public const REHYDRATION_LEVEL = "rehydration_level";
public const REPEATER_DELAY = "repeater_delay";
public const RESPAWN_ANCHOR_CHARGE = "respawn_anchor_charge";
public const ROTATION = "rotation";

View File

@ -175,7 +175,9 @@ final class BlockTypeNames{
public const BUBBLE_CORAL_FAN = "minecraft:bubble_coral_fan";
public const BUBBLE_CORAL_WALL_FAN = "minecraft:bubble_coral_wall_fan";
public const BUDDING_AMETHYST = "minecraft:budding_amethyst";
public const BUSH = "minecraft:bush";
public const CACTUS = "minecraft:cactus";
public const CACTUS_FLOWER = "minecraft:cactus_flower";
public const CAKE = "minecraft:cake";
public const CALCITE = "minecraft:calcite";
public const CALIBRATED_SCULK_SENSOR = "minecraft:calibrated_sculk_sensor";
@ -253,6 +255,7 @@ final class BlockTypeNames{
public const CONDUIT = "minecraft:conduit";
public const COPPER_BLOCK = "minecraft:copper_block";
public const COPPER_BULB = "minecraft:copper_bulb";
public const COPPER_CHEST = "minecraft:copper_chest";
public const COPPER_DOOR = "minecraft:copper_door";
public const COPPER_GRATE = "minecraft:copper_grate";
public const COPPER_ORE = "minecraft:copper_ore";
@ -390,6 +393,7 @@ final class BlockTypeNames{
public const DOUBLE_CUT_COPPER_SLAB = "minecraft:double_cut_copper_slab";
public const DRAGON_EGG = "minecraft:dragon_egg";
public const DRAGON_HEAD = "minecraft:dragon_head";
public const DRIED_GHAST = "minecraft:dried_ghast";
public const DRIED_KELP_BLOCK = "minecraft:dried_kelp_block";
public const DRIPSTONE_BLOCK = "minecraft:dripstone_block";
public const DROPPER = "minecraft:dropper";
@ -530,6 +534,7 @@ final class BlockTypeNames{
public const EXPOSED_CHISELED_COPPER = "minecraft:exposed_chiseled_copper";
public const EXPOSED_COPPER = "minecraft:exposed_copper";
public const EXPOSED_COPPER_BULB = "minecraft:exposed_copper_bulb";
public const EXPOSED_COPPER_CHEST = "minecraft:exposed_copper_chest";
public const EXPOSED_COPPER_DOOR = "minecraft:exposed_copper_door";
public const EXPOSED_COPPER_GRATE = "minecraft:exposed_copper_grate";
public const EXPOSED_COPPER_TRAPDOOR = "minecraft:exposed_copper_trapdoor";
@ -545,6 +550,7 @@ final class BlockTypeNames{
public const FIRE_CORAL_BLOCK = "minecraft:fire_coral_block";
public const FIRE_CORAL_FAN = "minecraft:fire_coral_fan";
public const FIRE_CORAL_WALL_FAN = "minecraft:fire_coral_wall_fan";
public const FIREFLY_BUSH = "minecraft:firefly_bush";
public const FLETCHING_TABLE = "minecraft:fletching_table";
public const FLOWER_POT = "minecraft:flower_pot";
public const FLOWERING_AZALEA = "minecraft:flowering_azalea";
@ -685,6 +691,7 @@ final class BlockTypeNames{
public const LARGE_AMETHYST_BUD = "minecraft:large_amethyst_bud";
public const LARGE_FERN = "minecraft:large_fern";
public const LAVA = "minecraft:lava";
public const LEAF_LITTER = "minecraft:leaf_litter";
public const LECTERN = "minecraft:lectern";
public const LEVER = "minecraft:lever";
public const LIGHT_BLOCK_0 = "minecraft:light_block_0";
@ -852,6 +859,7 @@ final class BlockTypeNames{
public const OXIDIZED_CHISELED_COPPER = "minecraft:oxidized_chiseled_copper";
public const OXIDIZED_COPPER = "minecraft:oxidized_copper";
public const OXIDIZED_COPPER_BULB = "minecraft:oxidized_copper_bulb";
public const OXIDIZED_COPPER_CHEST = "minecraft:oxidized_copper_chest";
public const OXIDIZED_COPPER_DOOR = "minecraft:oxidized_copper_door";
public const OXIDIZED_COPPER_GRATE = "minecraft:oxidized_copper_grate";
public const OXIDIZED_COPPER_TRAPDOOR = "minecraft:oxidized_copper_trapdoor";
@ -1043,6 +1051,7 @@ final class BlockTypeNames{
public const SEA_LANTERN = "minecraft:sea_lantern";
public const SEA_PICKLE = "minecraft:sea_pickle";
public const SEAGRASS = "minecraft:seagrass";
public const SHORT_DRY_GRASS = "minecraft:short_dry_grass";
public const SHORT_GRASS = "minecraft:short_grass";
public const SHROOMLIGHT = "minecraft:shroomlight";
public const SILVER_GLAZED_TERRACOTTA = "minecraft:silver_glazed_terracotta";
@ -1140,6 +1149,7 @@ final class BlockTypeNames{
public const SUSPICIOUS_GRAVEL = "minecraft:suspicious_gravel";
public const SUSPICIOUS_SAND = "minecraft:suspicious_sand";
public const SWEET_BERRY_BUSH = "minecraft:sweet_berry_bush";
public const TALL_DRY_GRASS = "minecraft:tall_dry_grass";
public const TALL_GRASS = "minecraft:tall_grass";
public const TARGET = "minecraft:target";
public const TINTED_GLASS = "minecraft:tinted_glass";
@ -1204,6 +1214,7 @@ final class BlockTypeNames{
public const WAXED_CHISELED_COPPER = "minecraft:waxed_chiseled_copper";
public const WAXED_COPPER = "minecraft:waxed_copper";
public const WAXED_COPPER_BULB = "minecraft:waxed_copper_bulb";
public const WAXED_COPPER_CHEST = "minecraft:waxed_copper_chest";
public const WAXED_COPPER_DOOR = "minecraft:waxed_copper_door";
public const WAXED_COPPER_GRATE = "minecraft:waxed_copper_grate";
public const WAXED_COPPER_TRAPDOOR = "minecraft:waxed_copper_trapdoor";
@ -1214,6 +1225,7 @@ final class BlockTypeNames{
public const WAXED_EXPOSED_CHISELED_COPPER = "minecraft:waxed_exposed_chiseled_copper";
public const WAXED_EXPOSED_COPPER = "minecraft:waxed_exposed_copper";
public const WAXED_EXPOSED_COPPER_BULB = "minecraft:waxed_exposed_copper_bulb";
public const WAXED_EXPOSED_COPPER_CHEST = "minecraft:waxed_exposed_copper_chest";
public const WAXED_EXPOSED_COPPER_DOOR = "minecraft:waxed_exposed_copper_door";
public const WAXED_EXPOSED_COPPER_GRATE = "minecraft:waxed_exposed_copper_grate";
public const WAXED_EXPOSED_COPPER_TRAPDOOR = "minecraft:waxed_exposed_copper_trapdoor";
@ -1224,6 +1236,7 @@ final class BlockTypeNames{
public const WAXED_OXIDIZED_CHISELED_COPPER = "minecraft:waxed_oxidized_chiseled_copper";
public const WAXED_OXIDIZED_COPPER = "minecraft:waxed_oxidized_copper";
public const WAXED_OXIDIZED_COPPER_BULB = "minecraft:waxed_oxidized_copper_bulb";
public const WAXED_OXIDIZED_COPPER_CHEST = "minecraft:waxed_oxidized_copper_chest";
public const WAXED_OXIDIZED_COPPER_DOOR = "minecraft:waxed_oxidized_copper_door";
public const WAXED_OXIDIZED_COPPER_GRATE = "minecraft:waxed_oxidized_copper_grate";
public const WAXED_OXIDIZED_COPPER_TRAPDOOR = "minecraft:waxed_oxidized_copper_trapdoor";
@ -1234,6 +1247,7 @@ final class BlockTypeNames{
public const WAXED_WEATHERED_CHISELED_COPPER = "minecraft:waxed_weathered_chiseled_copper";
public const WAXED_WEATHERED_COPPER = "minecraft:waxed_weathered_copper";
public const WAXED_WEATHERED_COPPER_BULB = "minecraft:waxed_weathered_copper_bulb";
public const WAXED_WEATHERED_COPPER_CHEST = "minecraft:waxed_weathered_copper_chest";
public const WAXED_WEATHERED_COPPER_DOOR = "minecraft:waxed_weathered_copper_door";
public const WAXED_WEATHERED_COPPER_GRATE = "minecraft:waxed_weathered_copper_grate";
public const WAXED_WEATHERED_COPPER_TRAPDOOR = "minecraft:waxed_weathered_copper_trapdoor";
@ -1244,6 +1258,7 @@ final class BlockTypeNames{
public const WEATHERED_CHISELED_COPPER = "minecraft:weathered_chiseled_copper";
public const WEATHERED_COPPER = "minecraft:weathered_copper";
public const WEATHERED_COPPER_BULB = "minecraft:weathered_copper_bulb";
public const WEATHERED_COPPER_CHEST = "minecraft:weathered_copper_chest";
public const WEATHERED_COPPER_DOOR = "minecraft:weathered_copper_door";
public const WEATHERED_COPPER_GRATE = "minecraft:weathered_copper_grate";
public const WEATHERED_COPPER_TRAPDOOR = "minecraft:weathered_copper_trapdoor";
@ -1267,6 +1282,7 @@ final class BlockTypeNames{
public const WHITE_TERRACOTTA = "minecraft:white_terracotta";
public const WHITE_TULIP = "minecraft:white_tulip";
public const WHITE_WOOL = "minecraft:white_wool";
public const WILDFLOWERS = "minecraft:wildflowers";
public const WITHER_ROSE = "minecraft:wither_rose";
public const WITHER_SKELETON_SKULL = "minecraft:wither_skeleton_skull";
public const WOODEN_BUTTON = "minecraft:wooden_button";

View File

@ -122,6 +122,7 @@ use pocketmine\block\RedstoneRepeater;
use pocketmine\block\RedstoneTorch;
use pocketmine\block\RedstoneWire;
use pocketmine\block\ResinClump;
use pocketmine\block\RespawnAnchor;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\Sapling;
use pocketmine\block\SeaPickle;
@ -214,6 +215,7 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$this->registerLeavesSerializers();
$this->registerSaplingSerializers();
$this->registerMobHeadSerializers();
$this->registerCopperSerializers();
$this->registerSimpleSerializers();
$this->registerSerializers();
}
@ -224,6 +226,10 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
return $this->cache[$stateId] ??= $this->serializeBlock(RuntimeBlockStateRegistry::getInstance()->fromStateId($stateId));
}
public function isRegistered(Block $block) : bool{
return isset($this->serializers[$block->getTypeId()]);
}
/**
* @phpstan-template TBlockType of Block
* @phpstan-param TBlockType $block
@ -791,6 +797,178 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
})->writeFacingWithoutDown($block->getFacing()));
}
private function registerCopperSerializers() : void{
$this->map(Blocks::COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation, Ids::WAXED_COPPER, Ids::WAXED_EXPOSED_COPPER, Ids::WAXED_WEATHERED_COPPER, Ids::WAXED_OXIDIZED_COPPER) :
Helper::selectCopperId($oxidation, Ids::COPPER_BLOCK, Ids::EXPOSED_COPPER, Ids::WEATHERED_COPPER, Ids::OXIDIZED_COPPER)
);
});
$this->map(Blocks::CHISELED_COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_CHISELED_COPPER,
Ids::WAXED_EXPOSED_CHISELED_COPPER,
Ids::WAXED_WEATHERED_CHISELED_COPPER,
Ids::WAXED_OXIDIZED_CHISELED_COPPER
) :
Helper::selectCopperId($oxidation,
Ids::CHISELED_COPPER,
Ids::EXPOSED_CHISELED_COPPER,
Ids::WEATHERED_CHISELED_COPPER,
Ids::OXIDIZED_CHISELED_COPPER
)
);
});
$this->map(Blocks::COPPER_GRATE(), function(CopperGrate $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_COPPER_GRATE,
Ids::WAXED_EXPOSED_COPPER_GRATE,
Ids::WAXED_WEATHERED_COPPER_GRATE,
Ids::WAXED_OXIDIZED_COPPER_GRATE
) :
Helper::selectCopperId($oxidation,
Ids::COPPER_GRATE,
Ids::EXPOSED_COPPER_GRATE,
Ids::WEATHERED_COPPER_GRATE,
Ids::OXIDIZED_COPPER_GRATE
)
);
});
$this->map(Blocks::CUT_COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation, Ids::WAXED_CUT_COPPER, Ids::WAXED_EXPOSED_CUT_COPPER, Ids::WAXED_WEATHERED_CUT_COPPER, Ids::WAXED_OXIDIZED_CUT_COPPER) :
Helper::selectCopperId($oxidation, Ids::CUT_COPPER, Ids::EXPOSED_CUT_COPPER, Ids::WEATHERED_CUT_COPPER, Ids::OXIDIZED_CUT_COPPER)
);
});
$this->map(Blocks::CUT_COPPER_SLAB(), function(CopperSlab $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeSlab(
$block,
($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB
) :
Helper::selectCopperId(
$oxidation,
Ids::CUT_COPPER_SLAB,
Ids::EXPOSED_CUT_COPPER_SLAB,
Ids::WEATHERED_CUT_COPPER_SLAB,
Ids::OXIDIZED_CUT_COPPER_SLAB
)
),
($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB
) :
Helper::selectCopperId(
$oxidation,
Ids::DOUBLE_CUT_COPPER_SLAB,
Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB
)
)
);
});
$this->map(Blocks::CUT_COPPER_STAIRS(), function(CopperStairs $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeStairs(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_CUT_COPPER_STAIRS,
Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS,
Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS,
Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS
) :
Helper::selectCopperId(
$oxidation,
Ids::CUT_COPPER_STAIRS,
Ids::EXPOSED_CUT_COPPER_STAIRS,
Ids::WEATHERED_CUT_COPPER_STAIRS,
Ids::OXIDIZED_CUT_COPPER_STAIRS
)
)
);
});
$this->map(Blocks::COPPER_BULB(), function(CopperBulb $block) : Writer{
$oxidation = $block->getOxidation();
return Writer::create($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_COPPER_BULB,
Ids::WAXED_EXPOSED_COPPER_BULB,
Ids::WAXED_WEATHERED_COPPER_BULB,
Ids::WAXED_OXIDIZED_COPPER_BULB) :
Helper::selectCopperId($oxidation,
Ids::COPPER_BULB,
Ids::EXPOSED_COPPER_BULB,
Ids::WEATHERED_COPPER_BULB,
Ids::OXIDIZED_COPPER_BULB
))
->writeBool(StateNames::LIT, $block->isLit())
->writeBool(StateNames::POWERED_BIT, $block->isPowered());
});
$this->map(Blocks::COPPER_DOOR(), function(CopperDoor $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeDoor(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_COPPER_DOOR,
Ids::WAXED_EXPOSED_COPPER_DOOR,
Ids::WAXED_WEATHERED_COPPER_DOOR,
Ids::WAXED_OXIDIZED_COPPER_DOOR
) :
Helper::selectCopperId(
$oxidation,
Ids::COPPER_DOOR,
Ids::EXPOSED_COPPER_DOOR,
Ids::WEATHERED_COPPER_DOOR,
Ids::OXIDIZED_COPPER_DOOR
)
)
);
});
$this->map(Blocks::COPPER_TRAPDOOR(), function(CopperTrapdoor $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeTrapdoor(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_COPPER_TRAPDOOR,
Ids::WAXED_EXPOSED_COPPER_TRAPDOOR,
Ids::WAXED_WEATHERED_COPPER_TRAPDOOR,
Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR
) :
Helper::selectCopperId(
$oxidation,
Ids::COPPER_TRAPDOOR,
Ids::EXPOSED_COPPER_TRAPDOOR,
Ids::WEATHERED_COPPER_TRAPDOOR,
Ids::OXIDIZED_COPPER_TRAPDOOR
)
)
);
});
}
private function registerSimpleSerializers() : void{
$this->mapSimple(Blocks::AIR(), Ids::AIR);
$this->mapSimple(Blocks::AMETHYST(), Ids::AMETHYST_BLOCK);
@ -1265,175 +1443,6 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
$this->mapSlab(Blocks::COBBLESTONE_SLAB(), Ids::COBBLESTONE_SLAB, Ids::COBBLESTONE_DOUBLE_SLAB);
$this->mapStairs(Blocks::COBBLESTONE_STAIRS(), Ids::STONE_STAIRS);
$this->map(Blocks::COBBLESTONE_WALL(), fn(Wall $block) => Helper::encodeWall($block, Writer::create(Ids::COBBLESTONE_WALL)));
$this->map(Blocks::COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation, Ids::WAXED_COPPER, Ids::WAXED_EXPOSED_COPPER, Ids::WAXED_WEATHERED_COPPER, Ids::WAXED_OXIDIZED_COPPER) :
Helper::selectCopperId($oxidation, Ids::COPPER_BLOCK, Ids::EXPOSED_COPPER, Ids::WEATHERED_COPPER, Ids::OXIDIZED_COPPER)
);
});
$this->map(Blocks::CHISELED_COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_CHISELED_COPPER,
Ids::WAXED_EXPOSED_CHISELED_COPPER,
Ids::WAXED_WEATHERED_CHISELED_COPPER,
Ids::WAXED_OXIDIZED_CHISELED_COPPER
) :
Helper::selectCopperId($oxidation,
Ids::CHISELED_COPPER,
Ids::EXPOSED_CHISELED_COPPER,
Ids::WEATHERED_CHISELED_COPPER,
Ids::OXIDIZED_CHISELED_COPPER
)
);
});
$this->map(Blocks::COPPER_GRATE(), function(CopperGrate $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_COPPER_GRATE,
Ids::WAXED_EXPOSED_COPPER_GRATE,
Ids::WAXED_WEATHERED_COPPER_GRATE,
Ids::WAXED_OXIDIZED_COPPER_GRATE
) :
Helper::selectCopperId($oxidation,
Ids::COPPER_GRATE,
Ids::EXPOSED_COPPER_GRATE,
Ids::WEATHERED_COPPER_GRATE,
Ids::OXIDIZED_COPPER_GRATE
)
);
});
$this->map(Blocks::CUT_COPPER(), function(Copper $block) : Writer{
$oxidation = $block->getOxidation();
return new Writer($block->isWaxed() ?
Helper::selectCopperId($oxidation, Ids::WAXED_CUT_COPPER, Ids::WAXED_EXPOSED_CUT_COPPER, Ids::WAXED_WEATHERED_CUT_COPPER, Ids::WAXED_OXIDIZED_CUT_COPPER) :
Helper::selectCopperId($oxidation, Ids::CUT_COPPER, Ids::EXPOSED_CUT_COPPER, Ids::WEATHERED_CUT_COPPER, Ids::OXIDIZED_CUT_COPPER)
);
});
$this->map(Blocks::CUT_COPPER_SLAB(), function(CopperSlab $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeSlab(
$block,
($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB
) :
Helper::selectCopperId(
$oxidation,
Ids::CUT_COPPER_SLAB,
Ids::EXPOSED_CUT_COPPER_SLAB,
Ids::WEATHERED_CUT_COPPER_SLAB,
Ids::OXIDIZED_CUT_COPPER_SLAB
)
),
($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB
) :
Helper::selectCopperId(
$oxidation,
Ids::DOUBLE_CUT_COPPER_SLAB,
Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB
)
)
);
});
$this->map(Blocks::CUT_COPPER_STAIRS(), function(CopperStairs $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeStairs(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_CUT_COPPER_STAIRS,
Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS,
Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS,
Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS
) :
Helper::selectCopperId(
$oxidation,
Ids::CUT_COPPER_STAIRS,
Ids::EXPOSED_CUT_COPPER_STAIRS,
Ids::WEATHERED_CUT_COPPER_STAIRS,
Ids::OXIDIZED_CUT_COPPER_STAIRS
)
)
);
});
$this->map(Blocks::COPPER_BULB(), function(CopperBulb $block) : Writer{
$oxidation = $block->getOxidation();
return Writer::create($block->isWaxed() ?
Helper::selectCopperId($oxidation,
Ids::WAXED_COPPER_BULB,
Ids::WAXED_EXPOSED_COPPER_BULB,
Ids::WAXED_WEATHERED_COPPER_BULB,
Ids::WAXED_OXIDIZED_COPPER_BULB) :
Helper::selectCopperId($oxidation,
Ids::COPPER_BULB,
Ids::EXPOSED_COPPER_BULB,
Ids::WEATHERED_COPPER_BULB,
Ids::OXIDIZED_COPPER_BULB
))
->writeBool(StateNames::LIT, $block->isLit())
->writeBool(StateNames::POWERED_BIT, $block->isPowered());
});
$this->map(Blocks::COPPER_DOOR(), function(CopperDoor $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeDoor(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_COPPER_DOOR,
Ids::WAXED_EXPOSED_COPPER_DOOR,
Ids::WAXED_WEATHERED_COPPER_DOOR,
Ids::WAXED_OXIDIZED_COPPER_DOOR
) :
Helper::selectCopperId(
$oxidation,
Ids::COPPER_DOOR,
Ids::EXPOSED_COPPER_DOOR,
Ids::WEATHERED_COPPER_DOOR,
Ids::OXIDIZED_COPPER_DOOR
)
)
);
});
$this->map(Blocks::COPPER_TRAPDOOR(), function(CopperTrapdoor $block) : Writer{
$oxidation = $block->getOxidation();
return Helper::encodeTrapdoor(
$block,
new Writer($block->isWaxed() ?
Helper::selectCopperId(
$oxidation,
Ids::WAXED_COPPER_TRAPDOOR,
Ids::WAXED_EXPOSED_COPPER_TRAPDOOR,
Ids::WAXED_WEATHERED_COPPER_TRAPDOOR,
Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR
) :
Helper::selectCopperId(
$oxidation,
Ids::COPPER_TRAPDOOR,
Ids::EXPOSED_COPPER_TRAPDOOR,
Ids::WEATHERED_COPPER_TRAPDOOR,
Ids::OXIDIZED_COPPER_TRAPDOOR
)
)
);
});
$this->map(Blocks::COCOA_POD(), function(CocoaBlock $block) : Writer{
return Writer::create(Ids::COCOA)
->writeInt(StateNames::AGE, $block->getAge())
@ -1746,6 +1755,10 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
return Writer::create(Ids::RESIN_CLUMP)
->writeFacingFlags($block->getFaces());
});
$this->map(Blocks::RESPAWN_ANCHOR(), function(RespawnAnchor $block) : Writer{
return Writer::create(Ids::RESPAWN_ANCHOR)
->writeInt(StateNames::RESPAWN_ANCHOR_CHARGE, $block->getCharges());
});
$this->map(Blocks::ROSE_BUSH(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::ROSE_BUSH)));
$this->mapSlab(Blocks::SANDSTONE_SLAB(), Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB);
$this->mapStairs(Blocks::SANDSTONE_STAIRS(), Ids::SANDSTONE_STAIRS);

View File

@ -126,7 +126,13 @@ final class BlockStateDeserializerHelper{
->setOutputSignalStrength($in->readBoundedInt(BlockStateNames::REDSTONE_SIGNAL, 0, 15));
}
/** @throws BlockStateDeserializeException */
/**
* @phpstan-template TDoor of Door
* @phpstan-param TDoor $block
* @phpstan-return TDoor
*
* @throws BlockStateDeserializeException
*/
public static function decodeDoor(Door $block, BlockStateReader $in) : Door{
//TODO: check if these need any special treatment to get the appropriate data to both halves of the door
return $block
@ -237,18 +243,36 @@ final class BlockStateDeserializerHelper{
return $block->setPressed($in->readBoundedInt(BlockStateNames::REDSTONE_SIGNAL, 0, 15) !== 0);
}
/** @throws BlockStateDeserializeException */
/**
* @phpstan-template TSlab of Slab
* @phpstan-param TSlab $block
* @phpstan-return TSlab
*
* @throws BlockStateDeserializeException
*/
public static function decodeSingleSlab(Slab $block, BlockStateReader $in) : Slab{
return $block->setSlabType($in->readSlabPosition());
}
/** @throws BlockStateDeserializeException */
/**
* @phpstan-template TSlab of Slab
* @phpstan-param TSlab $block
* @phpstan-return TSlab
*
* @throws BlockStateDeserializeException
*/
public static function decodeDoubleSlab(Slab $block, BlockStateReader $in) : Slab{
$in->ignored(StateNames::MC_VERTICAL_HALF);
return $block->setSlabType(SlabType::DOUBLE);
}
/** @throws BlockStateDeserializeException */
/**
* @phpstan-template TStair of Stair
* @phpstan-param TStair $block
* @phpstan-return TStair
*
* @throws BlockStateDeserializeException
*/
public static function decodeStairs(Stair $block, BlockStateReader $in) : Stair{
return $block
->setUpsideDown($in->readBool(BlockStateNames::UPSIDE_DOWN_BIT))
@ -265,7 +289,13 @@ final class BlockStateDeserializerHelper{
->setFacing($facing === Facing::DOWN ? Facing::UP : $facing);
}
/** @throws BlockStateDeserializeException */
/**
* @phpstan-template TTrapdoor of Trapdoor
* @phpstan-param TTrapdoor $block
* @phpstan-return TTrapdoor
*
* @throws BlockStateDeserializeException
*/
public static function decodeTrapdoor(Trapdoor $block, BlockStateReader $in) : Trapdoor{
return $block
->setFacing($in->read5MinusHorizontalFacing())

View File

@ -33,11 +33,13 @@ use pocketmine\block\DoublePitcherCrop;
use pocketmine\block\Opaque;
use pocketmine\block\PinkPetals;
use pocketmine\block\PitcherCrop;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\Slab;
use pocketmine\block\Stair;
use pocketmine\block\SweetBerryBush;
use pocketmine\block\utils\BrewingStandSlot;
use pocketmine\block\utils\ChiseledBookshelfSlot;
use pocketmine\block\utils\CopperMaterial;
use pocketmine\block\utils\CopperOxidation;
use pocketmine\block\utils\CoralType;
use pocketmine\block\utils\DirtType;
@ -59,6 +61,7 @@ use pocketmine\data\bedrock\block\convert\BlockStateDeserializerHelper as Helper
use pocketmine\data\bedrock\block\convert\BlockStateReader as Reader;
use pocketmine\math\Axis;
use pocketmine\math\Facing;
use pocketmine\utils\Utils;
use function array_key_exists;
use function count;
use function min;
@ -87,6 +90,7 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$this->registerSaplingDeserializers();
$this->registerLightDeserializers();
$this->registerMobHeadDeserializers();
$this->registerCopperDeserializers();
$this->registerSimpleDeserializers();
$this->registerDeserializers();
}
@ -94,19 +98,37 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
public function deserialize(BlockStateData $stateData) : int{
if(count($stateData->getStates()) === 0){
//if a block has zero properties, we can keep a map of string ID -> internal blockstate ID
return $this->simpleCache[$stateData->getName()] ??= $this->deserializeBlock($stateData)->getStateId();
return $this->simpleCache[$stateData->getName()] ??= $this->deserializeToStateId($stateData);
}
//we can't cache blocks that have properties - go ahead and deserialize the slow way
return $this->deserializeBlock($stateData)->getStateId();
return $this->deserializeToStateId($stateData);
}
private function deserializeToStateId(BlockStateData $stateData) : int{
$stateId = $this->deserializeBlock($stateData)->getStateId();
//plugin devs seem to keep missing this and causing core crashes, so we need to verify this at the earliest
//available opportunity
if(!RuntimeBlockStateRegistry::getInstance()->hasStateId($stateId)){
throw new \LogicException("State ID $stateId returned by deserializer for " . $stateData->getName() . " is not registered in RuntimeBlockStateRegistry");
}
return $stateId;
}
/** @phpstan-param \Closure(Reader) : Block $c */
public function map(string $id, \Closure $c) : void{
if(array_key_exists($id, $this->deserializeFuncs)){
throw new \InvalidArgumentException("Deserializer is already assigned for \"$id\"");
}
$this->deserializeFuncs[$id] = $c;
$this->simpleCache = [];
}
/**
* Returns the existing data deserializer for the given ID, or null if none exists.
* This may be useful if you need to override a deserializer, but still want to be able to fall back to the original.
*
* @phpstan-return ?\Closure(Reader) : Block
*/
public function getDeserializerForId(string $id) : ?\Closure{
return $this->deserializeFuncs[$id] ?? null;
}
/** @phpstan-param \Closure() : Block $getBlock */
@ -715,6 +737,150 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
}
}
/**
* @phpstan-param \Closure(Reader) : (CopperMaterial&Block) $deserializer
*/
private function mapCopper(
string $normalId,
string $waxedNormalId,
string $exposedId,
string $waxedExposedId,
string $weatheredId,
string $waxedWeatheredId,
string $oxidizedId,
string $waxedOxidizedId,
\Closure $deserializer
) : void{
foreach(Utils::stringifyKeys([
$normalId => [CopperOxidation::NONE, false],
$waxedNormalId => [CopperOxidation::NONE, true],
$exposedId => [CopperOxidation::EXPOSED, false],
$waxedExposedId => [CopperOxidation::EXPOSED, true],
$weatheredId => [CopperOxidation::WEATHERED, false],
$waxedWeatheredId => [CopperOxidation::WEATHERED, true],
$oxidizedId => [CopperOxidation::OXIDIZED, false],
$waxedOxidizedId => [CopperOxidation::OXIDIZED, true],
]) as $id => [$oxidation, $waxed]){
$this->map($id, fn(Reader $in) => $deserializer($in)->setOxidation($oxidation)->setWaxed($waxed));
}
}
private function registerCopperDeserializers() : void{
$this->mapCopper(
Ids::CUT_COPPER_SLAB,
Ids::WAXED_CUT_COPPER_SLAB,
Ids::EXPOSED_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_CUT_COPPER_SLAB,
Ids::WEATHERED_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_CUT_COPPER_SLAB,
Ids::OXIDIZED_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB,
fn(Reader $in) => Helper::decodeSingleSlab(Blocks::CUT_COPPER_SLAB(), $in)
);
$this->mapCopper(
Ids::DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_DOUBLE_CUT_COPPER_SLAB,
Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB,
Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB,
Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB,
Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB,
fn(Reader $in) => Helper::decodeDoubleSlab(Blocks::CUT_COPPER_SLAB(), $in)
);
$this->mapCopper(
Ids::COPPER_BULB,
Ids::WAXED_COPPER_BULB,
Ids::EXPOSED_COPPER_BULB,
Ids::WAXED_EXPOSED_COPPER_BULB,
Ids::WEATHERED_COPPER_BULB,
Ids::WAXED_WEATHERED_COPPER_BULB,
Ids::OXIDIZED_COPPER_BULB,
Ids::WAXED_OXIDIZED_COPPER_BULB,
fn(Reader $in) => Blocks::COPPER_BULB()
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT))
);
$this->mapCopper(
Ids::COPPER_DOOR,
Ids::WAXED_COPPER_DOOR,
Ids::EXPOSED_COPPER_DOOR,
Ids::WAXED_EXPOSED_COPPER_DOOR,
Ids::WEATHERED_COPPER_DOOR,
Ids::WAXED_WEATHERED_COPPER_DOOR,
Ids::OXIDIZED_COPPER_DOOR,
Ids::WAXED_OXIDIZED_COPPER_DOOR,
fn(Reader $in) => Helper::decodeDoor(Blocks::COPPER_DOOR(), $in)
);
$this->mapCopper(
Ids::COPPER_TRAPDOOR,
Ids::WAXED_COPPER_TRAPDOOR,
Ids::EXPOSED_COPPER_TRAPDOOR,
Ids::WAXED_EXPOSED_COPPER_TRAPDOOR,
Ids::WEATHERED_COPPER_TRAPDOOR,
Ids::WAXED_WEATHERED_COPPER_TRAPDOOR,
Ids::OXIDIZED_COPPER_TRAPDOOR,
Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR,
fn(Reader $in) => Helper::decodeTrapdoor(Blocks::COPPER_TRAPDOOR(), $in)
);
$this->mapCopper(
Ids::COPPER_BLOCK,
Ids::WAXED_COPPER,
Ids::EXPOSED_COPPER,
Ids::WAXED_EXPOSED_COPPER,
Ids::WEATHERED_COPPER,
Ids::WAXED_WEATHERED_COPPER,
Ids::OXIDIZED_COPPER,
Ids::WAXED_OXIDIZED_COPPER,
fn(Reader $in) => Blocks::COPPER()
);
$this->mapCopper(
Ids::CHISELED_COPPER,
Ids::WAXED_CHISELED_COPPER,
Ids::EXPOSED_CHISELED_COPPER,
Ids::WAXED_EXPOSED_CHISELED_COPPER,
Ids::WEATHERED_CHISELED_COPPER,
Ids::WAXED_WEATHERED_CHISELED_COPPER,
Ids::OXIDIZED_CHISELED_COPPER,
Ids::WAXED_OXIDIZED_CHISELED_COPPER,
fn(Reader $in) => Blocks::CHISELED_COPPER()
);
$this->mapCopper(
Ids::COPPER_GRATE,
Ids::WAXED_COPPER_GRATE,
Ids::EXPOSED_COPPER_GRATE,
Ids::WAXED_EXPOSED_COPPER_GRATE,
Ids::WEATHERED_COPPER_GRATE,
Ids::WAXED_WEATHERED_COPPER_GRATE,
Ids::OXIDIZED_COPPER_GRATE,
Ids::WAXED_OXIDIZED_COPPER_GRATE,
fn(Reader $in) => Blocks::COPPER_GRATE()
);
$this->mapCopper(
Ids::CUT_COPPER,
Ids::WAXED_CUT_COPPER,
Ids::EXPOSED_CUT_COPPER,
Ids::WAXED_EXPOSED_CUT_COPPER,
Ids::WEATHERED_CUT_COPPER,
Ids::WAXED_WEATHERED_CUT_COPPER,
Ids::OXIDIZED_CUT_COPPER,
Ids::WAXED_OXIDIZED_CUT_COPPER,
fn(Reader $in) => Blocks::CUT_COPPER()
);
$this->mapCopper(
Ids::CUT_COPPER_STAIRS,
Ids::WAXED_CUT_COPPER_STAIRS,
Ids::EXPOSED_CUT_COPPER_STAIRS,
Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS,
Ids::WEATHERED_CUT_COPPER_STAIRS,
Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS,
Ids::OXIDIZED_CUT_COPPER_STAIRS,
Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS,
fn(Reader $in) => Helper::decodeStairs(Blocks::CUT_COPPER_STAIRS(), $in)
);
}
private function registerSimpleDeserializers() : void{
$this->mapSimple(Ids::AIR, fn() => Blocks::AIR());
$this->mapSimple(Ids::AMETHYST_BLOCK, fn() => Blocks::AMETHYST());
@ -1220,18 +1386,6 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$this->map(Ids::COMPOUND_CREATOR, fn(Reader $in) => Blocks::COMPOUND_CREATOR()
->setFacing(Facing::opposite($in->readLegacyHorizontalFacing()))
);
$this->map(Ids::COPPER_BLOCK, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::NONE));
$this->map(Ids::COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in));
$this->map(Ids::COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE));
$this->map(Ids::COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in));
$this->map(Ids::CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE));
$this->mapSlab(Ids::CUT_COPPER_SLAB, Ids::DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE));
$this->mapStairs(Ids::CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE));
$this->mapSlab(Ids::CUT_RED_SANDSTONE_SLAB, Ids::CUT_RED_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::CUT_RED_SANDSTONE_SLAB());
$this->mapSlab(Ids::CUT_SANDSTONE_SLAB, Ids::CUT_SANDSTONE_DOUBLE_SLAB, fn() => Blocks::CUT_SANDSTONE_SLAB());
$this->mapSlab(Ids::DARK_PRISMARINE_SLAB, Ids::DARK_PRISMARINE_DOUBLE_SLAB, fn() => Blocks::DARK_PRISMARINE_SLAB());
@ -1286,19 +1440,6 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
return Blocks::ENDER_CHEST()
->setFacing($in->readCardinalHorizontalFacing());
});
$this->map(Ids::EXPOSED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::EXPOSED));
$this->map(Ids::EXPOSED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED));
$this->map(Ids::EXPOSED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED));
$this->map(Ids::EXPOSED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED));
$this->mapSlab(Ids::EXPOSED_CUT_COPPER_SLAB, Ids::EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED));
$this->mapStairs(Ids::EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED));
$this->map(Ids::EXPOSED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in));
$this->map(Ids::EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in));
$this->map(Ids::FARMLAND, function(Reader $in) : Block{
return Blocks::FARMLAND()
->setWetness($in->readBoundedInt(StateNames::MOISTURIZED_AMOUNT, 0, 7));
@ -1451,19 +1592,6 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$this->mapSlab(Ids::NORMAL_STONE_SLAB, Ids::NORMAL_STONE_DOUBLE_SLAB, fn() => Blocks::STONE_SLAB());
$this->mapStairs(Ids::NORMAL_STONE_STAIRS, fn() => Blocks::STONE_STAIRS());
$this->map(Ids::OCHRE_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::OCHRE)->setAxis($in->readPillarAxis()));
$this->map(Ids::OXIDIZED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED));
$this->map(Ids::OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED));
$this->map(Ids::OXIDIZED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED));
$this->map(Ids::OXIDIZED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED));
$this->mapSlab(Ids::OXIDIZED_CUT_COPPER_SLAB, Ids::OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED));
$this->mapStairs(Ids::OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED));
$this->map(Ids::OXIDIZED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in));
$this->map(Ids::OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in));
$this->map(Ids::PEARLESCENT_FROGLIGHT, fn(Reader $in) => Blocks::FROGLIGHT()->setFroglightType(FroglightType::PEARLESCENT)->setAxis($in->readPillarAxis()));
$this->mapSlab(Ids::PETRIFIED_OAK_SLAB, Ids::PETRIFIED_OAK_DOUBLE_SLAB, fn() => Blocks::FAKE_WOODEN_SLAB());
$this->map(Ids::PINK_PETALS, function(Reader $in) : Block{
@ -1589,6 +1717,10 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
$this->mapStairs(Ids::RESIN_BRICK_STAIRS, fn() => Blocks::RESIN_BRICK_STAIRS());
$this->map(Ids::RESIN_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RESIN_BRICK_WALL(), $in));
$this->map(Ids::RESIN_CLUMP, fn(Reader $in) => Blocks::RESIN_CLUMP()->setFaces($in->readFacingFlags()));
$this->map(Ids::RESPAWN_ANCHOR, function(Reader $in) : Block{
return Blocks::RESPAWN_ANCHOR()
->setCharges($in->readBoundedInt(StateNames::RESPAWN_ANCHOR_CHARGE, 0, 4));
});
$this->mapSlab(Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SANDSTONE_SLAB());
$this->mapStairs(Ids::SANDSTONE_STAIRS, fn() => Blocks::SANDSTONE_STAIRS());
$this->map(Ids::SANDSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::SANDSTONE_WALL(), $in));
@ -1736,71 +1868,7 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
->setFacing($in->readHorizontalFacing());
});
$this->map(Ids::WATER, fn(Reader $in) => Helper::decodeStillLiquid(Blocks::WATER(), $in));
$this->map(Ids::WAXED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::NONE));
$this->map(Ids::WAXED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::NONE));
$this->map(Ids::WAXED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::NONE));
$this->map(Ids::WAXED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::NONE));
$this->mapSlab(Ids::WAXED_CUT_COPPER_SLAB, Ids::WAXED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::NONE));
$this->mapStairs(Ids::WAXED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::NONE));
$this->map(Ids::WAXED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::NONE)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::WAXED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::NONE), $in));
$this->map(Ids::WAXED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::NONE), $in));
$this->map(Ids::WAXED_EXPOSED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::EXPOSED));
$this->map(Ids::WAXED_EXPOSED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::EXPOSED));
$this->map(Ids::WAXED_EXPOSED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::EXPOSED));
$this->map(Ids::WAXED_EXPOSED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::EXPOSED));
$this->mapSlab(Ids::WAXED_EXPOSED_CUT_COPPER_SLAB, Ids::WAXED_EXPOSED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::EXPOSED));
$this->mapStairs(Ids::WAXED_EXPOSED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::EXPOSED));
$this->map(Ids::WAXED_EXPOSED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::EXPOSED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::WAXED_EXPOSED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::EXPOSED), $in));
$this->map(Ids::WAXED_EXPOSED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::EXPOSED), $in));
$this->map(Ids::WAXED_OXIDIZED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::OXIDIZED));
$this->map(Ids::WAXED_OXIDIZED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::OXIDIZED));
$this->map(Ids::WAXED_OXIDIZED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::OXIDIZED));
$this->map(Ids::WAXED_OXIDIZED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::OXIDIZED));
$this->mapSlab(Ids::WAXED_OXIDIZED_CUT_COPPER_SLAB, Ids::WAXED_OXIDIZED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::OXIDIZED));
$this->mapStairs(Ids::WAXED_OXIDIZED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::OXIDIZED));
$this->map(Ids::WAXED_OXIDIZED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::OXIDIZED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::WAXED_OXIDIZED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::OXIDIZED), $in));
$this->map(Ids::WAXED_OXIDIZED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::OXIDIZED), $in));
$this->map(Ids::WAXED_WEATHERED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::COPPER(), CopperOxidation::WEATHERED));
$this->map(Ids::WAXED_WEATHERED_CHISELED_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED));
$this->map(Ids::WAXED_WEATHERED_COPPER_GRATE, fn() => Helper::decodeWaxedCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED));
$this->map(Ids::WAXED_WEATHERED_CUT_COPPER, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED));
$this->mapSlab(Ids::WAXED_WEATHERED_CUT_COPPER_SLAB, Ids::WAXED_WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED));
$this->mapStairs(Ids::WAXED_WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeWaxedCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED));
$this->map(Ids::WAXED_WEATHERED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeWaxedCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::WAXED_WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeWaxedCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in));
$this->map(Ids::WAXED_WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeWaxedCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in));
$this->map(Ids::WEATHERED_COPPER, fn() => Helper::decodeCopper(Blocks::COPPER(), CopperOxidation::WEATHERED));
$this->map(Ids::WEATHERED_CHISELED_COPPER, fn() => Helper::decodeCopper(Blocks::CHISELED_COPPER(), CopperOxidation::WEATHERED));
$this->map(Ids::WEATHERED_COPPER_GRATE, fn() => Helper::decodeCopper(Blocks::COPPER_GRATE(), CopperOxidation::WEATHERED));
$this->map(Ids::WEATHERED_CUT_COPPER, fn() => Helper::decodeCopper(Blocks::CUT_COPPER(), CopperOxidation::WEATHERED));
$this->mapSlab(Ids::WEATHERED_CUT_COPPER_SLAB, Ids::WEATHERED_DOUBLE_CUT_COPPER_SLAB, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_SLAB(), CopperOxidation::WEATHERED));
$this->mapStairs(Ids::WEATHERED_CUT_COPPER_STAIRS, fn() => Helper::decodeCopper(Blocks::CUT_COPPER_STAIRS(), CopperOxidation::WEATHERED));
$this->map(Ids::WEATHERED_COPPER_BULB, function(Reader $in) : Block{
return Helper::decodeCopper(Blocks::COPPER_BULB(), CopperOxidation::WEATHERED)
->setLit($in->readBool(StateNames::LIT))
->setPowered($in->readBool(StateNames::POWERED_BIT));
});
$this->map(Ids::WEATHERED_COPPER_DOOR, fn(Reader $in) => Helper::decodeDoor(Helper::decodeCopper(Blocks::COPPER_DOOR(), CopperOxidation::WEATHERED), $in));
$this->map(Ids::WEATHERED_COPPER_TRAPDOOR, fn(Reader $in) => Helper::decodeTrapdoor(Helper::decodeCopper(Blocks::COPPER_TRAPDOOR(), CopperOxidation::WEATHERED), $in));
$this->map(Ids::WEEPING_VINES, function(Reader $in) : Block{
return Blocks::WEEPING_VINES()
->setAge($in->readBoundedInt(StateNames::WEEPING_VINES_AGE, 0, 25));

View File

@ -51,12 +51,19 @@ final class ItemDeserializer{
* @phpstan-param \Closure(Data) : Item $deserializer
*/
public function map(string $id, \Closure $deserializer) : void{
if(isset($this->deserializers[$id])){
throw new \InvalidArgumentException("Deserializer is already assigned for \"$id\"");
}
$this->deserializers[$id] = $deserializer;
}
/**
* Returns the existing data deserializer for the given ID, or null if none exists.
* This may be useful if you need to override a deserializer, but still want to be able to fall back to the original.
*
* @phpstan-return ?\Closure(Data) : Item
*/
public function getDeserializerForId(string $id) : ?\Closure{
return $this->deserializers[$id] ?? null;
}
/**
* @phpstan-param \Closure(Data) : Block $deserializer
*/

View File

@ -68,6 +68,7 @@ final class ItemTypeNames{
public const BIRCH_SIGN = "minecraft:birch_sign";
public const BLACK_BUNDLE = "minecraft:black_bundle";
public const BLACK_DYE = "minecraft:black_dye";
public const BLACK_HARNESS = "minecraft:black_harness";
public const BLADE_POTTERY_SHERD = "minecraft:blade_pottery_sherd";
public const BLAZE_POWDER = "minecraft:blaze_powder";
public const BLAZE_ROD = "minecraft:blaze_rod";
@ -75,6 +76,8 @@ final class ItemTypeNames{
public const BLEACH = "minecraft:bleach";
public const BLUE_BUNDLE = "minecraft:blue_bundle";
public const BLUE_DYE = "minecraft:blue_dye";
public const BLUE_EGG = "minecraft:blue_egg";
public const BLUE_HARNESS = "minecraft:blue_harness";
public const BOARD = "minecraft:board";
public const BOAT = "minecraft:boat";
public const BOGGED_SPAWN_EGG = "minecraft:bogged_spawn_egg";
@ -93,6 +96,8 @@ final class ItemTypeNames{
public const BRICK = "minecraft:brick";
public const BROWN_BUNDLE = "minecraft:brown_bundle";
public const BROWN_DYE = "minecraft:brown_dye";
public const BROWN_EGG = "minecraft:brown_egg";
public const BROWN_HARNESS = "minecraft:brown_harness";
public const BRUSH = "minecraft:brush";
public const BUCKET = "minecraft:bucket";
public const BUNDLE = "minecraft:bundle";
@ -148,8 +153,19 @@ final class ItemTypeNames{
public const COOKED_RABBIT = "minecraft:cooked_rabbit";
public const COOKED_SALMON = "minecraft:cooked_salmon";
public const COOKIE = "minecraft:cookie";
public const COPPER_AXE = "minecraft:copper_axe";
public const COPPER_BOOTS = "minecraft:copper_boots";
public const COPPER_CHESTPLATE = "minecraft:copper_chestplate";
public const COPPER_DOOR = "minecraft:copper_door";
public const COPPER_GOLEM_SPAWN_EGG = "minecraft:copper_golem_spawn_egg";
public const COPPER_HELMET = "minecraft:copper_helmet";
public const COPPER_HOE = "minecraft:copper_hoe";
public const COPPER_INGOT = "minecraft:copper_ingot";
public const COPPER_LEGGINGS = "minecraft:copper_leggings";
public const COPPER_NUGGET = "minecraft:copper_nugget";
public const COPPER_PICKAXE = "minecraft:copper_pickaxe";
public const COPPER_SHOVEL = "minecraft:copper_shovel";
public const COPPER_SWORD = "minecraft:copper_sword";
public const CORAL = "minecraft:coral";
public const CORAL_BLOCK = "minecraft:coral_block";
public const CORAL_FAN = "minecraft:coral_fan";
@ -164,6 +180,7 @@ final class ItemTypeNames{
public const CROSSBOW = "minecraft:crossbow";
public const CYAN_BUNDLE = "minecraft:cyan_bundle";
public const CYAN_DYE = "minecraft:cyan_dye";
public const CYAN_HARNESS = "minecraft:cyan_harness";
public const DANGER_POTTERY_SHERD = "minecraft:danger_pottery_sherd";
public const DARK_OAK_BOAT = "minecraft:dark_oak_boat";
public const DARK_OAK_CHEST_BOAT = "minecraft:dark_oak_chest_boat";
@ -263,12 +280,15 @@ final class ItemTypeNames{
public const GOLDEN_SWORD = "minecraft:golden_sword";
public const GRAY_BUNDLE = "minecraft:gray_bundle";
public const GRAY_DYE = "minecraft:gray_dye";
public const GRAY_HARNESS = "minecraft:gray_harness";
public const GREEN_BUNDLE = "minecraft:green_bundle";
public const GREEN_DYE = "minecraft:green_dye";
public const GREEN_HARNESS = "minecraft:green_harness";
public const GUARDIAN_SPAWN_EGG = "minecraft:guardian_spawn_egg";
public const GUNPOWDER = "minecraft:gunpowder";
public const GUSTER_BANNER_PATTERN = "minecraft:guster_banner_pattern";
public const GUSTER_POTTERY_SHERD = "minecraft:guster_pottery_sherd";
public const HAPPY_GHAST_SPAWN_EGG = "minecraft:happy_ghast_spawn_egg";
public const HARD_STAINED_GLASS = "minecraft:hard_stained_glass";
public const HARD_STAINED_GLASS_PANE = "minecraft:hard_stained_glass_pane";
public const HEART_OF_THE_SEA = "minecraft:heart_of_the_sea";
@ -319,10 +339,13 @@ final class ItemTypeNames{
public const LIGHT_BLOCK = "minecraft:light_block";
public const LIGHT_BLUE_BUNDLE = "minecraft:light_blue_bundle";
public const LIGHT_BLUE_DYE = "minecraft:light_blue_dye";
public const LIGHT_BLUE_HARNESS = "minecraft:light_blue_harness";
public const LIGHT_GRAY_BUNDLE = "minecraft:light_gray_bundle";
public const LIGHT_GRAY_DYE = "minecraft:light_gray_dye";
public const LIGHT_GRAY_HARNESS = "minecraft:light_gray_harness";
public const LIME_BUNDLE = "minecraft:lime_bundle";
public const LIME_DYE = "minecraft:lime_dye";
public const LIME_HARNESS = "minecraft:lime_harness";
public const LINGERING_POTION = "minecraft:lingering_potion";
public const LLAMA_SPAWN_EGG = "minecraft:llama_spawn_egg";
public const LODESTONE_COMPASS = "minecraft:lodestone_compass";
@ -331,6 +354,7 @@ final class ItemTypeNames{
public const MACE = "minecraft:mace";
public const MAGENTA_BUNDLE = "minecraft:magenta_bundle";
public const MAGENTA_DYE = "minecraft:magenta_dye";
public const MAGENTA_HARNESS = "minecraft:magenta_harness";
public const MAGMA_CREAM = "minecraft:magma_cream";
public const MAGMA_CUBE_SPAWN_EGG = "minecraft:magma_cube_spawn_egg";
public const MANGROVE_BOAT = "minecraft:mangrove_boat";
@ -359,6 +383,7 @@ final class ItemTypeNames{
public const MUSIC_DISC_CREATOR = "minecraft:music_disc_creator";
public const MUSIC_DISC_CREATOR_MUSIC_BOX = "minecraft:music_disc_creator_music_box";
public const MUSIC_DISC_FAR = "minecraft:music_disc_far";
public const MUSIC_DISC_LAVA_CHICKEN = "minecraft:music_disc_lava_chicken";
public const MUSIC_DISC_MALL = "minecraft:music_disc_mall";
public const MUSIC_DISC_MELLOHI = "minecraft:music_disc_mellohi";
public const MUSIC_DISC_OTHERSIDE = "minecraft:music_disc_otherside";
@ -367,6 +392,7 @@ final class ItemTypeNames{
public const MUSIC_DISC_RELIC = "minecraft:music_disc_relic";
public const MUSIC_DISC_STAL = "minecraft:music_disc_stal";
public const MUSIC_DISC_STRAD = "minecraft:music_disc_strad";
public const MUSIC_DISC_TEARS = "minecraft:music_disc_tears";
public const MUSIC_DISC_WAIT = "minecraft:music_disc_wait";
public const MUSIC_DISC_WARD = "minecraft:music_disc_ward";
public const MUTTON = "minecraft:mutton";
@ -398,6 +424,7 @@ final class ItemTypeNames{
public const OMINOUS_TRIAL_KEY = "minecraft:ominous_trial_key";
public const ORANGE_BUNDLE = "minecraft:orange_bundle";
public const ORANGE_DYE = "minecraft:orange_dye";
public const ORANGE_HARNESS = "minecraft:orange_harness";
public const OXIDIZED_COPPER_DOOR = "minecraft:oxidized_copper_door";
public const PAINTING = "minecraft:painting";
public const PALE_OAK_BOAT = "minecraft:pale_oak_boat";
@ -417,6 +444,7 @@ final class ItemTypeNames{
public const PILLAGER_SPAWN_EGG = "minecraft:pillager_spawn_egg";
public const PINK_BUNDLE = "minecraft:pink_bundle";
public const PINK_DYE = "minecraft:pink_dye";
public const PINK_HARNESS = "minecraft:pink_harness";
public const PITCHER_POD = "minecraft:pitcher_pod";
public const PLANKS = "minecraft:planks";
public const PLENTY_POTTERY_SHERD = "minecraft:plenty_pottery_sherd";
@ -437,6 +465,7 @@ final class ItemTypeNames{
public const PUMPKIN_SEEDS = "minecraft:pumpkin_seeds";
public const PURPLE_BUNDLE = "minecraft:purple_bundle";
public const PURPLE_DYE = "minecraft:purple_dye";
public const PURPLE_HARNESS = "minecraft:purple_harness";
public const QUARTZ = "minecraft:quartz";
public const RABBIT = "minecraft:rabbit";
public const RABBIT_FOOT = "minecraft:rabbit_foot";
@ -453,6 +482,7 @@ final class ItemTypeNames{
public const RED_BUNDLE = "minecraft:red_bundle";
public const RED_DYE = "minecraft:red_dye";
public const RED_FLOWER = "minecraft:red_flower";
public const RED_HARNESS = "minecraft:red_harness";
public const REDSTONE = "minecraft:redstone";
public const REPEATER = "minecraft:repeater";
public const RESIN_BRICK = "minecraft:resin_brick";
@ -561,6 +591,7 @@ final class ItemTypeNames{
public const WHEAT_SEEDS = "minecraft:wheat_seeds";
public const WHITE_BUNDLE = "minecraft:white_bundle";
public const WHITE_DYE = "minecraft:white_dye";
public const WHITE_HARNESS = "minecraft:white_harness";
public const WILD_ARMOR_TRIM_SMITHING_TEMPLATE = "minecraft:wild_armor_trim_smithing_template";
public const WIND_CHARGE = "minecraft:wind_charge";
public const WITCH_SPAWN_EGG = "minecraft:witch_spawn_egg";
@ -581,6 +612,7 @@ final class ItemTypeNames{
public const WRITTEN_BOOK = "minecraft:written_book";
public const YELLOW_BUNDLE = "minecraft:yellow_bundle";
public const YELLOW_DYE = "minecraft:yellow_dye";
public const YELLOW_HARNESS = "minecraft:yellow_harness";
public const ZOGLIN_SPAWN_EGG = "minecraft:zoglin_spawn_egg";
public const ZOMBIE_HORSE_SPAWN_EGG = "minecraft:zombie_horse_spawn_egg";
public const ZOMBIE_PIGMAN_SPAWN_EGG = "minecraft:zombie_pigman_spawn_egg";

View File

@ -31,6 +31,7 @@ use pocketmine\block\Water;
use pocketmine\entity\animation\Animation;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityDespawnEvent;
use pocketmine\event\entity\EntityExtinguishEvent;
use pocketmine\event\entity\EntityMotionEvent;
use pocketmine\event\entity\EntityRegainHealthEvent;
use pocketmine\event\entity\EntitySpawnEvent;
@ -60,6 +61,7 @@ use pocketmine\player\Player;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\timings\TimingsHandler;
use pocketmine\utils\Limits;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use pocketmine\world\format\Chunk;
@ -76,6 +78,7 @@ use function floatval;
use function floor;
use function fmod;
use function get_class;
use function min;
use function sin;
use function spl_object_id;
use const M_PI_2;
@ -700,16 +703,26 @@ abstract class Entity{
* @throws \InvalidArgumentException
*/
public function setFireTicks(int $fireTicks) : void{
if($fireTicks < 0 || $fireTicks > 0x7fff){
throw new \InvalidArgumentException("Fire ticks must be in range 0 ... " . 0x7fff . ", got $fireTicks");
if($fireTicks < 0){
throw new \InvalidArgumentException("Fire ticks cannot be negative");
}
//Since the max value is not externally obvious or intuitive, many plugins use this without being aware that
//reasonably large values are not accepted. We even have such usages within PM itself. It doesn't make sense
//to force all those calls to be aware of this limitation, as it's not a functional limit but a limitation of
//the Mojang save format. Truncating this to the max acceptable value is the next best thing we can do.
$fireTicks = min($fireTicks, Limits::INT16_MAX);
if(!$this->isFireProof()){
$this->fireTicks = $fireTicks;
$this->networkPropertiesDirty = true;
}
}
public function extinguish() : void{
public function extinguish(int $cause = EntityExtinguishEvent::CAUSE_CUSTOM) : void{
$ev = new EntityExtinguishEvent($this, $cause);
$ev->call();
$this->fireTicks = 0;
$this->networkPropertiesDirty = true;
}
@ -720,7 +733,7 @@ abstract class Entity{
protected function doOnFireTick(int $tickDiff = 1) : bool{
if($this->isFireProof() && $this->isOnFire()){
$this->extinguish();
$this->extinguish(EntityExtinguishEvent::CAUSE_FIRE_PROOF);
return false;
}
@ -731,7 +744,7 @@ abstract class Entity{
}
if(!$this->isOnFire()){
$this->extinguish();
$this->extinguish(EntityExtinguishEvent::CAUSE_TICKING);
}else{
return true;
}
@ -1178,12 +1191,14 @@ abstract class Entity{
$moveBB->offset(0, 0, $dz);
if($this->stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){
$stepHeight = $this->getStepHeight();
if($stepHeight > 0 && $fallingFlag && ($wantedX !== $dx || $wantedZ !== $dz)){
$cx = $dx;
$cy = $dy;
$cz = $dz;
$dx = $wantedX;
$dy = $this->stepHeight;
$dy = $stepHeight;
$dz = $wantedZ;
$stepBB = clone $this->boundingBox;
@ -1253,6 +1268,14 @@ abstract class Entity{
Timings::$entityMove->stopTiming();
}
public function setStepHeight(float $stepHeight) : void{
$this->stepHeight = $stepHeight;
}
public function getStepHeight() : float{
return $this->stepHeight;
}
protected function checkGroundState(float $wantedX, float $wantedY, float $wantedZ, float $dx, float $dy, float $dz) : void{
$this->isCollidedVertically = $wantedY !== $dy;
$this->isCollidedHorizontally = ($wantedX !== $dx || $wantedZ !== $dz);

View File

@ -219,6 +219,13 @@ final class EntityFactory{
$this->saveNames[$className] = reset($saveNames);
}
/**
* @phpstan-param class-string<Entity> $class
*/
public function isRegistered(string $class) : bool{
return isset($this->saveNames[$class]);
}
/**
* Creates an entity from data stored on a chunk.
*

View File

@ -38,6 +38,7 @@ use pocketmine\event\entity\EntityDamageByChildEntityEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityDeathEvent;
use pocketmine\event\entity\EntityFrostWalkerEvent;
use pocketmine\inventory\ArmorInventory;
use pocketmine\inventory\CallbackInventoryListener;
use pocketmine\inventory\Inventory;
@ -721,19 +722,30 @@ abstract class Living extends Entity{
$y = $this->location->getFloorY() - 1;
$baseZ = $this->location->getFloorZ();
$frostedIce = VanillaBlocks::FROSTED_ICE();
$liquid = VanillaBlocks::WATER();
$targetBlock = VanillaBlocks::FROSTED_ICE();
if(EntityFrostWalkerEvent::hasHandlers()){
$ev = new EntityFrostWalkerEvent($this, $radius, $liquid, $targetBlock);
$ev->call();
if($ev->isCancelled()){
return;
}
$radius = $ev->getRadius();
$liquid = $ev->getLiquid();
$targetBlock = $ev->getTargetBlock();
}
for($x = $baseX - $radius; $x <= $baseX + $radius; $x++){
for($z = $baseZ - $radius; $z <= $baseZ + $radius; $z++){
$block = $world->getBlockAt($x, $y, $z);
if(
!$block instanceof Water ||
!$block->isSource() ||
!$block->isSameState($liquid) ||
$world->getBlockAt($x, $y + 1, $z)->getTypeId() !== BlockTypeIds::AIR ||
count($world->getNearbyEntities(AxisAlignedBB::one()->offset($x, $y, $z))) !== 0
){
continue;
}
$world->setBlockAt($x, $y, $z, $frostedIce);
$world->setBlockAt($x, $y, $z, $targetBlock);
}
}
}

View File

@ -129,7 +129,7 @@ class EndCrystal extends Entity implements Explosive{
$ev = new EntityPreExplodeEvent($this, 6);
$ev->call();
if(!$ev->isCancelled()){
$explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this);
$explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this, $ev->getFireChance());
if($ev->isBlockBreaking()){
$explosion->explodeA();
}

View File

@ -121,7 +121,7 @@ class PrimedTNT extends Entity implements Explosive{
$ev->call();
if(!$ev->isCancelled()){
//TODO: deal with underwater TNT (underwater TNT treats water as if it has a blast resistance of 0)
$explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this);
$explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this, $ev->getFireChance());
if($ev->isBlockBreaking()){
$explosion->explodeA();
}

View File

@ -0,0 +1,122 @@
<?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\block;
use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\utils\Utils;
use pocketmine\world\Position;
/**
* Called when a block explodes, after explosion impact has been calculated.
*
* @see BlockPreExplodeEvent
*/
class BlockExplodeEvent extends BlockEvent implements Cancellable{
use CancellableTrait;
/**
* @param Block[] $blocks
* @param Block[] $ignitions
*/
public function __construct(
Block $block,
private Position $position,
private array $blocks,
private float $yield,
private array $ignitions
){
parent::__construct($block);
Utils::checkFloatNotInfOrNaN("yield", $yield);
if($yield < 0.0 || $yield > 100.0){
throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0");
}
}
public function getPosition() : Position{
return $this->position;
}
/**
* Returns the percentage chance of drops from each block destroyed by the explosion.
*
* @return float 0-100
*/
public function getYield() : float{
return $this->yield;
}
/**
* Sets the percentage chance of drops from each block destroyed by the explosion.
*
* @param float $yield 0-100
*/
public function setYield(float $yield) : void{
Utils::checkFloatNotInfOrNaN("yield", $yield);
if($yield < 0.0 || $yield > 100.0){
throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0");
}
$this->yield = $yield;
}
/**
* Returns a list of blocks destroyed by the explosion.
*
* @return Block[]
*/
public function getAffectedBlocks() : array{
return $this->blocks;
}
/**
* Sets the blocks destroyed by the explosion.
*
* @param Block[] $blocks
*/
public function setAffectedBlocks(array $blocks) : void{
Utils::validateArrayValueType($blocks, fn(Block $block) => null);
$this->blocks = $blocks;
}
/**
* Returns a list of affected blocks that will be replaced by fire.
*
* @return Block[]
*/
public function getIgnitions() : array{
return $this->ignitions;
}
/**
* Set the list of blocks that will be replaced by fire.
*
* @param Block[] $ignitions
*/
public function setIgnitions(array $ignitions) : void{
Utils::validateArrayValueType($ignitions, fn(Block $block) => null);
$this->ignitions = $ignitions;
}
}

View File

@ -0,0 +1,129 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\event\block;
use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\player\Player;
use pocketmine\utils\Utils;
use pocketmine\world\Explosion;
/**
* Called when a block wants to explode, before the explosion impact is calculated.
* This allows changing the explosion force, fire chance and whether it will destroy blocks.
*
* @see BlockExplodeEvent
*/
class BlockPreExplodeEvent extends BlockEvent implements Cancellable{
use CancellableTrait;
private bool $blockBreaking = true;
public function __construct(
Block $block,
private float $radius,
private readonly ?Player $player = null,
private float $fireChance = 0.0
){
Utils::checkFloatNotInfOrNaN("radius", $radius);
if($radius <= 0){
throw new \InvalidArgumentException("Explosion radius must be positive");
}
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
}
parent::__construct($block);
}
public function getRadius() : float{
return $this->radius;
}
public function setRadius(float $radius) : void{
Utils::checkFloatNotInfOrNaN("radius", $radius);
if($radius <= 0){
throw new \InvalidArgumentException("Explosion radius must be positive");
}
$this->radius = $radius;
}
public function isBlockBreaking() : bool{
return $this->blockBreaking;
}
public function setBlockBreaking(bool $affectsBlocks) : void{
$this->blockBreaking = $affectsBlocks;
}
/**
* Returns whether the explosion will create a fire.
*/
public function isIncendiary() : bool{
return $this->fireChance > 0;
}
/**
* Sets whether the explosion will create a fire by filling fireChance with default values.
*
* If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0).
*/
public function setIncendiary(bool $incendiary) : void{
if(!$incendiary){
$this->fireChance = 0;
}elseif($this->fireChance <= 0){
$this->fireChance = Explosion::DEFAULT_FIRE_CHANCE;
}
}
/**
* Returns a chance between 0 and 1 of creating a fire.
*/
public function getFireChance() : float{
return $this->fireChance;
}
/**
* Sets a chance between 0 and 1 of creating a fire.
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
*
* @param float $fireChance 0 ... 1
*/
public function setFireChance(float $fireChance) : void{
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
}
$this->fireChance = $fireChance;
}
/**
* Returns the player who triggered the block explosion.
* Returns null if the block was exploded by other means.
*/
public function getPlayer() : ?Player{
return $this->player;
}
}

View File

@ -43,13 +43,15 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
/**
* @param Block[] $blocks
* @param float $yield 0-100
* @param float $yield 0-100
* @param Block[] $ignitions
*/
public function __construct(
Entity $entity,
protected Position $position,
protected array $blocks,
protected float $yield
protected float $yield,
private array $ignitions = []
){
$this->entity = $entity;
if($yield < 0.0 || $yield > 100.0){
@ -98,4 +100,23 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
}
$this->yield = $yield;
}
/**
* Set the list of blocks that will be replaced by fire.
*
* @param Block[] $ignitions
*/
public function setIgnitions(array $ignitions) : void{
Utils::validateArrayValueType($ignitions, fn(Block $block) => null);
$this->ignitions = $ignitions;
}
/**
* Returns a list of affected blocks that will be replaced by fire.
*
* @return Block[]
*/
public function getIgnitions() : array{
return $this->ignitions;
}
}

View File

@ -0,0 +1,53 @@
<?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\entity;
use pocketmine\entity\Entity;
/**
* Called when an entity on fire gets extinguished.
*
* @phpstan-extends EntityEvent<Entity>
*/
class EntityExtinguishEvent extends EntityEvent{
public const CAUSE_CUSTOM = 0;
public const CAUSE_WATER = 1;
public const CAUSE_WATER_CAULDRON = 2;
public const CAUSE_RESPAWN = 3;
public const CAUSE_FIRE_PROOF = 4;
public const CAUSE_TICKING = 5;
public const CAUSE_RAIN = 6;
public const CAUSE_POWDER_SNOW = 7;
public function __construct(
Entity $entity,
private int $cause
){
$this->entity = $entity;
}
public function getCause() : int{
return $this->cause;
}
}

View File

@ -0,0 +1,84 @@
<?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\entity;
use pocketmine\block\Block;
use pocketmine\block\Liquid;
use pocketmine\entity\Living;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
/**
* Called when an entity moves horizontally while wearing boots enchanted with Frost Walker.
*
* @phpstan-extends EntityEvent<Living>
*/
class EntityFrostWalkerEvent extends EntityEvent implements Cancellable{
use CancellableTrait;
public function __construct(
Living $entity,
private int $radius,
private Liquid $liquid,
private Block $targetBlock
){
$this->entity = $entity;
}
public function getRadius() : int{
return $this->radius;
}
public function setRadius(int $radius) : void{
$this->radius = $radius;
}
/**
* Returns the liquid that gets frozen
*/
public function getLiquid() : Liquid{
return $this->liquid;
}
/**
* Sets the liquid that gets frozen
*/
public function setLiquid(Liquid $liquid) : void{
$this->liquid = $liquid;
}
/**
* Returns the block that replaces the liquid
*/
public function getTargetBlock() : Block{
return $this->targetBlock;
}
/**
* Sets the block that replaces the liquid
*/
public function setTargetBlock(Block $targetBlock) : void{
$this->targetBlock = $targetBlock;
}
}

View File

@ -26,6 +26,8 @@ namespace pocketmine\event\entity;
use pocketmine\entity\Entity;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\utils\Utils;
use pocketmine\world\Explosion;
/**
* Called when an entity decides to explode, before the explosion's impact is calculated.
@ -42,11 +44,16 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{
public function __construct(
Entity $entity,
protected float $radius
protected float $radius,
private float $fireChance = 0.0,
){
if($radius <= 0){
throw new \InvalidArgumentException("Explosion radius must be positive");
}
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be between 0 and 1.");
}
$this->entity = $entity;
}
@ -61,6 +68,47 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{
$this->radius = $radius;
}
/**
* Returns whether the explosion will create a fire.
*/
public function isIncendiary() : bool{
return $this->fireChance > 0;
}
/**
* Sets whether the explosion will create a fire by filling fireChance with default values.
*
* If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0).
*/
public function setIncendiary(bool $incendiary) : void{
if(!$incendiary){
$this->fireChance = 0;
}elseif($this->fireChance <= 0){
$this->fireChance = Explosion::DEFAULT_FIRE_CHANCE;
}
}
/**
* Returns a chance between 0 and 1 of creating a fire.
*/
public function getFireChance() : float{
return $this->fireChance;
}
/**
* Sets a chance between 0 and 1 of creating a fire.
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
*
* @param float $fireChance 0 ... 1
*/
public function setFireChance(float $fireChance) : void{
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be between 0 and 1.");
}
$this->fireChance = $fireChance;
}
public function isBlockBreaking() : bool{
return $this->blockBreaking;
}

View File

@ -0,0 +1,56 @@
<?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\player;
use pocketmine\block\Block;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\player\Player;
class PlayerRespawnAnchorUseEvent extends PlayerEvent implements Cancellable{
use CancellableTrait;
public const ACTION_EXPLODE = 0;
public const ACTION_SET_SPAWN = 1;
public function __construct(
Player $player,
protected Block $block,
private int $action = self::ACTION_EXPLODE
){
$this->player = $player;
}
public function getBlock() : Block{
return $this->block;
}
public function getAction() : int{
return $this->action;
}
public function setAction(int $action) : void{
$this->action = $action;
}
}

View File

@ -256,7 +256,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$slotItem->setCount($slotItem->getCount() + $amount);
$this->setItem($i, $slotItem);
if($newItem->getCount() <= 0){
break;
return $newItem;
}
}
}
@ -270,7 +270,7 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{
$slotItem->setCount($amount);
$this->setItem($slotIndex, $slotItem);
if($newItem->getCount() <= 0){
break;
return $newItem;
}
}
}

View File

@ -144,8 +144,9 @@ class InventoryTransaction{
$needItems = [];
$haveItems = [];
foreach($this->actions as $key => $action){
if(!$action->getTargetItem()->isNull()){
$needItems[] = $action->getTargetItem();
$targetItem = $action->getTargetItem();
if(!$targetItem->isNull()){
$needItems[] = $targetItem;
}
try{
@ -154,8 +155,9 @@ class InventoryTransaction{
throw new TransactionValidationException(get_class($action) . "#" . spl_object_id($action) . ": " . $e->getMessage(), 0, $e);
}
if(!$action->getSourceItem()->isNull()){
$haveItems[] = $action->getSourceItem();
$sourceItem = $action->getSourceItem();
if(!$sourceItem->isNull()){
$haveItems[] = $sourceItem;
}
}

View File

@ -993,6 +993,7 @@ final class StringToItemParser extends StringToTParser{
$result->registerBlock("resin_brick_wall", fn() => Blocks::RESIN_BRICK_WALL());
$result->registerBlock("resin_bricks", fn() => Blocks::RESIN_BRICKS());
$result->registerBlock("resin_clump", fn() => Blocks::RESIN_CLUMP());
$result->registerBlock("respawn_anchor", fn() => Blocks::RESPAWN_ANCHOR());
$result->registerBlock("rooted_dirt", fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED));
$result->registerBlock("rose", fn() => Blocks::POPPY());
$result->registerBlock("rose_bush", fn() => Blocks::ROSE_BUSH());

View File

@ -28,6 +28,7 @@ use pocketmine\item\enchantment\ItemEnchantmentTags as Tags;
use pocketmine\item\enchantment\VanillaEnchantments as Enchantments;
use pocketmine\item\Item;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use function array_filter;
use function array_values;
use function count;
@ -129,6 +130,7 @@ final class AvailableEnchantmentRegistry{
if(!$this->isRegistered($enchantment)){
throw new \LogicException("Cannot set primary item tags for non-registered enchantment");
}
Utils::validateArrayValueType($tags, fn(string $v) => 1);
$this->primaryItemTags[spl_object_id($enchantment)] = array_values($tags);
}
@ -152,6 +154,7 @@ final class AvailableEnchantmentRegistry{
if(!$this->isRegistered($enchantment)){
throw new \LogicException("Cannot set secondary item tags for non-registered enchantment");
}
Utils::validateArrayValueType($tags, fn(string $v) => 1);
$this->secondaryItemTags[spl_object_id($enchantment)] = array_values($tags);
}

View File

@ -3035,6 +3035,14 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::TILE_BED_TOOFAR, []);
}
public static function tile_respawn_anchor_notValid() : Translatable{
return new Translatable(KnownTranslationKeys::TILE_RESPAWN_ANCHOR_NOTVALID, []);
}
public static function tile_respawn_anchor_respawnSet() : Translatable{
return new Translatable(KnownTranslationKeys::TILE_RESPAWN_ANCHOR_RESPAWNSET, []);
}
public static function view_distance() : Translatable{
return new Translatable(KnownTranslationKeys::VIEW_DISTANCE, []);
}

View File

@ -658,6 +658,8 @@ final class KnownTranslationKeys{
public const TILE_BED_NOSLEEP = "tile.bed.noSleep";
public const TILE_BED_OCCUPIED = "tile.bed.occupied";
public const TILE_BED_TOOFAR = "tile.bed.tooFar";
public const TILE_RESPAWN_ANCHOR_NOTVALID = "tile.respawn_anchor.notValid";
public const TILE_RESPAWN_ANCHOR_RESPAWNSET = "tile.respawn_anchor.respawnSet";
public const VIEW_DISTANCE = "view_distance";
public const WELCOME_TO_POCKETMINE = "welcome_to_pocketmine";
public const WHITELIST_ENABLE = "whitelist_enable";

View File

@ -41,6 +41,7 @@ use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\item\enchantment\EnchantingOption;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\Item;
use pocketmine\network\mcpe\cache\CreativeInventoryCache;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
@ -228,17 +229,25 @@ class InventoryManager{
return null;
}
private function addPredictedSlotChange(Inventory $inventory, int $slot, ItemStack $item) : void{
private function addPredictedSlotChangeInternal(Inventory $inventory, int $slot, ItemStack $item) : void{
$this->inventories[spl_object_id($inventory)]->predictions[$slot] = $item;
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
public function addPredictedSlotChange(Inventory $inventory, int $slot, Item $item) : void{
$typeConverter = $this->session->getTypeConverter();
$itemStack = $typeConverter->coreItemStackToNet($item);
$this->addPredictedSlotChangeInternal($inventory, $slot, $itemStack);
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
foreach($tx->getActions() as $action){
if($action instanceof SlotChangeAction){
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
$itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem());
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
$this->addPredictedSlotChange(
$action->getInventory(),
$action->getSlot(),
$action->getTargetItem()
);
}
}
}
@ -267,7 +276,7 @@ class InventoryManager{
}
[$inventory, $slot] = $info;
$this->addPredictedSlotChange($inventory, $slot, $action->newItem->getItemStack());
$this->addPredictedSlotChangeInternal($inventory, $slot, $action->newItem->getItemStack());
}
}

View File

@ -113,11 +113,10 @@ use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use pocketmine\world\Position;
use pocketmine\world\World;
use pocketmine\YmlServerProperties as Yml;
use pocketmine\YmlServerProperties;
use function array_map;
use function base64_encode;
use function bin2hex;
@ -127,9 +126,6 @@ use function implode;
use function in_array;
use function is_string;
use function json_encode;
use function max;
use function microtime;
use function min;
use function ord;
use function random_bytes;
use function str_split;
@ -140,7 +136,6 @@ use function substr;
use function time;
use function ucfirst;
use const JSON_THROW_ON_ERROR;
use const PHP_INT_MAX;
class NetworkSession{
private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions arrive separately
@ -149,17 +144,9 @@ class NetworkSession{
private const INCOMING_GAME_PACKETS_PER_TICK = 2;
private const INCOMING_GAME_PACKETS_BUFFER_TICKS = 100;
private const READ_OPS_PER_TICK = 100; //subject to change
private const READ_OPS_BUFFER_TICKS = 200; //subject to change
private PacketRateLimiter $packetBatchLimiter;
private PacketRateLimiter $gamePacketLimiter;
private PacketRateLimiter $readOpsLimiter;
private PacketRateLimiterAction $readOpsLimiterAction;
private bool $readOpsStats;
private \PrefixedLogger $logger;
private ?Player $player = null;
private ?PlayerInfo $info = null;
@ -187,7 +174,7 @@ class NetworkSession{
*/
private array $sendBufferAckPromises = [];
/** @phpstan-var \SplQueue<array{CompressBatchPromise|string, list<PromiseResolver<true>>}> */
/** @phpstan-var \SplQueue<array{CompressBatchPromise|string, list<PromiseResolver<true>>, bool}> */
private \SplQueue $compressedQueue;
private bool $forceAsyncCompression = true;
private bool $enableCompression = false; //disabled until handshake completed
@ -207,28 +194,6 @@ class NetworkSession{
*/
private ObjectSet $disposeHooks;
/**
* @var int[]
* @phpstan-var array<string, int>
*/
private array $readOpsPerPacketMin = [];
/**
* @var int[]
* @phpstan-var array<string, int>
*/
private array $readOpsPerPacketMax = [];
/**
* @var int[]
* @phpstan-var array<string, int>
*/
private array $readOpsPerPacketTotal = [];
/**
* @var int[]
* @phpstan-var array<string, int>
*/
private array $receivedPacketCounts = [];
private int $totalReadOpsUsed = 0;
public function __construct(
private Server $server,
private NetworkSessionManager $manager,
@ -251,15 +216,6 @@ class NetworkSession{
$this->packetBatchLimiter = new PacketRateLimiter("Packet Batches", self::INCOMING_PACKET_BATCH_PER_TICK, self::INCOMING_PACKET_BATCH_BUFFER_TICKS);
$this->gamePacketLimiter = new PacketRateLimiter("Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
$serverConfigGroup = $this->server->getConfigGroup();
$this->readOpsLimiter = new PacketRateLimiter(
"Packet Read Operations",
$serverConfigGroup->getPropertyInt(Yml::NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_PER_TICK, self::READ_OPS_PER_TICK),
$serverConfigGroup->getPropertyInt(Yml::NETWORK_PACKET_READ_OPS_LIMIT_SESSION_BUDGET_TICKS, self::READ_OPS_BUFFER_TICKS),
);
$this->readOpsLimiterAction = PacketRateLimiterAction::from($serverConfigGroup->getPropertyString(Yml::NETWORK_PACKET_READ_OPS_LIMIT_DEPLETE_ACTION, PacketRateLimiterAction::WARN->value));
$this->readOpsStats = $serverConfigGroup->getPropertyBool(Yml::NETWORK_PACKET_READ_OPS_LIMIT_COLLECT_STATS, false);
$this->setHandler(new SessionStartPacketHandler(
$this,
$this->onSessionStartSuccess(...)
@ -279,7 +235,7 @@ class NetworkSession{
private function onSessionStartSuccess() : void{
$this->logger->debug("Session start handshake completed, awaiting login packet");
$this->flushSendBuffer(true);
$this->flushGamePacketQueue();
$this->enableCompression = true;
$this->setHandler(new LoginPacketHandler(
$this->server,
@ -494,36 +450,7 @@ class NetworkSession{
try{
$stream = PacketSerializer::decoder($buffer, 0);
try{
if($this->readOpsLimiterAction === PacketRateLimiterAction::NONE){
$packet->decode($stream);
}else{
$this->readOpsLimiter->update();
$stream->setReadOpsLimit(
$this->readOpsLimiterAction === PacketRateLimiterAction::KICK ?
$this->readOpsLimiter->getBudget() :
PHP_INT_MAX //don't bail out of decoding if we're only warning
);
$packet->decode($stream);
$readOps = $stream->getReadOps();
//If we exceeded the budget, a PacketDecodeException has already been thrown if we're in KICK
//mode, so we can assume we're in WARN mode here
if($this->readOpsLimiter->getBudget() < $readOps){
$this->getLogger()->warning("Decoding " . $packet->getName() . " exceeded read ops budget! $readOps > " . $this->readOpsLimiter->getBudget());
$this->readOpsLimiter->reset();
}else{
$this->readOpsLimiter->decrement($readOps);
}
if($this->readOpsStats){
$this->totalReadOpsUsed += $readOps;
$key = $packet->getName();
$this->readOpsPerPacketTotal[$key] = ($this->readOpsPerPacketTotal[$key] ?? 0) + $readOps;
$this->readOpsPerPacketMin[$key] = min($this->readOpsPerPacketMin[$key] ?? PHP_INT_MAX, $readOps);
$this->readOpsPerPacketMax[$key] = max($this->readOpsPerPacketMax[$key] ?? 0, $readOps);
$this->receivedPacketCounts[$key] = ($this->receivedPacketCounts[$key] ?? 0) + 1;
}
}
$packet->decode($stream);
}catch(PacketDecodeException $e){
throw PacketHandlingException::wrap($e);
}
@ -556,30 +483,6 @@ class NetworkSession{
}
}
/**
* @return int[]|float[]
* @phpstan-return array<string, int|float|array<string, int|float>>
*/
public function dumpDecodeCostStats() : array{
if(!$this->readOpsStats){
throw new \LogicException("Not collecting stats for this session");
}
$sessionTime = microtime(true) - $this->connectTime;
$packetDecodeAverages = [];
$packetCostsPerSecondAverages = [];
foreach(Utils::stringifyKeys($this->readOpsPerPacketTotal) as $packet => $total){
$packetDecodeAverages[$packet] = $total / $this->receivedPacketCounts[$packet];
$packetCostsPerSecondAverages[$packet] = $total / $sessionTime;
}
return [
"readOpsAvgPerSecondTotal" => $this->totalReadOpsUsed / $sessionTime,
"readOpsAvgPerPacketPerSecond" => $packetCostsPerSecondAverages,
"readOpsAvgPerPacket" => $packetDecodeAverages,
"readOpsMinPerPacket" => $this->readOpsPerPacketMin,
"readOpsMaxPerPacket" => $this->readOpsPerPacketMax
];
}
public function handleAckReceipt(int $receiptId) : void{
if(!$this->connected){
return;
@ -626,7 +529,7 @@ class NetworkSession{
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket));
}
if($immediate){
$this->flushSendBuffer(true);
$this->flushGamePacketQueue();
}
return true;
@ -674,14 +577,12 @@ class NetworkSession{
$this->sendBuffer[] = $buffer;
}
private function flushSendBuffer(bool $immediate = false) : void{
private function flushGamePacketQueue() : void{
if(count($this->sendBuffer) > 0){
Timings::$playerNetworkSend->startTiming();
try{
$syncMode = null; //automatic
if($immediate){
$syncMode = true;
}elseif($this->forceAsyncCompression){
if($this->forceAsyncCompression){
$syncMode = false;
}
@ -696,7 +597,9 @@ class NetworkSession{
$this->sendBuffer = [];
$ackPromises = $this->sendBufferAckPromises;
$this->sendBufferAckPromises = [];
$this->queueCompressedNoBufferFlush($batch, $immediate, $ackPromises);
//these packets were already potentially buffered for up to 50ms - make sure the transport layer doesn't
//delay them any longer
$this->queueCompressedNoGamePacketFlush($batch, networkFlush: true, ackPromises: $ackPromises);
}finally{
Timings::$playerNetworkSend->stopTiming();
}
@ -716,8 +619,10 @@ class NetworkSession{
public function queueCompressed(CompressBatchPromise|string $payload, bool $immediate = false) : void{
Timings::$playerNetworkSend->startTiming();
try{
$this->flushSendBuffer($immediate); //Maintain ordering if possible
$this->queueCompressedNoBufferFlush($payload, $immediate);
//if the next packet causes a flush, avoid unnecessarily flushing twice
//however, if the next packet does *not* cause a flush, game packets should be flushed to avoid delays
$this->flushGamePacketQueue();
$this->queueCompressedNoGamePacketFlush($payload, $immediate);
}finally{
Timings::$playerNetworkSend->stopTiming();
}
@ -728,22 +633,13 @@ class NetworkSession{
*
* @phpstan-param list<PromiseResolver<true>> $ackPromises
*/
private function queueCompressedNoBufferFlush(CompressBatchPromise|string $batch, bool $immediate = false, array $ackPromises = []) : void{
private function queueCompressedNoGamePacketFlush(CompressBatchPromise|string $batch, bool $networkFlush = false, array $ackPromises = []) : void{
Timings::$playerNetworkSend->startTiming();
try{
$this->compressedQueue->enqueue([$batch, $ackPromises, $networkFlush]);
if(is_string($batch)){
if($immediate){
//Skips all queues
$this->sendEncoded($batch, true, $ackPromises);
}else{
$this->compressedQueue->enqueue([$batch, $ackPromises]);
$this->flushCompressedQueue();
}
}elseif($immediate){
//Skips all queues
$this->sendEncoded($batch->getResult(), true, $ackPromises);
$this->flushCompressedQueue();
}else{
$this->compressedQueue->enqueue([$batch, $ackPromises]);
$batch->onResolve(function() : void{
if($this->connected){
$this->flushCompressedQueue();
@ -760,14 +656,14 @@ class NetworkSession{
try{
while(!$this->compressedQueue->isEmpty()){
/** @var CompressBatchPromise|string $current */
[$current, $ackPromises] = $this->compressedQueue->bottom();
[$current, $ackPromises, $networkFlush] = $this->compressedQueue->bottom();
if(is_string($current)){
$this->compressedQueue->dequeue();
$this->sendEncoded($current, false, $ackPromises);
$this->sendEncoded($current, $networkFlush, $ackPromises);
}elseif($current->hasResult()){
$this->compressedQueue->dequeue();
$this->sendEncoded($current->getResult(), false, $ackPromises);
$this->sendEncoded($current->getResult(), $networkFlush, $ackPromises);
}else{
//can't send any more queued until this one is ready
@ -807,7 +703,7 @@ class NetworkSession{
$this->disconnectGuard = true;
$func();
$this->disconnectGuard = false;
$this->flushSendBuffer(true);
$this->flushGamePacketQueue();
$this->sender->close("");
foreach($this->disposeHooks as $callback){
$callback();
@ -954,7 +850,7 @@ class NetworkSession{
}
$this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO"));
$checkXUID = $this->server->getConfigGroup()->getPropertyBool(Yml::PLAYER_VERIFY_XUID, true);
$checkXUID = $this->server->getConfigGroup()->getPropertyBool(YmlServerProperties::PLAYER_VERIFY_XUID, true);
$myXUID = $this->info instanceof XboxLivePlayerInfo ? $this->info->getXuid() : "";
$kickForXUIDMismatch = function(string $xuid) use ($checkXUID, $myXUID) : bool{
if($checkXUID && $myXUID !== $xuid){
@ -1442,6 +1338,6 @@ class NetworkSession{
Timings::$playerNetworkSendInventorySync->stopTiming();
}
$this->flushSendBuffer();
$this->flushGamePacketQueue();
}
}

View File

@ -62,14 +62,6 @@ final class PacketRateLimiter{
$this->budget -= $amount;
}
public function getBudget() : int{
return $this->budget;
}
public function reset() : void{
$this->budget = $this->maxBudget;
}
public function update() : void{
$nowNs = hrtime(true);
$timeSinceLastUpdateNs = $nowNs - $this->lastUpdateTimeNs;

View File

@ -88,9 +88,13 @@ class ChunkCache implements ChunkListener{
private int $hits = 0;
private int $misses = 0;
/**
* @phpstan-param DimensionIds::* $dimensionId
*/
private function __construct(
private World $world,
private Compressor $compressor
private Compressor $compressor,
private int $dimensionId = DimensionIds::OVERWORLD
){}
private function prepareChunkAsync(int $chunkX, int $chunkZ, int $chunkHash) : CompressBatchPromise{
@ -109,7 +113,7 @@ class ChunkCache implements ChunkListener{
new ChunkRequestTask(
$chunkX,
$chunkZ,
DimensionIds::OVERWORLD, //TODO: not hardcode this
$this->dimensionId,
$chunk,
$promise,
$this->compressor

View File

@ -23,13 +23,22 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\color\Color;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\SavedDataLoadingException;
use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\types\biome\BiomeDefinitionEntry;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use pocketmine\world\biome\model\BiomeDefinitionEntryData;
use function count;
use function get_debug_type;
use function is_array;
use function json_decode;
class StaticPacketCache{
use SingletonTrait;
@ -41,9 +50,61 @@ class StaticPacketCache{
return new CacheableNbt((new NetworkNbtSerializer())->read(Filesystem::fileGetContents($filePath))->mustGetCompoundTag());
}
/**
* @return list<BiomeDefinitionEntry>
*/
private static function loadBiomeDefinitionModel(string $filePath) : array{
$biomeEntries = json_decode(Filesystem::fileGetContents($filePath), associative: true);
if(!is_array($biomeEntries)){
throw new SavedDataLoadingException("$filePath root should be an array, got " . get_debug_type($biomeEntries));
}
$jsonMapper = new \JsonMapper();
$jsonMapper->bExceptionOnMissingData = true;
$jsonMapper->bStrictObjectTypeChecking = true;
$jsonMapper->bEnforceMapType = false;
$entries = [];
foreach(Utils::promoteKeys($biomeEntries) as $biomeName => $entry){
if(!is_array($entry)){
throw new SavedDataLoadingException("$filePath should be an array of objects, got " . get_debug_type($entry));
}
try{
$biomeDefinition = $jsonMapper->map($entry, new BiomeDefinitionEntryData());
$mapWaterColour = $biomeDefinition->mapWaterColour;
$entries[] = new BiomeDefinitionEntry(
(string) $biomeName,
$biomeDefinition->id,
$biomeDefinition->temperature,
$biomeDefinition->downfall,
$biomeDefinition->redSporeDensity,
$biomeDefinition->blueSporeDensity,
$biomeDefinition->ashDensity,
$biomeDefinition->whiteAshDensity,
$biomeDefinition->depth,
$biomeDefinition->scale,
new Color(
$mapWaterColour->r,
$mapWaterColour->g,
$mapWaterColour->b,
$mapWaterColour->a
),
$biomeDefinition->rain,
count($biomeDefinition->tags) > 0 ? $biomeDefinition->tags : null,
);
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
}
return $entries;
}
private static function make() : self{
return new self(
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(BedrockDataFiles::BIOME_DEFINITIONS_NBT)),
BiomeDefinitionListPacket::fromDefinitions(self::loadBiomeDefinitionModel(BedrockDataFiles::BIOME_DEFINITIONS_JSON)),
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(BedrockDataFiles::ENTITY_IDENTIFIERS_NBT))
);
}

View File

@ -64,7 +64,6 @@ use pocketmine\network\mcpe\protocol\ItemStackResponsePacket;
use pocketmine\network\mcpe\protocol\LabTablePacket;
use pocketmine\network\mcpe\protocol\LecternUpdatePacket;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacketV1;
use pocketmine\network\mcpe\protocol\MapInfoRequestPacket;
use pocketmine\network\mcpe\protocol\MobArmorEquipmentPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
@ -74,7 +73,6 @@ use pocketmine\network\mcpe\protocol\NetworkStackLatencyPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
use pocketmine\network\mcpe\protocol\PlayerHotbarPacket;
use pocketmine\network\mcpe\protocol\PlayerInputPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
use pocketmine\network\mcpe\protocol\serializer\BitSet;
@ -138,6 +136,8 @@ class InGamePacketHandler extends PacketHandler{
protected ?float $lastPlayerAuthInputPitch = null;
protected ?BitSet $lastPlayerAuthInputFlags = null;
protected ?BlockPosition $lastBlockAttacked = null;
public bool $forceMoveSync = false;
protected ?string $lastRequestedFullSkinId = null;
@ -213,7 +213,7 @@ class InGamePacketHandler extends PacketHandler{
}
$inputFlags = $packet->getInputFlags();
if($inputFlags !== $this->lastPlayerAuthInputFlags){
if($this->lastPlayerAuthInputFlags === null || !$inputFlags->equals($this->lastPlayerAuthInputFlags)){
$this->lastPlayerAuthInputFlags = $inputFlags;
$sneaking = $inputFlags->get(PlayerAuthInputFlags::SNEAKING);
@ -250,6 +250,28 @@ class InGamePacketHandler extends PacketHandler{
$packetHandled = true;
$useItemTransaction = $packet->getItemInteractionData();
if($useItemTransaction !== null){
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
throw new PacketHandlingException("Too many actions in item use transaction");
}
$this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId());
$this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
$packetHandled = false;
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
}else{
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
}
$this->inventoryManager->setCurrentItemStackRequestId(null);
}
$itemStackRequest = $packet->getItemStackRequest();
$itemStackResponseBuilder = $itemStackRequest !== null ? $this->handleSingleItemStackRequest($itemStackRequest) : null;
//itemstack request or transaction may set predictions for the outcome of these actions, so these need to be
//processed last
$blockActions = $packet->getBlockActions();
if($blockActions !== null){
if(count($blockActions) > 100){
@ -270,36 +292,14 @@ class InGamePacketHandler extends PacketHandler{
}
}
$useItemTransaction = $packet->getItemInteractionData();
if($useItemTransaction !== null){
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
throw new PacketHandlingException("Too many actions in item use transaction");
}
$this->inventoryManager->setCurrentItemStackRequestId($useItemTransaction->getRequestId());
$this->inventoryManager->addRawPredictedSlotChanges($useItemTransaction->getTransactionData()->getActions());
if(!$this->handleUseItemTransaction($useItemTransaction->getTransactionData())){
$packetHandled = false;
$this->session->getLogger()->debug("Unhandled transaction in PlayerAuthInputPacket (type " . $useItemTransaction->getTransactionData()->getActionType() . ")");
}else{
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
}
$this->inventoryManager->setCurrentItemStackRequestId(null);
}
$itemStackRequest = $packet->getItemStackRequest();
if($itemStackRequest !== null){
$result = $this->handleSingleItemStackRequest($itemStackRequest);
$this->session->sendDataPacket(ItemStackResponsePacket::create([$result]));
$itemStackResponse = $itemStackResponseBuilder?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $itemStackRequest->getRequestId());
$this->session->sendDataPacket(ItemStackResponsePacket::create([$itemStackResponse]));
}
return $packetHandled;
}
public function handleLevelSoundEventPacketV1(LevelSoundEventPacketV1 $packet) : bool{
return true; //useless leftover from 1.8
}
public function handleActorEvent(ActorEventPacket $packet) : bool{
if($packet->actorRuntimeId !== $this->player->getId()){
//TODO HACK: EATING_ITEM is sent back to the server when the server sends it for other players (1.14 bug, maybe earlier)
@ -504,13 +504,6 @@ class InGamePacketHandler extends PacketHandler{
//if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
return true;
case UseItemTransactionData::ACTION_BREAK_BLOCK:
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
if(!$this->player->breakBlock($vBlockPos)){
$this->syncBlocksNearby($vBlockPos, null);
}
return true;
case UseItemTransactionData::ACTION_CLICK_AIR:
if($this->player->isUsingItem()){
if(!$this->player->consumeHeldItem()){
@ -586,7 +579,7 @@ class InGamePacketHandler extends PacketHandler{
return false;
}
private function handleSingleItemStackRequest(ItemStackRequest $request) : ItemStackResponse{
private function handleSingleItemStackRequest(ItemStackRequest $request) : ?ItemStackResponseBuilder{
if(count($request->getActions()) > 60){
//recipe book auto crafting can affect all slots of the inventory when consuming inputs or producing outputs
//this means there could be as many as 50 CraftingConsumeInput actions or Place (taking the result) actions
@ -603,7 +596,11 @@ class InGamePacketHandler extends PacketHandler{
$executor = new ItemStackRequestExecutor($this->player, $this->inventoryManager, $request);
try{
$transaction = $executor->generateInventoryTransaction();
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
if($transaction !== null){
$result = $this->executeInventoryTransaction($transaction, $request->getRequestId());
}else{
$result = true; //predictions only, just send responses
}
}catch(ItemStackRequestProcessException $e){
$result = false;
$this->session->getLogger()->debug("ItemStackRequest #" . $request->getRequestId() . " failed: " . $e->getMessage());
@ -611,10 +608,7 @@ class InGamePacketHandler extends PacketHandler{
$this->inventoryManager->requestSyncAll();
}
if(!$result){
return new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
}
return $executor->buildItemStackResponse();
return $result ? $executor->getItemStackResponseBuilder() : null;
}
public function handleItemStackRequest(ItemStackRequestPacket $packet) : bool{
@ -624,7 +618,7 @@ class InGamePacketHandler extends PacketHandler{
throw new PacketHandlingException("Too many requests in ItemStackRequestPacket");
}
foreach($packet->getRequests() as $request){
$responses[] = $this->handleSingleItemStackRequest($request);
$responses[] = $this->handleSingleItemStackRequest($request)?->build() ?? new ItemStackResponse(ItemStackResponse::RESULT_ERROR, $request->getRequestId());
}
$this->session->sendDataPacket(ItemStackResponsePacket::create($responses));
@ -687,16 +681,27 @@ class InGamePacketHandler extends PacketHandler{
switch($action){
case PlayerAction::START_BREAK:
case PlayerAction::CONTINUE_DESTROY_BLOCK: //destroy the next block while holding down left click
self::validateFacing($face);
if($this->lastBlockAttacked !== null && $blockPosition->equals($this->lastBlockAttacked)){
//the client will send CONTINUE_DESTROY_BLOCK for the currently targeted block directly before it
//sends PREDICT_DESTROY_BLOCK, but also when it starts to break the block
//this seems like a bug in the client and would cause spurious left-click events if we allowed it to
//be delivered to the player
$this->session->getLogger()->debug("Ignoring PlayerAction $action on $pos because we were already destroying this block");
break;
}
if(!$this->player->attackBlock($pos, $face)){
$this->syncBlocksNearby($pos, $face);
}
$this->lastBlockAttacked = $blockPosition;
break;
case PlayerAction::ABORT_BREAK:
case PlayerAction::STOP_BREAK:
$this->player->stopBreakBlock($pos);
$this->lastBlockAttacked = null;
break;
case PlayerAction::START_SLEEPING:
//unused
@ -707,11 +712,17 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::CRACK_BREAK:
self::validateFacing($face);
$this->player->continueBreakBlock($pos, $face);
$this->lastBlockAttacked = $blockPosition;
break;
case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now)
break;
case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//TODO: do we need to handle this?
case PlayerAction::PREDICT_DESTROY_BLOCK:
if(!$this->player->breakBlock($pos)){
$this->syncBlocksNearby($pos, $face);
}
$this->lastBlockAttacked = null;
break;
case PlayerAction::START_ITEM_USE_ON:
case PlayerAction::STOP_ITEM_USE_ON:
@ -786,10 +797,6 @@ class InGamePacketHandler extends PacketHandler{
return false;
}
public function handlePlayerInput(PlayerInputPacket $packet) : bool{
return false; //TODO
}
public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{
$gameMode = $this->session->getTypeConverter()->protocolGameModeToCore($packet->gamemode);
if($gameMode !== $this->player->getGamemode()){

View File

@ -33,9 +33,11 @@ use pocketmine\inventory\transaction\EnchantingTransaction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\inventory\transaction\TransactionBuilder;
use pocketmine\inventory\transaction\TransactionBuilderInventory;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
use pocketmine\network\mcpe\protocol\types\inventory\FullContainerName;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingCreateSpecificResultStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction;
@ -47,6 +49,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DropStackReque
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequestSlotInfo;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\MineBlockStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\PlaceStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\SwapStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\TakeStackRequestAction;
@ -362,6 +365,16 @@ class ItemStackRequestExecutor{
$this->setNextCreatedItem($nextResultItem);
}elseif($action instanceof DeprecatedCraftingResultsStackRequestAction){
//no obvious use
}elseif($action instanceof MineBlockStackRequestAction){
$slot = $action->getHotbarSlot();
$this->requestSlotInfos[] = new ItemStackRequestSlotInfo(new FullContainerName(ContainerUIIds::HOTBAR), $slot, $action->getStackId());
$inventory = $this->player->getInventory();
$usedItem = $inventory->slotExists($slot) ? $inventory->getItem($slot) : null;
$predictedDamage = $action->getPredictedDurability();
if($usedItem instanceof Durable && $predictedDamage >= 0 && $predictedDamage <= $usedItem->getMaxDurability()){
$usedItem->setDamage($predictedDamage);
$this->inventoryManager->addPredictedSlotChange($inventory, $slot, $usedItem);
}
}else{
throw new ItemStackRequestProcessException("Unhandled item stack request action");
}
@ -370,7 +383,7 @@ class ItemStackRequestExecutor{
/**
* @throws ItemStackRequestProcessException
*/
public function generateInventoryTransaction() : InventoryTransaction{
public function generateInventoryTransaction() : ?InventoryTransaction{
foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){
try{
$this->processItemStackRequestAction($action);
@ -380,6 +393,9 @@ class ItemStackRequestExecutor{
}
$this->setNextCreatedItem(null);
$inventoryActions = $this->builder->generateActions();
if(count($inventoryActions) === 0){
return null;
}
$transaction = $this->specialTransaction ?? new InventoryTransaction($this->player);
foreach($inventoryActions as $action){
@ -389,12 +405,16 @@ class ItemStackRequestExecutor{
return $transaction;
}
public function buildItemStackResponse() : ItemStackResponse{
public function getItemStackResponseBuilder() : ItemStackResponseBuilder{
$builder = new ItemStackResponseBuilder($this->request->getRequestId(), $this->inventoryManager);
foreach($this->requestSlotInfos as $requestInfo){
$builder->addSlot($requestInfo->getContainerName()->getContainerId(), $requestInfo->getSlotId());
}
return $builder->build();
return $builder;
}
public function buildItemStackResponse() : ItemStackResponse{
return $this->getItemStackResponseBuilder()->build();
}
}

View File

@ -33,6 +33,8 @@ use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
@ -42,7 +44,11 @@ use pocketmine\player\PlayerInfo;
use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use Ramsey\Uuid\Uuid;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use const JSON_THROW_ON_ERROR;
/**
* Handles the initial login phase of the session. This handler is used as the initial state.
@ -60,7 +66,9 @@ class LoginPacketHandler extends PacketHandler{
){}
public function handleLogin(LoginPacket $packet) : bool{
$extraData = $this->fetchAuthData($packet->chainDataJwt);
$authInfo = $this->parseAuthInfo($packet->authInfoJson);
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
$extraData = $this->fetchAuthData($jwtChain);
if(!Player::isValidUserName($extraData->displayName)){
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
@ -139,11 +147,61 @@ class LoginPacketHandler extends PacketHandler{
return true;
}
$this->processLogin($packet, $ev->isAuthRequired());
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired());
return true;
}
/**
* @throws PacketHandlingException
*/
protected function parseAuthInfo(string $authInfo) : AuthenticationInfo{
try{
$authInfoJson = json_decode($authInfo, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($authInfoJson)){
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
}
/**
* @throws PacketHandlingException
*/
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($jwtChainJson)){
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($jwtChainJson, new JwtChain());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
}
/**
* @throws PacketHandlingException
*/
@ -211,10 +269,15 @@ class LoginPacketHandler extends PacketHandler{
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication.
* In the future this won't be necessary.
*
* @param null|string[] $legacyCertificate
*
* @throws \InvalidArgumentException
*/
protected function processLogin(LoginPacket $packet, bool $authRequired) : void{
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback));
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{
if($legacyCertificate === null){
throw new PacketHandlingException("Legacy certificate cannot be null");
}
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
$this->session->setHandler(null); //drop packets received during login verification
}
}

View File

@ -40,7 +40,6 @@ use pocketmine\network\mcpe\protocol\types\Experiments;
use pocketmine\network\mcpe\protocol\types\LevelSettings;
use pocketmine\network\mcpe\protocol\types\NetworkPermissions;
use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings;
use pocketmine\network\mcpe\protocol\types\ServerAuthMovementMode;
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
use pocketmine\player\Player;
use pocketmine\Server;
@ -82,7 +81,8 @@ class PreSpawnPacketHandler extends PacketHandler{
$levelSettings->lightningLevel = 0;
$levelSettings->commandsEnabled = true;
$levelSettings->gameRules = [
"naturalregeneration" => new BoolGameRule(false, false) //Hack for client side regeneration
"naturalregeneration" => new BoolGameRule(false, false), //Hack for client side regeneration
"locatorbar" => new BoolGameRule(false, false) //Disable client-side tracking of nearby players
];
$levelSettings->experiments = new Experiments([], false);
@ -99,7 +99,7 @@ class PreSpawnPacketHandler extends PacketHandler{
$this->server->getMotd(),
"",
false,
new PlayerMovementSettings(ServerAuthMovementMode::SERVER_AUTHORITATIVE_V2, 0, false),
new PlayerMovementSettings(0, true),
0,
0,
"",
@ -108,6 +108,7 @@ class PreSpawnPacketHandler extends PacketHandler{
Uuid::fromString(Uuid::NIL),
false,
false,
false,
new NetworkPermissions(disableClientSounds: true),
[],
0,

View File

@ -120,7 +120,8 @@ class ResourcePacksPacketHandler extends PacketHandler{
hasAddons: false,
hasScripts: false,
worldTemplateId: Uuid::fromString(Uuid::NIL),
worldTemplateVersion: ""
worldTemplateVersion: "",
forceDisableVibrantVisuals: true,
));
$this->session->getLogger()->debug("Waiting for client to accept resource packs");
}

View File

@ -68,17 +68,17 @@ class RakLibServer extends Thread{
public function startAndWait(int $options = NativeThread::INHERIT_NONE) : void{
$this->start($options);
$this->synchronized(function() : void{
while(!$this->ready && $this->getCrashInfo() === null){
while(!$this->ready && !$this->isTerminated()){
$this->wait();
}
$crashInfo = $this->getCrashInfo();
if($crashInfo !== null){
if($crashInfo->getType() === SocketException::class){
throw new SocketException($crashInfo->getMessage());
}
throw new ThreadCrashException("RakLib failed to start", $crashInfo);
}
});
$crashInfo = $this->getCrashInfo();
if($crashInfo !== null){
if($crashInfo->getType() === SocketException::class){
throw new SocketException($crashInfo->getMessage());
}
throw new ThreadCrashException("RakLib failed to start", $crashInfo);
}
}
protected function onRun() : void{

View File

@ -26,6 +26,7 @@ namespace pocketmine\player;
use pocketmine\block\BaseSign;
use pocketmine\block\Bed;
use pocketmine\block\BlockTypeTags;
use pocketmine\block\RespawnAnchor;
use pocketmine\block\UnknownBlock;
use pocketmine\block\VanillaBlocks;
use pocketmine\command\CommandSender;
@ -45,6 +46,7 @@ use pocketmine\entity\projectile\Arrow;
use pocketmine\entity\Skin;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityExtinguishEvent;
use pocketmine\event\inventory\InventoryCloseEvent;
use pocketmine\event\inventory\InventoryOpenEvent;
use pocketmine\event\player\PlayerBedEnterEvent;
@ -135,6 +137,7 @@ use pocketmine\world\sound\EntityAttackNoDamageSound;
use pocketmine\world\sound\EntityAttackSound;
use pocketmine\world\sound\FireExtinguishSound;
use pocketmine\world\sound\ItemBreakSound;
use pocketmine\world\sound\RespawnAnchorDepleteSound;
use pocketmine\world\sound\Sound;
use pocketmine\world\World;
use pocketmine\YmlServerProperties;
@ -1641,7 +1644,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$newReplica = clone $oldHeldItem;
$newReplica->setCount($newHeldItem->getCount());
if($newReplica instanceof Durable && $newHeldItem instanceof Durable){
$newReplica->setDamage($newHeldItem->getDamage());
$newDamage = $newHeldItem->getDamage();
if($newDamage >= 0 && $newDamage <= $newReplica->getMaxDurability()){
$newReplica->setDamage($newDamage);
}
}
$damagedOrDeducted = $newReplica->equalsExact($newHeldItem);
@ -2537,6 +2543,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$this->logger->debug("Respawn position located, completing respawn");
$ev = new PlayerRespawnEvent($this, $safeSpawn);
$spawnPosition = $ev->getRespawnPosition();
$spawnBlock = $spawnPosition->getWorld()->getBlock($spawnPosition);
if($spawnBlock instanceof RespawnAnchor){
if($spawnBlock->getCharges() > 0){
$spawnPosition->getWorld()->setBlock($spawnPosition, $spawnBlock->setCharges($spawnBlock->getCharges() - 1));
$spawnPosition->getWorld()->addSound($spawnPosition, new RespawnAnchorDepleteSound());
}else{
$defaultSpawn = $this->server->getWorldManager()->getDefaultWorld()?->getSpawnLocation();
if($defaultSpawn !== null){
$this->setSpawn($defaultSpawn);
$ev->setRespawnPosition($defaultSpawn);
$this->sendMessage(KnownTranslationFactory::tile_respawn_anchor_notValid()->prefix(TextFormat::GRAY));
}
}
}
$ev->call();
$realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld());
@ -2546,7 +2567,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->setSneaking(false);
$this->setFlying(false);
$this->extinguish();
$this->extinguish(EntityExtinguishEvent::CAUSE_RESPAWN);
$this->setAirSupplyTicks($this->getMaxAirSupplyTicks());
$this->deadTicks = 0;
$this->noDamageTicks = 60;

View File

@ -25,6 +25,8 @@ namespace pocketmine\player;
use pocketmine\block\Block;
use pocketmine\entity\animation\ArmSwingAnimation;
use pocketmine\entity\effect\VanillaEffects;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelEventPacket;
@ -65,11 +67,29 @@ final class SurvivalBlockBreakHandler{
if(!$this->block->getBreakInfo()->isBreakable()){
return 0.0;
}
//TODO: improve this to take stuff like swimming, ladders, enchanted tools into account, fix wrong tool break time calculations for bad tools (pmmp/PocketMine-MP#211)
$breakTimePerTick = $this->block->getBreakInfo()->getBreakTime($this->player->getInventory()->getItemInHand()) * 20;
if(!$this->player->isOnGround() && !$this->player->isFlying()){
$breakTimePerTick *= 5;
}
if($this->player->isUnderwater() && !$this->player->getArmorInventory()->getHelmet()->hasEnchantment(VanillaEnchantments::AQUA_AFFINITY())){
$breakTimePerTick *= 5;
}
if($breakTimePerTick > 0){
return 1 / $breakTimePerTick;
$progressPerTick = 1 / $breakTimePerTick;
$haste = $this->player->getEffects()->get(VanillaEffects::HASTE());
if($haste !== null){
$hasteLevel = $haste->getEffectLevel();
$progressPerTick *= (1 + 0.2 * $hasteLevel) * (1.2 ** $hasteLevel);
}
$miningFatigue = $this->player->getEffects()->get(VanillaEffects::MINING_FATIGUE());
if($miningFatigue !== null){
$miningFatigueLevel = $miningFatigue->getEffectLevel();
$progressPerTick *= 0.21 ** $miningFatigueLevel;
}
return $progressPerTick;
}
return 1;
}
@ -82,7 +102,10 @@ final class SurvivalBlockBreakHandler{
$newBreakSpeed = $this->calculateBreakProgressPerTick();
if(abs($newBreakSpeed - $this->breakSpeed) > 0.0001){
$this->breakSpeed = $newBreakSpeed;
//TODO: sync with client
$this->player->getWorld()->broadcastPacketToViewers(
$this->blockPos,
LevelEventPacket::create(LevelEvent::BLOCK_BREAK_SPEED, (int) (65535 * $this->breakSpeed), $this->blockPos)
);
}
$this->breakProgress += $this->breakSpeed;

View File

@ -94,7 +94,19 @@ trait CommonThreadPartsTrait{
}
}
public function getCrashInfo() : ?ThreadCrashInfo{ return $this->crashInfo; }
public function getCrashInfo() : ?ThreadCrashInfo{
//TODO: Joining a crashed worker might be a bit sus, but we need to make sure the thread's shutdown
//handler has run before we try to collect the crash info. As of 6.1.1, pmmpthread sets isTerminated=true
//*before* the shutdown handler is invoked, so we might land here before the crash info has been set.
//In the future this should probably be fixed by running the shutdown handlers before setting isTerminated,
//but this workaround should be good enough for now.
//WARNING: Do not call this inside a synchronized block on this thread's context. Because the shutdown handler
//runs in a synchronized block, this will result in a deadlock.
if($this->isTerminated() && !$this->isJoined()){
$this->join();
}
return $this->crashInfo;
}
public function start(int $options = NativeThread::INHERIT_NONE) : bool{
ThreadManager::getInstance()->add($this);

View File

@ -84,6 +84,6 @@ final class ThreadCrashInfo extends ThreadSafe{
public function getThreadName() : string{ return $this->threadName; }
public function makePrettyMessage() : string{
return sprintf("%s: \"%s\" in \"%s\" on line %d", $this->type ?? "Fatal error", $this->message, Filesystem::cleanPath($this->file), $this->line);
return sprintf("%s: \"%s\" in \"%s\" on line %d", $this->type, $this->message, Filesystem::cleanPath($this->file), $this->line);
}
}

View File

@ -25,7 +25,6 @@ namespace pocketmine\utils;
use pocketmine\VersionInfo;
use function array_merge;
use function curl_close;
use function curl_error;
use function curl_exec;
use function curl_getinfo;
@ -217,34 +216,30 @@ class Internet{
CURLOPT_HTTPHEADER => array_merge(["User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0 " . VersionInfo::NAME . "/" . VersionInfo::VERSION()->getFullVersion(true)], $extraHeaders),
CURLOPT_HEADER => true
]);
try{
$raw = curl_exec($ch);
if($raw === false){
throw new InternetException(curl_error($ch));
}
if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set");
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($raw, 0, $headerSize);
$body = substr($raw, $headerSize);
$headers = [];
//TODO: explore if we can set these limits lower
foreach(explode("\r\n\r\n", $rawHeaders, limit: PHP_INT_MAX) as $rawHeaderGroup){
$headerGroup = [];
foreach(explode("\r\n", $rawHeaderGroup, limit: PHP_INT_MAX) as $line){
$nameValue = explode(":", $line, 2);
if(isset($nameValue[1])){
$headerGroup[trim(strtolower($nameValue[0]))] = trim($nameValue[1]);
}
}
$headers[] = $headerGroup;
}
if($onSuccess !== null){
$onSuccess($ch);
}
return new InternetRequestResult($headers, $body, $httpCode);
}finally{
curl_close($ch);
$raw = curl_exec($ch);
if($raw === false){
throw new InternetException(curl_error($ch));
}
if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set");
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawHeaders = substr($raw, 0, $headerSize);
$body = substr($raw, $headerSize);
$headers = [];
//TODO: explore if we can set these limits lower
foreach(explode("\r\n\r\n", $rawHeaders, limit: PHP_INT_MAX) as $rawHeaderGroup){
$headerGroup = [];
foreach(explode("\r\n", $rawHeaderGroup, limit: PHP_INT_MAX) as $line){
$nameValue = explode(":", $line, 2);
if(isset($nameValue[1])){
$headerGroup[trim(strtolower($nameValue[0]))] = trim($nameValue[1]);
}
}
$headers[] = $headerGroup;
}
if($onSuccess !== null){
$onSuccess($ch);
}
return new InternetRequestResult($headers, $body, $httpCode);
}
}

View File

@ -69,6 +69,7 @@ abstract class Terminal{
public static string $COLOR_MATERIAL_DIAMOND = "";
public static string $COLOR_MATERIAL_LAPIS = "";
public static string $COLOR_MATERIAL_AMETHYST = "";
public static string $COLOR_MATERIAL_RESIN = "";
private static ?bool $formattingCodes = null;
@ -131,6 +132,7 @@ abstract class Terminal{
self::$COLOR_MATERIAL_DIAMOND = $color(37);
self::$COLOR_MATERIAL_LAPIS = $color(24);
self::$COLOR_MATERIAL_AMETHYST = $color(98);
self::$COLOR_MATERIAL_RESIN = $color(208);
}
protected static function getEscapeCodes() : void{
@ -174,11 +176,12 @@ abstract class Terminal{
self::$COLOR_MATERIAL_DIAMOND = $colors >= 256 ? $setaf(37) : $setaf(14);
self::$COLOR_MATERIAL_LAPIS = $colors >= 256 ? $setaf(24) : $setaf(12);
self::$COLOR_MATERIAL_AMETHYST = $colors >= 256 ? $setaf(98) : $setaf(13);
self::$COLOR_MATERIAL_RESIN = $colors >= 256 ? $setaf(208) : $setaf(11);
}else{
self::$COLOR_BLACK = self::$COLOR_DARK_GRAY = self::$COLOR_MATERIAL_NETHERITE = $setaf(0);
self::$COLOR_RED = self::$COLOR_DARK_RED = self::$COLOR_MATERIAL_REDSTONE = self::$COLOR_MATERIAL_COPPER = $setaf(1);
self::$COLOR_GREEN = self::$COLOR_DARK_GREEN = self::$COLOR_MATERIAL_EMERALD = $setaf(2);
self::$COLOR_YELLOW = self::$COLOR_GOLD = self::$COLOR_MINECOIN_GOLD = self::$COLOR_MATERIAL_GOLD = $setaf(3);
self::$COLOR_YELLOW = self::$COLOR_GOLD = self::$COLOR_MINECOIN_GOLD = self::$COLOR_MATERIAL_GOLD = self::$COLOR_MATERIAL_RESIN = $setaf(3);
self::$COLOR_BLUE = self::$COLOR_DARK_BLUE = self::$COLOR_MATERIAL_LAPIS = $setaf(4);
self::$COLOR_LIGHT_PURPLE = self::$COLOR_PURPLE = self::$COLOR_MATERIAL_AMETHYST = $setaf(5);
self::$COLOR_AQUA = self::$COLOR_DARK_AQUA = self::$COLOR_MATERIAL_DIAMOND = $setaf(6);
@ -253,6 +256,7 @@ abstract class Terminal{
TextFormat::MATERIAL_DIAMOND => Terminal::$COLOR_MATERIAL_DIAMOND,
TextFormat::MATERIAL_LAPIS => Terminal::$COLOR_MATERIAL_LAPIS,
TextFormat::MATERIAL_AMETHYST => Terminal::$COLOR_MATERIAL_AMETHYST,
TextFormat::MATERIAL_RESIN => Terminal::$COLOR_MATERIAL_RESIN,
default => $token,
};
}

View File

@ -73,6 +73,7 @@ abstract class TextFormat{
public const MATERIAL_DIAMOND = TextFormat::ESCAPE . "s";
public const MATERIAL_LAPIS = TextFormat::ESCAPE . "t";
public const MATERIAL_AMETHYST = TextFormat::ESCAPE . "u";
public const MATERIAL_RESIN = TextFormat::ESCAPE . "v";
public const COLORS = [
self::BLACK => self::BLACK,
@ -102,6 +103,7 @@ abstract class TextFormat{
self::MATERIAL_DIAMOND => self::MATERIAL_DIAMOND,
self::MATERIAL_LAPIS => self::MATERIAL_LAPIS,
self::MATERIAL_AMETHYST => self::MATERIAL_AMETHYST,
self::MATERIAL_RESIN => self::MATERIAL_RESIN,
];
public const OBFUSCATED = TextFormat::ESCAPE . "k";
@ -150,7 +152,7 @@ abstract class TextFormat{
* @return string[]
*/
public static function tokenize(string $string) : array{
$result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-u])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-v])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
if($result === false) throw self::makePcreError();
return $result;
}
@ -164,7 +166,7 @@ abstract class TextFormat{
$string = mb_scrub($string, 'UTF-8');
$string = self::preg_replace("/[\x{E000}-\x{F8FF}]/u", "", $string); //remove unicode private-use-area characters (they might break the console)
if($removeFormat){
$string = str_replace(TextFormat::ESCAPE, "", self::preg_replace("/" . TextFormat::ESCAPE . "[0-9a-u]/u", "", $string));
$string = str_replace(TextFormat::ESCAPE, "", self::preg_replace("/" . TextFormat::ESCAPE . "[0-9a-v]/u", "", $string));
}
return str_replace("\x1b", "", self::preg_replace("/\x1b[\\(\\][[0-9;\\[\\(]+[Bm]/u", "", $string));
}
@ -175,7 +177,7 @@ abstract class TextFormat{
* @param string $placeholder default "&"
*/
public static function colorize(string $string, string $placeholder = "&") : string{
return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-u])/u', TextFormat::ESCAPE . '$1', $string);
return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-v])/u', TextFormat::ESCAPE . '$1', $string);
}
/**
@ -252,6 +254,7 @@ abstract class TextFormat{
TextFormat::MATERIAL_DIAMOND => "color:#2cb9a8",
TextFormat::MATERIAL_LAPIS => "color:#20487a",
TextFormat::MATERIAL_AMETHYST => "color:#9a5cc5",
TextFormat::MATERIAL_RESIN => "color:#fc7812",
TextFormat::BOLD => "font-weight:bold",
TextFormat::ITALIC => "font-style:italic",
default => null

View File

@ -584,7 +584,7 @@ final class Utils{
/**
* @phpstan-template TMemberType
* @phpstan-param array<mixed, TMemberType> $array
* @phpstan-param \Closure(TMemberType) : void $validator
* @phpstan-param \Closure(TMemberType) : mixed $validator
*/
public static function validateArrayValueType(array $array, \Closure $validator) : void{
foreach(Utils::promoteKeys($array) as $k => $v){

View File

@ -26,16 +26,20 @@ namespace pocketmine\world;
use pocketmine\block\Block;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\TNT;
use pocketmine\block\utils\SupportType;
use pocketmine\block\VanillaBlocks;
use pocketmine\entity\Entity;
use pocketmine\event\block\BlockExplodeEvent;
use pocketmine\event\entity\EntityDamageByBlockEvent;
use pocketmine\event\entity\EntityDamageByEntityEvent;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\event\entity\EntityExplodeEvent;
use pocketmine\item\VanillaItems;
use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use pocketmine\world\format\SubChunk;
use pocketmine\world\particle\HugeExplodeSeedParticle;
use pocketmine\world\sound\ExplodeSound;
@ -48,25 +52,36 @@ use function mt_rand;
use function sqrt;
class Explosion{
public const DEFAULT_FIRE_CHANCE = 1.0 / 3.0;
private int $rays = 16;
public World $world;
/** @var Block[] */
/**
* @var Block[]
* @phpstan-var array<int, Block>
*/
public array $affectedBlocks = [];
public float $stepLen = 0.3;
/** @var Block[] */
private array $fireIgnitions = [];
private SubChunkExplorer $subChunkExplorer;
public function __construct(
public Position $source,
public float $radius,
private Entity|Block|null $what = null
private Entity|Block|null $what = null,
private float $fireChance = 0.0
){
if(!$this->source->isValid()){
throw new \InvalidArgumentException("Position does not have a valid world");
}
$this->world = $this->source->getWorld();
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
}
if($radius <= 0){
throw new \InvalidArgumentException("Explosion radius must be greater than 0, got $radius");
}
@ -85,6 +100,7 @@ class Explosion{
$blockFactory = RuntimeBlockStateRegistry::getInstance();
$mRays = $this->rays - 1;
$incendiary = $this->fireChance > 0;
for($i = 0; $i < $this->rays; ++$i){
for($j = 0; $j < $this->rays; ++$j){
for($k = 0; $k < $this->rays; ++$k){
@ -127,7 +143,12 @@ class Explosion{
$_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false);
foreach($_block->getAffectedBlocks() as $_affectedBlock){
$_affectedBlockPos = $_affectedBlock->getPosition();
$this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock;
$posHash = World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z);
$this->affectedBlocks[$posHash] = $_affectedBlock;
if($incendiary && Utils::getRandomFloat() <= $this->fireChance){
$this->fireIgnitions[$posHash] = $_affectedBlock;
}
}
}
}
@ -150,13 +171,32 @@ class Explosion{
$yield = min(100, (1 / $this->radius) * 100);
if($this->what instanceof Entity){
$ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield);
$ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield, $this->fireIgnitions);
$ev->call();
if($ev->isCancelled()){
return false;
}
$yield = $ev->getYield();
$this->affectedBlocks = $ev->getBlockList();
$this->fireIgnitions = $ev->getIgnitions();
}elseif($this->what instanceof Block){
$ev = new BlockExplodeEvent(
$this->what,
$this->source,
$this->affectedBlocks,
$yield,
$this->fireIgnitions,
);
$ev->call();
if($ev->isCancelled()){
return false;
}else{
$yield = $ev->getYield();
$this->affectedBlocks = $ev->getBlockList();
$this->affectedBlocks = $ev->getAffectedBlocks();
$this->fireIgnitions = $ev->getIgnitions();
}
}
@ -198,8 +238,9 @@ class Explosion{
$air = VanillaItems::AIR();
$airBlock = VanillaBlocks::AIR();
$fireBlock = VanillaBlocks::FIRE();
foreach($this->affectedBlocks as $block){
foreach($this->affectedBlocks as $hash => $block){
$pos = $block->getPosition();
if($block instanceof TNT){
$block->ignite(mt_rand(10, 30));
@ -212,7 +253,13 @@ class Explosion{
if(($t = $this->world->getTileAt($pos->x, $pos->y, $pos->z)) !== null){
$t->onBlockDestroyed(); //needed to create drops for inventories
}
$this->world->setBlockAt($pos->x, $pos->y, $pos->z, $airBlock);
$targetBlock =
isset($this->fireIgnitions[$hash]) &&
$block->getSide(Facing::DOWN)->getSupportType(Facing::UP) === SupportType::FULL ?
$fireBlock :
$airBlock;
$this->world->setBlockAt($pos->x, $pos->y, $pos->z, $targetBlock);
}
}
@ -221,4 +268,18 @@ class Explosion{
return true;
}
/**
* Sets a chance between 0 and 1 of creating a fire.
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
*
* @param float $fireChance 0 ... 1
*/
public function setFireChance(float $fireChance) : void{
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
if($fireChance < 0.0 || $fireChance > 1.0){
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
}
$this->fireChance = $fireChance;
}
}

View File

@ -93,9 +93,11 @@ use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\WritableWorldProvider;
use pocketmine\world\format\LightArray;
use pocketmine\world\format\SubChunk;
use pocketmine\world\generator\executor\AsyncGeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutor;
use pocketmine\world\generator\executor\GeneratorExecutorSetupParameters;
use pocketmine\world\generator\executor\SyncGeneratorExecutor;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\generator\GeneratorRegisterTask;
use pocketmine\world\generator\GeneratorUnregisterTask;
use pocketmine\world\generator\PopulationTask;
use pocketmine\world\light\BlockLightUpdate;
use pocketmine\world\light\LightPopulationTask;
@ -336,11 +338,7 @@ class World implements ChunkManager{
*/
private array $chunkPopulationRequestQueueIndex = [];
/**
* @var true[]
* @phpstan-var array<int, true>
*/
private array $generatorRegisteredWorkers = [];
private readonly GeneratorExecutor $generatorExecutor;
private bool $autoSave = true;
@ -360,9 +358,6 @@ class World implements ChunkManager{
private bool $doingTick = false;
/** @phpstan-var class-string<\pocketmine\world\generator\Generator> */
private string $generator;
private bool $unloaded = false;
/**
* @var \Closure[]
@ -375,6 +370,8 @@ class World implements ChunkManager{
private \Logger $logger;
private RuntimeBlockStateRegistry $blockStateRegistry;
/**
* @phpstan-return ChunkPosHash
*/
@ -488,6 +485,7 @@ class World implements ChunkManager{
$this->displayName = $this->provider->getWorldData()->getName();
$this->logger = new \PrefixedLogger($server->getLogger(), "World: $this->displayName");
$this->blockStateRegistry = RuntimeBlockStateRegistry::getInstance();
$this->minY = $this->provider->getWorldMinY();
$this->maxY = $this->provider->getWorldMaxY();
@ -495,7 +493,23 @@ class World implements ChunkManager{
$generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
$generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
$this->generator = $generator->getGeneratorClass();
$executorSetupParameters = new GeneratorExecutorSetupParameters(
worldMinY: $this->minY,
worldMaxY: $this->maxY,
generatorSeed: $this->getSeed(),
generatorClass: $generator->getGeneratorClass(),
generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
);
$this->generatorExecutor = $generator->isFast() ?
new SyncGeneratorExecutor($executorSetupParameters) :
new AsyncGeneratorExecutor(
$this->logger,
$this->workerPool,
$executorSetupParameters,
$this->worldId
);
$this->chunkPopulationRequestQueue = new \SplQueue();
$this->addOnUnloadCallback(function() : void{
$this->logger->debug("Cancelling unfulfilled generation requests");
@ -531,17 +545,6 @@ class World implements ChunkManager{
$this->initRandomTickBlocksFromConfig($cfg);
$this->timings = new WorldTimings($this);
$this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
unset($this->generatorRegisteredWorkers[$workerId]);
}
});
$workerPool = $this->workerPool;
$this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
$workerPool->removeWorkerStartHook($workerStartHook);
});
}
private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
@ -559,7 +562,7 @@ class World implements ChunkManager{
}catch(BlockStateDeserializeException){
continue;
}
$block = RuntimeBlockStateRegistry::getInstance()->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
$block = $this->blockStateRegistry->fromStateId(GlobalBlockStateHandlers::getDeserializer()->deserialize($blockStateData));
}else{
//TODO: we probably ought to log an error here
continue;
@ -570,7 +573,7 @@ class World implements ChunkManager{
}
}
foreach(RuntimeBlockStateRegistry::getInstance()->getAllKnownStates() as $state){
foreach($this->blockStateRegistry->getAllKnownStates() as $state){
$dontTickName = $dontTickBlocks[$state->getTypeId()] ?? null;
if($dontTickName === null && $state->ticksRandomly()){
$this->randomTickBlocks[$state->getStateId()] = true;
@ -582,21 +585,6 @@ class World implements ChunkManager{
return $this->tickRateTime;
}
public function registerGeneratorToWorker(int $worker) : void{
$this->logger->debug("Registering generator on worker $worker");
$this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
$this->generatorRegisteredWorkers[$worker] = true;
}
public function unregisterGenerator() : void{
foreach($this->workerPool->getRunningWorkers() as $i){
if(isset($this->generatorRegisteredWorkers[$i])){
$this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
}
}
$this->generatorRegisteredWorkers = [];
}
public function getServer() : Server{
return $this->server;
}
@ -654,7 +642,7 @@ class World implements ChunkManager{
$this->save();
$this->unregisterGenerator();
$this->generatorExecutor->shutdown();
$this->provider->close();
$this->blockCache = [];
@ -1394,7 +1382,7 @@ class World implements ChunkManager{
$entity->onRandomUpdate();
}
$blockFactory = RuntimeBlockStateRegistry::getInstance();
$blockFactory = $this->blockStateRegistry;
foreach($chunk->getSubChunks() as $Y => $subChunk){
if(!$subChunk->isEmptyFast()){
$k = 0;
@ -1528,24 +1516,48 @@ class World implements ChunkManager{
$collides = [];
$collisionInfo = $this->blockStateRegistry->collisionInfo;
if($targetFirst){
for($z = $minZ; $z <= $maxZ; ++$z){
$zOverflow = $z === $minZ || $z === $maxZ;
for($x = $minX; $x <= $maxX; ++$x){
$zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
for($y = $minY; $y <= $maxY; ++$y){
$block = $this->getBlockAt($x, $y, $z);
if($block->collidesWithBB($bb)){
return [$block];
$overflow = $zxOverflow || $y === $minY || $y === $maxY;
$stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
if($overflow ?
$stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
match ($stateCollisionInfo) {
RuntimeBlockStateRegistry::COLLISION_CUBE => true,
RuntimeBlockStateRegistry::COLLISION_NONE => false,
default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
}
){
return [$this->getBlockAt($x, $y, $z)];
}
}
}
}
}else{
//TODO: duplicated code :( this way is better for performance though
for($z = $minZ; $z <= $maxZ; ++$z){
$zOverflow = $z === $minZ || $z === $maxZ;
for($x = $minX; $x <= $maxX; ++$x){
$zxOverflow = $zOverflow || $x === $minX || $x === $maxX;
for($y = $minY; $y <= $maxY; ++$y){
$block = $this->getBlockAt($x, $y, $z);
if($block->collidesWithBB($bb)){
$collides[] = $block;
$overflow = $zxOverflow || $y === $minY || $y === $maxY;
$stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
if($overflow ?
$stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW && $this->getBlockAt($x, $y, $z)->collidesWithBB($bb) :
match ($stateCollisionInfo) {
RuntimeBlockStateRegistry::COLLISION_CUBE => true,
RuntimeBlockStateRegistry::COLLISION_NONE => false,
default => $this->getBlockAt($x, $y, $z)->collidesWithBB($bb)
}
){
$collides[] = $this->getBlockAt($x, $y, $z);
}
}
}
@ -1555,24 +1567,64 @@ class World implements ChunkManager{
return $collides;
}
/**
* @param int[] $collisionInfo
* @phpstan-param array<int, int> $collisionInfo
*/
private function getBlockCollisionInfo(int $x, int $y, int $z, array $collisionInfo) : int{
if(!$this->isInWorld($x, $y, $z)){
return RuntimeBlockStateRegistry::COLLISION_NONE;
}
$chunk = $this->getChunk($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
if($chunk === null){
return RuntimeBlockStateRegistry::COLLISION_NONE;
}
$stateId = $chunk
->getSubChunk($y >> SubChunk::COORD_BIT_SIZE)
->getBlockStateId(
$x & SubChunk::COORD_MASK,
$y & SubChunk::COORD_MASK,
$z & SubChunk::COORD_MASK
);
return $collisionInfo[$stateId];
}
/**
* Returns a list of all block AABBs which overlap the full block area at the given coordinates.
* This checks a padding of 1 block around the coordinates to account for oversized AABBs of blocks like fences.
* Larger AABBs (>= 2 blocks on any axis) are not accounted for.
*
* @param int[] $collisionInfo
* @phpstan-param array<int, int> $collisionInfo
*
* @return AxisAlignedBB[]
* @phpstan-return list<AxisAlignedBB>
*/
private function getBlockCollisionBoxesForCell(int $x, int $y, int $z) : array{
$block = $this->getBlockAt($x, $y, $z);
$boxes = $block->getCollisionBoxes();
private function getBlockCollisionBoxesForCell(int $x, int $y, int $z, array $collisionInfo) : array{
$stateCollisionInfo = $this->getBlockCollisionInfo($x, $y, $z, $collisionInfo);
$boxes = match($stateCollisionInfo){
RuntimeBlockStateRegistry::COLLISION_NONE => [],
RuntimeBlockStateRegistry::COLLISION_CUBE => [AxisAlignedBB::one()->offset($x, $y, $z)],
default => $this->getBlockAt($x, $y, $z)->getCollisionBoxes()
};
$cellBB = AxisAlignedBB::one()->offset($x, $y, $z);
foreach(Facing::OFFSET as [$dx, $dy, $dz]){
$extraBoxes = $this->getBlockAt($x + $dx, $y + $dy, $z + $dz)->getCollisionBoxes();
foreach($extraBoxes as $extraBox){
if($extraBox->intersectsWith($cellBB)){
$boxes[] = $extraBox;
//overlapping AABBs can't make any difference if this is a cube, so we can save some CPU cycles in this common case
if($stateCollisionInfo !== RuntimeBlockStateRegistry::COLLISION_CUBE){
$cellBB = null;
foreach(Facing::OFFSET as [$dx, $dy, $dz]){
$offsetX = $x + $dx;
$offsetY = $y + $dy;
$offsetZ = $z + $dz;
$stateCollisionInfo = $this->getBlockCollisionInfo($offsetX, $offsetY, $offsetZ, $collisionInfo);
if($stateCollisionInfo === RuntimeBlockStateRegistry::COLLISION_MAY_OVERFLOW){
//avoid allocating this unless it's needed
$cellBB ??= AxisAlignedBB::one()->offset($x, $y, $z);
$extraBoxes = $this->getBlockAt($offsetX, $offsetY, $offsetZ)->getCollisionBoxes();
foreach($extraBoxes as $extraBox){
if($extraBox->intersectsWith($cellBB)){
$boxes[] = $extraBox;
}
}
}
}
}
@ -1594,13 +1646,15 @@ class World implements ChunkManager{
$collides = [];
$collisionInfo = $this->blockStateRegistry->collisionInfo;
for($z = $minZ; $z <= $maxZ; ++$z){
for($x = $minX; $x <= $maxX; ++$x){
$chunkPosHash = World::chunkHash($x >> Chunk::COORD_BIT_SIZE, $z >> Chunk::COORD_BIT_SIZE);
for($y = $minY; $y <= $maxY; ++$y){
$relativeBlockHash = World::chunkBlockHash($x, $y, $z);
$boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z);
$boxes = $this->blockCollisionBoxCache[$chunkPosHash][$relativeBlockHash] ??= $this->getBlockCollisionBoxesForCell($x, $y, $z, $collisionInfo);
foreach($boxes as $blockBB){
if($blockBB->intersectsWith($bb)){
@ -1795,7 +1849,7 @@ class World implements ChunkManager{
return;
}
$blockFactory = RuntimeBlockStateRegistry::getInstance();
$blockFactory = $this->blockStateRegistry;
$this->timings->doBlockSkyLightUpdates->startTiming();
if($this->skyLightUpdate === null){
$this->skyLightUpdate = new SkyLightUpdate(new SubChunkExplorer($this), $blockFactory->lightFilter, $blockFactory->blocksDirectSkyLight);
@ -1914,7 +1968,7 @@ class World implements ChunkManager{
$chunk = $this->chunks[$chunkHash] ?? null;
if($chunk !== null){
$block = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
$block = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($x & Chunk::COORD_MASK, $y, $z & Chunk::COORD_MASK));
}else{
$addToCache = false;
$block = VanillaBlocks::AIR();
@ -1978,6 +2032,15 @@ class World implements ChunkManager{
throw new WorldException("Cannot set a block in un-generated terrain");
}
//TODO: this computes state ID twice (we do it again in writeStateToWorld()). Not great for performance :(
$stateId = $block->getStateId();
if(!$this->blockStateRegistry->hasStateId($stateId)){
throw new \LogicException("Block state ID not known to RuntimeBlockStateRegistry (probably not registered)");
}
if(!GlobalBlockStateHandlers::getSerializer()->isRegistered($block)){
throw new \LogicException("Block not registered with GlobalBlockStateHandlers serializer");
}
$this->timings->setBlock->startTiming();
$this->unlockChunk($chunkX, $chunkZ, null);
@ -2573,7 +2636,7 @@ class World implements ChunkManager{
$localY = $tilePosition->getFloorY();
$localZ = $tilePosition->getFloorZ() & Chunk::COORD_MASK;
$newBlock = RuntimeBlockStateRegistry::getInstance()->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
$newBlock = $this->blockStateRegistry->fromStateId($chunk->getBlockStateId($localX, $localY, $localZ));
$expectedTileClass = $newBlock->getIdInfo()->getTileClass();
if(
$expectedTileClass === null || //new block doesn't expect a tile
@ -2700,6 +2763,11 @@ class World implements ChunkManager{
throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
}
}
if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof Player){
//canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
//later on. Better we just force all entities to have a save ID, even if it might not be needed.
throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
}
$pos = $entity->getPosition()->asVector3();
$this->entitiesByChunk[World::chunkHash($pos->getFloorX() >> Chunk::COORD_BIT_SIZE, $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE)][$entity->getId()] = $entity;
$this->entityLastKnownPositions[$entity->getId()] = $pos;
@ -2801,6 +2869,9 @@ class World implements ChunkManager{
if(!$this->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ())){
throw new \InvalidArgumentException("Tile position is outside the world bounds");
}
if(!TileFactory::getInstance()->isRegistered($tile::class)){
throw new \LogicException("Tile " . $tile::class . " is not registered for a save ID in TileFactory");
}
$chunkX = $pos->getFloorX() >> Chunk::COORD_BIT_SIZE;
$chunkZ = $pos->getFloorZ() >> Chunk::COORD_BIT_SIZE;
@ -2911,6 +2982,8 @@ class World implements ChunkManager{
if(count($chunkData->getEntityNBT()) !== 0){
$this->timings->syncChunkLoadEntities->startTiming();
$entityFactory = EntityFactory::getInstance();
$deletedEntities = [];
foreach($chunkData->getEntityNBT() as $k => $nbt){
try{
$entity = $entityFactory->createFromData($this, $nbt);
@ -2927,18 +3000,23 @@ class World implements ChunkManager{
}elseif($saveIdTag instanceof IntTag){ //legacy MCPE format
$saveId = "legacy(" . $saveIdTag->getValue() . ")";
}
$logger->warning("Deleted unknown entity type $saveId");
$deletedEntities[$saveId] = ($deletedEntities[$saveId] ?? 0) + 1;
}
//TODO: we can't prevent entities getting added to unloaded chunks if they were saved in the wrong place
//here, because entities currently add themselves to the world
}
foreach(Utils::promoteKeys($deletedEntities) as $saveId => $count){
$logger->warning("Deleted unknown entity type $saveId x$count");
}
$this->timings->syncChunkLoadEntities->stopTiming();
}
if(count($chunkData->getTileNBT()) !== 0){
$this->timings->syncChunkLoadTileEntities->startTiming();
$tileFactory = TileFactory::getInstance();
$deletedTiles = [];
foreach($chunkData->getTileNBT() as $k => $nbt){
try{
$tile = $tileFactory->createFromData($this, $nbt);
@ -2948,7 +3026,8 @@ class World implements ChunkManager{
continue;
}
if($tile === null){
$logger->warning("Deleted unknown tile entity type " . $nbt->getString("id", "<unknown>"));
$saveId = $nbt->getString("id", "<unknown>");
$deletedTiles[$saveId] = ($deletedTiles[$saveId] ?? 0) + 1;
continue;
}
@ -2964,6 +3043,10 @@ class World implements ChunkManager{
}
}
foreach(Utils::promoteKeys($deletedTiles) as $saveId => $count){
$logger->warning("Deleted unknown tile entity type $saveId x$count");
}
$this->timings->syncChunkLoadTileEntities->stopTiming();
}
}
@ -3400,8 +3483,8 @@ class World implements ChunkManager{
$centerChunk = $this->loadChunk($chunkX, $chunkZ);
$adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
$task = new PopulationTask(
$this->worldId,
$this->generatorExecutor->populate(
$chunkX,
$chunkZ,
$centerChunk,
@ -3414,15 +3497,6 @@ class World implements ChunkManager{
$this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
}
);
$workerId = $this->workerPool->selectWorker();
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
unset($this->generatorRegisteredWorkers[$workerId]);
}
if(!isset($this->generatorRegisteredWorkers[$workerId])){
$this->registerGeneratorToWorker($workerId);
}
$this->workerPool->submitTaskToWorker($task, $workerId);
return $resolver->getPromise();
}finally{

View File

@ -0,0 +1,69 @@
<?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\biome\model;
/**
* Model for loading biome definition entries data from JSON.
*/
final class BiomeDefinitionEntryData{
/** @required */
public int $id;
/** @required */
public float $temperature;
/** @required */
public float $downfall;
/** @required */
public float $redSporeDensity;
/** @required */
public float $blueSporeDensity;
/** @required */
public float $ashDensity;
/** @required */
public float $whiteAshDensity;
/** @required */
public float $depth;
/** @required */
public float $scale;
/** @required */
public ColorData $mapWaterColour;
/** @required */
public bool $rain;
/**
* @required
* @var string[]
* @phpstan-var list<string>
*/
public array $tags;
}

View File

@ -21,10 +21,21 @@
declare(strict_types=1);
namespace pocketmine\network\mcpe;
namespace pocketmine\world\biome\model;
enum PacketRateLimiterAction : string{
case NONE = "none";
case WARN = "warn";
case KICK = "kick";
/**
* Model for loading color data from JSON.
*/
final class ColorData{
/** @required */
public int $r;
/** @required */
public int $g;
/** @required */
public int $b;
/** @required */
public int $a;
}

View File

@ -134,11 +134,8 @@ class SubChunk{
foreach($this->blockLayers as $layer){
$layer->collectGarbage();
foreach($layer->getPalette() as $p){
if($p !== $this->emptyBlockId){
$cleanedLayers[] = $layer;
continue 2;
}
if($layer->getBitsPerBlock() !== 0 || $layer->get(0, 0, 0) !== $this->emptyBlockId){
$cleanedLayers[] = $layer;
}
}
$this->blockLayers = $cleanedLayers;

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\world\format\io\data;
use pocketmine\data\bedrock\WorldDataVersions;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\CompoundTag;
@ -50,15 +51,9 @@ use function time;
class BedrockWorldData extends BaseNbtWorldData{
public const CURRENT_STORAGE_VERSION = 10;
public const CURRENT_STORAGE_NETWORK_VERSION = 776;
public const CURRENT_CLIENT_VERSION_TARGET = [
1, //major
21, //minor
60, //patch
33, //revision
0 //is beta
];
public const CURRENT_STORAGE_VERSION = WorldDataVersions::STORAGE;
public const CURRENT_STORAGE_NETWORK_VERSION = WorldDataVersions::NETWORK;
public const CURRENT_CLIENT_VERSION_TARGET = WorldDataVersions::LAST_OPENED_IN;
public const GENERATOR_LIMITED = 0;
public const GENERATOR_INFINITE = 1;

View File

@ -27,6 +27,7 @@ use pocketmine\block\Block;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
use pocketmine\data\bedrock\block\convert\UnsupportedBlockStateException;
use pocketmine\data\bedrock\WorldDataVersions;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\NbtDataException;
@ -35,6 +36,7 @@ use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\BaseWorldProvider;
@ -78,8 +80,8 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
protected const ENTRY_FLAT_WORLD_LAYERS = "game_flatworldlayers";
protected const CURRENT_LEVEL_CHUNK_VERSION = ChunkVersion::v1_21_40;
protected const CURRENT_LEVEL_SUBCHUNK_VERSION = SubChunkVersion::PALETTED_MULTI;
protected const CURRENT_LEVEL_CHUNK_VERSION = WorldDataVersions::CHUNK;
protected const CURRENT_LEVEL_SUBCHUNK_VERSION = WorldDataVersions::SUBCHUNK;
private const CAVES_CLIFFS_EXPERIMENTAL_SUBCHUNK_KEY_OFFSET = 4;
@ -203,23 +205,29 @@ class LevelDB extends BaseWorldProvider implements WritableWorldProvider{
$blockStateData = $this->blockDataUpgrader->upgradeBlockStateNbt($blockStateNbt);
}catch(BlockStateDeserializeException $e){
//while not ideal, this is not a fatal error
$blockDecodeErrors[] = "Palette offset $i / Upgrade error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$errorMessage = "Upgrade error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$blockDecodeErrors[$errorMessage][] = $i;
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
continue;
}
try{
$palette[] = $this->blockStateDeserializer->deserialize($blockStateData);
}catch(UnsupportedBlockStateException $e){
$blockDecodeErrors[] = "Palette offset $i / " . $e->getMessage();
$blockDecodeErrors[$e->getMessage()][] = $i;
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}catch(BlockStateDeserializeException $e){
$blockDecodeErrors[] = "Palette offset $i / Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$errorMessage = "Deserialize error: " . $e->getMessage() . ", NBT: " . $blockStateNbt->toString();
$blockDecodeErrors[$errorMessage][] = $i;
$palette[] = $this->blockStateDeserializer->deserialize(GlobalBlockStateHandlers::getUnknownBlockStateData());
}
}
if(count($blockDecodeErrors) > 0){
$logger->error("Errors decoding blocks:\n - " . implode("\n - ", $blockDecodeErrors));
$finalErrors = [];
foreach(Utils::promoteKeys($blockDecodeErrors) as $errorMessage => $paletteOffsets){
$finalErrors[] = "$errorMessage (palette offsets: " . implode(", ", $paletteOffsets) . ")";
}
$logger->error("Errors decoding blocks:\n - " . implode("\n - ", $finalErrors));
}
//TODO: exceptions

View File

@ -50,7 +50,7 @@ final class GeneratorManager{
}catch(InvalidGeneratorOptionsException $e){
return $e;
}
});
}, fast: true);
$this->addGenerator(Normal::class, "normal", fn() => null);
$this->addAlias("normal", "default");
$this->addGenerator(Nether::class, "nether", fn() => null);
@ -62,6 +62,7 @@ final class GeneratorManager{
* @param string $name Alias for this generator type that can be written in configs
* @param \Closure $presetValidator Callback to validate generator options for new worlds
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
* @param bool $fast Whether this generator is fast enough to run without async tasks
*
* @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
*
@ -69,7 +70,7 @@ final class GeneratorManager{
*
* @throws \InvalidArgumentException
*/
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{
Utils::testValidInstance($class, Generator::class);
$name = strtolower($name);
@ -77,7 +78,7 @@ final class GeneratorManager{
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
}
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast);
}
/**

Some files were not shown because too many files have changed in this diff Show More