Compare commits

..

78 Commits

Author SHA1 Message Date
084feaef53 more work on ItemCombine 2025-03-23 17:14:20 +01:00
eaab4b35d2 add unit testing on MaterialRepairRecipe 2025-03-22 23:00:15 +01:00
f346799920 fill in CraftingManager for MaterialRepair 2025-03-22 21:47:20 +01:00
11135c2fd8 remove dead code 2025-03-22 21:22:08 +01:00
b4cb09fe5e continue refactoring, not finished, not tested
This is a bump from my workspace
2025-03-22 20:21:56 +01:00
a1695a52d5 Merge remote-tracking branch 'upstream/feat/anvil' into feat/anvil 2025-03-22 19:24:16 +01:00
51d45bedce Merge branch 'minor-next' into feat/anvil 2025-03-09 01:58:16 +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
7af5eb3da2 crafting: validate array inputs
this makes sure wrong parameters don't show up as core errors, as seen in crash report 12373907
closes #6642
2025-03-09 01:10:12 +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
aad2bce9e4 readme: call out Easy tasks issues 2025-03-09 00:23:59 +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
50a1e59aa4 5.25.3 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/13662905668
2025-03-04 21:05:10 +00:00
e8824a36b9 Merge pull request #6645 from pmmp/explode-limit
5.25.2
2025-03-04 21:03:52 +00:00
b1e63e544f Merge branch 'stable' into explode-limit 2025-03-04 21:00:40 +00:00
9e9f8a4870 Prepare 5.25.2 release 2025-03-04 20:57:47 +00:00
d0d84d4c51 New rule: explode() limit parameter must be set 2025-03-04 20:44:01 +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
e3e0c14275 Bump the github-actions group with 2 updates (#6644) 2025-03-01 10:04:01 +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
3a2d0d77d1 Update composer dependencies 2025-02-26 17:30:20 +00:00
32b98dcbde draft-release: add a warning about bug reporting
too many people just spam on discord and expect that to somehow do something ...
2025-02-26 17:23:27 +00:00
092ea07d51 5.25.2 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/13549549222
2025-02-26 17:14:49 +00:00
706f391068 Release 5.25.1 (#6641) 2025-02-26 17:13:44 +00:00
7c654271a8 Bump phpstan/phpstan in the development-patch-updates group (#6640) 2025-02-25 13:40:11 +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
3df2bdb879 Fixed door facing
this was broken in 1.21.60 update.

should've known better to expect a blockstate upgrade to mean a
blockstate fix...
2025-02-24 01:04:52 +00:00
1fed9f6cb5 BlockBreakInfo: fixed confusing error message 2025-02-23 20:02:27 +00:00
3050af0bc0 ResourcePackManager: validate pack UUIDs
fixes CrashArchive ##12248760
2025-02-23 19:45:38 +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
77be5f8e25 Update PHPStan 2025-02-17 17:51:39 +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
34f801ee3c 5.25.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/13359320328
2025-02-16 22:18:20 +00:00
d96ef21c4d Prepare 5.25.0 release (#6631) 2025-02-16 22:16:47 +00:00
246c1776df InventoryAction: avoid throwaway Item clones 2025-02-16 21:47:35 +00:00
03e4b53ac4 BedrockDataFiles: added constants for folders as well as files
we probably should have it recurse too, but this is an easy win.
2025-02-16 20:57:16 +00:00
91ac64783f Bedrock 1.21.60 (#6627)
Co-authored-by: Dylan K. Taylor <dktapps@pmmp.io>
2025-02-16 20:51:53 +00:00
9402a20ee3 Update Utils::getOS() doc comment
closes #6628
2025-02-16 16:12:29 +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
5ef89200c6 Bump phpstan/phpstan in the development-patch-updates group (#6620) 2025-02-10 22:14:30 +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
39e69276a1 Bump tests/plugins/DevTools from c6dca35 to a030d39 (#6617) 2025-02-03 11:43:03 +00:00
94797e3afa Bump build/php from ae94694 to 1549433 (#6616) 2025-02-03 11:42:37 +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
9d6a0cc738 Update composer dependencies 2025-02-02 19:46:54 +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
07987890a0 Bump the github-actions group with 2 updates (#6613) 2025-02-01 13:38:25 +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
f60120a75e Bump the development-patch-updates group with 3 updates (#6603) 2025-01-27 10:43:28 +00:00
fc86d3a44e 5.24.1 is next
Commit created by: https://github.com/pmmp/RestrictedActions/actions/runs/12916833718
2025-01-22 20:50:42 +00:00
b3f0ed23ca Merge remote-tracking branch 'upstream/minor-next' into feat/anvil 2024-12-14 15:16:39 +01:00
5b9dc2c275 rewrote the system with CraftingManager 2024-12-14 15:15:15 +01:00
947c8a0621 remove phpstan docs 2024-11-18 14:51:53 +01:00
c77a72f15a some work on anvil 2024-11-18 14:31:08 +01:00
b9df798796 made AnvilAction constructor final 2024-08-19 22:30:39 +02:00
7cfb6eea51 first look at anvil actions 2024-08-19 22:27:45 +02:00
1cc809c332 Merge branch 'minor-next' into feat/anvil 2024-08-19 19:04:21 +01:00
b1a773ceb8 Merge branch 'minor-next' into feat/anvil 2024-08-11 00:06:05 +02:00
654b44447e add sound and anvil damage 2024-08-10 23:51:38 +02:00
804731d87f fix PHPstan 2024-08-10 23:33:14 +02:00
726e2cba23 Add anvil event 2024-08-10 23:26:07 +02:00
54f746fc11 finalize anvil transaction 2024-08-10 23:16:54 +02:00
44c3e03598 fix PHPstan 2024-08-10 19:10:10 +02:00
e4f979dadf first look at anvil 2024-08-10 19:04:17 +02:00
88 changed files with 2796 additions and 310 deletions

View File

@ -53,7 +53,7 @@ jobs:
run: echo NAME=$(echo "${GITHUB_REPOSITORY,,}") >> $GITHUB_OUTPUT
- name: Build image for tag
uses: docker/build-push-action@v6.10.0
uses: docker/build-push-action@v6.15.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.10.0
uses: docker/build-push-action@v6.15.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.10.0
uses: docker/build-push-action@v6.15.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.10.0
uses: docker/build-push-action@v6.15.0
with:
push: true
context: ./pocketmine-mp

View File

@ -165,7 +165,7 @@ jobs:
${{ github.workspace }}/core-permissions.rst
- name: Create draft release
uses: ncipollo/release-action@v1.14.0
uses: ncipollo/release-action@v1.16.0
id: create-draft
with:
artifacts: ${{ github.workspace }}/PocketMine-MP.phar,${{ github.workspace }}/start.*,${{ github.workspace }}/build_info.json,${{ github.workspace }}/core-permissions.rst
@ -182,6 +182,8 @@ jobs:
:information_source: Download the recommended PHP binary [here](${{ steps.php-binary-url.outputs.PHP_BINARY_URL }}).
:warning: Found a bug? Report it on our [issue tracker](${{ github.server_url }}/${{ github.repository }}/issues). **We can't fix bugs if you don't report them.**
- name: Post draft release URL on PR
if: github.event_name == 'pull_request_target'
uses: thollander/actions-comment-pull-request@v3

View File

@ -65,6 +65,8 @@ PocketMine-MP accepts community contributions! The following resources will be u
* [Building and running PocketMine-MP from source](BUILDING.md)
* [Contributing Guidelines](CONTRIBUTING.md)
New here? Check out [issues with the "Easy task" label](https://github.com/pmmp/PocketMine-MP/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Easy%20task%22) for things you could work to familiarise yourself with the codebase.
## Donate
PocketMine-MP is free, but it requires a lot of time and effort from unpaid volunteers to develop. Donations enable us to keep delivering support for new versions and adding features your players love.

View File

@ -36,7 +36,7 @@ require dirname(__DIR__) . '/vendor/autoload.php';
*/
$options = [
"base_version" => VersionInfo::BASE_VERSION,
"major_version" => fn() => explode(".", VersionInfo::BASE_VERSION)[0],
"major_version" => fn() => explode(".", VersionInfo::BASE_VERSION, limit: 2)[0],
"mcpe_version" => ProtocolInfo::MINECRAFT_VERSION_NETWORK,
"is_dev" => VersionInfo::IS_DEVELOPMENT_BUILD,
"changelog_file_name" => function() : string{

View File

@ -28,6 +28,7 @@ use function dirname;
use function fclose;
use function fopen;
use function fwrite;
use function is_dir;
use function is_file;
use function scandir;
use function str_replace;
@ -59,7 +60,7 @@ foreach($files as $file){
continue;
}
$path = Path::join(BEDROCK_DATA_PATH, $file);
if(!is_file($path)){
if(!is_file($path) && !is_dir($path)){
continue;
}
@ -67,6 +68,7 @@ foreach($files as $file){
'README.md',
'LICENSE',
'composer.json',
'.github'
] as $ignored){
if($file === $ignored){
continue 2;

52
changelogs/5.25.md Normal file
View File

@ -0,0 +1,52 @@
# 5.25.0
Released 16th February 2025.
This is a support release for Minecraft: Bedrock Edition 1.21.60. It also includes some minor API additions supporting new features in this version.
**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.60.
- Removed support for earlier versions.
## Documentation
- Fixed the documentation of `Utils::getOS()`. It now refers to the `Utils::OS_*` constants instead of a list of hardcoded strings.
## API
### `pocketmine\inventory`
This release allows plugins to decide which creative tab they want to add their new items to.
It also allows creating new collapsible groups of items, and modifying or removing existing ones.
- The following new methods have been added:
- `public CreativeInventory->getAllEntries() : list<CreativeInventoryEntry>` - returns an array of objects, each containing a creative item and information about its category and collapsible group (if applicable).
- `public CreativeInventory->getEntry(int $index) : ?CreativeInventoryEntry` - returns the creative item with the specified identifier, or `null` if not found
- The following methods have signature changes:
- `CreativeInventory->add()` now accepts two additional optional parameters: `CreativeCategory $category, ?CreativeGroup $group`. If not specified, the item will be added to the Items tab without a group.
- The following new classes have been added:
- `CreativeCategory` - enum of possible creative inventory categories (each has its own tab on the GUI)
- `CreativeGroup` - contains information about a collapsible group of creative items, including tooltip text and icon
- `CreativeInventoryEntry` - contains information about a creative inventory item, including its category and collapsible group (if applicable)
## Internals
- `CreativeContentPacket` is no longer fully cached due to the requirement for translation context during construction. The individual items are still cached (which is the most expensive part), but the packet itself is now constructed on demand, and group entries are constructed on the fly. This may affect performance, but this has not been investigated.
- `BedrockDataFiles` now includes constants for folders at the top level of `BedrockData` as well as files.
- The structure of creative data in `BedrockData` was changed to accommodate item category and grouping information. `creativeitems.json` has been replaced by `creative/*.json`, which contain information about item grouping and also segregates item lists per category.
- New information was added to `required_item_list.json` in `BedrockData`, as the server is now required to send item component NBT data in some cases.
# 5.25.1
Released 26th February 2025.
## Fixes
- Fixed confusing exception message when a block-breaking tool has an efficiency value of zero.
- Fixed incorrect facing of doors since 1.21.60 (resulted in mismatched AABBs between client & server, rendering glitches etc.)
- Resource pack UUIDs are now validated on load. Previously, invalid UUIDs would be accepted, and potentially cause a server crash on player join.
# 5.25.2
Released 4th March 2025.
## Fixes
- Added limits to various `explode()` calls.

View File

@ -33,18 +33,18 @@
"composer-runtime-api": "^2.0",
"adhocore/json-comment": "~1.2.0",
"netresearch/jsonmapper": "~v5.0.0",
"pocketmine/bedrock-block-upgrade-schema": "~5.0.0+bedrock-1.21.40",
"pocketmine/bedrock-data": "~2.15.0+bedrock-1.21.50",
"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": "~35.0.0+bedrock-1.21.50",
"pocketmine/bedrock-protocol": "~36.0.0+bedrock-1.21.60",
"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.22.0",
"pocketmine/locale-data": "~2.24.0",
"pocketmine/log": "^0.4.0",
"pocketmine/math": "~1.0.0",
"pocketmine/nbt": "~1.0.0",
"pocketmine/nbt": "~1.1.0",
"pocketmine/raklib": "~1.1.0",
"pocketmine/raklib-ipc": "~1.0.0",
"pocketmine/snooze": "^0.5.0",
@ -52,7 +52,7 @@
"symfony/filesystem": "~6.4.0"
},
"require-dev": {
"phpstan/phpstan": "2.1.1",
"phpstan/phpstan": "2.1.6",
"phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.24"

156
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": "994ccffe45f066768542019f6f9d237b",
"content-hash": "2a56fc6dee1dac2ade34d965aa49dc82",
"packages": [
{
"name": "adhocore/json-comment",
@ -67,16 +67,16 @@
},
{
"name": "brick/math",
"version": "0.12.1",
"version": "0.12.2",
"source": {
"type": "git",
"url": "https://github.com/brick/math.git",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1"
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1",
"url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40",
"shasum": ""
},
"require": {
@ -85,7 +85,7 @@
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^10.1",
"vimeo/psalm": "5.16.0"
"vimeo/psalm": "6.8.8"
},
"type": "library",
"autoload": {
@ -115,7 +115,7 @@
],
"support": {
"issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.12.1"
"source": "https://github.com/brick/math/tree/0.12.2"
},
"funding": [
{
@ -123,7 +123,7 @@
"type": "github"
}
],
"time": "2023-11-29T23:19:16+00:00"
"time": "2025-02-26T10:21:45+00:00"
},
{
"name": "netresearch/jsonmapper",
@ -178,16 +178,16 @@
},
{
"name": "pocketmine/bedrock-block-upgrade-schema",
"version": "5.0.0",
"version": "5.1.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockBlockUpgradeSchema.git",
"reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52"
"reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/20dd5c11e9915bacea4fe2cf649e1d23697a6e52",
"reference": "20dd5c11e9915bacea4fe2cf649e1d23697a6e52",
"url": "https://api.github.com/repos/pmmp/BedrockBlockUpgradeSchema/zipball/2218512e4b91f5bfd09ef55f7a4c4b04e169e41a",
"reference": "2218512e4b91f5bfd09ef55f7a4c4b04e169e41a",
"shasum": ""
},
"type": "library",
@ -198,22 +198,22 @@
"description": "Schemas describing how to upgrade saved block data in older Minecraft: Bedrock Edition world saves",
"support": {
"issues": "https://github.com/pmmp/BedrockBlockUpgradeSchema/issues",
"source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.0.0"
"source": "https://github.com/pmmp/BedrockBlockUpgradeSchema/tree/5.1.0"
},
"time": "2024-11-03T14:13:50+00:00"
"time": "2025-02-11T17:41:44+00:00"
},
{
"name": "pocketmine/bedrock-data",
"version": "2.15.0+bedrock-1.21.50",
"version": "4.0.0+bedrock-1.21.60",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockData.git",
"reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad"
"reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/6e819f36d781866ce63d2406be2ce7f2d1afd9ad",
"reference": "6e819f36d781866ce63d2406be2ce7f2d1afd9ad",
"url": "https://api.github.com/repos/pmmp/BedrockData/zipball/2e5f16ec2facac653f3f894f22eb831d880ba98e",
"reference": "2e5f16ec2facac653f3f894f22eb831d880ba98e",
"shasum": ""
},
"type": "library",
@ -224,9 +224,9 @@
"description": "Blobs of data generated from Minecraft: Bedrock Edition, used by PocketMine-MP",
"support": {
"issues": "https://github.com/pmmp/BedrockData/issues",
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.50"
"source": "https://github.com/pmmp/BedrockData/tree/bedrock-1.21.60"
},
"time": "2024-12-04T12:59:12+00:00"
"time": "2025-02-16T15:56:56+00:00"
},
{
"name": "pocketmine/bedrock-item-upgrade-schema",
@ -256,16 +256,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "35.0.3+bedrock-1.21.50",
"version": "36.0.0+bedrock-1.21.60",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "c4d62581cb62d29ec426914c6b4d7e0ff835da9c"
"reference": "2057de319c5c551001c2a544e08d1bc7727d9963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/c4d62581cb62d29ec426914c6b4d7e0ff835da9c",
"reference": "c4d62581cb62d29ec426914c6b4d7e0ff835da9c",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2057de319c5c551001c2a544e08d1bc7727d9963",
"reference": "2057de319c5c551001c2a544e08d1bc7727d9963",
"shasum": ""
},
"require": {
@ -296,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/35.0.3+bedrock-1.21.50"
"source": "https://github.com/pmmp/BedrockProtocol/tree/36.0.0+bedrock-1.21.60"
},
"time": "2025-01-07T23:06:29+00:00"
"time": "2025-02-16T15:59:08+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -471,16 +471,16 @@
},
{
"name": "pocketmine/locale-data",
"version": "2.22.1",
"version": "2.24.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/Language.git",
"reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49"
"reference": "6ec5e92c77a2102b2692763733e4763012facae9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/Language/zipball/fa4e377c437391cfcfdedd08eea3a848eabd1b49",
"reference": "fa4e377c437391cfcfdedd08eea3a848eabd1b49",
"url": "https://api.github.com/repos/pmmp/Language/zipball/6ec5e92c77a2102b2692763733e4763012facae9",
"reference": "6ec5e92c77a2102b2692763733e4763012facae9",
"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.22.1"
"source": "https://github.com/pmmp/Language/tree/2.24.0"
},
"time": "2024-12-06T14:44:17+00:00"
"time": "2025-02-16T20:46:34+00:00"
},
{
"name": "pocketmine/log",
@ -576,16 +576,16 @@
},
{
"name": "pocketmine/nbt",
"version": "1.0.1",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/pmmp/NBT.git",
"reference": "53db37487bc5ddbfbd84247966e1a073bdcfdb7d"
"reference": "cfd53a86166b851786967fc560cdb372e66fde96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/NBT/zipball/53db37487bc5ddbfbd84247966e1a073bdcfdb7d",
"reference": "53db37487bc5ddbfbd84247966e1a073bdcfdb7d",
"url": "https://api.github.com/repos/pmmp/NBT/zipball/cfd53a86166b851786967fc560cdb372e66fde96",
"reference": "cfd53a86166b851786967fc560cdb372e66fde96",
"shasum": ""
},
"require": {
@ -612,9 +612,9 @@
"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.0"
},
"time": "2025-01-07T22:47:46+00:00"
"time": "2025-02-01T21:20:26+00:00"
},
{
"name": "pocketmine/raklib",
@ -1013,8 +1013,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1092,8 +1092,8 @@
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
@ -1150,16 +1150,16 @@
"packages-dev": [
{
"name": "myclabs/deep-copy",
"version": "1.12.1",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845"
"reference": "024473a478be9df5fdaca2c793f2232fe788e414"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845",
"reference": "123267b2c49fbf30d78a7b2d333f6be754b94845",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
"reference": "024473a478be9df5fdaca2c793f2232fe788e414",
"shasum": ""
},
"require": {
@ -1198,7 +1198,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.12.1"
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
},
"funding": [
{
@ -1206,20 +1206,20 @@
"type": "tidelift"
}
],
"time": "2024-11-08T17:47:46+00:00"
"time": "2025-02-12T12:17:51+00:00"
},
{
"name": "nikic/php-parser",
"version": "v5.3.1",
"version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
"reference": "447a020a1f875a434d62f2a401f53b82a396e494"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
"reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494",
"reference": "447a020a1f875a434d62f2a401f53b82a396e494",
"shasum": ""
},
"require": {
@ -1262,9 +1262,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0"
},
"time": "2024-10-08T18:51:32+00:00"
"time": "2024-12-30T11:07:19+00:00"
},
{
"name": "phar-io/manifest",
@ -1386,16 +1386,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.1",
"version": "2.1.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7"
"reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"reference": "cd6e973e04b4c2b94c86e8612b5a65f0da0e08e7",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
"reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
"shasum": ""
},
"require": {
@ -1440,20 +1440,20 @@
"type": "github"
}
],
"time": "2025-01-05T16:43:48+00:00"
"time": "2025-02-19T15:46:42+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
"version": "2.0.3",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18"
"reference": "d09e152f403c843998d7a52b5d87040c937525dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e32ac656788a5bf3dedda89e6a2cad5643bf1a18",
"reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/d09e152f403c843998d7a52b5d87040c937525dd",
"reference": "d09e152f403c843998d7a52b5d87040c937525dd",
"shasum": ""
},
"require": {
@ -1489,22 +1489,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.3"
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.4"
},
"time": "2024-12-19T09:14:43+00:00"
"time": "2025-01-22T13:07:38+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
"version": "2.0.1",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3"
"reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
"reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/8b88b5f818bfa301e0c99154ab622dace071c3ba",
"reference": "8b88b5f818bfa301e0c99154ab622dace071c3ba",
"shasum": ""
},
"require": {
@ -1537,9 +1537,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.1"
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.3"
},
"time": "2024-12-12T20:21:10+00:00"
"time": "2025-01-21T10:52:14+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -1864,16 +1864,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.38",
"version": "10.5.45",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132"
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8",
"reference": "bd68a781d8e30348bc297449f5234b3458267ae8",
"shasum": ""
},
"require": {
@ -1883,7 +1883,7 @@
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.0",
"myclabs/deep-copy": "^1.12.1",
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.1",
@ -1945,7 +1945,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.38"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45"
},
"funding": [
{
@ -1961,7 +1961,7 @@
"type": "tidelift"
}
],
"time": "2024-10-28T13:06:21+00:00"
"time": "2025-02-06T16:08:12+00:00"
},
{
"name": "sebastian/cli-parser",

View File

@ -11,8 +11,10 @@ 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\ThreadedSupportedTypesRule

View File

@ -264,7 +264,7 @@ JIT_WARNING
$composerGitHash = InstalledVersions::getReference('pocketmine/pocketmine-mp');
if($composerGitHash !== null){
//we can't verify dependency versions if we were installed without using git
$currentGitHash = explode("-", VersionInfo::GIT_HASH())[0];
$currentGitHash = explode("-", VersionInfo::GIT_HASH(), 2)[0];
if($currentGitHash !== $composerGitHash){
critical_error("Composer dependencies and/or autoloader are out of sync.");
critical_error("- Current revision is $currentGitHash");

View File

@ -36,6 +36,7 @@ use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\CraftingManagerFromDataHelper;
use pocketmine\crash\CrashDump;
use pocketmine\crash\CrashDumpRenderer;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\entity\EntityDataHelper;
use pocketmine\entity\Location;
use pocketmine\event\HandlerListManager;
@ -698,7 +699,7 @@ class Server{
public function removeOp(string $name) : void{
$lowercaseName = strtolower($name);
foreach($this->operators->getAll() as $operatorName => $_){
foreach(Utils::promoteKeys($this->operators->getAll()) as $operatorName => $_){
$operatorName = (string) $operatorName;
if($lowercaseName === strtolower($operatorName)){
$this->operators->remove($operatorName);
@ -1005,7 +1006,7 @@ class Server{
$this->commandMap = new SimpleCommandMap($this);
$this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes"));
$this->craftingManager = CraftingManagerFromDataHelper::make(BedrockDataFiles::RECIPES);
$this->resourceManager = new ResourcePackManager(Path::join($this->dataPath, "resource_packs"), $this->logger);

View File

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

View File

@ -154,7 +154,7 @@ class BlockBreakInfo{
$efficiency = $item->getMiningEfficiency(($this->toolType & $item->getBlockToolType()) !== 0);
if($efficiency <= 0){
throw new \InvalidArgumentException(get_class($item) . " has invalid mining efficiency: expected >= 0, got $efficiency");
throw new \InvalidArgumentException(get_class($item) . " must have a positive mining efficiency, but got $efficiency");
}
$base /= $efficiency;

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;
}

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);
}
}

View File

@ -25,6 +25,7 @@ namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\item\Item;
use pocketmine\world\Position;
class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
@ -37,4 +38,12 @@ class AnvilInventory extends SimpleInventory implements BlockInventory, Temporar
$this->holder = $holder;
parent::__construct(2);
}
public function getInput() : Item {
return $this->getItem(self::SLOT_INPUT);
}
public function getMaterial() : Item {
return $this->getItem(self::SLOT_MATERIAL);
}
}

View File

@ -62,9 +62,10 @@ class Sign extends Spawnable{
/**
* @return string[]
* @deprecated
*/
public static function fixTextBlob(string $blob) : array{
return array_slice(array_pad(explode("\n", $blob), 4, ""), 0, 4);
return array_slice(array_pad(explode("\n", $blob, limit: 5), 4, ""), 0, 4);
}
protected SignText $text;

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\block\utils;
use pocketmine\crafting\AnvilCraftResult;
use pocketmine\item\Item;
use pocketmine\Server;
final class AnvilHelper{
private const COST_LIMIT = 39;
/**
* Attempts to calculate the result of an anvil operation.
*
* Returns null if the operation can't do anything.
*/
public static function calculateResult(Item $base, Item $material, ?string $customName, bool $isCreative) : ?AnvilCraftResult{
$recipe = Server::getInstance()->getCraftingManager()->matchAnvilRecipe($base, $material);
if($recipe === null){
return null;
}
$result = $recipe->getResultFor($base, $material);
if($result !== null){
$resultItem = $result->getOutput();
$xpCost = $result->getXpCost();
if(($customName === null || $customName === "") && $resultItem->hasCustomName()){
$xpCost++;
$resultItem->clearCustomName();
}elseif($customName !== null && $resultItem->getCustomName() !== $customName){
$xpCost++;
$resultItem->setCustomName($customName);
}
$result = new AnvilCraftResult($xpCost, $resultItem, $result->getSacrificeResult());
}
if($result === null || $result->getXpCost() <= 0 || ($result->getXpCost() > self::COST_LIMIT && !$isCreative)){
return null;
}
return $result;
}
}

View File

@ -79,7 +79,7 @@ class SignText{
* @throws \InvalidArgumentException if the text is not valid UTF-8
*/
public static function fromBlob(string $blob, ?Color $baseColor = null, bool $glowing = false) : SignText{
return new self(array_slice(array_pad(explode("\n", $blob), self::LINE_COUNT, ""), 0, self::LINE_COUNT), $baseColor, $glowing);
return new self(array_slice(array_pad(explode("\n", $blob, limit: self::LINE_COUNT + 1), self::LINE_COUNT, ""), 0, self::LINE_COUNT), $baseColor, $glowing);
}
/**

View File

@ -37,6 +37,7 @@ use function array_values;
use function explode;
use function implode;
use function str_replace;
use const PHP_INT_MAX;
abstract class Command{
@ -113,7 +114,7 @@ abstract class Command{
}
public function setPermission(?string $permission) : void{
$this->setPermissions($permission === null ? [] : explode(";", $permission));
$this->setPermissions($permission === null ? [] : explode(";", $permission, limit: PHP_INT_MAX));
}
public function testPermission(CommandSender $target, ?string $permission = null) : bool{

View File

@ -39,6 +39,7 @@ use function ksort;
use function min;
use function sort;
use function strtolower;
use const PHP_INT_MAX;
use const SORT_FLAG_CASE;
use const SORT_NATURAL;
@ -108,7 +109,7 @@ class HelpCommand extends VanillaCommand{
$usage = $cmd->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString)))
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString, limit: PHP_INT_MAX)))
->prefix(TextFormat::GOLD));
$aliases = $cmd->getAliases();

View File

@ -219,7 +219,11 @@ class ParticleCommand extends VanillaCommand{
break;
case "blockdust":
if($data !== null){
$d = explode("_", $data);
//to preserve the old unlimited explode behaviour, allow this to split into at most 5 parts
//this allows the 4th argument to be processed normally if given without forcing it to also consume
//any unexpected parts
//we probably ought to error in this case, but this will do for now
$d = explode("_", $data, limit: 5);
if(count($d) >= 3){
return new DustParticle(new Color(
((int) $d[0]) & 0xff,

View File

@ -62,7 +62,7 @@ class ConsoleCommandSender implements CommandSender{
$message = $this->getLanguage()->translate($message);
}
foreach(explode("\n", trim($message)) as $line){
foreach(explode("\n", trim($message), limit: PHP_INT_MAX) as $line){
Terminal::writeLine(TextFormat::GREEN . "Command output | " . TextFormat::addBase(TextFormat::WHITE, $line));
}
}

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\crafting;
use pocketmine\item\Item;
/**
* This class is here to hold the result of an anvil crafting process.
*/
final class AnvilCraftResult{
/**
* @param Item|null $sacrificeResult If the given item is considered as null (count <= 0), the value will be set to null.
*/
public function __construct(
private int $xpCost,
private Item $output,
private ?Item $sacrificeResult
){
if($this->sacrificeResult !== null && $this->sacrificeResult->isNull()){
$this->sacrificeResult = null;
}
}
/**
* Represent the amount of experience points required to craft the output item.
*/
public function getXpCost() : int{
return $this->xpCost;
}
/**
* Represent the item given as output of the crafting process.
*/
public function getOutput() : Item{
return $this->output;
}
/**
* This result has to be null if the sacrifice slot need to be emptied.
* If not null, it represent the item that will be left in the sacrifice slot after the crafting process.
*/
public function getSacrificeResult() : ?Item{
return $this->sacrificeResult;
}
}

View File

@ -0,0 +1,78 @@
<?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\crafting;
use pocketmine\item\Durable;
use pocketmine\item\ToolTier;
use pocketmine\item\VanillaArmorMaterials;
use pocketmine\item\VanillaItems;
use pocketmine\world\format\io\GlobalItemDataHandlers;
final class AnvilCraftingManagerDataFiller{
public static function fillData(CraftingManager $manager) : CraftingManager{
foreach([
[
VanillaItems::DIAMOND(),
[VanillaArmorMaterials::DIAMOND(), ToolTier::DIAMOND]
], [
VanillaItems::GOLD_INGOT(),
[VanillaArmorMaterials::GOLD(), ToolTier::GOLD]
], [
VanillaItems::IRON_INGOT(),
[VanillaArmorMaterials::IRON(), ToolTier::IRON]
], [
VanillaItems::NETHERITE_INGOT(),
[VanillaArmorMaterials::NETHERITE(), ToolTier::NETHERITE]
], [
VanillaItems::SCUTE(),
[VanillaArmorMaterials::TURTLE(), null]
], [
VanillaItems::LEATHER(),
[VanillaArmorMaterials::LEATHER(), null]
]
] as [$item, [$armorMaterial, $toolTier]]){
$manager->registerAnvilRecipe(new MaterialRepairRecipe(
new ArmorRecipeIngredient($armorMaterial),
new ExactRecipeIngredient($item)
));
if($toolTier !== null){
$manager->registerAnvilRecipe(new MaterialRepairRecipe(
new TieredToolRecipeIngredient($toolTier),
new ExactRecipeIngredient($item)
));
}
}
foreach(VanillaItems::getAll() as $item){
if($item instanceof Durable){
$itemId = GlobalItemDataHandlers::getSerializer()->serializeType($item)->getName();
$manager->registerAnvilRecipe(new ItemSelfCombineRecipe(
new MetaWildcardRecipeIngredient($itemId)
));
}
}
return $manager;
}
}

View File

@ -0,0 +1,30 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\crafting;
use pocketmine\item\Item;
interface AnvilRecipe{
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult;
}

View File

@ -0,0 +1,50 @@
<?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\crafting;
use pocketmine\item\Armor;
use pocketmine\item\ArmorMaterial;
use pocketmine\item\Item;
use function spl_object_id;
class ArmorRecipeIngredient implements RecipeIngredient{
public function __construct(
private ArmorMaterial $material
){
}
public function getMaterial() : ArmorMaterial{ return $this->material; }
public function accepts(Item $item) : bool{
if($item->getCount() < 1){
return false;
}
return $item instanceof Armor && $item->getMaterial() === $this->material;
}
public function __toString() : string{
return "ArmorRecipeIngredient(ArmorMaterial@" . spl_object_id($this->material) . ")";
}
}

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;
@ -76,6 +80,18 @@ class CraftingManager{
*/
private array $brewingRecipeCache = [];
/**
* @var AnvilRecipe[]
* @phpstan-var list<AnvilRecipe>
*/
private array $anvilRecipes = [];
/**
* @var AnvilRecipe[][]
* @phpstan-var array<int, array<int, AnvilRecipe>>
*/
private array $anvilRecipeCache = [];
/** @phpstan-var ObjectSet<\Closure() : void> */
private ObjectSet $recipeRegisteredCallbacks;
@ -100,6 +116,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 +125,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);
}
/**
@ -197,6 +199,14 @@ class CraftingManager{
return $this->potionContainerChangeRecipes;
}
/**
* @return AnvilRecipe[][]
* @phpstan-return list<AnvilRecipe>
*/
public function getAnvilRecipes() : array{
return $this->anvilRecipes;
}
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
$this->craftingRecipeIndex[] = $recipe;
@ -231,6 +241,14 @@ class CraftingManager{
}
}
public function registerAnvilRecipe(AnvilRecipe $recipe) : void{
$this->anvilRecipes[] = $recipe;
foreach($this->recipeRegisteredCallbacks as $callback){
$callback();
}
}
/**
* @param Item[] $outputs
*/
@ -304,4 +322,21 @@ class CraftingManager{
return null;
}
public function matchAnvilRecipe(Item $input, Item $material) : ?AnvilRecipe{
$inputHash = $input->getStateId();
$materialHash = $material->getStateId();
$cached = $this->anvilRecipeCache[$inputHash][$materialHash] ?? null;
if($cached !== null){
return $cached;
}
foreach($this->anvilRecipes as $recipe){
if($recipe->getResultFor($input, $material) !== null){
return $this->anvilRecipeCache[$inputHash][$materialHash] = $recipe;
}
}
return null;
}
}

View File

@ -185,6 +185,7 @@ final class CraftingManagerFromDataHelper{
/**
* @param mixed[] $data
*
* @return object[]
*
* @phpstan-template TRecipeData of object
@ -210,7 +211,7 @@ final class CraftingManagerFromDataHelper{
$result = new CraftingManager();
foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'shapeless_crafting.json'), ShapelessRecipeData::class) as $recipe){
$recipeType = match($recipe->block){
$recipeType = match ($recipe->block) {
"crafting_table" => ShapelessRecipeType::CRAFTING,
"stonecutter" => ShapelessRecipeType::STONECUTTER,
"smithing_table" => ShapelessRecipeType::SMITHING,
@ -271,7 +272,7 @@ final class CraftingManagerFromDataHelper{
));
}
foreach(self::loadJsonArrayOfObjectsFile(Path::join($directoryPath, 'smelting.json'), FurnaceRecipeData::class) as $recipe){
$furnaceType = match ($recipe->block){
$furnaceType = match ($recipe->block) {
"furnace" => FurnaceType::FURNACE,
"blast_furnace" => FurnaceType::BLAST_FURNACE,
"smoker" => FurnaceType::SMOKER,
@ -333,6 +334,8 @@ final class CraftingManagerFromDataHelper{
));
}
$result = AnvilCraftingManagerDataFiller::fillData($result);
//TODO: smithing
return $result;

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\crafting;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\Durable;
use pocketmine\item\EnchantedBook;
use pocketmine\item\enchantment\AvailableEnchantmentRegistry;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\Rarity;
use pocketmine\item\Item;
use function floor;
use function max;
use function min;
abstract class ItemCombineRecipe implements AnvilRecipe{
abstract protected function validate(Item $input, Item $material) : bool;
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult{
if($this->validate($input, $material)){
$result = (clone $input);
$xpCost = 0;
if($result instanceof Durable && $material instanceof Durable && $this->repair($result, $material)){
// The two items are compatible for repair
$xpCost = 2;
}
// combining enchantments
foreach($material->getEnchantments() as $instance){
$enchantment = $instance->getType();
$level = $instance->getLevel();
if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $input)){
continue;
}
if(($targetEnchantment = $input->getEnchantment($enchantment)) !== null){
// Enchant already present on the target item
$targetLevel = $targetEnchantment->getLevel();
$newLevel = ($targetLevel === $level ? $targetLevel + 1 : max($targetLevel, $level));
$level = min($newLevel, $enchantment->getMaxLevel());
$instance = new EnchantmentInstance($enchantment, $level);
}else{
// Check if the enchantment is compatible with the existing enchantments
foreach($input->getEnchantments() as $testedInstance){
$testedEnchantment = $testedInstance->getType();
if(!$testedEnchantment->isCompatibleWith($enchantment)){
$xpCost++;
//TODO: XP COST
continue 2;
}
}
}
$costAddition = match ($enchantment->getRarity()) {
Rarity::COMMON => 1,
Rarity::UNCOMMON => 2,
Rarity::RARE => 4,
Rarity::MYTHIC => 8,
default => throw new TransactionValidationException("Invalid rarity " . $enchantment->getRarity() . " found")
};
if($material instanceof EnchantedBook){
// Enchanted books are half as expensive to combine
$costAddition = max(1, $costAddition / 2);
}
$levelDifference = $instance->getLevel() - $input->getEnchantmentLevel($instance->getType());
$xpCost += (int) floor($costAddition * $levelDifference);
$result->addEnchantment($instance);
$xpCost += 2 ** $input->getAnvilRepairCost() - 1;
$xpCost += 2 ** $material->getAnvilRepairCost() - 1;
$result->setAnvilRepairCost(
max($result->getAnvilRepairCost(), $material->getAnvilRepairCost()) + 1
);
}
if($xpCost !== 0){
return new AnvilCraftResult($xpCost, $result, null);
}
}
return null;
}
private function repair(Durable $result, Durable $material) : bool{
$damage = $result->getDamage();
if($damage === 0){
return false;
}
$baseMaxDurability = $result->getMaxDurability();
$baseDurability = $baseMaxDurability - $damage;
$materialDurability = $material->getMaxDurability() - $material->getDamage();
$addDurability = (int) ($baseMaxDurability * 12 / 100);
$result->setDamage($baseMaxDurability - min(
$baseMaxDurability,
$baseDurability + $materialDurability + $addDurability
));
return true;
}
}

View File

@ -0,0 +1,41 @@
<?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\crafting;
use pocketmine\item\Item;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class ItemDifferentCombineRecipe extends ItemCombineRecipe{
public function __construct(
private RecipeIngredient $base,
private RecipeIngredient $material
){
}
protected function validate(Item $input, Item $material) : bool{
return $this->base->accepts($input) && $this->material->accepts($material);
}
}

View File

@ -0,0 +1,44 @@
<?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\crafting;
use pocketmine\item\Item;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class ItemSelfCombineRecipe extends ItemCombineRecipe{
/**
* @param RecipeIngredient $target The item that will be concerned by the combinaison.
* The input and material have to be accepted by this ingredient to be able to combine them.
*/
public function __construct(
private RecipeIngredient $target
){
}
protected function validate(Item $input, Item $material) : bool{
return $this->target->accepts($input) && $this->target->accepts($material);
}
}

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\crafting;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use function ceil;
use function floor;
use function max;
use function min;
/**
* Represent a recipe that repair an item with a material in an anvil.
*/
class MaterialRepairRecipe implements AnvilRecipe{
public function __construct(
private RecipeIngredient $input,
private RecipeIngredient $material
){
}
public function getInput() : RecipeIngredient{
return $this->input;
}
public function getMaterial() : RecipeIngredient{
return $this->material;
}
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult{
if($this->input->accepts($input) && $this->material->accepts($material) && $input instanceof Durable){
$damage = $input->getDamage();
if($damage !== 0){
$quarter = min($damage, (int) floor($input->getMaxDurability() / 4));
$numberRepair = min($material->getCount(), (int) ceil($damage / $quarter));
$damage -= $quarter * $numberRepair;
return new AnvilCraftResult(
$numberRepair,
(clone $input)->setDamage(max(0, $damage)),
(clone $material)->setCount($material->getCount() - $numberRepair)
);
}
}
return null;
}
}

View File

@ -97,6 +97,7 @@ class ShapedRecipe implements CraftingRecipe{
$this->shape = $shape;
Utils::validateArrayValueType($ingredients, function(RecipeIngredient $_) : void{});
foreach(Utils::stringifyKeys($ingredients) as $char => $i){
if(!str_contains(implode($this->shape), $char)){
throw new \InvalidArgumentException("Symbol '$char' does not appear in the recipe shape");
@ -105,6 +106,7 @@ class ShapedRecipe implements CraftingRecipe{
$this->ingredientList[$char] = clone $i;
}
Utils::validateArrayValueType($results, function(Item $_) : void{});
$this->results = Utils::cloneObjectArray($results);
}

View File

@ -53,7 +53,9 @@ class ShapelessRecipe implements CraftingRecipe{
if(count($ingredients) > 9){
throw new \InvalidArgumentException("Shapeless recipes cannot have more than 9 ingredients");
}
Utils::validateArrayValueType($ingredients, function(RecipeIngredient $_) : void{});
$this->ingredients = $ingredients;
Utils::validateArrayValueType($results, function(Item $_) : void{});
$this->results = Utils::cloneObjectArray($results);
}

View File

@ -0,0 +1,49 @@
<?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\crafting;
use pocketmine\item\Item;
use pocketmine\item\TieredTool;
use pocketmine\item\ToolTier;
class TieredToolRecipeIngredient implements RecipeIngredient{
public function __construct(
private ToolTier $tier
){
}
public function getTier() : ToolTier{ return $this->tier; }
public function accepts(Item $item) : bool{
if($item->getCount() < 1){
return false;
}
return $item instanceof TieredTool && $item->getTier() === $this->tier;
}
public function __toString() : string{
return "TieredToolRecipeIngredient(" . $this->tier->name . ")";
}
}

View File

@ -39,14 +39,16 @@ final class BedrockDataFiles{
public const BLOCK_STATE_META_MAP_JSON = BEDROCK_DATA_PATH . '/block_state_meta_map.json';
public const CANONICAL_BLOCK_STATES_NBT = BEDROCK_DATA_PATH . '/canonical_block_states.nbt';
public const COMMAND_ARG_TYPES_JSON = BEDROCK_DATA_PATH . '/command_arg_types.json';
public const CREATIVEITEMS_JSON = BEDROCK_DATA_PATH . '/creativeitems.json';
public const CREATIVE = BEDROCK_DATA_PATH . '/creative';
public const ENTITY_ID_MAP_JSON = BEDROCK_DATA_PATH . '/entity_id_map.json';
public const ENTITY_IDENTIFIERS_NBT = BEDROCK_DATA_PATH . '/entity_identifiers.nbt';
public const ENUMS = BEDROCK_DATA_PATH . '/enums';
public const ENUMS_PY = BEDROCK_DATA_PATH . '/enums.py';
public const ITEM_TAGS_JSON = BEDROCK_DATA_PATH . '/item_tags.json';
public const LEVEL_SOUND_ID_MAP_JSON = BEDROCK_DATA_PATH . '/level_sound_id_map.json';
public const PARTICLE_ID_MAP_JSON = BEDROCK_DATA_PATH . '/particle_id_map.json';
public const PROTOCOL_INFO_JSON = BEDROCK_DATA_PATH . '/protocol_info.json';
public const R12_TO_CURRENT_BLOCK_MAP_BIN = BEDROCK_DATA_PATH . '/r12_to_current_block_map.bin';
public const R16_TO_CURRENT_ITEM_MAP_JSON = BEDROCK_DATA_PATH . '/r16_to_current_item_map.json';
public const RECIPES = BEDROCK_DATA_PATH . '/recipes';
public const REQUIRED_ITEM_LIST_JSON = BEDROCK_DATA_PATH . '/required_item_list.json';
}

View File

@ -45,8 +45,8 @@ final class BlockStateData{
public const CURRENT_VERSION =
(1 << 24) | //major
(21 << 16) | //minor
(40 << 8) | //patch
(1); //revision
(60 << 8) | //patch
(33); //revision
public const TAG_NAME = "name";
public const TAG_STATES = "states";

View File

@ -59,6 +59,7 @@ final class BlockStateNames{
public const COVERED_BIT = "covered_bit";
public const CRACKED_STATE = "cracked_state";
public const CRAFTING = "crafting";
public const CREAKING_HEART_STATE = "creaking_heart_state";
public const DEAD_BIT = "dead_bit";
public const DEPRECATED = "deprecated";
public const DIRECTION = "direction";

View File

@ -56,6 +56,10 @@ final class BlockStateStringValues{
public const CRACKED_STATE_MAX_CRACKED = "max_cracked";
public const CRACKED_STATE_NO_CRACKS = "no_cracks";
public const CREAKING_HEART_STATE_AWAKE = "awake";
public const CREAKING_HEART_STATE_DORMANT = "dormant";
public const CREAKING_HEART_STATE_UPROOTED = "uprooted";
public const DRIPSTONE_THICKNESS_BASE = "base";
public const DRIPSTONE_THICKNESS_FRUSTUM = "frustum";
public const DRIPSTONE_THICKNESS_MERGE = "merge";

View File

@ -131,7 +131,8 @@ final class BlockStateDeserializerHelper{
//TODO: check if these need any special treatment to get the appropriate data to both halves of the door
return $block
->setTop($in->readBool(BlockStateNames::UPPER_BLOCK_BIT))
->setFacing(Facing::rotateY($in->readLegacyHorizontalFacing(), false))
//a door facing "east" is actually facing north - thanks mojang
->setFacing(Facing::rotateY($in->readCardinalHorizontalFacing(), clockwise: false))
->setHingeRight($in->readBool(BlockStateNames::DOOR_HINGE_BIT))
->setOpen($in->readBool(BlockStateNames::OPEN_BIT));
}
@ -145,7 +146,7 @@ final class BlockStateDeserializerHelper{
/** @throws BlockStateDeserializeException */
public static function decodeFenceGate(FenceGate $block, BlockStateReader $in) : FenceGate{
return $block
->setFacing($in->readLegacyHorizontalFacing())
->setFacing($in->readCardinalHorizontalFacing())
->setInWall($in->readBool(BlockStateNames::IN_WALL_BIT))
->setOpen($in->readBool(BlockStateNames::OPEN_BIT));
}

View File

@ -100,7 +100,8 @@ final class BlockStateSerializerHelper{
public static function encodeDoor(Door $block, Writer $out) : Writer{
return $out
->writeBool(BlockStateNames::UPPER_BLOCK_BIT, $block->isTop())
->writeLegacyHorizontalFacing(Facing::rotateY($block->getFacing(), true))
//a door facing north is encoded as "east"
->writeCardinalHorizontalFacing(Facing::rotateY($block->getFacing(), clockwise: true))
->writeBool(BlockStateNames::DOOR_HINGE_BIT, $block->isHingeRight())
->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen());
}
@ -112,7 +113,7 @@ final class BlockStateSerializerHelper{
public static function encodeFenceGate(FenceGate $block, Writer $out) : Writer{
return $out
->writeLegacyHorizontalFacing($block->getFacing())
->writeCardinalHorizontalFacing($block->getFacing())
->writeBool(BlockStateNames::IN_WALL_BIT, $block->isInWall())
->writeBool(BlockStateNames::OPEN_BIT, $block->isOpen());
}

View File

@ -103,10 +103,18 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
/** @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 */

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

@ -513,6 +513,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
AbilitiesLayer::LAYER_BASE,
array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false),
0.0,
0.0,
0.0
)
])),

View File

@ -0,0 +1,86 @@
<?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\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\item\Item;
use pocketmine\player\Player;
/**
* Called when a player uses an anvil (renaming, repairing, combining items).
* This event is called once per action even if multiple tasks are performed at once.
*/
class PlayerUseAnvilEvent extends PlayerEvent implements Cancellable{
use CancellableTrait;
public function __construct(
Player $player,
private Item $baseItem,
private ?Item $materialItem,
private Item $resultItem,
private ?string $customName,
private int $xpCost
){
$this->player = $player;
}
/**
* Returns the item that the player is using as the base item (left slot).
*/
public function getBaseItem() : Item{
return $this->baseItem;
}
/**
* Returns the item that the player is using as the material item (right slot), or null if there is no material item
* (e.g. when renaming an item).
*/
public function getMaterialItem() : ?Item{
return $this->materialItem;
}
/**
* Returns the item that the player will receive as a result of the anvil operation.
*/
public function getResultItem() : Item{
return $this->resultItem;
}
/**
* Returns the custom name that the player is setting on the item, or null if the player is not renaming the item.
*
* This value is defined when the base item is already renamed.
*/
public function getCustomName() : ?string{
return $this->customName;
}
/**
* Returns the amount of XP levels that the player will spend on this anvil operation.
*/
public function getXpCost() : int{
return $this->xpCost;
}
}

View File

@ -0,0 +1,34 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory;
/**
* Available tabs in the creative inventory that an item can be displayed in.
*/
enum CreativeCategory{
case CONSTRUCTION;
case NATURE;
case EQUIPMENT;
case ITEMS;
}

View File

@ -0,0 +1,51 @@
<?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\inventory;
use pocketmine\item\Item;
use pocketmine\lang\Translatable;
use function strlen;
/**
* Info for an item group in the creative inventory menu.
*/
final class CreativeGroup{
/**
* @param Translatable|string $name Tooltip shown to the player on hover
* @param Item $icon Item shown when the group is collapsed
*/
public function __construct(
private readonly Translatable|string $name,
private readonly Item $icon
){
$nameLength = $name instanceof Translatable ? strlen($name->getText()) : strlen($name);
if($nameLength === 0){
throw new \InvalidArgumentException("Creative group name cannot be empty");
}
}
public function getName() : Translatable|string{ return $this->name; }
public function getIcon() : Item{ return clone $this->icon; }
}

View File

@ -24,21 +24,24 @@ declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\crafting\CraftingManagerFromDataHelper;
use pocketmine\crafting\json\ItemStackData;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\inventory\json\CreativeGroupData;
use pocketmine\item\Item;
use pocketmine\lang\Translatable;
use pocketmine\utils\DestructorCallbackTrait;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function array_filter;
use function array_map;
final class CreativeInventory{
use SingletonTrait;
use DestructorCallbackTrait;
/**
* @var Item[]
* @phpstan-var array<int, Item>
* @var CreativeInventoryEntry[]
* @phpstan-var array<int, CreativeInventoryEntry>
*/
private array $creative = [];
@ -47,17 +50,32 @@ final class CreativeInventory{
private function __construct(){
$this->contentChangedCallbacks = new ObjectSet();
$creativeItems = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile(
BedrockDataFiles::CREATIVEITEMS_JSON,
ItemStackData::class
);
foreach($creativeItems as $data){
$item = CraftingManagerFromDataHelper::deserializeItemStack($data);
if($item === null){
//unknown item
continue;
foreach([
"construction" => CreativeCategory::CONSTRUCTION,
"nature" => CreativeCategory::NATURE,
"equipment" => CreativeCategory::EQUIPMENT,
"items" => CreativeCategory::ITEMS,
] as $categoryId => $categoryEnum){
$groups = CraftingManagerFromDataHelper::loadJsonArrayOfObjectsFile(
Path::join(BedrockDataFiles::CREATIVE, $categoryId . ".json"),
CreativeGroupData::class
);
foreach($groups as $groupData){
$icon = $groupData->group_icon === null ? null : CraftingManagerFromDataHelper::deserializeItemStack($groupData->group_icon);
$group = $icon === null ? null : new CreativeGroup(
new Translatable($groupData->group_name),
$icon
);
$items = array_filter(array_map(static fn($itemStack) => CraftingManagerFromDataHelper::deserializeItemStack($itemStack), $groupData->items));
foreach($items as $item){
$this->add($item, $categoryEnum, $group);
}
}
$this->add($item);
}
}
@ -75,16 +93,28 @@ final class CreativeInventory{
* @phpstan-return array<int, Item>
*/
public function getAll() : array{
return Utils::cloneObjectArray($this->creative);
return array_map(fn(CreativeInventoryEntry $entry) => $entry->getItem(), $this->creative);
}
/**
* @return CreativeInventoryEntry[]
* @phpstan-return array<int, CreativeInventoryEntry>
*/
public function getAllEntries() : array{
return $this->creative;
}
public function getItem(int $index) : ?Item{
return isset($this->creative[$index]) ? clone $this->creative[$index] : null;
return $this->getEntry($index)?->getItem();
}
public function getEntry(int $index) : ?CreativeInventoryEntry{
return $this->creative[$index] ?? null;
}
public function getItemIndex(Item $item) : int{
foreach($this->creative as $i => $d){
if($item->equals($d, true, false)){
if($d->matchesItem($item)){
return $i;
}
}
@ -96,8 +126,8 @@ final class CreativeInventory{
* Adds an item to the creative menu.
* Note: Players who are already online when this is called will not see this change.
*/
public function add(Item $item) : void{
$this->creative[] = clone $item;
public function add(Item $item, CreativeCategory $category = CreativeCategory::ITEMS, ?CreativeGroup $group = null) : void{
$this->creative[] = new CreativeInventoryEntry($item, $category, $group);
$this->onContentChange();
}

View File

@ -0,0 +1,48 @@
<?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\inventory;
use pocketmine\item\Item;
final class CreativeInventoryEntry{
private readonly Item $item;
public function __construct(
Item $item,
private readonly CreativeCategory $category,
private readonly ?CreativeGroup $group = null
){
$this->item = clone $item;
}
public function getItem() : Item{ return clone $this->item; }
public function getCategory() : CreativeCategory{ return $this->category; }
public function getGroup() : ?CreativeGroup{ return $this->group; }
public function matchesItem(Item $item) : bool{
return $item->equals($this->item, checkDamage: true, checkCompound: false);
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\inventory\json;
use pocketmine\crafting\json\ItemStackData;
final class CreativeGroupData{
/** @required */
public string $group_name;
/** @required */
public ?ItemStackData $group_icon;
/**
* @var \pocketmine\crafting\json\ItemStackData[]
* @required
*/
public array $items;
}

View File

@ -0,0 +1,155 @@
<?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\inventory\transaction;
use pocketmine\block\Anvil;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\utils\AnvilHelper;
use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\AnvilCraftResult;
use pocketmine\event\player\PlayerUseAnvilEvent;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\world\sound\AnvilBreakSound;
use pocketmine\world\sound\AnvilUseSound;
use function count;
use function mt_rand;
class AnvilTransaction extends InventoryTransaction{
private ?Item $baseItem = null;
private ?Item $materialItem = null;
public function __construct(
Player $source,
private readonly AnvilCraftResult $expectedResult,
private readonly ?string $customName
){
parent::__construct($source);
}
private function validateFiniteResources(int $xpSpent) : void{
$expectedXpCost = $this->expectedResult->getXpCost();
if($xpSpent !== $expectedXpCost){
throw new TransactionValidationException("Expected the amount of xp spent to be $expectedXpCost, but received $xpSpent");
}
$xpLevel = $this->source->getXpManager()->getXpLevel();
if($xpLevel < $expectedXpCost){
throw new TransactionValidationException("Player's XP level $xpLevel is less than the required XP level $expectedXpCost");
}
}
private function validateInputs(Item $base, Item $material, Item $expectedOutput) : ?int{
$calculAttempt = AnvilHelper::calculateResult($base, $material, $this->customName, $this->source->isCreative());
if($calculAttempt === null){
return null;
}
$result = $calculAttempt->getOutput();
if(!$result->equalsExact($expectedOutput)){
return null;
}
$this->baseItem = $base;
$this->materialItem = $material;
return $calculAttempt->getXpCost();
}
public function validate() : void{
if(count($this->actions) < 1){
throw new TransactionValidationException("Transaction must have at least one action to be executable");
}
/** @var Item[] $inputs */
$inputs = [];
/** @var Item[] $outputs */
$outputs = [];
$this->matchItems($outputs, $inputs);
if(($outputCount = count($outputs)) !== 1){
throw new TransactionValidationException("Expected 1 output item, but received $outputCount");
}
$outputItem = $outputs[0];
if(($inputCount = count($inputs)) < 1){
throw new TransactionValidationException("Expected at least 1 input item, but received $inputCount");
}
if($inputCount > 2){
throw new TransactionValidationException("Expected at most 2 input items, but received $inputCount");
}
if(count($inputs) < 2){
$xpCost = $this->validateInputs($inputs[0], VanillaItems::AIR(), $outputItem) ??
throw new TransactionValidationException("Inputs do not match expected result");
}else{
$xpCost = $this->validateInputs($inputs[0], $inputs[1], $outputItem) ??
$this->validateInputs($inputs[1], $inputs[0], $outputItem) ??
throw new TransactionValidationException("Inputs do not match expected result");
}
if($this->source->hasFiniteResources()){
$this->validateFiniteResources($xpCost);
}
}
public function execute() : void{
parent::execute();
if($this->source->hasFiniteResources()){
$this->source->getXpManager()->subtractXpLevels($this->expectedResult->getXpCost());
}
$inventory = $this->source->getCurrentWindow();
if($inventory instanceof AnvilInventory){
$world = $inventory->getHolder()->getWorld();
if(mt_rand(0, 12) === 0){
$anvilBlock = $world->getBlock($inventory->getHolder());
if($anvilBlock instanceof Anvil){
$newDamage = $anvilBlock->getDamage() + 1;
if($newDamage > Anvil::VERY_DAMAGED){
$newBlock = VanillaBlocks::AIR();
$world->addSound($inventory->getHolder(), new AnvilBreakSound());
}else{
$newBlock = $anvilBlock->setDamage($newDamage);
}
$world->setBlock($inventory->getHolder(), $newBlock);
}
}
$world->addSound($inventory->getHolder(), new AnvilUseSound());
}
}
protected function callExecuteEvent() : bool{
if($this->baseItem === null){
throw new AssumptionFailedError("Expected that baseItem are not null before executing the event");
}
$ev = new PlayerUseAnvilEvent($this->source, $this->baseItem, $this->materialItem, $this->expectedResult->getOutput(), $this->customName, $this->expectedResult->getXpCost());
$ev->call();
return !$ev->isCancelled();
}
}

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

@ -69,6 +69,7 @@ class Item implements \JsonSerializable{
public const TAG_DISPLAY_NAME = "Name";
public const TAG_DISPLAY_LORE = "Lore";
public const TAG_REPAIR_COST = "RepairCost";
public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death";
@ -84,6 +85,7 @@ class Item implements \JsonSerializable{
protected string $customName = "";
/** @var string[] */
protected array $lore = [];
protected int $anvilRepairCost = 0;
/** TODO: this needs to die in a fire */
protected ?CompoundTag $blockEntityTag = null;
@ -282,6 +284,30 @@ class Item implements \JsonSerializable{
return $this;
}
/**
* Returns the anvil repair cost of the item.
* This value is used in anvil to determine the XP cost of repairing the item.
*
* In vanilla, this value is stored in the "RepairCost" tag.
*/
public function getAnvilRepairCost() : int{
return $this->anvilRepairCost;
}
/**
* Sets the anvil repair cost value of the item.
* This value is used in anvil to determine the XP cost of repairing the item.
* Higher cost means more XP is required to repair the item.
*
* In vanilla, this value is stored in the "RepairCost" tag.
*
* @return $this
*/
public function setAnvilRepairCost(int $cost) : self{
$this->anvilRepairCost = $cost;
return $this;
}
/**
* @throws NbtException
*/
@ -338,6 +364,7 @@ class Item implements \JsonSerializable{
}
$this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0;
$this->anvilRepairCost = $tag->getInt(self::TAG_REPAIR_COST, 0);
}
protected function serializeCompoundTag(CompoundTag $tag) : void{
@ -406,6 +433,12 @@ class Item implements \JsonSerializable{
}else{
$tag->removeTag(self::TAG_KEEP_ON_DEATH);
}
if($this->anvilRepairCost > 0){
$tag->setInt(self::TAG_REPAIR_COST, $this->anvilRepairCost);
}else{
$tag->removeTag(self::TAG_REPAIR_COST);
}
}
public function getCount() : int{

View File

@ -111,7 +111,8 @@ final class LegacyStringToItemParser{
*/
public function parse(string $input) : Item{
$key = $this->reprocess($input);
$b = explode(":", $key);
//TODO: this should be limited to 2 parts, but 3 preserves old behaviour when given a string like 351:4:1
$b = explode(":", $key, limit: 3);
if(!isset($b[1])){
$meta = 0;

View File

@ -897,6 +897,18 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::ENCHANTMENT_FROSTWALKER, []);
}
public static function enchantment_heavy_weapon_breach() : Translatable{
return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_BREACH, []);
}
public static function enchantment_heavy_weapon_density() : Translatable{
return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_DENSITY, []);
}
public static function enchantment_heavy_weapon_windburst() : Translatable{
return new Translatable(KnownTranslationKeys::ENCHANTMENT_HEAVY_WEAPON_WINDBURST, []);
}
public static function enchantment_knockback() : Translatable{
return new Translatable(KnownTranslationKeys::ENCHANTMENT_KNOCKBACK, []);
}
@ -1108,6 +1120,318 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::ITEM_RECORD_WARD_DESC, []);
}
public static function itemGroup_name_anvil() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ANVIL, []);
}
public static function itemGroup_name_arrow() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ARROW, []);
}
public static function itemGroup_name_axe() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_AXE, []);
}
public static function itemGroup_name_banner() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BANNER, []);
}
public static function itemGroup_name_banner_pattern() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BANNER_PATTERN, []);
}
public static function itemGroup_name_bed() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BED, []);
}
public static function itemGroup_name_boat() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BOAT, []);
}
public static function itemGroup_name_boots() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BOOTS, []);
}
public static function itemGroup_name_bundles() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BUNDLES, []);
}
public static function itemGroup_name_buttons() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_BUTTONS, []);
}
public static function itemGroup_name_candles() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CANDLES, []);
}
public static function itemGroup_name_chalkboard() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHALKBOARD, []);
}
public static function itemGroup_name_chest() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHEST, []);
}
public static function itemGroup_name_chestboat() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHESTBOAT, []);
}
public static function itemGroup_name_chestplate() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CHESTPLATE, []);
}
public static function itemGroup_name_compounds() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_COMPOUNDS, []);
}
public static function itemGroup_name_concrete() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CONCRETE, []);
}
public static function itemGroup_name_concretePowder() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CONCRETEPOWDER, []);
}
public static function itemGroup_name_cookedFood() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_COOKEDFOOD, []);
}
public static function itemGroup_name_coral() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CORAL, []);
}
public static function itemGroup_name_coral_decorations() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CORAL_DECORATIONS, []);
}
public static function itemGroup_name_crop() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_CROP, []);
}
public static function itemGroup_name_door() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_DOOR, []);
}
public static function itemGroup_name_dye() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_DYE, []);
}
public static function itemGroup_name_enchantedBook() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ENCHANTEDBOOK, []);
}
public static function itemGroup_name_fence() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FENCE, []);
}
public static function itemGroup_name_fenceGate() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FENCEGATE, []);
}
public static function itemGroup_name_firework() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FIREWORK, []);
}
public static function itemGroup_name_fireworkStars() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FIREWORKSTARS, []);
}
public static function itemGroup_name_flower() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_FLOWER, []);
}
public static function itemGroup_name_glass() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLASS, []);
}
public static function itemGroup_name_glassPane() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLASSPANE, []);
}
public static function itemGroup_name_glazedTerracotta() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GLAZEDTERRACOTTA, []);
}
public static function itemGroup_name_goatHorn() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GOATHORN, []);
}
public static function itemGroup_name_grass() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_GRASS, []);
}
public static function itemGroup_name_helmet() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HELMET, []);
}
public static function itemGroup_name_hoe() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HOE, []);
}
public static function itemGroup_name_horseArmor() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_HORSEARMOR, []);
}
public static function itemGroup_name_leaves() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LEAVES, []);
}
public static function itemGroup_name_leggings() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LEGGINGS, []);
}
public static function itemGroup_name_lingeringPotion() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LINGERINGPOTION, []);
}
public static function itemGroup_name_log() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_LOG, []);
}
public static function itemGroup_name_minecart() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MINECART, []);
}
public static function itemGroup_name_miscFood() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MISCFOOD, []);
}
public static function itemGroup_name_mobEgg() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MOBEGG, []);
}
public static function itemGroup_name_monsterStoneEgg() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MONSTERSTONEEGG, []);
}
public static function itemGroup_name_mushroom() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_MUSHROOM, []);
}
public static function itemGroup_name_netherWartBlock() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_NETHERWARTBLOCK, []);
}
public static function itemGroup_name_ominousBottle() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_OMINOUSBOTTLE, []);
}
public static function itemGroup_name_ore() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_ORE, []);
}
public static function itemGroup_name_permission() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PERMISSION, []);
}
public static function itemGroup_name_pickaxe() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PICKAXE, []);
}
public static function itemGroup_name_planks() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PLANKS, []);
}
public static function itemGroup_name_potion() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_POTION, []);
}
public static function itemGroup_name_pressurePlate() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PRESSUREPLATE, []);
}
public static function itemGroup_name_products() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_PRODUCTS, []);
}
public static function itemGroup_name_rail() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RAIL, []);
}
public static function itemGroup_name_rawFood() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RAWFOOD, []);
}
public static function itemGroup_name_record() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_RECORD, []);
}
public static function itemGroup_name_sandstone() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SANDSTONE, []);
}
public static function itemGroup_name_sapling() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SAPLING, []);
}
public static function itemGroup_name_seed() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SEED, []);
}
public static function itemGroup_name_shovel() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SHOVEL, []);
}
public static function itemGroup_name_shulkerBox() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SHULKERBOX, []);
}
public static function itemGroup_name_sign() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SIGN, []);
}
public static function itemGroup_name_skull() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SKULL, []);
}
public static function itemGroup_name_slab() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SLAB, []);
}
public static function itemGroup_name_splashPotion() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SPLASHPOTION, []);
}
public static function itemGroup_name_stainedClay() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STAINEDCLAY, []);
}
public static function itemGroup_name_stairs() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STAIRS, []);
}
public static function itemGroup_name_stone() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STONE, []);
}
public static function itemGroup_name_stoneBrick() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_STONEBRICK, []);
}
public static function itemGroup_name_sword() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_SWORD, []);
}
public static function itemGroup_name_trapdoor() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_TRAPDOOR, []);
}
public static function itemGroup_name_walls() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WALLS, []);
}
public static function itemGroup_name_wood() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOD, []);
}
public static function itemGroup_name_wool() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOL, []);
}
public static function itemGroup_name_woolCarpet() : Translatable{
return new Translatable(KnownTranslationKeys::ITEMGROUP_NAME_WOOLCARPET, []);
}
public static function kick_admin() : Translatable{
return new Translatable(KnownTranslationKeys::KICK_ADMIN, []);
}

View File

@ -194,6 +194,9 @@ final class KnownTranslationKeys{
public const ENCHANTMENT_FIRE = "enchantment.fire";
public const ENCHANTMENT_FISHINGSPEED = "enchantment.fishingSpeed";
public const ENCHANTMENT_FROSTWALKER = "enchantment.frostwalker";
public const ENCHANTMENT_HEAVY_WEAPON_BREACH = "enchantment.heavy_weapon.breach";
public const ENCHANTMENT_HEAVY_WEAPON_DENSITY = "enchantment.heavy_weapon.density";
public const ENCHANTMENT_HEAVY_WEAPON_WINDBURST = "enchantment.heavy_weapon.windburst";
public const ENCHANTMENT_KNOCKBACK = "enchantment.knockback";
public const ENCHANTMENT_LOOTBONUS = "enchantment.lootBonus";
public const ENCHANTMENT_LOOTBONUSDIGGER = "enchantment.lootBonusDigger";
@ -245,6 +248,84 @@ final class KnownTranslationKeys{
public const ITEM_RECORD_STRAD_DESC = "item.record_strad.desc";
public const ITEM_RECORD_WAIT_DESC = "item.record_wait.desc";
public const ITEM_RECORD_WARD_DESC = "item.record_ward.desc";
public const ITEMGROUP_NAME_ANVIL = "itemGroup.name.anvil";
public const ITEMGROUP_NAME_ARROW = "itemGroup.name.arrow";
public const ITEMGROUP_NAME_AXE = "itemGroup.name.axe";
public const ITEMGROUP_NAME_BANNER = "itemGroup.name.banner";
public const ITEMGROUP_NAME_BANNER_PATTERN = "itemGroup.name.banner_pattern";
public const ITEMGROUP_NAME_BED = "itemGroup.name.bed";
public const ITEMGROUP_NAME_BOAT = "itemGroup.name.boat";
public const ITEMGROUP_NAME_BOOTS = "itemGroup.name.boots";
public const ITEMGROUP_NAME_BUNDLES = "itemGroup.name.bundles";
public const ITEMGROUP_NAME_BUTTONS = "itemGroup.name.buttons";
public const ITEMGROUP_NAME_CANDLES = "itemGroup.name.candles";
public const ITEMGROUP_NAME_CHALKBOARD = "itemGroup.name.chalkboard";
public const ITEMGROUP_NAME_CHEST = "itemGroup.name.chest";
public const ITEMGROUP_NAME_CHESTBOAT = "itemGroup.name.chestboat";
public const ITEMGROUP_NAME_CHESTPLATE = "itemGroup.name.chestplate";
public const ITEMGROUP_NAME_COMPOUNDS = "itemGroup.name.compounds";
public const ITEMGROUP_NAME_CONCRETE = "itemGroup.name.concrete";
public const ITEMGROUP_NAME_CONCRETEPOWDER = "itemGroup.name.concretePowder";
public const ITEMGROUP_NAME_COOKEDFOOD = "itemGroup.name.cookedFood";
public const ITEMGROUP_NAME_CORAL = "itemGroup.name.coral";
public const ITEMGROUP_NAME_CORAL_DECORATIONS = "itemGroup.name.coral_decorations";
public const ITEMGROUP_NAME_CROP = "itemGroup.name.crop";
public const ITEMGROUP_NAME_DOOR = "itemGroup.name.door";
public const ITEMGROUP_NAME_DYE = "itemGroup.name.dye";
public const ITEMGROUP_NAME_ENCHANTEDBOOK = "itemGroup.name.enchantedBook";
public const ITEMGROUP_NAME_FENCE = "itemGroup.name.fence";
public const ITEMGROUP_NAME_FENCEGATE = "itemGroup.name.fenceGate";
public const ITEMGROUP_NAME_FIREWORK = "itemGroup.name.firework";
public const ITEMGROUP_NAME_FIREWORKSTARS = "itemGroup.name.fireworkStars";
public const ITEMGROUP_NAME_FLOWER = "itemGroup.name.flower";
public const ITEMGROUP_NAME_GLASS = "itemGroup.name.glass";
public const ITEMGROUP_NAME_GLASSPANE = "itemGroup.name.glassPane";
public const ITEMGROUP_NAME_GLAZEDTERRACOTTA = "itemGroup.name.glazedTerracotta";
public const ITEMGROUP_NAME_GOATHORN = "itemGroup.name.goatHorn";
public const ITEMGROUP_NAME_GRASS = "itemGroup.name.grass";
public const ITEMGROUP_NAME_HELMET = "itemGroup.name.helmet";
public const ITEMGROUP_NAME_HOE = "itemGroup.name.hoe";
public const ITEMGROUP_NAME_HORSEARMOR = "itemGroup.name.horseArmor";
public const ITEMGROUP_NAME_LEAVES = "itemGroup.name.leaves";
public const ITEMGROUP_NAME_LEGGINGS = "itemGroup.name.leggings";
public const ITEMGROUP_NAME_LINGERINGPOTION = "itemGroup.name.lingeringPotion";
public const ITEMGROUP_NAME_LOG = "itemGroup.name.log";
public const ITEMGROUP_NAME_MINECART = "itemGroup.name.minecart";
public const ITEMGROUP_NAME_MISCFOOD = "itemGroup.name.miscFood";
public const ITEMGROUP_NAME_MOBEGG = "itemGroup.name.mobEgg";
public const ITEMGROUP_NAME_MONSTERSTONEEGG = "itemGroup.name.monsterStoneEgg";
public const ITEMGROUP_NAME_MUSHROOM = "itemGroup.name.mushroom";
public const ITEMGROUP_NAME_NETHERWARTBLOCK = "itemGroup.name.netherWartBlock";
public const ITEMGROUP_NAME_OMINOUSBOTTLE = "itemGroup.name.ominousBottle";
public const ITEMGROUP_NAME_ORE = "itemGroup.name.ore";
public const ITEMGROUP_NAME_PERMISSION = "itemGroup.name.permission";
public const ITEMGROUP_NAME_PICKAXE = "itemGroup.name.pickaxe";
public const ITEMGROUP_NAME_PLANKS = "itemGroup.name.planks";
public const ITEMGROUP_NAME_POTION = "itemGroup.name.potion";
public const ITEMGROUP_NAME_PRESSUREPLATE = "itemGroup.name.pressurePlate";
public const ITEMGROUP_NAME_PRODUCTS = "itemGroup.name.products";
public const ITEMGROUP_NAME_RAIL = "itemGroup.name.rail";
public const ITEMGROUP_NAME_RAWFOOD = "itemGroup.name.rawFood";
public const ITEMGROUP_NAME_RECORD = "itemGroup.name.record";
public const ITEMGROUP_NAME_SANDSTONE = "itemGroup.name.sandstone";
public const ITEMGROUP_NAME_SAPLING = "itemGroup.name.sapling";
public const ITEMGROUP_NAME_SEED = "itemGroup.name.seed";
public const ITEMGROUP_NAME_SHOVEL = "itemGroup.name.shovel";
public const ITEMGROUP_NAME_SHULKERBOX = "itemGroup.name.shulkerBox";
public const ITEMGROUP_NAME_SIGN = "itemGroup.name.sign";
public const ITEMGROUP_NAME_SKULL = "itemGroup.name.skull";
public const ITEMGROUP_NAME_SLAB = "itemGroup.name.slab";
public const ITEMGROUP_NAME_SPLASHPOTION = "itemGroup.name.splashPotion";
public const ITEMGROUP_NAME_STAINEDCLAY = "itemGroup.name.stainedClay";
public const ITEMGROUP_NAME_STAIRS = "itemGroup.name.stairs";
public const ITEMGROUP_NAME_STONE = "itemGroup.name.stone";
public const ITEMGROUP_NAME_STONEBRICK = "itemGroup.name.stoneBrick";
public const ITEMGROUP_NAME_SWORD = "itemGroup.name.sword";
public const ITEMGROUP_NAME_TRAPDOOR = "itemGroup.name.trapdoor";
public const ITEMGROUP_NAME_WALLS = "itemGroup.name.walls";
public const ITEMGROUP_NAME_WOOD = "itemGroup.name.wood";
public const ITEMGROUP_NAME_WOOL = "itemGroup.name.wool";
public const ITEMGROUP_NAME_WOOLCARPET = "itemGroup.name.woolCarpet";
public const KICK_ADMIN = "kick.admin";
public const KICK_ADMIN_REASON = "kick.admin.reason";
public const KICK_REASON_CHEAT = "kick.reason.cheat";

View File

@ -71,7 +71,7 @@ class Language{
foreach($files as $file){
try{
$code = explode(".", $file)[0];
$code = explode(".", $file, limit: 2)[0];
$strings = self::loadLang($path, $code);
if(isset($strings[KnownTranslationKeys::LANGUAGE_NAME])){
$result[$code] = $strings[KnownTranslationKeys::LANGUAGE_NAME];

View File

@ -690,7 +690,7 @@ class InventoryManager{
}
public function syncCreative() : void{
$this->session->sendDataPacket(CreativeInventoryCache::getInstance()->getCache($this->player->getCreativeInventory()));
$this->session->sendDataPacket(CreativeInventoryCache::getInstance()->buildPacket($this->player->getCreativeInventory(), $this->session));
}
/**

View File

@ -72,9 +72,11 @@ final class JwtUtils{
* @throws JwtException
*/
public static function split(string $jwt) : array{
$v = explode(".", $jwt);
//limit of 4 allows us to detect too many parts without having to split the string up into a potentially large
//number of parts
$v = explode(".", $jwt, limit: 4);
if(count($v) !== 3){
throw new JwtException("Expected exactly 3 JWT parts, got " . count($v));
throw new JwtException("Expected exactly 3 JWT parts delimited by a period");
}
return [$v[0], $v[1], $v[2]]; //workaround phpstan bug
}

View File

@ -1058,7 +1058,7 @@ class NetworkSession{
];
$layers = [
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 0.1),
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, $for->getFlightSpeedMultiplier(), 1, 0.1),
];
if(!$for->hasBlockCollision()){
//TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a
@ -1068,7 +1068,7 @@ class NetworkSession{
$layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
AbilitiesLayer::ABILITY_FLYING => true,
], null, null);
], null, null, null);
}
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(

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,23 +23,30 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\inventory\CreativeCategory;
use pocketmine\inventory\CreativeInventory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeItemEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\utils\SingletonTrait;
use function is_string;
use function spl_object_id;
use const PHP_INT_MIN;
final class CreativeInventoryCache{
use SingletonTrait;
/**
* @var CreativeContentPacket[]
* @phpstan-var array<int, CreativeContentPacket>
* @var CreativeInventoryCacheEntry[]
* @phpstan-var array<int, CreativeInventoryCacheEntry>
*/
private array $caches = [];
public function getCache(CreativeInventory $inventory) : CreativeContentPacket{
private function getCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{
$id = spl_object_id($inventory);
if(!isset($this->caches[$id])){
$inventory->getDestructorCallbacks()->add(function() use ($id) : void{
@ -48,7 +55,7 @@ final class CreativeInventoryCache{
$inventory->getContentChangedCallbacks()->add(function() use ($id) : void{
unset($this->caches[$id]);
});
$this->caches[$id] = $this->buildCreativeInventoryCache($inventory);
$this->caches[$id] = $this->buildCacheEntry($inventory);
}
return $this->caches[$id];
}
@ -56,14 +63,91 @@ final class CreativeInventoryCache{
/**
* Rebuild the cache for the given inventory.
*/
private function buildCreativeInventoryCache(CreativeInventory $inventory) : CreativeContentPacket{
$entries = [];
private function buildCacheEntry(CreativeInventory $inventory) : CreativeInventoryCacheEntry{
$categories = [];
$groups = [];
$typeConverter = TypeConverter::getInstance();
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
foreach($inventory->getAll() as $k => $item){
$entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item));
$nextIndex = 0;
$groupIndexes = [];
$itemGroupIndexes = [];
foreach($inventory->getAllEntries() as $k => $entry){
$group = $entry->getGroup();
$category = $entry->getCategory();
if($group === null){
$groupId = PHP_INT_MIN;
}else{
$groupId = spl_object_id($group);
unset($groupIndexes[$category->name][PHP_INT_MIN]); //start a new anonymous group for this category
}
//group object may be reused by multiple categories
if(!isset($groupIndexes[$category->name][$groupId])){
$groupIndexes[$category->name][$groupId] = $nextIndex++;
$categories[] = $category;
$groups[] = $group;
}
$itemGroupIndexes[$k] = $groupIndexes[$category->name][$groupId];
}
return CreativeContentPacket::create($entries);
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
$items = [];
foreach($inventory->getAllEntries() as $k => $entry){
$items[] = new CreativeItemEntry(
$k,
$typeConverter->coreItemStackToNet($entry->getItem()),
$itemGroupIndexes[$k]
);
}
return new CreativeInventoryCacheEntry($categories, $groups, $items);
}
public function buildPacket(CreativeInventory $inventory, NetworkSession $session) : CreativeContentPacket{
$player = $session->getPlayer() ?? throw new \LogicException("Cannot prepare creative data for a session without a player");
$language = $player->getLanguage();
$forceLanguage = $player->getServer()->isLanguageForced();
$typeConverter = $session->getTypeConverter();
$cachedEntry = $this->getCacheEntry($inventory);
$translate = function(Translatable|string $translatable) use ($session, $language, $forceLanguage) : string{
if(is_string($translatable)){
$message = $translatable;
}elseif(!$forceLanguage){
[$message,] = $session->prepareClientTranslatableMessage($translatable);
}else{
$message = $language->translate($translatable);
}
return $message;
};
$groupEntries = [];
foreach($cachedEntry->categories as $index => $category){
$group = $cachedEntry->groups[$index];
$categoryId = match ($category) {
CreativeCategory::CONSTRUCTION => CreativeContentPacket::CATEGORY_CONSTRUCTION,
CreativeCategory::NATURE => CreativeContentPacket::CATEGORY_NATURE,
CreativeCategory::EQUIPMENT => CreativeContentPacket::CATEGORY_EQUIPMENT,
CreativeCategory::ITEMS => CreativeContentPacket::CATEGORY_ITEMS
};
if($group === null){
$groupEntries[] = new CreativeGroupEntry($categoryId, "", ItemStack::null());
}else{
$groupIcon = $group->getIcon();
//TODO: HACK! In 1.21.60, Workaround glitchy behaviour when an item is used as an icon for a group it
//doesn't belong to. Without this hack, both instances of the item will show a +, but neither of them
//will actually expand the group work correctly.
$groupIcon->getNamedTag()->setInt("___GroupBugWorkaround___", $index);
$groupName = $group->getName();
$groupEntries[] = new CreativeGroupEntry(
$categoryId,
$translate($groupName),
$typeConverter->coreItemStackToNet($groupIcon)
);
}
}
return CreativeContentPacket::create($groupEntries, $cachedEntry->items);
}
}

View File

@ -0,0 +1,48 @@
<?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\network\mcpe\cache;
use pocketmine\inventory\CreativeCategory;
use pocketmine\inventory\CreativeGroup;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeItemEntry;
final class CreativeInventoryCacheEntry{
/**
* @param CreativeCategory[] $categories
* @param CreativeGroup[]|null[] $groups
* @param CreativeItemEntry[] $items
*
* @phpstan-param list<CreativeCategory> $categories
* @phpstan-param list<CreativeGroup|null> $groups
* @phpstan-param list<CreativeItemEntry> $items
*/
public function __construct(
public readonly array $categories,
public readonly array $groups,
public readonly array $items,
){
//NOOP
}
}

View File

@ -23,10 +23,15 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use function base64_decode;
use function is_array;
use function is_bool;
use function is_int;
@ -41,12 +46,15 @@ final class ItemTypeDictionaryFromDataHelper{
throw new AssumptionFailedError("Invalid item list format");
}
$emptyNBT = new CacheableNbt(new CompoundTag());
$nbtSerializer = new LittleEndianNbtSerializer();
$params = [];
foreach(Utils::promoteKeys($table) as $name => $entry){
if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"])){
if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"], $entry["version"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"]) || !is_int($entry["version"]) || !(is_string($componentNbt = $entry["component_nbt"] ?? null) || $componentNbt === null)){
throw new AssumptionFailedError("Invalid item list format");
}
$params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"]);
$params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"], $entry["version"], $componentNbt === null ? $emptyNBT : new CacheableNbt($nbtSerializer->read(ErrorToExceptionHandler::trapAndRemoveFalse(fn() => base64_decode($componentNbt, true)))->mustGetCompoundTag()));
}
return new ItemTypeDictionary($params);
}

View File

@ -23,11 +23,14 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\utils\AnvilHelper;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\AnvilTransaction;
use pocketmine\inventory\transaction\CraftingTransaction;
use pocketmine\inventory\transaction\EnchantingTransaction;
use pocketmine\inventory\transaction\InventoryTransaction;
@ -39,6 +42,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
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;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeOptionalStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CreativeCreateStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction;
@ -290,7 +294,7 @@ class ItemStackRequestExecutor{
* @throws ItemStackRequestProcessException
*/
private function assertDoingCrafting() : void{
if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction){
if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction && !$this->specialTransaction instanceof AnvilTransaction){
if($this->specialTransaction === null){
throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action");
}else{
@ -348,6 +352,15 @@ class ItemStackRequestExecutor{
}
}elseif($action instanceof CraftRecipeAutoStackRequestAction){
$this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
}elseif($action instanceof CraftRecipeOptionalStackRequestAction){
$window = $this->player->getCurrentWindow();
if($window instanceof AnvilInventory){
$result = AnvilHelper::calculateResult($window->getInput(), $window->getMaterial(), $this->request->getFilterStrings()[0] ?? null, $this->player->isCreative());
if($result !== null){
$this->specialTransaction = new AnvilTransaction($this->player, $result, $this->request->getFilterStrings()[0] ?? null);
$this->setNextCreatedItem($result->getOutput());
}
}
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
$this->assertDoingCrafting();
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance

View File

@ -28,6 +28,7 @@ use pocketmine\network\mcpe\cache\CraftingDataCache;
use pocketmine\network\mcpe\cache\StaticPacketCache;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ItemRegistryPacket;
use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
@ -110,9 +111,11 @@ class PreSpawnPacketHandler extends PacketHandler{
new NetworkPermissions(disableClientSounds: true),
[],
0,
$typeConverter->getItemTypeDictionary()->getEntries(),
));
$this->session->getLogger()->debug("Sending items");
$this->session->sendDataPacket(ItemRegistryPacket::create($typeConverter->getItemTypeDictionary()->getEntries()));
$this->session->getLogger()->debug("Sending actor identifiers");
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());

View File

@ -148,7 +148,9 @@ class BanEntry{
return null;
}
$parts = explode("|", trim($str));
//we expect at most 5 parts, but accept 6 in case of an extra unexpected delimiter
//we don't want to include unexpected data into the ban reason
$parts = explode("|", trim($str), limit: 6);
$entry = new BanEntry(trim(array_shift($parts)));
if(count($parts) > 0){
$entry->setCreated(self::parseDate(array_shift($parts)));

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

@ -26,6 +26,7 @@ namespace pocketmine\resourcepacks;
use pocketmine\utils\Config;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Filesystem\Path;
use function array_keys;
use function copy;
@ -103,9 +104,14 @@ class ResourcePackManager{
try{
$newPack = $this->loadPackFromPath(Path::join($this->path, $pack));
$this->resourcePacks[] = $newPack;
$index = strtolower($newPack->getPackId());
if(!Uuid::isValid($index)){
//TODO: we should use Uuid in ResourcePack interface directly but that would break BC
//for now we need to validate this here to make sure it doesn't cause crashes later on
throw new ResourcePackException("Invalid UUID ($index)");
}
$this->uuidList[$index] = $newPack;
$this->resourcePacks[] = $newPack;
$keyPath = Path::join($this->path, $pack . ".key");
if(file_exists($keyPath)){
@ -190,6 +196,11 @@ class ResourcePackManager{
$resourcePacks = [];
foreach($resourceStack as $pack){
$uuid = strtolower($pack->getPackId());
if(!Uuid::isValid($uuid)){
//TODO: we should use Uuid in ResourcePack interface directly but that would break BC
//for now we need to validate this here to make sure it doesn't cause crashes later on
throw new \InvalidArgumentException("Invalid resource pack UUID ($uuid)");
}
if(isset($uuidList[$uuid])){
throw new \InvalidArgumentException("Cannot load two resource pack with the same UUID ($uuid)");
}

View File

@ -54,6 +54,7 @@ use const CASE_LOWER;
use const JSON_BIGINT_AS_STRING;
use const JSON_PRETTY_PRINT;
use const JSON_THROW_ON_ERROR;
use const PHP_INT_MAX;
use const YAML_UTF8_ENCODING;
/**
@ -339,7 +340,7 @@ class Config{
}
public function setNested(string $key, mixed $value) : void{
$vars = explode(".", $key);
$vars = explode(".", $key, limit: PHP_INT_MAX);
$base = array_shift($vars);
if(!isset($this->config[$base])){
@ -366,7 +367,7 @@ class Config{
return $this->nestedCache[$key];
}
$vars = explode(".", $key);
$vars = explode(".", $key, limit: PHP_INT_MAX);
$base = array_shift($vars);
if(isset($this->config[$base])){
$base = $this->config[$base];
@ -390,7 +391,7 @@ class Config{
$this->nestedCache = [];
$this->changed = true;
$vars = explode(".", $key);
$vars = explode(".", $key, limit: PHP_INT_MAX);
$currentNode = &$this->config;
while(count($vars) > 0){
@ -495,7 +496,7 @@ class Config{
*/
public static function parseList(string $content) : array{
$result = [];
foreach(explode("\n", trim(str_replace("\r\n", "\n", $content))) as $v){
foreach(explode("\n", trim(str_replace("\r\n", "\n", $content)), limit: PHP_INT_MAX) as $v){
$v = trim($v);
if($v === ""){
continue;

View File

@ -60,6 +60,7 @@ use const CURLOPT_RETURNTRANSFER;
use const CURLOPT_SSL_VERIFYHOST;
use const CURLOPT_SSL_VERIFYPEER;
use const CURLOPT_TIMEOUT_MS;
use const PHP_INT_MAX;
use const SOCK_DGRAM;
use const SOL_UDP;
@ -227,9 +228,10 @@ class Internet{
$rawHeaders = substr($raw, 0, $headerSize);
$body = substr($raw, $headerSize);
$headers = [];
foreach(explode("\r\n\r\n", $rawHeaders) as $rawHeaderGroup){
//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) as $line){
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]);

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

@ -264,14 +264,7 @@ final class Utils{
}
/**
* Returns the current Operating System
* Windows => win
* MacOS => mac
* iOS => ios
* Android => android
* Linux => Linux
* BSD => bsd
* Other => other
* @return string one of the Utils::OS_* constants
*/
public static function getOS(bool $recalculate = false) : string{
if(self::$os === null || $recalculate){
@ -376,7 +369,7 @@ final class Utils{
debug_zval_dump($value);
$contents = ob_get_contents();
if($contents === false) throw new AssumptionFailedError("ob_get_contents() should never return false here");
$ret = explode("\n", $contents);
$ret = explode("\n", $contents, limit: 2);
ob_end_clean();
if(preg_match('/^.* refcount\\(([0-9]+)\\)\\{$/', trim($ret[0]), $m) > 0){
@ -594,7 +587,7 @@ final class Utils{
* @phpstan-param \Closure(TMemberType) : void $validator
*/
public static function validateArrayValueType(array $array, \Closure $validator) : void{
foreach($array as $k => $v){
foreach(Utils::promoteKeys($array) as $k => $v){
try{
$validator($v);
}catch(\TypeError $e){

View File

@ -375,6 +375,8 @@ class World implements ChunkManager{
private \Logger $logger;
private RuntimeBlockStateRegistry $blockStateRegistry;
/**
* @phpstan-return ChunkPosHash
*/
@ -488,6 +490,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();
@ -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;
@ -1394,7 +1397,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 +1531,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 +1582,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 +1661,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 +1864,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 +1983,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();
@ -2573,7 +2642,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

View File

@ -51,12 +51,12 @@ use function time;
class BedrockWorldData extends BaseNbtWorldData{
public const CURRENT_STORAGE_VERSION = 10;
public const CURRENT_STORAGE_NETWORK_VERSION = 748;
public const CURRENT_STORAGE_NETWORK_VERSION = 776;
public const CURRENT_CLIENT_VERSION_TARGET = [
1, //major
21, //minor
40, //patch
1, //revision
60, //patch
33, //revision
0 //is beta
];

View File

@ -26,10 +26,12 @@ namespace pocketmine\world\generator;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\item\LegacyStringToItemParser;
use pocketmine\item\LegacyStringToItemParserException;
use pocketmine\world\World;
use function array_map;
use function explode;
use function preg_match;
use function preg_match_all;
use const PHP_INT_MAX;
/**
* @internal
@ -70,7 +72,7 @@ final class FlatGeneratorOptions{
*/
public static function parseLayers(string $layers) : array{
$result = [];
$split = array_map('\trim', explode(',', $layers));
$split = array_map('\trim', explode(',', $layers, limit: World::Y_MAX - World::Y_MIN));
$y = 0;
$itemParser = LegacyStringToItemParser::getInstance();
foreach($split as $line){
@ -96,7 +98,7 @@ final class FlatGeneratorOptions{
* @throws InvalidGeneratorOptionsException
*/
public static function parsePreset(string $presetString) : self{
$preset = explode(";", $presetString);
$preset = explode(";", $presetString, limit: 4);
$blocks = $preset[1] ?? "";
$biomeId = (int) ($preset[2] ?? BiomeIds::PLAINS);
$optionsString = $preset[3] ?? "";
@ -109,9 +111,10 @@ final class FlatGeneratorOptions{
$params = true;
if($matches[3][$i] !== ""){
$params = [];
$p = explode(" ", $matches[3][$i]);
$p = explode(" ", $matches[3][$i], limit: PHP_INT_MAX);
foreach($p as $k){
$k = explode("=", $k);
//TODO: this should be limited to 2 parts, but 3 preserves old behaviour when given e.g. treecount=20=1
$k = explode("=", $k, limit: 3);
if(isset($k[1])){
$params[$k[0]] = $k[1];
}

View File

@ -18,6 +18,12 @@ parameters:
count: 1
path: ../../../src/Server.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/Server.php
-
message: '#^Method pocketmine\\Server\:\:getCommandAliases\(\) should return array\<string, list\<string\>\> but returns array\<string, array\<mixed\>\>\.$#'
identifier: return.type
@ -48,18 +54,18 @@ parameters:
count: 1
path: ../../../src/VersionInfo.php
-
message: '#^Method pocketmine\\VersionInfo\:\:GIT_HASH\(\) should return string but returns mixed\.$#'
identifier: return.type
count: 1
path: ../../../src/VersionInfo.php
-
message: '#^Static property pocketmine\\VersionInfo\:\:\$gitHash \(string\|null\) does not accept mixed\.$#'
identifier: assign.propertyType
count: 1
path: ../../../src/VersionInfo.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/block/Block.php
-
message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getBlockAt\(\) expects int, float\|int given\.$#'
identifier: argument.type
@ -516,6 +522,12 @@ parameters:
count: 3
path: ../../../src/block/tile/Spawnable.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/block/tile/TileFactory.php
-
message: '#^Parameter \#1 \$x of method pocketmine\\world\\World\:\:getPotentialLightAt\(\) expects int, float\|int given\.$#'
identifier: argument.type
@ -918,24 +930,6 @@ parameters:
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Parameter \#1 \$haystack of function stripos expects string, mixed given\.$#'
identifier: argument.type
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Parameter \#2 \$subject of function preg_match expects string, mixed given\.$#'
identifier: argument.type
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Parameter \#3 \$subject of function str_replace expects array\<string\>\|string, mixed given\.$#'
identifier: argument.type
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$authors \(array\<string\>\) does not accept list\<mixed\>\.$#'
identifier: assign.propertyType
@ -961,13 +955,13 @@ parameters:
path: ../../../src/plugin/PluginManager.php
-
message: '#^Method pocketmine\\resourcepacks\\ZippedResourcePack\:\:getPackSize\(\) should return int but returns int\<0, max\>\|false\.$#'
identifier: return.type
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/resourcepacks/ZippedResourcePack.php
path: ../../../src/plugin/PluginManager.php
-
message: '#^Method pocketmine\\resourcepacks\\ZippedResourcePack\:\:getSha256\(\) should return string but returns string\|false\.$#'
message: '#^Method pocketmine\\resourcepacks\\ZippedResourcePack\:\:getPackSize\(\) should return int but returns int\<0, max\>\|false\.$#'
identifier: return.type
count: 1
path: ../../../src/resourcepacks/ZippedResourcePack.php
@ -1009,7 +1003,7 @@ parameters:
path: ../../../src/utils/Config.php
-
message: '#^Parameter \#1 \$config of static method pocketmine\\utils\\Config\:\:writeProperties\(\) expects array\<int\|string, bool\|float\|int\|string\>, array\<int\|string, mixed\> given\.$#'
message: '#^Parameter \#1 \$config of static method pocketmine\\utils\\Config\:\:writeProperties\(\) expects array\<int\|string, bool\|float\|int\|string\>, array\<mixed\> given\.$#'
identifier: argument.type
count: 1
path: ../../../src/utils/Config.php
@ -1278,6 +1272,12 @@ parameters:
count: 1
path: ../../../src/world/format/io/region/RegionLoader.php
-
message: '#^Dynamic new is not allowed\.$#'
identifier: pocketmine.new.dynamic
count: 1
path: ../../../src/world/generator/GeneratorRegisterTask.php
-
message: '#^Method pocketmine\\world\\generator\\biome\\BiomeSelector\:\:pickBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#'
identifier: return.type

View File

@ -114,12 +114,6 @@ parameters:
count: 1
path: ../../../src/crash/CrashDump.php
-
message: '#^Call to function assert\(\) with false and ''unknown hit type'' will always evaluate to false\.$#'
identifier: function.impossibleType
count: 1
path: ../../../src/entity/projectile/Projectile.php
-
message: '#^Property pocketmine\\item\\WritableBookBase\:\:\$pages \(list\<pocketmine\\item\\WritableBookPage\>\) does not accept non\-empty\-array\<int, pocketmine\\item\\WritableBookPage\>\.$#'
identifier: assign.propertyType

View File

@ -0,0 +1,55 @@
<?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\phpstan\rules;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\New_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
/**
* @phpstan-implements Rule<New_>
*/
final class DisallowDynamicNewRule implements Rule{
public function getNodeType() : string{
return New_::class;
}
public function processNode(Node $node, Scope $scope) : array{
/** @var New_ $node */
if($node->class instanceof Expr){
return [
RuleErrorBuilder::message("Dynamic new is not allowed.")
->tip("For factories, use closures instead. Closures can implement custom logic, are statically analyzable, and don't restrict the constructor signature.")
->identifier("pocketmine.new.dynamic")
->build()
];
}
return [];
}
}

View File

@ -0,0 +1,92 @@
<?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\phpstan\rules;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function count;
/**
* @phpstan-implements Rule<FuncCall>
*/
final class ExplodeLimitRule implements Rule{
private ReflectionProvider $reflectionProvider;
public function __construct(
ReflectionProvider $reflectionProvider
){
$this->reflectionProvider = $reflectionProvider;
}
public function getNodeType() : string{
return FuncCall::class;
}
public function processNode(Node $node, Scope $scope) : array{
if(!$node->name instanceof Name){
return [];
}
if(!$this->reflectionProvider->hasFunction($node->name, $scope)){
return [];
}
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
if($functionReflection->getName() !== 'explode'){
return [];
}
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$node->getArgs(),
$functionReflection->getVariants(),
$functionReflection->getNamedArgumentsVariants(),
);
$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
if($normalizedFuncCall === null){
return [];
}
$count = count($normalizedFuncCall->getArgs());
if($count !== 3){
return [
RuleErrorBuilder::message('The $limit parameter of explode() must be set to prevent malicious client data wasting resources.')
->identifier("pocketmine.explode.limit")
->build()
];
}
return [];
}
}

View File

@ -0,0 +1,194 @@
<?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\crafting;
use Generator;
use PHPUnit\Framework\TestCase;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\VanillaEnchantments;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use function floor;
class AnvilCraftTest extends TestCase{
public static function materialRepairRecipeProvider() : Generator{
yield "No repair available" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND(),
null
];
yield "Repair one damage" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(1),
VanillaItems::DIAMOND(),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair one damage with more materials than expected" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(1),
VanillaItems::DIAMOND()->setCount(2),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), VanillaItems::DIAMOND())
];
$diamondPickaxeQuarter = (int) floor(VanillaItems::DIAMOND_PICKAXE()->getMaxDurability() / 4);
yield "Repair one quarter" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter),
VanillaItems::DIAMOND()->setCount(1),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair one quarter plus 1" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter + 1),
VanillaItems::DIAMOND()->setCount(1),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE()->setDamage(1), null)
];
yield "Repair more than one quarter" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter * 2),
VanillaItems::DIAMOND()->setCount(2),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE(), null)
];
yield "Repair more than one quarter with more materials than expected" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage($diamondPickaxeQuarter * 2),
VanillaItems::DIAMOND()->setCount(3),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE(), VanillaItems::DIAMOND()->setCount(1))
];
}
/**
* @dataProvider materialRepairRecipeProvider
*/
public function testMaterialRepairRecipe(Item $base, Item $material, ?AnvilCraftResult $expected) : void{
$recipe = new MaterialRepairRecipe(
new WildcardRecipeIngredient(),
new WildcardRecipeIngredient()
);
self::assertAnvilCraftResultEquals($expected, $recipe->getResultFor($base, $material));
}
public static function itemSelfCombineRecipeProvider() : Generator{
yield "Combine two identical items without damage and enchants" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE(),
null
];
yield "Enchant on base item and no enchants on material with no damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
VanillaItems::DIAMOND_PICKAXE(),
null
];
yield "No enchant on base item and one enchant on material" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)), null)
];
yield "Combine two identical items with damage" => [
VanillaItems::DIAMOND_PICKAXE()->setDamage(10),
VanillaItems::DIAMOND_PICKAXE(),
new AnvilCraftResult(1, VanillaItems::DIAMOND_PICKAXE()->setDamage(0), null)
];
yield "Combine two identical items with damage for material" => [
VanillaItems::DIAMOND_PICKAXE(),
VanillaItems::DIAMOND_PICKAXE()->setDamage(10),
null
];
yield "Combine two identical items with different enchantments" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with different enchantments with damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))->setDamage(10),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
new AnvilCraftResult(4, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with different enchantments with damage for material" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1))->setDamage(10),
new AnvilCraftResult(2, VanillaItems::DIAMOND_PICKAXE()
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 2))
->addEnchantment(new EnchantmentInstance(VanillaEnchantments::UNBREAKING(), 1)),
null)
];
yield "Combine two identical items with same enchantment level" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
new AnvilCraftResult(8, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2)), null)
];
yield "Combine two identical items with same enchantment level and damage" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1))->setDamage(10),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
new AnvilCraftResult(10, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2))->setDamage(0), null)
];
yield "Combine two identical items with same enchantment level and damage for material" => [
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1)),
VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 1))->setDamage(10),
new AnvilCraftResult(8, VanillaItems::DIAMOND_PICKAXE()->addEnchantment(new EnchantmentInstance(VanillaEnchantments::FORTUNE(), 2)), null)
];
}
/**
* @dataProvider itemSelfCombineRecipeProvider
*/
public function testItemSelfCombineRecipe(Item $base, Item $combined, ?AnvilCraftResult $expected) : void{
$recipe = new ItemSelfCombineRecipe(new WildcardRecipeIngredient());
self::assertAnvilCraftResultEquals($expected, $recipe->getResultFor($base, $combined));
}
private static function assertAnvilCraftResultEquals(?AnvilCraftResult $expected, ?AnvilCraftResult $actual) : void{
if($expected === null){
self::assertNull($actual, "Recipe did not match expected result");
return;
}else{
self::assertNotNull($actual, "Recipe did not match expected result");
}
self::assertEquals($expected->getXpCost(), $actual->getXpCost(), "XP cost did not match expected result");
self::assertTrue($expected->getOutput()->equalsExact($actual->getOutput()), "Recipe output did not match expected result");
$sacrificeResult = $expected->getSacrificeResult();
if($sacrificeResult !== null){
$resultExpected = $actual->getSacrificeResult();
self::assertNotNull($resultExpected, "Recipe sacrifice result did not match expected result");
self::assertTrue($sacrificeResult->equalsExact($resultExpected), "Recipe sacrifice result did not match expected result");
}else{
self::assertNull($actual->getSacrificeResult(), "Recipe sacrifice result did not match expected result");
}
}
}

View File

@ -0,0 +1,37 @@
<?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\crafting;
use pocketmine\item\Item;
final class WildcardRecipeIngredient implements RecipeIngredient{
public function __toString() : string{
return "WildcardRecipeIngredient()";
}
public function accepts(Item $item) : bool{
return true;
}
}

View File

@ -51,8 +51,10 @@ use function array_unique;
use function array_values;
use function count;
use function dirname;
use function fclose;
use function file_exists;
use function file_put_contents;
use function fopen;
use function fwrite;
use function get_class;
use function get_debug_type;
@ -885,6 +887,44 @@ function cmdUpdateAll(array $argv) : int{
return 0;
}
/**
* @param string[] $argv
*/
function cmdDumpTable(array $argv) : int{
$tableFile = $argv[2];
$outputFile = $argv[3];
$output = fopen($outputFile, 'wb');
if($output === false){
fwrite(STDERR, "Failed to open output file: $outputFile\n");
return 1;
}
$table = loadUpgradeTableFromFile($tableFile, reverse: false);
ksort($table, SORT_STRING);
foreach(Utils::stringifyKeys($table) as $oldName => $mappings){
fwrite($output, "---------- MAPPING LIST: $oldName ----------\n");
foreach($mappings as $mapping){
$oldNbt = $mapping->old->toVanillaNbt();
$oldNbt->setInt("version", $mapping->new->getVersion());
//intentionally not reused result of toVanillaNbt otherwise output wouldn't include version
fwrite($output, "OLD: " . $mapping->old->toVanillaNbt() . "\n");
if(!$oldNbt->equals($mapping->new->toVanillaNbt())){
fwrite($output, "NEW: " . $mapping->new->toVanillaNbt() . "\n");
}else{
fwrite($output, "NEW: version bump only (" . $mapping->new->getVersion() . ")\n");
}
fwrite($output, "-----\n");
}
}
fclose($output);
\GlobalLogger::get()->info("Table dump file $outputFile generated successfully.");
return 0;
}
/**
* @param string[] $argv
*/
@ -893,7 +933,8 @@ function main(array $argv) : int{
"generate" => [["palette upgrade table file", "schema output file"], cmdGenerate(...)],
"test" => [["palette upgrade table file", "schema output file"], cmdTest(...)],
"update" => [["schema input file", "old palette file", "updated schema output file"], cmdUpdate(...)],
"update-all" => [["schema folder", "path to BlockPaletteArchive"], cmdUpdateAll(...)]
"update-all" => [["schema folder", "path to BlockPaletteArchive"], cmdUpdateAll(...)],
"dump-table" => [["palette upgrade table file", "txt output file"], cmdDumpTable(...)]
];
$selected = $argv[1] ?? null;

View File

@ -35,6 +35,7 @@ use pocketmine\crafting\json\SmithingTrimRecipeData;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\data\bedrock\item\ItemTypeNames;
use pocketmine\inventory\json\CreativeGroupData;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\CompoundTag;
@ -48,15 +49,17 @@ use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\ItemRegistryPacket;
use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeGroupEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraDataShield;
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\network\mcpe\protocol\types\recipe\ComplexAliasItemDescriptor;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
@ -134,6 +137,19 @@ class ParserPacketHandler extends PacketHandler{
return base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($statePropertiesTag)));
}
/**
* @param ItemStackData[] $items
*/
private function creativeGroupEntryToJson(CreativeGroupEntry $entry, array $items) : CreativeGroupData{
$data = new CreativeGroupData();
$data->group_name = $entry->getCategoryName();
$data->group_icon = $entry->getIcon()->getId() === 0 ? null : $this->itemStackToJson($entry->getIcon());
$data->items = $items;
return $data;
}
private function itemStackToJson(ItemStack $itemStack) : ItemStackData{
if($itemStack->getId() === 0){
throw new InvalidArgumentException("Cannot serialize a null itemstack");
@ -234,31 +250,68 @@ class ParserPacketHandler extends PacketHandler{
}
public function handleStartGame(StartGamePacket $packet) : bool{
$this->itemTypeDictionary = new ItemTypeDictionary($packet->itemTable);
echo "updating legacy item ID mapping table\n";
$table = [];
foreach($packet->itemTable as $entry){
$table[$entry->getStringId()] = [
"runtime_id" => $entry->getNumericId(),
"component_based" => $entry->isComponentBased()
];
}
ksort($table, SORT_STRING);
file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n");
foreach(Utils::promoteKeys($packet->levelSettings->experiments->getExperiments()) as $name => $experiment){
echo "Experiment \"$name\" is " . ($experiment ? "" : "not ") . "active\n";
}
return true;
}
public function handleItemRegistry(ItemRegistryPacket $packet) : bool{
$this->itemTypeDictionary = new ItemTypeDictionary($packet->getEntries());
echo "updating legacy item ID mapping table\n";
$emptyNBT = new CompoundTag();
$table = [];
foreach($packet->getEntries() as $entry){
$table[$entry->getStringId()] = [
"runtime_id" => $entry->getNumericId(),
"component_based" => $entry->isComponentBased(),
"version" => $entry->getVersion(),
];
$componentNBT = $entry->getComponentNbt()->getRoot();
if(!$componentNBT->equals($emptyNBT)){
$table[$entry->getStringId()]["component_nbt"] = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($componentNBT)));
}
}
ksort($table, SORT_STRING);
file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n");
echo "updating item registry\n";
$items = array_map(function(ItemTypeEntry $entry) : array{
return self::objectToOrderedArray($entry);
}, $packet->getEntries());
file_put_contents($this->bedrockDataPath . '/item_registry.json', json_encode($items, JSON_PRETTY_PRINT) . "\n");
return true;
}
public function handleCreativeContent(CreativeContentPacket $packet) : bool{
echo "updating creative inventory data\n";
$items = array_map(function(CreativeContentEntry $entry) : array{
return self::objectToOrderedArray($this->itemStackToJson($entry->getItem()));
}, $packet->getEntries());
file_put_contents($this->bedrockDataPath . '/creativeitems.json', json_encode($items, JSON_PRETTY_PRINT) . "\n");
$groupItems = [];
foreach($packet->getItems() as $itemEntry){
$groupItems[$itemEntry->getGroupId()][] = $this->itemStackToJson($itemEntry->getItem());
}
static $typeMap = [
CreativeContentPacket::CATEGORY_CONSTRUCTION => "construction",
CreativeContentPacket::CATEGORY_NATURE => "nature",
CreativeContentPacket::CATEGORY_EQUIPMENT => "equipment",
CreativeContentPacket::CATEGORY_ITEMS => "items",
];
$groupCategories = [];
foreach(Utils::promoteKeys($packet->getGroups()) as $groupId => $group){
$category = $typeMap[$group->getCategoryId()] ?? throw new PacketHandlingException("Unknown creative category ID " . $group->getCategoryId());
//FIXME: objectToOrderedArray might mess with the order of groupItems
//this isn't a problem right now because it's a list, but could cause problems in the future
$groupCategories[$category][] = self::objectToOrderedArray($this->creativeGroupEntryToJson($group, $groupItems[$groupId]));
}
foreach(Utils::promoteKeys($groupCategories) as $category => $categoryGroups){
file_put_contents($this->bedrockDataPath . '/creative/' . $category . '.json', json_encode($categoryGroups, JSON_PRETTY_PRINT) . "\n");
}
return true;
}
@ -571,7 +624,7 @@ function main(array $argv) : int{
}
foreach($packets as $lineNum => $line){
$parts = explode(':', $line);
$parts = explode(':', $line, limit: 3);
if(count($parts) !== 2){
fwrite(STDERR, 'Wrong packet format at line ' . ($lineNum + 1) . ', expected read:base64 or write:base64');
return 1;