mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-10-16 03:51:37 +00:00
104
changelogs/5.34.md
Normal file
104
changelogs/5.34.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# 5.34.0
|
||||
Released 26th September 2025.
|
||||
|
||||
This is a minor feature release containing performance improvements, new gameplay features, new API additions and network changes.
|
||||
|
||||
**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
|
||||
- PocketMine-MP now requires and uses [`pmmp/ext-encoding`](https://github.com/pmmp/ext-encoding) version `1.0.0`, a PHP extension designed to significantly improve performance of data encoding and decoding (@dktapps).
|
||||
- This first pass only implements support in low-risk areas, such as network protocol and read-only data. Further integration will follow in future minor versions with additional performance improvements.
|
||||
- While ext-encoding has been heavily tested, we can't be sure there won't be major issues once it reaches production. Please report any bugs you find on the GitHub issue tracker.
|
||||
- New Bedrock OpenID authentication support has been implemented (@dries-c, @dktapps).
|
||||
- This system fetches keys from an API provided by Microsoft. Therefore, your server must now have internet access to authenticate players.
|
||||
- `/timings paste` now creates private reports by default on `timings.pmmp.io` (@dktapps).
|
||||
- Private reports require an access token to view, so your timings reports can no longer be viewed by others just by guessing the ID.
|
||||
- If you're using a custom timings host, be sure to update it to get support for this feature.
|
||||
- The command will generate a warning in the console if the target timings host doesn't support private reports.
|
||||
|
||||
## Performance
|
||||
- Significantly improved performance of packet encoding and decoding using `ext-encoding` (@dktapps).
|
||||
- Unnecessary NBT is now stripped from items before sending them over the network. This significantly improves performance when working with writable books, shulker boxes, etc. (@dktapps).
|
||||
- Improved performance of item saving in `ItemSerializer` by avoiding slow `hasNamedTag()` call followed by `getNamedTag()` (both will rebuild the NBT) (@dktapps).
|
||||
|
||||
## Gameplay
|
||||
- Implemented basic Trident functionality (@IvanCraft623).
|
||||
- Implemented Firework and Firework Star (@IvanCraft623).
|
||||
- Editing the rear side of signs is now supported (@dktapps).
|
||||
- Sneaking hitbox height has been adjusted to match vanilla (@Dasciam).
|
||||
|
||||
## API
|
||||
### General
|
||||
- `pocketmine/nbt` version `1.2.0` is now used ([changelog](https://github.com/pmmp/NBT/releases/tag/1.2.0)).
|
||||
- `pmmp/ext-encoding` version `1.0.0` is now required and used.
|
||||
- This can be used as a faster alternative to `BinaryStream` and `Binary` in most use cases. However, please note that its API is very different, and it hasn't been battle-tested yet.
|
||||
- A recent JetBrains IDE stub can be found in our [custom stubs repository](https://github.com/pmmp/phpstorm-stubs/blob/fork/encoding/encoding.php).
|
||||
|
||||
### `pocketmine\block`
|
||||
- The following API methods have been added:
|
||||
- `public BaseSign->getFaceText(bool $frontFace) : SignText`
|
||||
- `public BaseSign->setFaceText(bool $frontFace, SignText $text) : $this`
|
||||
- `public BaseSign->updateFaceText(Player $author, bool $frontFace, SignText $text) : bool` - called by the network system when a player edits a sign, triggers `SignChangeEvent` etc.
|
||||
- `protected BaseSign->getHitboxCenter() : Vector3` - returns the center of the sign's hitbox, used to decide which face of the sign the player is editing
|
||||
- `protected BaseSign->getFacingDegrees() : float` (to become abstract in PM6) - returns the horizontal facing of the sign in degrees, used to decide which face of the sign the player is editing
|
||||
- The following API methods have been deprecated:
|
||||
- `public BaseSign->getText() : SignText` - use `getFaceText()` instead
|
||||
- `public BaseSign->setText(SignText $text) : $this` - use `setFaceText()` instead
|
||||
- `public BaseSign->updateText(Player $author, SignText $text) : bool` - use `updateFaceText()` instead
|
||||
|
||||
### `pocketmine\entity`
|
||||
- The following API classes have been added:
|
||||
- `NeverSavedWithChunkEntity` - implement this instead of overriding `canSaveWithChunk()` if your entity will never need a save ID
|
||||
- Used currently by `Player` and `FireworkRocket`.
|
||||
- `animation\FireworkParticlesAnimation`
|
||||
- `object\FireworkRocket`
|
||||
- `projectile\Trident`
|
||||
- The following API methods have been added:
|
||||
- `public Living->getSneakOffset() : float` - returns how much the entity's hitbox is shortened and eye height lowered when sneaking (0 by default)
|
||||
- `protected Projectile->despawnsOnEntityHit() : bool` - returns `true` by default, overridden by tridents (to be removed in a future major version in favour of cleaner BC-breaking methods)
|
||||
|
||||
### `pocketmine\event\block`
|
||||
- The following API methods have been added:
|
||||
- `public SignChangeEvent->isFrontFace() : bool` - returns `true` if the front face of the sign is being edited, `false` for the rear face
|
||||
|
||||
### `pocketmine\inventory\transaction`
|
||||
- `InventoryTransaction` no longer shuffles actions before executing a transaction.
|
||||
- This was intended to prevent dependency on weird client behaviour, but it is no longer necessary, as the order is now consistent since the introduction of the `ItemStackRequest` system.
|
||||
|
||||
### `pocketmine\item`
|
||||
- The following API classes have been added:
|
||||
- `FireworkRocket`
|
||||
- `FireworkRocketExplosion`
|
||||
- `FireworkRocketType` (enum)
|
||||
- `FireworkStar`
|
||||
- `Trident`
|
||||
- The following API methods have been added:
|
||||
- `VanillaItems::FIREWORK_ROCKET() : FireworkRocket`
|
||||
- `VanillaItems::FIREWORK_STAR() : FireworkStar`
|
||||
- `VanillaItems::TRIDENT() : Trident`
|
||||
|
||||
### `pocketmine\player`
|
||||
- The following API methods have signature changes:
|
||||
- `Player->openSignEditor()` now accepts an optional `bool $frontFace = true` parameter
|
||||
|
||||
### `pocketmine\world\sound`
|
||||
- The following API classes have been added:
|
||||
- `FireworkCrackleSound`
|
||||
- `FireworkExplosionSound`
|
||||
- `FireworkLargeExplosionSound`
|
||||
- `FireworkLaunchSound`
|
||||
- `TridentHitEntitySound`
|
||||
- `TridentHitBlockSound`
|
||||
- `TridentThrowSound`
|
||||
|
||||
## Internals
|
||||
- Many low-risk data handling areas have been switched to use `ext-encoding`, including:
|
||||
- Bedrock packets
|
||||
- Bedrock chunk serialization
|
||||
- `FastChunkSerializer` (used for transmitting chunks between threads)
|
||||
- GS4 Query
|
||||
- Auxiliary read-only data loading in the `pocketmine\data\bedrock` package
|
@@ -12,6 +12,7 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-date": "*",
|
||||
"ext-encoding": "~1.0.0",
|
||||
"ext-gmp": "*",
|
||||
"ext-hash": "*",
|
||||
"ext-igbinary": "^3.0.1",
|
||||
@@ -36,7 +37,7 @@
|
||||
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
|
||||
"pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100",
|
||||
"pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100",
|
||||
"pocketmine/bedrock-protocol": "~40.0.0+bedrock-1.21.100",
|
||||
"pocketmine/bedrock-protocol": "~50.0.0+bedrock-1.21.100",
|
||||
"pocketmine/binaryutils": "^0.2.1",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/color": "^0.3.0",
|
||||
@@ -44,7 +45,7 @@
|
||||
"pocketmine/locale-data": "~2.25.0",
|
||||
"pocketmine/log": "^0.4.0",
|
||||
"pocketmine/math": "~1.0.0",
|
||||
"pocketmine/nbt": "~1.1.0",
|
||||
"pocketmine/nbt": "~1.2.0",
|
||||
"pocketmine/raklib": "~1.2.0",
|
||||
"pocketmine/raklib-ipc": "~1.0.0",
|
||||
"pocketmine/snooze": "^0.5.0",
|
||||
|
31
composer.lock
generated
31
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "008c888b5812dda09a0ec6e425453153",
|
||||
"content-hash": "0d71d3fba23ba8c4734cac59b9e10129",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@@ -256,19 +256,20 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/bedrock-protocol",
|
||||
"version": "40.0.0+bedrock-1.21.100",
|
||||
"version": "50.0.0+bedrock-1.21.100",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/BedrockProtocol.git",
|
||||
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca"
|
||||
"reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
|
||||
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
|
||||
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/2d7aa27a5537ae593fb1c39158648ea462fef72a",
|
||||
"reference": "2d7aa27a5537ae593fb1c39158648ea462fef72a",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-encoding": "~1.0.0",
|
||||
"ext-json": "*",
|
||||
"php": "^8.1",
|
||||
"pocketmine/binaryutils": "^0.2.0",
|
||||
@@ -296,9 +297,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/40.0.0+bedrock-1.21.100"
|
||||
"source": "https://github.com/pmmp/BedrockProtocol/tree/50.0.0+bedrock-1.21.100"
|
||||
},
|
||||
"time": "2025-08-06T15:13:45+00:00"
|
||||
"time": "2025-09-20T23:09:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
@@ -576,16 +577,16 @@
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/nbt",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pmmp/NBT.git",
|
||||
"reference": "c3c7b0a7295daeaf7873d90fed5c5d10381d12e1"
|
||||
"reference": "51b8d6a97065fb93e0b4f660b65164b6e1ed2fff"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pmmp/NBT/zipball/c3c7b0a7295daeaf7873d90fed5c5d10381d12e1",
|
||||
"reference": "c3c7b0a7295daeaf7873d90fed5c5d10381d12e1",
|
||||
"url": "https://api.github.com/repos/pmmp/NBT/zipball/51b8d6a97065fb93e0b4f660b65164b6e1ed2fff",
|
||||
"reference": "51b8d6a97065fb93e0b4f660b65164b6e1ed2fff",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -595,7 +596,8 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.0",
|
||||
"phpstan/phpstan": "2.1.0",
|
||||
"phpstan/phpstan": "2.1.27",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
@@ -612,9 +614,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.1.1"
|
||||
"source": "https://github.com/pmmp/NBT/tree/1.2.0"
|
||||
},
|
||||
"time": "2025-03-09T01:46:03+00:00"
|
||||
"time": "2025-09-19T18:09:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/raklib",
|
||||
@@ -2796,6 +2798,7 @@
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-date": "*",
|
||||
"ext-encoding": "~1.0.0",
|
||||
"ext-gmp": "*",
|
||||
"ext-hash": "*",
|
||||
"ext-igbinary": "^3.0.1",
|
||||
|
@@ -98,6 +98,7 @@ namespace pocketmine {
|
||||
"crypto" => "php-crypto",
|
||||
"ctype" => "ctype",
|
||||
"date" => "Date",
|
||||
"encoding" => "pmmp/ext-encoding",
|
||||
"gmp" => "GMP",
|
||||
"hash" => "Hash",
|
||||
"igbinary" => "igbinary",
|
||||
@@ -155,6 +156,12 @@ namespace pocketmine {
|
||||
}
|
||||
}
|
||||
|
||||
if(($encoding_version = phpversion("encoding")) !== false){
|
||||
if(version_compare($encoding_version, "1.0.0") < 0 || version_compare($encoding_version, "2.0.0") >= 0){
|
||||
$messages[] = "pmmp/ext-encoding ^1.0.0 is required, while you have $encoding_version.";
|
||||
}
|
||||
}
|
||||
|
||||
if(extension_loaded("pocketmine")){
|
||||
$messages[] = "The native PocketMine extension is no longer supported.";
|
||||
}
|
||||
|
@@ -50,6 +50,7 @@ use pocketmine\lang\Language;
|
||||
use pocketmine\lang\LanguageNotFoundException;
|
||||
use pocketmine\lang\Translatable;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\network\mcpe\auth\AuthKeyProvider;
|
||||
use pocketmine\network\mcpe\compression\CompressBatchPromise;
|
||||
use pocketmine\network\mcpe\compression\CompressBatchTask;
|
||||
use pocketmine\network\mcpe\compression\Compressor;
|
||||
@@ -270,6 +271,7 @@ class Server{
|
||||
private int $maxPlayers;
|
||||
|
||||
private bool $onlineMode = true;
|
||||
private AuthKeyProvider $authKeyProvider;
|
||||
|
||||
private Network $network;
|
||||
private bool $networkCompressionAsync = true;
|
||||
@@ -982,6 +984,8 @@ class Server{
|
||||
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
|
||||
}
|
||||
|
||||
$this->authKeyProvider = new AuthKeyProvider(new \PrefixedLogger($this->logger, "Minecraft Auth Key Provider"), $this->asyncPool);
|
||||
|
||||
if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
|
||||
$this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
|
||||
}
|
||||
@@ -1800,6 +1804,13 @@ class Server{
|
||||
return $this->forceLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getAuthKeyProvider() : AuthKeyProvider{
|
||||
return $this->authKeyProvider;
|
||||
}
|
||||
|
||||
public function getNetwork() : Network{
|
||||
return $this->network;
|
||||
}
|
||||
|
@@ -31,8 +31,8 @@ use function str_repeat;
|
||||
|
||||
final class VersionInfo{
|
||||
public const NAME = "PocketMine-MP";
|
||||
public const BASE_VERSION = "5.33.3";
|
||||
public const IS_DEVELOPMENT_BUILD = true;
|
||||
public const BASE_VERSION = "5.34.0";
|
||||
public const IS_DEVELOPMENT_BUILD = false;
|
||||
public const BUILD_CHANNEL = "stable";
|
||||
|
||||
/**
|
||||
|
@@ -41,14 +41,19 @@ use pocketmine\utils\TextFormat;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\sound\DyeUseSound;
|
||||
use pocketmine\world\sound\InkSacUseSound;
|
||||
use function abs;
|
||||
use function array_map;
|
||||
use function assert;
|
||||
use function atan2;
|
||||
use function fmod;
|
||||
use function rad2deg;
|
||||
use function strlen;
|
||||
|
||||
abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
use WoodTypeTrait;
|
||||
|
||||
protected SignText $text;
|
||||
protected SignText $text; //TODO: rename this (BC break)
|
||||
protected SignText $backText;
|
||||
private bool $waxed = false;
|
||||
|
||||
protected ?int $editorEntityRuntimeId = null;
|
||||
@@ -63,6 +68,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
$this->woodType = $woodType;
|
||||
parent::__construct($idInfo, $name, $typeInfo);
|
||||
$this->text = new SignText();
|
||||
$this->backText = new SignText();
|
||||
$this->asItemCallback = $asItemCallback;
|
||||
}
|
||||
|
||||
@@ -71,6 +77,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
$tile = $this->position->getWorld()->getTile($this->position);
|
||||
if($tile instanceof TileSign){
|
||||
$this->text = $tile->getText();
|
||||
$this->backText = $tile->getBackText();
|
||||
$this->waxed = $tile->isWaxed();
|
||||
$this->editorEntityRuntimeId = $tile->getEditorEntityRuntimeId();
|
||||
}
|
||||
@@ -83,6 +90,7 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
$tile = $this->position->getWorld()->getTile($this->position);
|
||||
assert($tile instanceof TileSign);
|
||||
$tile->setText($this->text);
|
||||
$tile->setBackText($this->backText);
|
||||
$tile->setWaxed($this->waxed);
|
||||
$tile->setEditorEntityRuntimeId($this->editorEntityRuntimeId);
|
||||
}
|
||||
@@ -127,11 +135,11 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
}
|
||||
}
|
||||
|
||||
private function doSignChange(SignText $newText, Player $player, Item $item) : bool{
|
||||
$ev = new SignChangeEvent($this, $player, $newText);
|
||||
private function doSignChange(SignText $newText, Player $player, Item $item, bool $frontFace) : bool{
|
||||
$ev = new SignChangeEvent($this, $player, $newText, $frontFace);
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$this->text = $ev->getNewText();
|
||||
$this->setFaceText($frontFace, $ev->getNewText());
|
||||
$this->position->getWorld()->setBlock($this->position, $this);
|
||||
$item->pop();
|
||||
return true;
|
||||
@@ -140,8 +148,9 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
return false;
|
||||
}
|
||||
|
||||
private function changeSignGlowingState(bool $glowing, Player $player, Item $item) : bool{
|
||||
if($this->text->isGlowing() !== $glowing && $this->doSignChange(new SignText($this->text->getLines(), $this->text->getBaseColor(), $glowing), $player, $item)){
|
||||
private function changeSignGlowingState(bool $glowing, Player $player, Item $item, bool $frontFace) : bool{
|
||||
$text = $this->getFaceText($frontFace);
|
||||
if($text->isGlowing() !== $glowing && $this->doSignChange(new SignText($text->getLines(), $text->getBaseColor(), $glowing), $player, $item, $frontFace)){
|
||||
$this->position->getWorld()->addSound($this->position, new InkSacUseSound());
|
||||
return true;
|
||||
}
|
||||
@@ -168,6 +177,8 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
return true;
|
||||
}
|
||||
|
||||
$frontFace = $this->interactsFront($this->getHitboxCenter(), $player->getPosition(), $this->getFacingDegrees());
|
||||
|
||||
$dyeColor = $item instanceof Dye ? $item->getColor() : match($item->getTypeId()){
|
||||
ItemTypeIds::BONE_MEAL => DyeColor::WHITE,
|
||||
ItemTypeIds::LAPIS_LAZULI => DyeColor::BLUE,
|
||||
@@ -176,40 +187,82 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
};
|
||||
if($dyeColor !== null){
|
||||
$color = $dyeColor === DyeColor::BLACK ? new Color(0, 0, 0) : $dyeColor->getRgbValue();
|
||||
$text = $this->getFaceText($frontFace);
|
||||
if(
|
||||
$color->toARGB() !== $this->text->getBaseColor()->toARGB() &&
|
||||
$this->doSignChange(new SignText($this->text->getLines(), $color, $this->text->isGlowing()), $player, $item)
|
||||
$color->toARGB() !== $text->getBaseColor()->toARGB() &&
|
||||
$this->doSignChange(new SignText($text->getLines(), $color, $text->isGlowing()), $player, $item, $frontFace)
|
||||
){
|
||||
$this->position->getWorld()->addSound($this->position, new DyeUseSound());
|
||||
return true;
|
||||
}
|
||||
}elseif(match($item->getTypeId()){
|
||||
ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item),
|
||||
ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item),
|
||||
ItemTypeIds::INK_SAC => $this->changeSignGlowingState(false, $player, $item, $frontFace),
|
||||
ItemTypeIds::GLOW_INK_SAC => $this->changeSignGlowingState(true, $player, $item, $frontFace),
|
||||
ItemTypeIds::HONEYCOMB => $this->wax($player, $item),
|
||||
default => false
|
||||
}){
|
||||
return true;
|
||||
}
|
||||
|
||||
$player->openSignEditor($this->position);
|
||||
$player->openSignEditor($this->position, $frontFace);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function interactsFront(Vector3 $hitboxCenter, Vector3 $playerPosition, float $signFacingDegrees) : bool{
|
||||
$playerCenterDiffX = $playerPosition->x - $hitboxCenter->x;
|
||||
$playerCenterDiffZ = $playerPosition->z - $hitboxCenter->z;
|
||||
|
||||
$f1 = rad2deg(atan2($playerCenterDiffZ, $playerCenterDiffX)) - 90.0;
|
||||
|
||||
$rotationDiff = $signFacingDegrees - $f1;
|
||||
$rotation = fmod($rotationDiff + 180.0, 360.0) - 180.0; // Normalize to [-180, 180]
|
||||
return abs($rotation) <= 90.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of the sign's hitbox. Used to decide which face of the sign to open when a player interacts.
|
||||
*/
|
||||
protected function getHitboxCenter() : Vector3{
|
||||
return $this->position->add(0.5, 0.5, 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: make this abstract (BC break)
|
||||
*/
|
||||
protected function getFacingDegrees() : float{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing information about the sign text.
|
||||
* @deprecated
|
||||
* @see self::getFaceText()
|
||||
*/
|
||||
public function getText() : SignText{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/** @return $this */
|
||||
/**
|
||||
* @deprecated
|
||||
* @see self::setFaceText()
|
||||
* @return $this
|
||||
*/
|
||||
public function setText(SignText $text) : self{
|
||||
$this->text = $text;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFaceText(bool $frontFace) : SignText{
|
||||
return $frontFace ? $this->text : $this->backText;
|
||||
}
|
||||
|
||||
/** @return $this */
|
||||
public function setFaceText(bool $frontFace, SignText $text) : self{
|
||||
$frontFace ? $this->text = $text : $this->backText = $text;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the sign has been waxed using a honeycomb. If true, the sign cannot be edited by a player.
|
||||
*/
|
||||
@@ -234,13 +287,21 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @see self::updateFaceText()
|
||||
*/
|
||||
public function updateText(Player $author, SignText $text) : bool{
|
||||
return $this->updateFaceText($author, true, $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the player controller (network session) to update the sign text, firing events as appropriate.
|
||||
*
|
||||
* @return bool if the sign update was successful.
|
||||
* @throws \UnexpectedValueException if the text payload is too large
|
||||
*/
|
||||
public function updateText(Player $author, SignText $text) : bool{
|
||||
public function updateFaceText(Player $author, bool $frontFace, SignText $text) : bool{
|
||||
$size = 0;
|
||||
foreach($text->getLines() as $line){
|
||||
$size += strlen($line);
|
||||
@@ -248,15 +309,16 @@ abstract class BaseSign extends Transparent implements WoodMaterial{
|
||||
if($size > 1000){
|
||||
throw new \UnexpectedValueException($author->getName() . " tried to write $size bytes of text onto a sign (bigger than max 1000)");
|
||||
}
|
||||
$oldText = $this->getFaceText($frontFace);
|
||||
$ev = new SignChangeEvent($this, $author, new SignText(array_map(function(string $line) : string{
|
||||
return TextFormat::clean($line, false);
|
||||
}, $text->getLines()), $this->text->getBaseColor(), $this->text->isGlowing()));
|
||||
}, $text->getLines()), $oldText->getBaseColor(), $oldText->isGlowing()), $frontFace);
|
||||
if($this->waxed || $this->editorEntityRuntimeId !== $author->getId()){
|
||||
$ev->cancel();
|
||||
}
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$this->setText($ev->getNewText());
|
||||
$this->setFaceText($frontFace, $ev->getNewText());
|
||||
$this->setEditorEntityRuntimeId(null);
|
||||
$this->position->getWorld()->setBlock($this->position, $this);
|
||||
return true;
|
||||
|
@@ -26,6 +26,8 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pmmp\encoding\BE;
|
||||
use pmmp\encoding\LE;
|
||||
use pocketmine\block\tile\Spawnable;
|
||||
use pocketmine\block\tile\Tile;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
@@ -49,7 +51,6 @@ use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\Position;
|
||||
@@ -98,9 +99,10 @@ class Block{
|
||||
* of operations required to compute the state ID (micro optimization).
|
||||
*/
|
||||
private static function computeStateIdXorMask(int $typeId) : int{
|
||||
//TODO: the mixed byte order here is probably a mistake, but it doesn't break anything for now
|
||||
return
|
||||
$typeId << self::INTERNAL_STATE_DATA_BITS |
|
||||
(Binary::readLong(hash('xxh3', Binary::writeLLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK);
|
||||
(BE::unpackSignedLong(hash('xxh3', LE::packSignedLong($typeId), binary: true)) & self::INTERNAL_STATE_DATA_MASK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -58,4 +58,8 @@ final class CeilingCenterHangingSign extends BaseSign implements SignLikeRotatio
|
||||
$supportBlock->getSupportType(Facing::DOWN)->hasCenterSupport() ||
|
||||
$supportBlock->hasTypeTag(BlockTypeTags::HANGING_SIGN);
|
||||
}
|
||||
|
||||
protected function getFacingDegrees() : float{
|
||||
return $this->rotation * 22.5;
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
|
||||
final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing{
|
||||
@@ -65,4 +66,14 @@ final class CeilingEdgesHangingSign extends BaseSign implements HorizontalFacing
|
||||
$supportBlock->getSupportType(Facing::DOWN) === SupportType::FULL ||
|
||||
(($supportBlock instanceof WallHangingSign || $supportBlock instanceof CeilingEdgesHangingSign) && Facing::axis($supportBlock->getFacing()) === Facing::axis($this->facing));
|
||||
}
|
||||
|
||||
protected function getFacingDegrees() : float{
|
||||
return match($this->facing){
|
||||
Facing::SOUTH => 0,
|
||||
Facing::WEST => 90,
|
||||
Facing::NORTH => 180,
|
||||
Facing::EAST => 270,
|
||||
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -48,4 +48,8 @@ final class FloorSign extends BaseSign implements SignLikeRotation{
|
||||
}
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
protected function getFacingDegrees() : float{
|
||||
return $this->rotation * 22.5;
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
|
||||
final class WallHangingSign extends BaseSign implements HorizontalFacing{
|
||||
@@ -78,4 +79,14 @@ final class WallHangingSign extends BaseSign implements HorizontalFacing{
|
||||
($block instanceof WallHangingSign && Facing::axis(Facing::rotateY($block->getFacing(), clockwise: true)) === Facing::axis($face)) ||
|
||||
$block->getSupportType(Facing::opposite($face)) === SupportType::FULL;
|
||||
}
|
||||
|
||||
protected function getFacingDegrees() : float{
|
||||
return match($this->facing){
|
||||
Facing::SOUTH => 0,
|
||||
Facing::WEST => 90,
|
||||
Facing::NORTH => 180,
|
||||
Facing::EAST => 270,
|
||||
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ use pocketmine\math\Axis;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\world\BlockTransaction;
|
||||
|
||||
final class WallSign extends BaseSign implements HorizontalFacing{
|
||||
@@ -46,4 +47,25 @@ final class WallSign extends BaseSign implements HorizontalFacing{
|
||||
$this->facing = $face;
|
||||
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
|
||||
}
|
||||
|
||||
protected function getHitboxCenter() : Vector3{
|
||||
[$xOffset, $zOffset] = match($this->facing){
|
||||
Facing::NORTH => [0, 15 / 16],
|
||||
Facing::SOUTH => [0, 1 / 16],
|
||||
Facing::WEST => [15 / 16, 0],
|
||||
Facing::EAST => [1 / 16, 0],
|
||||
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
|
||||
};
|
||||
return $this->position->add($xOffset, 0.5, $zOffset);
|
||||
}
|
||||
|
||||
protected function getFacingDegrees() : float{
|
||||
return match($this->facing){
|
||||
Facing::SOUTH => 0,
|
||||
Facing::WEST => 90,
|
||||
Facing::NORTH => 180,
|
||||
Facing::EAST => 270,
|
||||
default => throw new AssumptionFailedError("Invalid facing direction: " . $this->facing),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -69,9 +69,8 @@ class Banner extends Spawnable{
|
||||
|
||||
$patternTypeIdMap = BannerPatternTypeIdMap::getInstance();
|
||||
|
||||
$patterns = $nbt->getListTag(self::TAG_PATTERNS);
|
||||
$patterns = $nbt->getListTag(self::TAG_PATTERNS, CompoundTag::class);
|
||||
if($patterns !== null){
|
||||
/** @var CompoundTag $pattern */
|
||||
foreach($patterns as $pattern){
|
||||
$patternColor = $colorIdMap->fromInvertedId($pattern->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK; //TODO: missing pattern colour should be an error
|
||||
$patternType = $patternTypeIdMap->fromId($pattern->getString(self::TAG_PATTERN_NAME));
|
||||
|
@@ -34,6 +34,7 @@ use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\UnexpectedTagTypeException;
|
||||
use pocketmine\world\World;
|
||||
use function count;
|
||||
|
||||
@@ -86,13 +87,18 @@ class ChiseledBookshelf extends Tile implements Container{
|
||||
}
|
||||
|
||||
protected function loadItems(CompoundTag $tag) : void{
|
||||
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
try{
|
||||
$inventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class);
|
||||
}catch(UnexpectedTagTypeException){
|
||||
//preserve the old behaviour of not throwing on wrong types
|
||||
$inventoryTag = null;
|
||||
}
|
||||
if($inventoryTag !== null){
|
||||
$inventory = $this->getRealInventory();
|
||||
$listeners = $inventory->getListeners()->toArray();
|
||||
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
|
||||
|
||||
$newContents = [];
|
||||
/** @var CompoundTag $itemNBT */
|
||||
foreach($inventoryTag as $slot => $itemNBT){
|
||||
try{
|
||||
$count = $itemNBT->getByte(SavedItemStackData::TAG_COUNT);
|
||||
|
@@ -31,6 +31,7 @@ use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\nbt\UnexpectedTagTypeException;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
/**
|
||||
@@ -43,13 +44,18 @@ trait ContainerTrait{
|
||||
abstract public function getRealInventory() : Inventory;
|
||||
|
||||
protected function loadItems(CompoundTag $tag) : void{
|
||||
if(($inventoryTag = $tag->getTag(Container::TAG_ITEMS)) instanceof ListTag && $inventoryTag->getTagType() === NBT::TAG_Compound){
|
||||
try{
|
||||
$inventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class);
|
||||
}catch(UnexpectedTagTypeException){
|
||||
//preserve the old behaviour of not throwing on wrong types
|
||||
$inventoryTag = null;
|
||||
}
|
||||
if($inventoryTag !== null){
|
||||
$inventory = $this->getRealInventory();
|
||||
$listeners = $inventory->getListeners()->toArray();
|
||||
$inventory->getListeners()->remove(...$listeners); //prevent any events being fired by initialization
|
||||
|
||||
$newContents = [];
|
||||
/** @var CompoundTag $itemNBT */
|
||||
foreach($inventoryTag as $itemNBT){
|
||||
try{
|
||||
$newContents[$itemNBT->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($itemNBT);
|
||||
|
@@ -70,16 +70,18 @@ class Sign extends Spawnable{
|
||||
}
|
||||
|
||||
protected SignText $text;
|
||||
protected SignText $backText;
|
||||
private bool $waxed = false;
|
||||
|
||||
protected ?int $editorEntityRuntimeId = null;
|
||||
|
||||
public function __construct(World $world, Vector3 $pos){
|
||||
$this->text = new SignText();
|
||||
$this->backText = new SignText();
|
||||
parent::__construct($world, $pos);
|
||||
}
|
||||
|
||||
private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : void{
|
||||
private function readTextTag(CompoundTag $nbt, bool $lightingBugResolved) : SignText{
|
||||
$baseColor = new Color(0, 0, 0);
|
||||
$glowingText = false;
|
||||
if(($baseColorTag = $nbt->getTag(self::TAG_TEXT_COLOR)) instanceof IntTag){
|
||||
@@ -90,19 +92,27 @@ class Sign extends Spawnable{
|
||||
//see https://bugs.mojang.com/browse/MCPE-117835
|
||||
$glowingText = $glowingTextTag->getValue() !== 0;
|
||||
}
|
||||
$this->text = SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText);
|
||||
return SignText::fromBlob(mb_scrub($nbt->getString(self::TAG_TEXT_BLOB), 'UTF-8'), $baseColor, $glowingText);
|
||||
}
|
||||
|
||||
private function writeTextTag(SignText $text) : CompoundTag{
|
||||
return CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $text->getLines()), "\n"))
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($text->getBaseColor()->toARGB()))
|
||||
->setByte(self::TAG_GLOWING_TEXT, $text->isGlowing() ? 1 : 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1);
|
||||
}
|
||||
|
||||
public function readSaveData(CompoundTag $nbt) : void{
|
||||
$frontTextTag = $nbt->getTag(self::TAG_FRONT_TEXT);
|
||||
if($frontTextTag instanceof CompoundTag){
|
||||
$this->readTextTag($frontTextTag, true);
|
||||
$this->text = $this->readTextTag($frontTextTag, true);
|
||||
}elseif($nbt->getTag(self::TAG_TEXT_BLOB) instanceof StringTag){ //MCPE 1.2 save format
|
||||
$lightingBugResolved = false;
|
||||
if(($lightingBugResolvedTag = $nbt->getTag(self::TAG_LEGACY_BUG_RESOLVE)) instanceof ByteTag){
|
||||
$lightingBugResolved = $lightingBugResolvedTag->getValue() !== 0;
|
||||
}
|
||||
$this->readTextTag($nbt, $lightingBugResolved);
|
||||
$this->text = $this->readTextTag($nbt, $lightingBugResolved);
|
||||
}else{
|
||||
$text = [];
|
||||
for($i = 0; $i < SignText::LINE_COUNT; ++$i){
|
||||
@@ -113,22 +123,14 @@ class Sign extends Spawnable{
|
||||
}
|
||||
$this->text = new SignText($text);
|
||||
}
|
||||
$backTextTag = $nbt->getTag(self::TAG_BACK_TEXT);
|
||||
$this->backText = $backTextTag instanceof CompoundTag ? $this->readTextTag($backTextTag, true) : new SignText();
|
||||
$this->waxed = $nbt->getByte(self::TAG_WAXED, 0) !== 0;
|
||||
}
|
||||
|
||||
protected function writeSaveData(CompoundTag $nbt) : void{
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $this->text->getLines()), "\n"))
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB()))
|
||||
->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1)
|
||||
);
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, "")
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
|
||||
->setByte(self::TAG_GLOWING_TEXT, 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1)
|
||||
);
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text));
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText));
|
||||
|
||||
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
|
||||
}
|
||||
@@ -141,6 +143,10 @@ class Sign extends Spawnable{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function getBackText() : SignText{ return $this->backText; }
|
||||
|
||||
public function setBackText(SignText $backText) : void{ $this->backText = $backText; }
|
||||
|
||||
public function isWaxed() : bool{ return $this->waxed; }
|
||||
|
||||
public function setWaxed(bool $waxed) : void{ $this->waxed = $waxed; }
|
||||
@@ -162,19 +168,8 @@ class Sign extends Spawnable{
|
||||
}
|
||||
|
||||
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, rtrim(implode("\n", $this->text->getLines()), "\n"))
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt($this->text->getBaseColor()->toARGB()))
|
||||
->setByte(self::TAG_GLOWING_TEXT, $this->text->isGlowing() ? 1 : 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1) //TODO: not sure what this is used for
|
||||
);
|
||||
//TODO: this is not yet used by the server, but needed to rollback any client-side changes to the back text
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, CompoundTag::create()
|
||||
->setString(self::TAG_TEXT_BLOB, "")
|
||||
->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00))
|
||||
->setByte(self::TAG_GLOWING_TEXT, 0)
|
||||
->setByte(self::TAG_PERSIST_FORMATTING, 1)
|
||||
);
|
||||
$nbt->setTag(self::TAG_FRONT_TEXT, $this->writeTextTag($this->text));
|
||||
$nbt->setTag(self::TAG_BACK_TEXT, $this->writeTextTag($this->backText));
|
||||
$nbt->setByte(self::TAG_WAXED, $this->waxed ? 1 : 0);
|
||||
$nbt->setLong(self::TAG_LOCKED_FOR_EDITING_BY, $this->editorEntityRuntimeId ?? -1);
|
||||
}
|
||||
|
@@ -148,7 +148,8 @@ class TimingsCommand extends VanillaCommand{
|
||||
private function uploadReport(array $lines, CommandSender $sender) : void{
|
||||
$data = [
|
||||
"browser" => $agent = $sender->getServer()->getName() . " " . $sender->getServer()->getPocketMineVersion(),
|
||||
"data" => implode("\n", $lines)
|
||||
"data" => implode("\n", $lines),
|
||||
"private" => "true"
|
||||
];
|
||||
|
||||
$host = $sender->getServer()->getConfigGroup()->getPropertyString(YmlServerProperties::TIMINGS_HOST, "timings.pmmp.io");
|
||||
@@ -181,8 +182,13 @@ class TimingsCommand extends VanillaCommand{
|
||||
}
|
||||
$response = json_decode($result->getBody(), true);
|
||||
if(is_array($response) && isset($response["id"]) && (is_int($response["id"]) || is_string($response["id"]))){
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead(
|
||||
"https://" . $host . "/?id=" . $response["id"]));
|
||||
$url = "https://" . $host . "/?id=" . $response["id"];
|
||||
if(isset($response["access_token"]) && is_string($response["access_token"])){
|
||||
$url .= "&access_token=" . $response["access_token"];
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->warning("Your chosen timings host does not support private reports. Anyone will be able to see your report if they guess the ID.");
|
||||
}
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_timingsRead($url));
|
||||
}else{
|
||||
$sender->getServer()->getLogger()->debug("Invalid response from timings server (" . $result->getCode() . "): " . $result->getBody());
|
||||
Command::broadcastCommandMessage($sender, KnownTranslationFactory::pocketmine_command_timings_pasteError());
|
||||
|
@@ -23,10 +23,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\crafting;
|
||||
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pmmp\encoding\VarInt;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\DestructorCallbackTrait;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use function array_shift;
|
||||
@@ -114,11 +115,13 @@ class CraftingManager{
|
||||
}
|
||||
|
||||
private static function hashOutput(Item $output) : string{
|
||||
$write = new BinaryStream();
|
||||
$write->putVarInt($output->getStateId());
|
||||
$write->put((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
|
||||
$write = new ByteBufferWriter();
|
||||
VarInt::writeSignedInt($write, $output->getStateId());
|
||||
//TODO: the NBT serializer allocates its own ByteBufferWriter, we should change the API in the future to
|
||||
//allow passing our own to avoid this extra allocation
|
||||
$write->writeByteArray((new LittleEndianNbtSerializer())->write(new TreeRoot($output->getNamedTag())));
|
||||
|
||||
return $write->getBuffer();
|
||||
return $write->getData();
|
||||
}
|
||||
|
||||
/**
|
||||
|
45
src/data/bedrock/FireworkRocketTypeIdMap.php
Normal file
45
src/data/bedrock/FireworkRocketTypeIdMap.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
use pocketmine\item\FireworkRocketType;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
|
||||
final class FireworkRocketTypeIdMap{
|
||||
use SingletonTrait;
|
||||
/** @phpstan-use IntSaveIdMapTrait<FireworkRocketType> */
|
||||
use IntSaveIdMapTrait;
|
||||
|
||||
private function __construct(){
|
||||
foreach(FireworkRocketType::cases() as $case){
|
||||
$this->register(match($case){
|
||||
FireworkRocketType::SMALL_BALL => FireworkRocketTypeIds::SMALL_BALL,
|
||||
FireworkRocketType::LARGE_BALL => FireworkRocketTypeIds::LARGE_BALL,
|
||||
FireworkRocketType::STAR => FireworkRocketTypeIds::STAR,
|
||||
FireworkRocketType::CREEPER => FireworkRocketTypeIds::CREEPER,
|
||||
FireworkRocketType::BURST => FireworkRocketTypeIds::BURST,
|
||||
}, $case);
|
||||
}
|
||||
}
|
||||
}
|
32
src/data/bedrock/FireworkRocketTypeIds.php
Normal file
32
src/data/bedrock/FireworkRocketTypeIds.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\data\bedrock;
|
||||
|
||||
final class FireworkRocketTypeIds{
|
||||
public const SMALL_BALL = 0;
|
||||
public const LARGE_BALL = 1;
|
||||
public const STAR = 2;
|
||||
public const CREEPER = 3;
|
||||
public const BURST = 4;
|
||||
}
|
@@ -23,11 +23,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\data\bedrock\block\upgrade;
|
||||
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pmmp\encoding\DataDecodeException;
|
||||
use pmmp\encoding\VarInt;
|
||||
use pocketmine\data\bedrock\block\BlockStateData;
|
||||
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
|
||||
/**
|
||||
* Handles translating legacy 1.12 block ID/meta into modern blockstates.
|
||||
@@ -84,25 +85,25 @@ final class BlockIdMetaUpgrader{
|
||||
public static function loadFromString(string $data, LegacyBlockIdToStringIdMap $idMap, BlockStateUpgrader $blockStateUpgrader) : self{
|
||||
$mappingTable = [];
|
||||
|
||||
$legacyStateMapReader = new BinaryStream($data);
|
||||
$legacyStateMapReader = new ByteBufferReader($data);
|
||||
$nbtReader = new LittleEndianNbtSerializer();
|
||||
|
||||
$idCount = $legacyStateMapReader->getUnsignedVarInt();
|
||||
$idCount = VarInt::readUnsignedInt($legacyStateMapReader);
|
||||
for($idIndex = 0; $idIndex < $idCount; $idIndex++){
|
||||
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt());
|
||||
$id = $legacyStateMapReader->readByteArray(VarInt::readUnsignedInt($legacyStateMapReader));
|
||||
|
||||
$metaCount = $legacyStateMapReader->getUnsignedVarInt();
|
||||
$metaCount = VarInt::readUnsignedInt($legacyStateMapReader);
|
||||
for($metaIndex = 0; $metaIndex < $metaCount; $metaIndex++){
|
||||
$meta = $legacyStateMapReader->getUnsignedVarInt();
|
||||
$meta = VarInt::readUnsignedInt($legacyStateMapReader);
|
||||
|
||||
$offset = $legacyStateMapReader->getOffset();
|
||||
$state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag();
|
||||
$state = $nbtReader->read($legacyStateMapReader->getData(), $offset)->mustGetCompoundTag();
|
||||
$legacyStateMapReader->setOffset($offset);
|
||||
$mappingTable[$id][$meta] = $blockStateUpgrader->upgrade(BlockStateData::fromNbt($state));
|
||||
}
|
||||
}
|
||||
if(!$legacyStateMapReader->feof()){
|
||||
throw new BinaryDataException("Unexpected trailing data in legacy state map data");
|
||||
if($legacyStateMapReader->getUnreadLength() > 0){
|
||||
throw new DataDecodeException("Unexpected trailing data in legacy state map data");
|
||||
}
|
||||
|
||||
return new self($mappingTable, $idMap);
|
||||
|
@@ -117,8 +117,8 @@ final class ItemSerializer{
|
||||
$data = $serializer($item);
|
||||
}
|
||||
|
||||
if($item->hasNamedTag()){
|
||||
$resultTag = $item->getNamedTag();
|
||||
$resultTag = $item->getNamedTag();
|
||||
if($resultTag->count() > 0){
|
||||
$extraTag = $data->getTag();
|
||||
if($extraTag !== null){
|
||||
$resultTag = $resultTag->merge($extraTag);
|
||||
|
@@ -40,6 +40,7 @@ use pocketmine\data\bedrock\PotionTypeIdMap;
|
||||
use pocketmine\data\bedrock\SuspiciousStewTypeIdMap;
|
||||
use pocketmine\item\Banner;
|
||||
use pocketmine\item\Dye;
|
||||
use pocketmine\item\FireworkStar;
|
||||
use pocketmine\item\GoatHorn;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\Medicine;
|
||||
@@ -246,6 +247,7 @@ final class ItemSerializerDeserializerRegistrar{
|
||||
$this->map1to1Item(Ids::EYE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::EYE_ARMOR_TRIM_SMITHING_TEMPLATE());
|
||||
$this->map1to1Item(Ids::FEATHER, Items::FEATHER());
|
||||
$this->map1to1Item(Ids::FERMENTED_SPIDER_EYE, Items::FERMENTED_SPIDER_EYE());
|
||||
$this->map1to1Item(Ids::FIREWORK_ROCKET, Items::FIREWORK_ROCKET());
|
||||
$this->map1to1Item(Ids::FIRE_CHARGE, Items::FIRE_CHARGE());
|
||||
$this->map1to1Item(Ids::FISHING_ROD, Items::FISHING_ROD());
|
||||
$this->map1to1Item(Ids::FLINT, Items::FLINT());
|
||||
@@ -403,6 +405,7 @@ final class ItemSerializerDeserializerRegistrar{
|
||||
$this->map1to1Item(Ids::TORCHFLOWER_SEEDS, Items::TORCHFLOWER_SEEDS());
|
||||
$this->map1to1Item(Ids::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE, Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE());
|
||||
$this->map1to1Item(Ids::TOTEM_OF_UNDYING, Items::TOTEM());
|
||||
$this->map1to1Item(Ids::TRIDENT, Items::TRIDENT());
|
||||
$this->map1to1Item(Ids::TROPICAL_FISH, Items::CLOWNFISH());
|
||||
$this->map1to1Item(Ids::TURTLE_HELMET, Items::TURTLE_HELMET());
|
||||
$this->map1to1Item(Ids::VEX_ARMOR_TRIM_SMITHING_TEMPLATE, Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE());
|
||||
@@ -500,6 +503,14 @@ final class ItemSerializerDeserializerRegistrar{
|
||||
* in a unified manner.
|
||||
*/
|
||||
private function register1to1ItemWithMetaMappings() : void{
|
||||
$this->map1to1ItemWithMeta(
|
||||
Ids::FIREWORK_STAR,
|
||||
Items::FIREWORK_STAR(),
|
||||
function(FireworkStar $item, int $meta) : void{
|
||||
// Colors will be defined by CompoundTag deserialization.
|
||||
},
|
||||
fn(FireworkStar $item) => DyeColorIdMap::getInstance()->toInvertedId($item->getExplosion()->getFlashColor())
|
||||
);
|
||||
$this->map1to1ItemWithMeta(
|
||||
Ids::GOAT_HORN,
|
||||
Items::GOAT_HORN(),
|
||||
|
@@ -29,16 +29,14 @@ use pocketmine\data\bedrock\item\BlockItemIdMap;
|
||||
use pocketmine\data\bedrock\item\SavedItemData;
|
||||
use pocketmine\data\bedrock\item\SavedItemStackData;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\NbtException;
|
||||
use pocketmine\nbt\tag\ByteTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\ShortTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\BlockStateDictionary;
|
||||
use pocketmine\utils\Binary;
|
||||
use function assert;
|
||||
use function array_map;
|
||||
|
||||
final class ItemDataUpgrader{
|
||||
private const TAG_LEGACY_ID = "id"; //TAG_Short (or TAG_String for Java itemstacks)
|
||||
@@ -169,26 +167,6 @@ final class ItemDataUpgrader{
|
||||
return new SavedItemData($newNameId, $newMeta, $blockStateData, $tag->getCompoundTag(SavedItemData::TAG_TAG));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws SavedDataLoadingException
|
||||
*/
|
||||
private static function deserializeListOfStrings(?ListTag $list, string $tagName) : array{
|
||||
if($list === null){
|
||||
return [];
|
||||
}
|
||||
if($list->getTagType() !== NBT::TAG_String){
|
||||
throw new SavedDataLoadingException("Unexpected type of list for tag '$tagName', expected TAG_String");
|
||||
}
|
||||
$result = [];
|
||||
foreach($list as $item){
|
||||
assert($item instanceof StringTag);
|
||||
$result[] = $item->getValue();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SavedDataLoadingException
|
||||
*/
|
||||
@@ -205,8 +183,8 @@ final class ItemDataUpgrader{
|
||||
//optional
|
||||
$slot = ($slotTag = $tag->getTag(SavedItemStackData::TAG_SLOT)) instanceof ByteTag ? Binary::unsignByte($slotTag->getValue()) : null;
|
||||
$wasPickedUp = ($wasPickedUpTag = $tag->getTag(SavedItemStackData::TAG_WAS_PICKED_UP)) instanceof ByteTag ? $wasPickedUpTag->getValue() : null;
|
||||
$canPlaceOnList = $tag->getListTag(SavedItemStackData::TAG_CAN_PLACE_ON);
|
||||
$canDestroyList = $tag->getListTag(SavedItemStackData::TAG_CAN_DESTROY);
|
||||
$canPlaceOnList = $tag->getListTag(SavedItemStackData::TAG_CAN_PLACE_ON, StringTag::class);
|
||||
$canDestroyList = $tag->getListTag(SavedItemStackData::TAG_CAN_DESTROY, StringTag::class);
|
||||
}catch(NbtException $e){
|
||||
throw new SavedDataLoadingException($e->getMessage(), 0, $e);
|
||||
}
|
||||
@@ -216,8 +194,8 @@ final class ItemDataUpgrader{
|
||||
$count,
|
||||
$slot,
|
||||
$wasPickedUp !== 0,
|
||||
self::deserializeListOfStrings($canPlaceOnList, SavedItemStackData::TAG_CAN_PLACE_ON),
|
||||
self::deserializeListOfStrings($canDestroyList, SavedItemStackData::TAG_CAN_DESTROY)
|
||||
$canPlaceOnList === null ? [] : array_map(fn(StringTag $t) => $t->getValue(), $canPlaceOnList->getValue()),
|
||||
$canDestroyList === null ? [] : array_map(fn(StringTag $t) => $t->getValue(), $canDestroyList->getValue())
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -492,7 +492,7 @@ abstract class Entity{
|
||||
new FloatTag($this->location->pitch)
|
||||
]));
|
||||
|
||||
if(!($this instanceof Player)){
|
||||
if(!($this instanceof NeverSavedWithChunkEntity)){
|
||||
EntityFactory::getInstance()->injectSaveId(get_class($this), $nbt);
|
||||
|
||||
if($this->getNameTag() !== ""){
|
||||
|
@@ -25,7 +25,6 @@ namespace pocketmine\entity;
|
||||
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\DoubleTag;
|
||||
use pocketmine\nbt\tag\FloatTag;
|
||||
@@ -59,11 +58,10 @@ final class EntityDataHelper{
|
||||
public static function parseLocation(CompoundTag $nbt, World $world) : Location{
|
||||
$pos = self::parseVec3($nbt, Entity::TAG_POS, false);
|
||||
|
||||
$yawPitch = $nbt->getTag(Entity::TAG_ROTATION);
|
||||
if(!($yawPitch instanceof ListTag) || $yawPitch->getTagType() !== NBT::TAG_Float){
|
||||
$generic = $nbt->getTag(Entity::TAG_ROTATION);
|
||||
if(!($generic instanceof ListTag) || ($yawPitch = $generic->cast(FloatTag::class)) === null){
|
||||
throw new SavedDataLoadingException("'" . Entity::TAG_ROTATION . "' should be a List<Float>");
|
||||
}
|
||||
/** @var FloatTag[] $values */
|
||||
$values = $yawPitch->getValue();
|
||||
if(count($values) !== 2){
|
||||
throw new SavedDataLoadingException("Expected exactly 2 entries for 'Rotation'");
|
||||
@@ -78,14 +76,13 @@ final class EntityDataHelper{
|
||||
* @throws SavedDataLoadingException
|
||||
*/
|
||||
public static function parseVec3(CompoundTag $nbt, string $tagName, bool $optional) : Vector3{
|
||||
$pos = $nbt->getTag($tagName);
|
||||
if($pos === null && $optional){
|
||||
$generic = $nbt->getTag($tagName);
|
||||
if($generic === null && $optional){
|
||||
return Vector3::zero();
|
||||
}
|
||||
if(!($pos instanceof ListTag) || ($pos->getTagType() !== NBT::TAG_Double && $pos->getTagType() !== NBT::TAG_Float)){
|
||||
if(!($generic instanceof ListTag) || ($pos = $generic->cast(DoubleTag::class) ?? $generic->cast(FloatTag::class)) === null){
|
||||
throw new SavedDataLoadingException("'$tagName' should be a List<Double> or List<Float>");
|
||||
}
|
||||
/** @var DoubleTag[]|FloatTag[] $values */
|
||||
$values = $pos->getValue();
|
||||
if(count($values) !== 3){
|
||||
throw new SavedDataLoadingException("Expected exactly 3 entries in '$tagName' tag");
|
||||
|
@@ -46,6 +46,7 @@ use pocketmine\entity\projectile\ExperienceBottle;
|
||||
use pocketmine\entity\projectile\IceBomb;
|
||||
use pocketmine\entity\projectile\Snowball;
|
||||
use pocketmine\entity\projectile\SplashPotion;
|
||||
use pocketmine\entity\projectile\Trident;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
@@ -171,6 +172,24 @@ final class EntityFactory{
|
||||
return new SplashPotion(Helper::parseLocation($nbt, $world), null, $potionType, $nbt);
|
||||
}, ['ThrownPotion', 'minecraft:potion', 'thrownpotion']);
|
||||
|
||||
$this->register(Trident::class, function(World $world, CompoundTag $nbt) : Trident{
|
||||
$itemTag = $nbt->getCompoundTag(Trident::TAG_ITEM);
|
||||
if($itemTag === null){
|
||||
throw new SavedDataLoadingException("Expected \"" . Trident::TAG_ITEM . "\" NBT tag not found");
|
||||
}
|
||||
|
||||
$item = Item::nbtDeserialize($itemTag);
|
||||
if($item->isNull()){
|
||||
throw new SavedDataLoadingException("Trident item is invalid");
|
||||
}
|
||||
return new Trident(Helper::parseLocation($nbt, $world), $item, null, $nbt);
|
||||
}, [
|
||||
'minecraft:trident', //java
|
||||
'minecraft:thrown_trident', //bedrock
|
||||
'Trident', //backwards compat for people who used #4547 before it was merged, since it was sitting around for 4 years...
|
||||
'ThrownTrident' //as above
|
||||
]);
|
||||
|
||||
$this->register(Squid::class, function(World $world, CompoundTag $nbt) : Squid{
|
||||
return new Squid(Helper::parseLocation($nbt, $world), $nbt);
|
||||
}, ['Squid', 'minecraft:squid']);
|
||||
|
@@ -247,6 +247,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
return $this->enderInventory;
|
||||
}
|
||||
|
||||
public function getSneakOffset() : float{
|
||||
return 0.31;
|
||||
}
|
||||
|
||||
/**
|
||||
* For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT.
|
||||
*/
|
||||
@@ -295,12 +299,11 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
$this->enderInventory = new PlayerEnderInventory($this);
|
||||
$this->initHumanData($nbt);
|
||||
|
||||
$inventoryTag = $nbt->getListTag(self::TAG_INVENTORY);
|
||||
$inventoryTag = $nbt->getListTag(self::TAG_INVENTORY, CompoundTag::class);
|
||||
if($inventoryTag !== null){
|
||||
$inventoryItems = [];
|
||||
$armorInventoryItems = [];
|
||||
|
||||
/** @var CompoundTag $item */
|
||||
foreach($inventoryTag as $i => $item){
|
||||
$slot = $item->getByte(SavedItemStackData::TAG_SLOT);
|
||||
if($slot >= 0 && $slot < 9){ //Hotbar
|
||||
@@ -324,11 +327,10 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onMobOffHandItemChange($recipients, $this)
|
||||
)));
|
||||
|
||||
$enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY);
|
||||
$enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY, CompoundTag::class);
|
||||
if($enderChestInventoryTag !== null){
|
||||
$enderChestInventoryItems = [];
|
||||
|
||||
/** @var CompoundTag $item */
|
||||
foreach($enderChestInventoryTag as $i => $item){
|
||||
$enderChestInventoryItems[$item->getByte(SavedItemStackData::TAG_SLOT)] = Item::nbtDeserialize($item);
|
||||
}
|
||||
|
@@ -181,8 +181,7 @@ abstract class Living extends Entity{
|
||||
|
||||
$this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS));
|
||||
|
||||
/** @var CompoundTag[]|ListTag|null $activeEffectsTag */
|
||||
$activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS);
|
||||
$activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS, CompoundTag::class);
|
||||
if($activeEffectsTag !== null){
|
||||
foreach($activeEffectsTag as $e){
|
||||
$effect = EffectIdMap::getInstance()->fromId($e->getByte(self::TAG_EFFECT_ID));
|
||||
@@ -242,6 +241,10 @@ abstract class Living extends Entity{
|
||||
$this->absorptionAttr->setValue($absorption);
|
||||
}
|
||||
|
||||
public function getSneakOffset() : float{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
public function isSneaking() : bool{
|
||||
return $this->sneaking;
|
||||
}
|
||||
@@ -292,7 +295,7 @@ abstract class Living extends Entity{
|
||||
$width = $size->getWidth();
|
||||
$this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale()));
|
||||
}elseif($this->isSneaking()){
|
||||
$this->setSize((new EntitySizeInfo(3 / 4 * $size->getHeight(), $size->getWidth(), 3 / 4 * $size->getEyeHeight()))->scale($this->getScale()));
|
||||
$this->setSize((new EntitySizeInfo($size->getHeight() - $this->getSneakOffset(), $size->getWidth(), $size->getEyeHeight() - $this->getSneakOffset()))->scale($this->getScale()));
|
||||
}else{
|
||||
$this->setSize($size->scale($this->getScale()));
|
||||
}
|
||||
|
36
src/entity/NeverSavedWithChunkEntity.php
Normal file
36
src/entity/NeverSavedWithChunkEntity.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\entity;
|
||||
|
||||
/**
|
||||
* Decorator for entities that will never be saved with a chunk.
|
||||
* Entities implementing this interface are not required to register a save ID.
|
||||
*
|
||||
* This differs from {@link Entity::canSaveWithChunk()} because it can't be changed after the entity is created.
|
||||
* We can't use canSaveWithChunk() to decide whether an entity needs a save ID, but we can use an interface like this.
|
||||
* An attribute would also work, but `instanceof NonSaveable` is easier.
|
||||
*/
|
||||
interface NeverSavedWithChunkEntity{
|
||||
|
||||
}
|
41
src/entity/animation/FireworkParticlesAnimation.php
Normal file
41
src/entity/animation/FireworkParticlesAnimation.php
Normal 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\entity\animation;
|
||||
|
||||
use pocketmine\entity\object\FireworkRocket;
|
||||
use pocketmine\network\mcpe\protocol\ActorEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\ActorEvent;
|
||||
|
||||
final class FireworkParticlesAnimation implements Animation{
|
||||
|
||||
public function __construct(
|
||||
private FireworkRocket $entity
|
||||
){}
|
||||
|
||||
public function encode() : array{
|
||||
return [
|
||||
ActorEventPacket::create($this->entity->getId(), ActorEvent::FIREWORK_PARTICLES, 0)
|
||||
];
|
||||
}
|
||||
}
|
204
src/entity/object/FireworkRocket.php
Normal file
204
src/entity/object/FireworkRocket.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?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\entity\object;
|
||||
|
||||
use pocketmine\entity\animation\FireworkParticlesAnimation;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\EntitySizeInfo;
|
||||
use pocketmine\entity\Explosive;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\entity\NeverSavedWithChunkEntity;
|
||||
use pocketmine\event\entity\EntityDamageByEntityEvent;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use pocketmine\item\FireworkRocket as FireworkItem;
|
||||
use pocketmine\item\FireworkRocketExplosion;
|
||||
use pocketmine\math\VoxelRayTrace;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\sound\FireworkCrackleSound;
|
||||
use pocketmine\world\sound\FireworkLaunchSound;
|
||||
use function count;
|
||||
use function sqrt;
|
||||
|
||||
class FireworkRocket extends Entity implements Explosive, NeverSavedWithChunkEntity{
|
||||
|
||||
public static function getNetworkTypeId() : string{ return EntityIds::FIREWORKS_ROCKET; }
|
||||
|
||||
protected int $maxFlightTimeTicks;
|
||||
|
||||
/** @var FireworkRocketExplosion[] */
|
||||
protected array $explosions = [];
|
||||
|
||||
/**
|
||||
* @param FireworkRocketExplosion[] $explosions
|
||||
*/
|
||||
public function __construct(Location $location, int $maxFlightTimeTicks, array $explosions, ?CompoundTag $nbt = null){
|
||||
if($maxFlightTimeTicks < 0){
|
||||
throw new \InvalidArgumentException("Life ticks cannot be negative");
|
||||
}
|
||||
$this->maxFlightTimeTicks = $maxFlightTimeTicks;
|
||||
$this->setExplosions($explosions);
|
||||
|
||||
parent::__construct($location, $nbt);
|
||||
}
|
||||
|
||||
protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.25, 0.25); }
|
||||
|
||||
protected function getInitialDragMultiplier() : float{ return 0.0; }
|
||||
|
||||
protected function getInitialGravity() : float{ return 0.0; }
|
||||
|
||||
/**
|
||||
* Returns the total number of ticks the firework will fly for before it explodes.
|
||||
*/
|
||||
public function getMaxFlightTimeTicks() : int{
|
||||
return $this->maxFlightTimeTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of ticks the firework will fly for before it explodes.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMaxFlightTimeTicks(int $maxFlightTimeTicks) : self{
|
||||
if($maxFlightTimeTicks < 0){
|
||||
throw new \InvalidArgumentException("Max flight time ticks cannot be negative");
|
||||
}
|
||||
$this->maxFlightTimeTicks = $maxFlightTimeTicks;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FireworkRocketExplosion[]
|
||||
*/
|
||||
public function getExplosions() : array{
|
||||
return $this->explosions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FireworkRocketExplosion[] $explosions
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setExplosions(array $explosions) : self{
|
||||
Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{});
|
||||
$this->explosions = $explosions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function onFirstUpdate(int $currentTick) : void{
|
||||
parent::onFirstUpdate($currentTick);
|
||||
|
||||
$this->broadcastSound(new FireworkLaunchSound());
|
||||
}
|
||||
|
||||
protected function entityBaseTick(int $tickDiff = 1) : bool{
|
||||
$hasUpdate = parent::entityBaseTick($tickDiff);
|
||||
|
||||
if(!$this->isFlaggedForDespawn()){
|
||||
//Don't keep accelerating long-lived fireworks - this gets very rapidly out of control and makes the server
|
||||
//die. Vanilla fireworks will only live for about 52 ticks maximum anyway, so this only makes sure plugin
|
||||
//created fireworks don't murder the server
|
||||
if($this->ticksLived < 60){
|
||||
$this->addMotion($this->motion->x * 0.15, 0.04, $this->motion->z * 0.15);
|
||||
}
|
||||
|
||||
if($this->ticksLived >= $this->maxFlightTimeTicks){
|
||||
$this->flagForDespawn();
|
||||
$this->explode();
|
||||
}
|
||||
}
|
||||
|
||||
return $hasUpdate;
|
||||
}
|
||||
|
||||
public function explode() : void{
|
||||
if(($explosionCount = count($this->explosions)) !== 0){
|
||||
$this->broadcastAnimation(new FireworkParticlesAnimation($this));
|
||||
foreach($this->explosions as $explosion){
|
||||
$this->broadcastSound($explosion->getType()->getExplosionSound());
|
||||
if($explosion->willTwinkle()){
|
||||
$this->broadcastSound(new FireworkCrackleSound());
|
||||
}
|
||||
}
|
||||
|
||||
$force = ($explosionCount * 2) + 5;
|
||||
$world = $this->getWorld();
|
||||
foreach($world->getCollidingEntities($this->getBoundingBox()->expandedCopy(5, 5, 5), $this) as $entity){
|
||||
if(!$entity instanceof Living){
|
||||
continue;
|
||||
}
|
||||
|
||||
$position = $entity->getPosition();
|
||||
$distance = $position->distanceSquared($this->location);
|
||||
if($distance > 25){
|
||||
continue;
|
||||
}
|
||||
|
||||
//cast two rays - one to the entity's feet and another to halfway up its body (according to Java, anyway)
|
||||
//this seems like it'd miss some cases but who am I to argue with vanilla logic :>
|
||||
$height = $entity->getBoundingBox()->getYLength();
|
||||
for($i = 0; $i < 2; $i++){
|
||||
$target = $position->add(0, 0.5 * $i * $height, 0);
|
||||
foreach(VoxelRayTrace::betweenPoints($this->location, $target) as $blockPos){
|
||||
if($world->getBlock($blockPos)->calculateIntercept($this->location, $target) !== null){
|
||||
continue 2; //obstruction, try another path
|
||||
}
|
||||
}
|
||||
|
||||
//no obstruction
|
||||
$damage = $force * sqrt((5 - $position->distance($this->location)) / 5);
|
||||
$ev = new EntityDamageByEntityEvent($this, $entity, EntityDamageEvent::CAUSE_ENTITY_EXPLOSION, $damage);
|
||||
$entity->attack($ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function canBeCollidedWith() : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function syncNetworkData(EntityMetadataCollection $properties) : void{
|
||||
parent::syncNetworkData($properties);
|
||||
|
||||
$explosions = new ListTag();
|
||||
foreach($this->explosions as $explosion){
|
||||
$explosions->push($explosion->toCompoundTag());
|
||||
}
|
||||
$fireworksData = CompoundTag::create()
|
||||
->setTag(FireworkItem::TAG_FIREWORK_DATA, CompoundTag::create()
|
||||
->setTag(FireworkItem::TAG_EXPLOSIONS, $explosions)
|
||||
);
|
||||
|
||||
$properties->setCompoundTag(EntityMetadataProperties::FIREWORK_ITEM, new CacheableNbt($fireworksData));
|
||||
}
|
||||
}
|
@@ -39,7 +39,6 @@ use pocketmine\event\entity\ProjectileHitEvent;
|
||||
use pocketmine\math\RayTraceResult;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\math\VoxelRayTrace;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
@@ -81,12 +80,11 @@ abstract class Projectile extends Entity{
|
||||
$this->setHealth(1);
|
||||
$this->damage = $nbt->getDouble(self::TAG_DAMAGE, $this->damage);
|
||||
|
||||
if(($stuckOnBlockPosTag = $nbt->getListTag(self::TAG_STUCK_ON_BLOCK_POS)) !== null){
|
||||
if($stuckOnBlockPosTag->getTagType() !== NBT::TAG_Int || count($stuckOnBlockPosTag) !== 3){
|
||||
if(($stuckOnBlockPosTag = $nbt->getListTag(self::TAG_STUCK_ON_BLOCK_POS, IntTag::class)) !== null){
|
||||
if(count($stuckOnBlockPosTag) !== 3){
|
||||
throw new SavedDataLoadingException(self::TAG_STUCK_ON_BLOCK_POS . " tag should be a list of 3 TAG_Int");
|
||||
}
|
||||
|
||||
/** @var IntTag[] $values */
|
||||
$values = $stuckOnBlockPosTag->getValue();
|
||||
|
||||
$this->blockHit = new Vector3($values[0]->getValue(), $values[1]->getValue(), $values[2]->getValue());
|
||||
@@ -227,12 +225,15 @@ abstract class Projectile extends Entity{
|
||||
$specificHitFunc = fn() => $this->onHitBlock($objectHit, $rayTraceResult);
|
||||
}
|
||||
|
||||
$motionBeforeOnHit = clone $this->motion;
|
||||
$ev->call();
|
||||
$this->onHit($ev);
|
||||
$specificHitFunc();
|
||||
|
||||
$this->isCollided = $this->onGround = true;
|
||||
$this->motion = Vector3::zero();
|
||||
if($motionBeforeOnHit->equals($this->motion)){
|
||||
$this->motion = Vector3::zero();
|
||||
}
|
||||
}else{
|
||||
$this->isCollided = $this->onGround = false;
|
||||
$this->blockHit = null;
|
||||
@@ -295,7 +296,9 @@ abstract class Projectile extends Entity{
|
||||
}
|
||||
}
|
||||
|
||||
$this->flagForDespawn();
|
||||
if($this->despawnsOnEntityHit()){
|
||||
$this->flagForDespawn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,4 +308,11 @@ abstract class Projectile extends Entity{
|
||||
$this->blockHit = $blockHit->getPosition()->asVector3();
|
||||
$blockHit->onProjectileHit($this, $hitResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This will be dropped in favor of deciding whether to despawn within `onHitEntity()` method.
|
||||
*/
|
||||
protected function despawnsOnEntityHit() : bool{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
183
src/entity/projectile/Trident.php
Normal file
183
src/entity/projectile/Trident.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?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\entity\projectile;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\EntitySizeInfo;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\event\entity\EntityItemPickupEvent;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\math\RayTraceResult;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\network\mcpe\EntityEventBroadcaster;
|
||||
use pocketmine\network\mcpe\NetworkBroadcastUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataCollection;
|
||||
use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataFlags;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\sound\TridentHitBlockSound;
|
||||
use pocketmine\world\sound\TridentHitEntitySound;
|
||||
|
||||
class Trident extends Projectile{
|
||||
|
||||
public const TAG_ITEM = "Trident"; //TAG_Compound
|
||||
protected const TAG_SPAWNED_IN_CREATIVE = "isCreative"; //TAG_Byte
|
||||
|
||||
public static function getNetworkTypeId() : string{ return EntityIds::THROWN_TRIDENT; }
|
||||
|
||||
protected Item $item;
|
||||
|
||||
protected float $damage = 8.0;
|
||||
|
||||
protected bool $canCollide = true;
|
||||
|
||||
protected bool $spawnedInCreative = false;
|
||||
|
||||
public function __construct(
|
||||
Location $location,
|
||||
Item $item,
|
||||
?Entity $shootingEntity,
|
||||
?CompoundTag $nbt = null
|
||||
){
|
||||
if($item->isNull()){
|
||||
throw new \InvalidArgumentException("Trident must have a count of at least 1");
|
||||
}
|
||||
$this->item = clone $item;
|
||||
parent::__construct($location, $shootingEntity, $nbt);
|
||||
}
|
||||
|
||||
protected function getInitialSizeInfo() : EntitySizeInfo{ return new EntitySizeInfo(0.35, 0.25); }
|
||||
|
||||
protected function getInitialDragMultiplier() : float{ return 0.01; }
|
||||
|
||||
protected function getInitialGravity() : float{ return 0.1; }
|
||||
|
||||
protected function initEntity(CompoundTag $nbt) : void{
|
||||
parent::initEntity($nbt);
|
||||
|
||||
$this->spawnedInCreative = $nbt->getByte(self::TAG_SPAWNED_IN_CREATIVE, 0) === 1;
|
||||
}
|
||||
|
||||
public function saveNBT() : CompoundTag{
|
||||
$nbt = parent::saveNBT();
|
||||
$nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize());
|
||||
$nbt->setByte(self::TAG_SPAWNED_IN_CREATIVE, $this->spawnedInCreative ? 1 : 0);
|
||||
return $nbt;
|
||||
}
|
||||
|
||||
protected function onFirstUpdate(int $currentTick) : void{
|
||||
$owner = $this->getOwningEntity();
|
||||
$this->spawnedInCreative = $owner instanceof Player && $owner->isCreative();
|
||||
|
||||
parent::onFirstUpdate($currentTick);
|
||||
}
|
||||
|
||||
protected function entityBaseTick(int $tickDiff = 1) : bool{
|
||||
if($this->closed){
|
||||
return false;
|
||||
}
|
||||
//TODO: Loyalty enchantment.
|
||||
|
||||
return parent::entityBaseTick($tickDiff);
|
||||
}
|
||||
|
||||
protected function despawnsOnEntityHit() : bool{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function onHitEntity(Entity $entityHit, RayTraceResult $hitResult) : void{
|
||||
parent::onHitEntity($entityHit, $hitResult);
|
||||
|
||||
$this->canCollide = false;
|
||||
$this->broadcastSound(new TridentHitEntitySound());
|
||||
$this->setMotion(new Vector3($this->motion->x * -0.01, $this->motion->y * -0.1, $this->motion->z * -0.01));
|
||||
}
|
||||
|
||||
protected function onHitBlock(Block $blockHit, RayTraceResult $hitResult) : void{
|
||||
parent::onHitBlock($blockHit, $hitResult);
|
||||
$this->canCollide = true;
|
||||
$this->broadcastSound(new TridentHitBlockSound());
|
||||
}
|
||||
|
||||
public function getItem() : Item{
|
||||
return clone $this->item;
|
||||
}
|
||||
|
||||
public function setItem(Item $item) : void{
|
||||
if($item->isNull()){
|
||||
throw new \InvalidArgumentException("Trident must have a count of at least 1");
|
||||
}
|
||||
if($this->item->hasEnchantments() !== $item->hasEnchantments()){
|
||||
$this->networkPropertiesDirty = true;
|
||||
}
|
||||
$this->item = clone $item;
|
||||
}
|
||||
|
||||
public function canCollideWith(Entity $entity) : bool{
|
||||
return $this->canCollide && $entity->getId() !== $this->ownerId && parent::canCollideWith($entity);
|
||||
}
|
||||
|
||||
public function onCollideWithPlayer(Player $player) : void{
|
||||
if($this->blockHit !== null){
|
||||
$this->pickup($player);
|
||||
}
|
||||
}
|
||||
|
||||
private function pickup(Player $player) : void{
|
||||
$shouldDespawn = false;
|
||||
|
||||
$playerInventory = $player->getInventory();
|
||||
$ev = new EntityItemPickupEvent($player, $this, $this->getItem(), $playerInventory);
|
||||
if($player->hasFiniteResources() && !$playerInventory->canAddItem($ev->getItem())){
|
||||
$ev->cancel();
|
||||
}
|
||||
if($this->spawnedInCreative){
|
||||
$ev->cancel();
|
||||
$shouldDespawn = true;
|
||||
}
|
||||
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$ev->getInventory()?->addItem($ev->getItem());
|
||||
$shouldDespawn = true;
|
||||
}
|
||||
|
||||
if($shouldDespawn){
|
||||
//even if the item was not actually picked up, the animation must be displayed.
|
||||
NetworkBroadcastUtils::broadcastEntityEvent(
|
||||
$this->getViewers(),
|
||||
fn(EntityEventBroadcaster $broadcaster, array $recipients) => $broadcaster->onPickUpItem($recipients, $player, $this)
|
||||
);
|
||||
$this->flagForDespawn();
|
||||
}
|
||||
}
|
||||
|
||||
protected function syncNetworkData(EntityMetadataCollection $properties) : void{
|
||||
parent::syncNetworkData($properties);
|
||||
|
||||
$properties->setGenericFlag(EntityMetadataFlags::ENCHANTED, $this->item->hasEnchantments());
|
||||
}
|
||||
}
|
@@ -35,11 +35,15 @@ use pocketmine\player\Player;
|
||||
class SignChangeEvent extends BlockEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
private SignText $oldText;
|
||||
|
||||
public function __construct(
|
||||
private BaseSign $sign,
|
||||
private Player $player,
|
||||
private SignText $text
|
||||
private SignText $text,
|
||||
private bool $frontFace = true
|
||||
){
|
||||
$this->oldText = $this->sign->getFaceText($this->frontFace);
|
||||
parent::__construct($sign);
|
||||
}
|
||||
|
||||
@@ -55,7 +59,7 @@ class SignChangeEvent extends BlockEvent implements Cancellable{
|
||||
* Returns the text currently on the sign.
|
||||
*/
|
||||
public function getOldText() : SignText{
|
||||
return $this->sign->getText();
|
||||
return $this->oldText;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,4 +75,6 @@ class SignChangeEvent extends BlockEvent implements Cancellable{
|
||||
public function setNewText(SignText $text) : void{
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function isFrontFace() : bool{ return $this->frontFace; }
|
||||
}
|
||||
|
@@ -26,7 +26,10 @@ namespace pocketmine\event\player;
|
||||
use pocketmine\block\BlockTypeIds;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\entity\object\FallingBlock;
|
||||
use pocketmine\entity\object\FireworkRocket;
|
||||
use pocketmine\entity\projectile\Trident;
|
||||
use pocketmine\event\entity\EntityDamageByBlockEvent;
|
||||
use pocketmine\event\entity\EntityDamageByChildEntityEvent;
|
||||
use pocketmine\event\entity\EntityDamageByEntityEvent;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use pocketmine\event\entity\EntityDeathEvent;
|
||||
@@ -113,10 +116,15 @@ class PlayerDeathEvent extends EntityDeathEvent{
|
||||
}
|
||||
break;
|
||||
case EntityDamageEvent::CAUSE_PROJECTILE:
|
||||
if($deathCause instanceof EntityDamageByEntityEvent){
|
||||
if($deathCause instanceof EntityDamageByChildEntityEvent){
|
||||
$e = $deathCause->getDamager();
|
||||
if($e instanceof Living){
|
||||
return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName());
|
||||
$child = $deathCause->getChild();
|
||||
if($child instanceof Trident){
|
||||
return KnownTranslationFactory::death_attack_trident($name, $e->getDisplayName());
|
||||
}else{
|
||||
return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -157,7 +165,9 @@ class PlayerDeathEvent extends EntityDeathEvent{
|
||||
case EntityDamageEvent::CAUSE_ENTITY_EXPLOSION:
|
||||
if($deathCause instanceof EntityDamageByEntityEvent){
|
||||
$e = $deathCause->getDamager();
|
||||
if($e instanceof Living){
|
||||
if($e instanceof FireworkRocket){
|
||||
return KnownTranslationFactory::death_attack_fireworks($name);
|
||||
}elseif($e instanceof Living){
|
||||
return KnownTranslationFactory::death_attack_explosion_player($name, $e->getDisplayName());
|
||||
}
|
||||
}
|
||||
|
@@ -30,13 +30,11 @@ use pocketmine\inventory\transaction\action\SlotChangeAction;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function min;
|
||||
use function shuffle;
|
||||
use function spl_object_hash;
|
||||
use function spl_object_id;
|
||||
|
||||
@@ -95,10 +93,13 @@ class InventoryTransaction{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an **unordered** set of actions involved in this transaction.
|
||||
* Returns a set of actions involved in this transaction.
|
||||
*
|
||||
* WARNING: This system is **explicitly designed NOT to care about ordering**. Any order seen in this set has NO
|
||||
* significance and should not be relied on.
|
||||
* Note: This system is designed to care only about item balances. While you can usually assume that the actions
|
||||
* are provided in the correct order, it will still successfully complete transactions whose actions are provided in
|
||||
* the "wrong" order, as long as the transaction balances.
|
||||
* For example, you may see that an action setting a slot to a particular item may appear before the action that
|
||||
* removes that item from its original slot. While unintuitive, this is still valid.
|
||||
*
|
||||
* @return InventoryAction[]
|
||||
* @phpstan-return array<int, InventoryAction>
|
||||
@@ -119,19 +120,6 @@ class InventoryTransaction{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles actions in the transaction to prevent external things relying on any implicit ordering.
|
||||
*/
|
||||
private function shuffleActions() : void{
|
||||
$keys = array_keys($this->actions);
|
||||
shuffle($keys);
|
||||
$actions = [];
|
||||
foreach($keys as $key){
|
||||
$actions[$key] = $this->actions[$key];
|
||||
}
|
||||
$this->actions = $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Item[] $needItems
|
||||
* @param Item[] $haveItems
|
||||
@@ -308,8 +296,6 @@ class InventoryTransaction{
|
||||
throw new TransactionValidationException("Transaction has already been executed");
|
||||
}
|
||||
|
||||
$this->shuffleActions();
|
||||
|
||||
$this->validate();
|
||||
|
||||
if(!$this->callExecuteEvent()){
|
||||
|
@@ -29,7 +29,6 @@ use pocketmine\block\utils\DyeColor;
|
||||
use pocketmine\data\bedrock\BannerPatternTypeIdMap;
|
||||
use pocketmine\data\bedrock\DyeColorIdMap;
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use function count;
|
||||
@@ -92,9 +91,8 @@ class Banner extends ItemBlockWallOrFloor{
|
||||
|
||||
$colorIdMap = DyeColorIdMap::getInstance();
|
||||
$patternIdMap = BannerPatternTypeIdMap::getInstance();
|
||||
$patterns = $tag->getListTag(self::TAG_PATTERNS);
|
||||
if($patterns !== null && $patterns->getTagType() === NBT::TAG_Compound){
|
||||
/** @var CompoundTag $t */
|
||||
$patterns = $tag->getListTag(self::TAG_PATTERNS, CompoundTag::class);
|
||||
if($patterns !== null){
|
||||
foreach($patterns as $t){
|
||||
$patternColor = $colorIdMap->fromInvertedId($t->getInt(self::TAG_PATTERN_COLOR)) ?? DyeColor::BLACK; //TODO: missing pattern colour should be an error
|
||||
$patternType = $patternIdMap->fromId($t->getString(self::TAG_PATTERN_NAME));
|
||||
|
141
src/item/FireworkRocket.php
Normal file
141
src/item/FireworkRocket.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?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\item;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\entity\object\FireworkRocket as FireworkEntity;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_map;
|
||||
use function mt_rand;
|
||||
|
||||
class FireworkRocket extends Item{
|
||||
|
||||
public const TAG_FIREWORK_DATA = "Fireworks"; //TAG_Compound
|
||||
protected const TAG_FLIGHT_TIME_MULTIPLIER = "Flight"; //TAG_Byte
|
||||
public const TAG_EXPLOSIONS = "Explosions"; //TAG_List
|
||||
|
||||
protected int $flightTimeMultiplier = 1;
|
||||
|
||||
/** @var FireworkRocketExplosion[] */
|
||||
protected array $explosions = [];
|
||||
|
||||
/**
|
||||
* Returns the value that will be used to calculate a randomized flight duration
|
||||
* for the firework (equals the amount of gunpowder used in crafting the rocket).
|
||||
*
|
||||
* The higher this value, the longer the flight duration.
|
||||
*/
|
||||
public function getFlightTimeMultiplier() : int{
|
||||
return $this->flightTimeMultiplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value that will be used to calculate a randomized flight duration
|
||||
* for the firework.
|
||||
*
|
||||
* The higher this value, the longer the flight duration.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFlightTimeMultiplier(int $multiplier) : self{
|
||||
if($multiplier < 1 || $multiplier > 127){
|
||||
throw new \InvalidArgumentException("Flight time multiplier must be in range 1-127");
|
||||
}
|
||||
$this->flightTimeMultiplier = $multiplier;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FireworkRocketExplosion[]
|
||||
*/
|
||||
public function getExplosions() : array{
|
||||
return $this->explosions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FireworkRocketExplosion[] $explosions
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setExplosions(array $explosions) : self{
|
||||
Utils::validateArrayValueType($explosions, function(FireworkRocketExplosion $_) : void{});
|
||||
$this->explosions = $explosions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onInteractBlock(Player $player, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, array &$returnedItems) : ItemUseResult{
|
||||
//TODO: this would be nicer if Vector3::getSide() accepted floats for distance
|
||||
$position = $blockClicked->getPosition()->addVector($clickVector)->addVector(Vector3::zero()->getSide($face)->multiply(0.15));
|
||||
|
||||
$randomDuration = (($this->flightTimeMultiplier + 1) * 10) + mt_rand(0, 12);
|
||||
|
||||
$entity = new FireworkEntity(Location::fromObject($position, $player->getWorld(), Utils::getRandomFloat() * 360, 90), $randomDuration, $this->explosions);
|
||||
$entity->setOwningEntity($player);
|
||||
$entity->setMotion(new Vector3(
|
||||
(Utils::getRandomFloat() - Utils::getRandomFloat()) * 0.0023,
|
||||
0.05,
|
||||
(Utils::getRandomFloat() - Utils::getRandomFloat()) * 0.0023
|
||||
));
|
||||
$entity->spawnToAll();
|
||||
|
||||
$this->pop();
|
||||
|
||||
return ItemUseResult::SUCCESS;
|
||||
}
|
||||
|
||||
protected function deserializeCompoundTag(CompoundTag $tag) : void{
|
||||
parent::deserializeCompoundTag($tag);
|
||||
|
||||
$fireworkData = $tag->getCompoundTag(self::TAG_FIREWORK_DATA);
|
||||
if($fireworkData === null){
|
||||
throw new SavedDataLoadingException("Missing firework data");
|
||||
}
|
||||
|
||||
$this->setFlightTimeMultiplier($fireworkData->getByte(self::TAG_FLIGHT_TIME_MULTIPLIER, 1));
|
||||
|
||||
if(($explosions = $fireworkData->getListTag(self::TAG_EXPLOSIONS, CompoundTag::class)) !== null){
|
||||
foreach($explosions as $explosion){
|
||||
$this->explosions[] = FireworkRocketExplosion::fromCompoundTag($explosion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function serializeCompoundTag(CompoundTag $tag) : void{
|
||||
parent::serializeCompoundTag($tag);
|
||||
|
||||
$fireworkData = CompoundTag::create();
|
||||
$fireworkData->setByte(self::TAG_FLIGHT_TIME_MULTIPLIER, $this->flightTimeMultiplier);
|
||||
$fireworkData->setTag(self::TAG_EXPLOSIONS, new ListTag(array_map(fn(FireworkRocketExplosion $e) => $e->toCompoundTag(), $this->explosions)));
|
||||
|
||||
$tag->setTag(self::TAG_FIREWORK_DATA, $fireworkData);
|
||||
}
|
||||
}
|
190
src/item/FireworkRocketExplosion.php
Normal file
190
src/item/FireworkRocketExplosion.php
Normal file
@@ -0,0 +1,190 @@
|
||||
<?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\item;
|
||||
|
||||
use pocketmine\block\utils\DyeColor;
|
||||
use pocketmine\color\Color;
|
||||
use pocketmine\data\bedrock\DyeColorIdMap;
|
||||
use pocketmine\data\bedrock\FireworkRocketTypeIdMap;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\utils\Utils;
|
||||
use function array_key_first;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function ord;
|
||||
use function strlen;
|
||||
|
||||
class FireworkRocketExplosion{
|
||||
|
||||
protected const TAG_TYPE = "FireworkType"; //TAG_Byte
|
||||
protected const TAG_COLORS = "FireworkColor"; //TAG_ByteArray
|
||||
protected const TAG_FADE_COLORS = "FireworkFade"; //TAG_ByteArray
|
||||
protected const TAG_TWINKLE = "FireworkFlicker"; //TAG_Byte
|
||||
protected const TAG_TRAIL = "FireworkTrail"; //TAG_Byte
|
||||
|
||||
/**
|
||||
* @throws SavedDataLoadingException
|
||||
*/
|
||||
public static function fromCompoundTag(CompoundTag $tag) : self{
|
||||
$colors = self::decodeColors($tag->getByteArray(self::TAG_COLORS));
|
||||
if(count($colors) === 0){
|
||||
throw new SavedDataLoadingException("Colors list cannot be empty");
|
||||
}
|
||||
|
||||
return new self(
|
||||
FireworkRocketTypeIdMap::getInstance()->fromId($tag->getByte(self::TAG_TYPE)) ?? throw new SavedDataLoadingException("Invalid firework type"),
|
||||
$colors,
|
||||
self::decodeColors($tag->getByteArray(self::TAG_FADE_COLORS)),
|
||||
$tag->getByte(self::TAG_TWINKLE, 0) !== 0,
|
||||
$tag->getByte(self::TAG_TRAIL, 0) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DyeColor[]
|
||||
* @phpstan-return list<DyeColor>
|
||||
* @throws SavedDataLoadingException
|
||||
*/
|
||||
protected static function decodeColors(string $colorsBytes) : array{
|
||||
$colors = [];
|
||||
|
||||
$dyeColorIdMap = DyeColorIdMap::getInstance();
|
||||
for($i = 0, $len = strlen($colorsBytes); $i < $len; $i++){
|
||||
$colorByte = ord($colorsBytes[$i]);
|
||||
$color = $dyeColorIdMap->fromInvertedId($colorByte);
|
||||
if($color !== null){
|
||||
$colors[] = $color;
|
||||
}else{
|
||||
throw new SavedDataLoadingException("Unknown color $colorByte");
|
||||
}
|
||||
}
|
||||
|
||||
return $colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DyeColor[] $colors
|
||||
*/
|
||||
protected static function encodeColors(array $colors) : string{
|
||||
$colorsBytes = "";
|
||||
|
||||
$dyeColorIdMap = DyeColorIdMap::getInstance();
|
||||
foreach($colors as $color){
|
||||
$colorsBytes .= chr($dyeColorIdMap->toInvertedId($color));
|
||||
}
|
||||
|
||||
return $colorsBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DyeColor[] $colors
|
||||
* @param DyeColor[] $fadeColors
|
||||
* @phpstan-param non-empty-list<DyeColor> $colors
|
||||
* @phpstan-param list<DyeColor> $fadeColors
|
||||
*/
|
||||
public function __construct(
|
||||
protected FireworkRocketType $type,
|
||||
protected array $colors,
|
||||
protected array $fadeColors = [],
|
||||
protected bool $twinkle = false,
|
||||
protected bool $trail = false
|
||||
){
|
||||
if(count($colors) === 0){
|
||||
throw new \InvalidArgumentException("Colors list cannot be empty");
|
||||
}
|
||||
|
||||
$colorsValidator = function(DyeColor $_) : void{};
|
||||
|
||||
Utils::validateArrayValueType($colors, $colorsValidator);
|
||||
Utils::validateArrayValueType($fadeColors, $colorsValidator);
|
||||
}
|
||||
|
||||
public function getType() : FireworkRocketType{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the colors of the particles.
|
||||
*
|
||||
* @return DyeColor[]
|
||||
* @phpstan-return non-empty-list<DyeColor>
|
||||
*/
|
||||
public function getColors() : array{
|
||||
return $this->colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flash color of the explosion.
|
||||
*/
|
||||
public function getFlashColor() : DyeColor{
|
||||
return $this->colors[array_key_first($this->colors)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mixure of colors from {@link FireworkRocketExplosion::getColors()})
|
||||
*/
|
||||
public function getColorMix() : Color{
|
||||
/** @var Color[] $colors */
|
||||
$colors = [];
|
||||
foreach($this->colors as $dyeColor){
|
||||
$colors[] = $dyeColor->getRgbValue();
|
||||
}
|
||||
return Color::mix(...$colors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the colors to which the particles will change their color after a few seconds.
|
||||
* If it is empty, there will be no color change in the particles.
|
||||
*
|
||||
* @return DyeColor[]
|
||||
* @phpstan-return list<DyeColor>
|
||||
*/
|
||||
public function getFadeColors() : array{
|
||||
return $this->fadeColors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the explosion has a flickering effect.
|
||||
*/
|
||||
public function willTwinkle() : bool{
|
||||
return $this->twinkle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the particles have a trail effect.
|
||||
*/
|
||||
public function getTrail() : bool{
|
||||
return $this->trail;
|
||||
}
|
||||
|
||||
public function toCompoundTag() : CompoundTag{
|
||||
return CompoundTag::create()
|
||||
->setByte(self::TAG_TYPE, FireworkRocketTypeIdMap::getInstance()->toId($this->type))
|
||||
->setByteArray(self::TAG_COLORS, self::encodeColors($this->colors))
|
||||
->setByteArray(self::TAG_FADE_COLORS, self::encodeColors($this->fadeColors))
|
||||
->setByte(self::TAG_TWINKLE, $this->twinkle ? 1 : 0)
|
||||
->setByte(self::TAG_TRAIL, $this->trail ? 1 : 0);
|
||||
}
|
||||
}
|
46
src/item/FireworkRocketType.php
Normal file
46
src/item/FireworkRocketType.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\item;
|
||||
|
||||
use pocketmine\world\sound\FireworkExplosionSound;
|
||||
use pocketmine\world\sound\FireworkLargeExplosionSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
|
||||
enum FireworkRocketType{
|
||||
case SMALL_BALL;
|
||||
case LARGE_BALL;
|
||||
case STAR;
|
||||
case CREEPER;
|
||||
case BURST;
|
||||
|
||||
public function getExplosionSound() : Sound{
|
||||
return match($this){
|
||||
self::SMALL_BALL,
|
||||
self::STAR,
|
||||
self::CREEPER,
|
||||
self::BURST => new FireworkExplosionSound(),
|
||||
self::LARGE_BALL => new FireworkLargeExplosionSound(),
|
||||
};
|
||||
}
|
||||
}
|
112
src/item/FireworkStar.php
Normal file
112
src/item/FireworkStar.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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\item;
|
||||
|
||||
use pocketmine\block\utils\DyeColor;
|
||||
use pocketmine\color\Color;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\utils\Binary;
|
||||
|
||||
class FireworkStar extends Item{
|
||||
|
||||
protected const TAG_EXPLOSION = "FireworksItem"; //TAG_Compound
|
||||
protected const TAG_CUSTOM_COLOR = "customColor"; //TAG_Int
|
||||
|
||||
protected FireworkRocketExplosion $explosion;
|
||||
|
||||
protected ?Color $customColor = null;
|
||||
|
||||
public function __construct(ItemIdentifier $identifier, string $name){
|
||||
parent::__construct($identifier, $name);
|
||||
|
||||
$this->explosion = new FireworkRocketExplosion(
|
||||
FireworkRocketType::SMALL_BALL,
|
||||
colors: [DyeColor::BLACK],
|
||||
fadeColors: [],
|
||||
twinkle: false,
|
||||
trail: false
|
||||
);
|
||||
}
|
||||
|
||||
public function getExplosion() : FireworkRocketExplosion{
|
||||
return $this->explosion;
|
||||
}
|
||||
|
||||
/** @return $this */
|
||||
public function setExplosion(FireworkRocketExplosion $explosion) : self{
|
||||
$this->explosion = $explosion;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the displayed color of the item.
|
||||
* The mixture of explosion colors, or the custom color if it is set.
|
||||
*/
|
||||
public function getColor() : Color{
|
||||
return $this->customColor ?? $this->explosion->getColorMix();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the displayed custom color of the item that overrides
|
||||
* the mixture of explosion colors, or null is it is not set.
|
||||
*/
|
||||
public function getCustomColor() : ?Color{
|
||||
return $this->customColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the displayed custom color of the item that overrides
|
||||
* the mixture of explosion colors, or removes if $color is null.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCustomColor(?Color $color) : self{
|
||||
$this->customColor = $color;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function deserializeCompoundTag(CompoundTag $tag) : void{
|
||||
parent::deserializeCompoundTag($tag);
|
||||
|
||||
$explosionTag = $tag->getTag(self::TAG_EXPLOSION);
|
||||
if(!$explosionTag instanceof CompoundTag){
|
||||
throw new SavedDataLoadingException("Missing explosion data");
|
||||
}
|
||||
$this->explosion = FireworkRocketExplosion::fromCompoundTag($explosionTag);
|
||||
|
||||
$customColor = Color::fromARGB(Binary::unsignInt($tag->getInt(self::TAG_CUSTOM_COLOR)));
|
||||
$color = $this->explosion->getColorMix();
|
||||
if(!$customColor->equals($color)){ //check that $customColor is actually custom.
|
||||
$this->customColor = $customColor;
|
||||
}
|
||||
}
|
||||
|
||||
protected function serializeCompoundTag(CompoundTag $tag) : void{
|
||||
parent::serializeCompoundTag($tag);
|
||||
|
||||
$tag->setTag(self::TAG_EXPLOSION, $this->explosion->toCompoundTag());
|
||||
$tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->getColor()->toARGB()));
|
||||
}
|
||||
}
|
@@ -293,9 +293,8 @@ class Item implements \JsonSerializable{
|
||||
$display = $tag->getCompoundTag(self::TAG_DISPLAY);
|
||||
if($display !== null){
|
||||
$this->customName = $display->getString(self::TAG_DISPLAY_NAME, $this->customName);
|
||||
$lore = $display->getListTag(self::TAG_DISPLAY_LORE);
|
||||
if($lore !== null && $lore->getTagType() === NBT::TAG_String){
|
||||
/** @var StringTag $t */
|
||||
$lore = $display->getListTag(self::TAG_DISPLAY_LORE, StringTag::class);
|
||||
if($lore !== null){
|
||||
foreach($lore as $t){
|
||||
$this->lore[] = $t->getValue();
|
||||
}
|
||||
@@ -303,9 +302,8 @@ class Item implements \JsonSerializable{
|
||||
}
|
||||
|
||||
$this->removeEnchantments();
|
||||
$enchantments = $tag->getListTag(self::TAG_ENCH);
|
||||
if($enchantments !== null && $enchantments->getTagType() === NBT::TAG_Compound){
|
||||
/** @var CompoundTag $enchantment */
|
||||
$enchantments = $tag->getListTag(self::TAG_ENCH, CompoundTag::class);
|
||||
if($enchantments !== null){
|
||||
foreach($enchantments as $enchantment){
|
||||
$magicNumber = $enchantment->getShort(self::TAG_ENCH_ID, -1);
|
||||
$level = $enchantment->getShort(self::TAG_ENCH_LVL, 0);
|
||||
@@ -322,17 +320,15 @@ class Item implements \JsonSerializable{
|
||||
$this->blockEntityTag = $tag->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG);
|
||||
|
||||
$this->canPlaceOn = [];
|
||||
$canPlaceOn = $tag->getListTag(self::TAG_CAN_PLACE_ON);
|
||||
if($canPlaceOn !== null && $canPlaceOn->getTagType() === NBT::TAG_String){
|
||||
/** @var StringTag $entry */
|
||||
$canPlaceOn = $tag->getListTag(self::TAG_CAN_PLACE_ON, StringTag::class);
|
||||
if($canPlaceOn !== null){
|
||||
foreach($canPlaceOn as $entry){
|
||||
$this->canPlaceOn[$entry->getValue()] = $entry->getValue();
|
||||
}
|
||||
}
|
||||
$this->canDestroy = [];
|
||||
$canDestroy = $tag->getListTag(self::TAG_CAN_DESTROY);
|
||||
if($canDestroy !== null && $canDestroy->getTagType() === NBT::TAG_String){
|
||||
/** @var StringTag $entry */
|
||||
$canDestroy = $tag->getListTag(self::TAG_CAN_DESTROY, StringTag::class);
|
||||
if($canDestroy !== null){
|
||||
foreach($canDestroy as $entry){
|
||||
$this->canDestroy[$entry->getValue()] = $entry->getValue();
|
||||
}
|
||||
|
@@ -346,8 +346,11 @@ final class ItemTypeIds{
|
||||
public const PALE_OAK_HANGING_SIGN = 20307;
|
||||
public const SPRUCE_HANGING_SIGN = 20308;
|
||||
public const WARPED_HANGING_SIGN = 20309;
|
||||
public const TRIDENT = 20310;
|
||||
public const FIREWORK_ROCKET = 20311;
|
||||
public const FIREWORK_STAR = 20312;
|
||||
|
||||
public const FIRST_UNUSED_ITEM_ID = 20310;
|
||||
public const FIRST_UNUSED_ITEM_ID = 20313;
|
||||
|
||||
private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID;
|
||||
|
||||
|
@@ -1366,6 +1366,9 @@ final class StringToItemParser extends StringToTParser{
|
||||
$result->register("eye_drops", fn() => Items::MEDICINE()->setType(MedicineType::EYE_DROPS));
|
||||
$result->register("feather", fn() => Items::FEATHER());
|
||||
$result->register("fermented_spider_eye", fn() => Items::FERMENTED_SPIDER_EYE());
|
||||
$result->register("firework_rocket", fn() => Items::FIREWORK_ROCKET());
|
||||
$result->register("firework_star", fn() => Items::FIREWORK_STAR());
|
||||
$result->register("fireworks", fn() => Items::FIREWORK_ROCKET());
|
||||
$result->register("fire_charge", fn() => Items::FIRE_CHARGE());
|
||||
$result->register("fish", fn() => Items::RAW_FISH());
|
||||
$result->register("fishing_rod", fn() => Items::FISHING_ROD());
|
||||
@@ -1560,6 +1563,7 @@ final class StringToItemParser extends StringToTParser{
|
||||
$result->register("torchflower_seeds", fn() => Items::TORCHFLOWER_SEEDS());
|
||||
$result->register("tide_armor_trim_smithing_template", fn() => Items::TIDE_ARMOR_TRIM_SMITHING_TEMPLATE());
|
||||
$result->register("totem", fn() => Items::TOTEM());
|
||||
$result->register("trident", fn() => Items::TRIDENT());
|
||||
$result->register("turtle_helmet", fn() => Items::TURTLE_HELMET());
|
||||
$result->register("vex_armor_trim_smithing_template", fn() => Items::VEX_ARMOR_TRIM_SMITHING_TEMPLATE());
|
||||
$result->register("turtle_shell_piece", fn() => Items::SCUTE());
|
||||
|
93
src/item/Trident.php
Normal file
93
src/item/Trident.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?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\item;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\entity\projectile\Trident as TridentEntity;
|
||||
use pocketmine\event\entity\ProjectileLaunchEvent;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\world\sound\TridentThrowSound;
|
||||
use function min;
|
||||
|
||||
class Trident extends Tool implements Releasable{
|
||||
|
||||
public function getMaxDurability() : int{
|
||||
return 251;
|
||||
}
|
||||
|
||||
public function onReleaseUsing(Player $player, array &$returnedItems) : ItemUseResult{
|
||||
$location = $player->getLocation();
|
||||
|
||||
$diff = $player->getItemUseDuration();
|
||||
if($diff < 14){
|
||||
return ItemUseResult::FAIL;
|
||||
}
|
||||
|
||||
$item = $this->pop();
|
||||
if($player->hasFiniteResources()){
|
||||
$item->applyDamage(1);
|
||||
}
|
||||
$entity = new TridentEntity(Location::fromObject(
|
||||
$player->getEyePos(),
|
||||
$player->getWorld(),
|
||||
($location->yaw > 180 ? 360 : 0) - $location->yaw,
|
||||
-$location->pitch
|
||||
), $item, $player);
|
||||
$p = $diff / 20;
|
||||
$baseForce = min((($p ** 2) + $p * 2) / 3, 1) * 2.4;
|
||||
$entity->setMotion($player->getDirectionVector()->multiply($baseForce));
|
||||
|
||||
$ev = new ProjectileLaunchEvent($entity);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
$ev->getEntity()->flagForDespawn();
|
||||
return ItemUseResult::FAIL;
|
||||
}
|
||||
$ev->getEntity()->spawnToAll();
|
||||
$location->getWorld()->addSound($location, new TridentThrowSound());
|
||||
|
||||
return ItemUseResult::SUCCESS;
|
||||
}
|
||||
|
||||
public function getAttackPoints() : int{
|
||||
return 9;
|
||||
}
|
||||
|
||||
public function canStartUsingItem(Player $player) : bool{
|
||||
return $this->damage < $this->getMaxDurability();
|
||||
}
|
||||
|
||||
public function onAttackEntity(Entity $victim, array &$returnedItems) : bool{
|
||||
return $this->applyDamage(1);
|
||||
}
|
||||
|
||||
public function onDestroyBlock(Block $block, array &$returnedItems) : bool{
|
||||
if(!$block->getBreakInfo()->breaksInstantly()){
|
||||
return $this->applyDamage(2);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -168,6 +168,8 @@ use function strtolower;
|
||||
* @method static Item EYE_ARMOR_TRIM_SMITHING_TEMPLATE()
|
||||
* @method static Item FEATHER()
|
||||
* @method static Item FERMENTED_SPIDER_EYE()
|
||||
* @method static FireworkRocket FIREWORK_ROCKET()
|
||||
* @method static FireworkStar FIREWORK_STAR()
|
||||
* @method static FireCharge FIRE_CHARGE()
|
||||
* @method static FishingRod FISHING_ROD()
|
||||
* @method static Item FLINT()
|
||||
@@ -335,6 +337,7 @@ use function strtolower;
|
||||
* @method static Item TIDE_ARMOR_TRIM_SMITHING_TEMPLATE()
|
||||
* @method static TorchflowerSeeds TORCHFLOWER_SEEDS()
|
||||
* @method static Totem TOTEM()
|
||||
* @method static Trident TRIDENT()
|
||||
* @method static TurtleHelmet TURTLE_HELMET()
|
||||
* @method static Item VEX_ARMOR_TRIM_SMITHING_TEMPLATE()
|
||||
* @method static SpawnEgg VILLAGER_SPAWN_EGG()
|
||||
@@ -510,6 +513,8 @@ final class VanillaItems{
|
||||
self::register("experience_bottle", fn(IID $id) => new ExperienceBottle($id, "Bottle o' Enchanting"));
|
||||
self::register("feather", fn(IID $id) => new Item($id, "Feather"));
|
||||
self::register("fermented_spider_eye", fn(IID $id) => new Item($id, "Fermented Spider Eye"));
|
||||
self::register("firework_rocket", fn(IID $id) => new FireworkRocket($id, "Firework Rocket"));
|
||||
self::register("firework_star", fn(IID $id) => new FireworkStar($id, "Firework Star"));
|
||||
self::register("fire_charge", fn(IID $id) => new FireCharge($id, "Fire Charge"));
|
||||
self::register("fishing_rod", fn(IID $id) => new FishingRod($id, "Fishing Rod", [EnchantmentTags::FISHING_ROD]));
|
||||
self::register("flint", fn(IID $id) => new Item($id, "Flint"));
|
||||
@@ -630,6 +635,7 @@ final class VanillaItems{
|
||||
self::register("sweet_berries", fn(IID $id) => new SweetBerries($id, "Sweet Berries"));
|
||||
self::register("torchflower_seeds", fn(IID $id) => new TorchflowerSeeds($id, "Torchflower Seeds"));
|
||||
self::register("totem", fn(IID $id) => new Totem($id, "Totem of Undying"));
|
||||
self::register("trident", fn(IID $id) => new Trident($id, "Trident"));
|
||||
self::register("warped_sign", fn(IID $id) => new ItemBlockWallOrFloor($id, Blocks::WARPED_SIGN(), Blocks::WARPED_WALL_SIGN()));
|
||||
self::register("warped_hanging_sign", fn(IID $id) => new HangingSign($id, "Warped Hanging Sign", Blocks::WARPED_CEILING_CENTER_HANGING_SIGN(), Blocks::WARPED_CEILING_EDGES_HANGING_SIGN(), Blocks::WARPED_WALL_HANGING_SIGN()));
|
||||
self::register("water_bucket", fn(IID $id) => new LiquidBucket($id, "Water Bucket", Blocks::WATER()));
|
||||
|
@@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\item;
|
||||
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
@@ -167,14 +166,12 @@ abstract class WritableBookBase extends Item{
|
||||
|
||||
$pages = $tag->getListTag(self::TAG_PAGES);
|
||||
if($pages !== null){
|
||||
if($pages->getTagType() === NBT::TAG_Compound){ //PE format
|
||||
/** @var CompoundTag $page */
|
||||
foreach($pages as $page){
|
||||
if(($compoundPages = $pages->cast(CompoundTag::class)) !== null){ //PE format
|
||||
foreach($compoundPages as $page){
|
||||
$this->pages[] = new WritableBookPage(mb_scrub($page->getString(self::TAG_PAGE_TEXT), 'UTF-8'), $page->getString(self::TAG_PAGE_PHOTONAME, ""));
|
||||
}
|
||||
}elseif($pages->getTagType() === NBT::TAG_String){ //PC format
|
||||
/** @var StringTag $page */
|
||||
foreach($pages as $page){
|
||||
}elseif(($stringPages = $pages->cast(StringTag::class)) !== null){ //PC format
|
||||
foreach($stringPages as $page){
|
||||
$this->pages[] = new WritableBookPage(mb_scrub($page->getValue(), 'UTF-8'));
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pocketmine\network\mcpe\compression\CompressBatchPromise;
|
||||
use pocketmine\network\mcpe\compression\Compressor;
|
||||
use pocketmine\network\mcpe\convert\TypeConverter;
|
||||
@@ -33,7 +34,6 @@ use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\network\mcpe\serializer\ChunkSerializer;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\FastChunkSerializer;
|
||||
use function chr;
|
||||
@@ -73,11 +73,11 @@ class ChunkRequestTask extends AsyncTask{
|
||||
$converter = TypeConverter::getInstance();
|
||||
$payload = ChunkSerializer::serializeFullChunk($chunk, $dimensionId, $converter->getBlockTranslator(), $this->tiles);
|
||||
|
||||
$stream = new BinaryStream();
|
||||
$stream = new ByteBufferWriter();
|
||||
PacketBatch::encodePackets($stream, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $dimensionId, $subCount, false, null, $payload)]);
|
||||
|
||||
$compressor = $this->compressor->deserialize();
|
||||
$this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getBuffer()));
|
||||
$this->setResult(chr($compressor->getNetworkId()) . $compressor->compress($stream->getData()));
|
||||
}
|
||||
|
||||
public function onCompletion() : void{
|
||||
|
@@ -23,8 +23,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pmmp\encoding\BE;
|
||||
use pmmp\encoding\Byte;
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\Utils;
|
||||
use function base64_decode;
|
||||
use function base64_encode;
|
||||
@@ -32,6 +34,7 @@ use function bin2hex;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function hex2bin;
|
||||
use function is_array;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
@@ -54,6 +57,7 @@ use function strlen;
|
||||
use function strtr;
|
||||
use function substr;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
use const OPENSSL_ALGO_SHA384;
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
@@ -130,17 +134,17 @@ final class JwtUtils{
|
||||
return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence;
|
||||
}
|
||||
|
||||
private static function signaturePartFromAsn1(BinaryStream $stream) : string{
|
||||
$prefix = $stream->get(1);
|
||||
private static function signaturePartFromAsn1(ByteBufferReader $stream) : string{
|
||||
$prefix = $stream->readByteArray(1);
|
||||
if($prefix !== self::ASN1_INTEGER_TAG){
|
||||
throw new \InvalidArgumentException("Expected an ASN.1 INTEGER tag, got " . bin2hex($prefix));
|
||||
}
|
||||
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
|
||||
$length = $stream->getByte();
|
||||
$length = Byte::readUnsigned($stream);
|
||||
if($length > self::SIGNATURE_PART_LENGTH + 1){ //each part may have an extra leading 0 byte to prevent it being interpreted as a negative number
|
||||
throw new \InvalidArgumentException("Expected at most 49 bytes for signature R or S, got $length");
|
||||
}
|
||||
$part = $stream->get($length);
|
||||
$part = $stream->readByteArray($length);
|
||||
return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
@@ -156,11 +160,11 @@ final class JwtUtils{
|
||||
throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts));
|
||||
}
|
||||
|
||||
$stream = new BinaryStream($parts);
|
||||
$stream = new ByteBufferReader($parts);
|
||||
$rRaw = self::signaturePartFromAsn1($stream);
|
||||
$sRaw = self::signaturePartFromAsn1($stream);
|
||||
|
||||
if(!$stream->feof()){
|
||||
if($stream->getUnreadLength() > 0){
|
||||
throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data");
|
||||
}
|
||||
|
||||
@@ -170,17 +174,17 @@ final class JwtUtils{
|
||||
/**
|
||||
* @throws JwtException
|
||||
*/
|
||||
public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{
|
||||
public static function verify(string $jwt, string $signingKeyDer, bool $ec) : bool{
|
||||
[$header, $body, $signature] = self::split($jwt);
|
||||
|
||||
$rawSignature = self::b64UrlDecode($signature);
|
||||
$derSignature = self::rawSignatureToDer($rawSignature);
|
||||
$derSignature = $ec ? self::rawSignatureToDer($rawSignature) : $rawSignature;
|
||||
|
||||
$v = openssl_verify(
|
||||
$header . '.' . $body,
|
||||
$derSignature,
|
||||
$signingKey,
|
||||
self::SIGNATURE_ALGORITHM
|
||||
self::derPublicKeyToPem($signingKeyDer),
|
||||
$ec ? self::SIGNATURE_ALGORITHM : OPENSSL_ALGO_SHA256
|
||||
);
|
||||
switch($v){
|
||||
case 0: return false;
|
||||
@@ -238,22 +242,56 @@ final class JwtUtils{
|
||||
throw new AssumptionFailedError("OpenSSL resource contains invalid public key");
|
||||
}
|
||||
|
||||
/**
|
||||
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
|
||||
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
|
||||
*/
|
||||
private static function encodeDerLength(int $length) : string{
|
||||
if ($length <= 0x7F) {
|
||||
return chr($length);
|
||||
}
|
||||
|
||||
$lengthBytes = ltrim(BE::packUnsignedInt($length), "\x00");
|
||||
|
||||
return chr(0x80 | strlen($lengthBytes)) . $lengthBytes;
|
||||
}
|
||||
|
||||
private static function encodeDerBytes(int $tag, string $data) : string{
|
||||
return chr($tag) . self::encodeDerLength(strlen($data)) . $data;
|
||||
}
|
||||
|
||||
public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{
|
||||
$signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey)));
|
||||
$signingKeyOpenSSL = openssl_pkey_get_public(self::derPublicKeyToPem($derKey));
|
||||
if($signingKeyOpenSSL === false){
|
||||
throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string());
|
||||
}
|
||||
$details = openssl_pkey_get_details($signingKeyOpenSSL);
|
||||
if($details === false){
|
||||
throw new JwtException("OpenSSL failed to get details from key: " . openssl_error_string());
|
||||
}
|
||||
if(!isset($details['ec']['curve_name'])){
|
||||
throw new JwtException("Expected an EC key");
|
||||
}
|
||||
$curve = $details['ec']['curve_name'];
|
||||
if($curve !== self::BEDROCK_SIGNING_KEY_CURVE_NAME){
|
||||
throw new JwtException("Key must belong to curve " . self::BEDROCK_SIGNING_KEY_CURVE_NAME . ", got $curve");
|
||||
}
|
||||
return $signingKeyOpenSSL;
|
||||
}
|
||||
|
||||
public static function derPublicKeyToPem(string $derKey) : string{
|
||||
return sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a public key represented in DER format from RSA modulus and exponent information
|
||||
*
|
||||
* @param string $nBase64 The RSA modulus encoded in Base64
|
||||
* @param string $eBase64 The RSA exponent encoded in Base64
|
||||
*/
|
||||
public static function rsaPublicKeyModExpToDer(string $nBase64, string $eBase64) : string{
|
||||
$mod = self::b64UrlDecode($nBase64);
|
||||
$exp = self::b64UrlDecode($eBase64);
|
||||
|
||||
$modulus = self::encodeDerBytes(2, $mod);
|
||||
$publicExponent = self::encodeDerBytes(2, $exp);
|
||||
|
||||
$rsaPublicKey = self::encodeDerBytes(48, $modulus . $publicExponent);
|
||||
|
||||
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
|
||||
$rsaOID = hex2bin('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
|
||||
$rsaPublicKey = chr(0) . $rsaPublicKey;
|
||||
$rsaPublicKey = self::encodeDerBytes(3, $rsaPublicKey);
|
||||
|
||||
return self::encodeDerBytes(48, $rsaOID . $rsaPublicKey);
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pmmp\encoding\DataDecodeException;
|
||||
use pocketmine\entity\effect\EffectInstance;
|
||||
use pocketmine\event\player\PlayerDuplicateLoginEvent;
|
||||
use pocketmine\event\player\PlayerResourcePackOfferEvent;
|
||||
@@ -70,7 +73,6 @@ use pocketmine\network\mcpe\protocol\PlayerStartItemCooldownPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\ServerboundPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetDifficultyPacket;
|
||||
@@ -109,8 +111,6 @@ use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\utils\ObjectSet;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\world\format\io\GlobalItemDataHandlers;
|
||||
@@ -401,7 +401,7 @@ class NetworkSession{
|
||||
}
|
||||
|
||||
try{
|
||||
$stream = new BinaryStream($decompressed);
|
||||
$stream = new ByteBufferReader($decompressed);
|
||||
foreach(PacketBatch::decodeRaw($stream) as $buffer){
|
||||
$this->gamePacketLimiter->decrement();
|
||||
$packet = $this->packetPool->getPacket($buffer);
|
||||
@@ -421,7 +421,7 @@ class NetworkSession{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}catch(PacketDecodeException|BinaryDataException $e){
|
||||
}catch(PacketDecodeException|DataDecodeException $e){
|
||||
$this->logger->logException($e);
|
||||
throw PacketHandlingException::wrap($e, "Packet batch decode error");
|
||||
}
|
||||
@@ -453,14 +453,14 @@ class NetworkSession{
|
||||
$decodeTimings = Timings::getDecodeDataPacketTimings($packet);
|
||||
$decodeTimings->startTiming();
|
||||
try{
|
||||
$stream = PacketSerializer::decoder($buffer, 0);
|
||||
$stream = new ByteBufferReader($buffer);
|
||||
try{
|
||||
$packet->decode($stream);
|
||||
}catch(PacketDecodeException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!$stream->feof()){
|
||||
$remains = substr($stream->getBuffer(), $stream->getOffset());
|
||||
if($stream->getUnreadLength() > 0){
|
||||
$remains = substr($stream->getData(), $stream->getOffset());
|
||||
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
|
||||
}
|
||||
}finally{
|
||||
@@ -478,7 +478,7 @@ class NetworkSession{
|
||||
$handlerTimings->startTiming();
|
||||
try{
|
||||
if($this->handler === null || !$packet->handle($this->handler)){
|
||||
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
|
||||
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getData()));
|
||||
}
|
||||
}finally{
|
||||
$handlerTimings->stopTiming();
|
||||
@@ -530,8 +530,10 @@ class NetworkSession{
|
||||
if($ackReceiptResolver !== null){
|
||||
$this->sendBufferAckPromises[] = $ackReceiptResolver;
|
||||
}
|
||||
$writer = new ByteBufferWriter();
|
||||
foreach($packets as $evPacket){
|
||||
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder(), $evPacket));
|
||||
$writer->clear(); //memory reuse let's gooooo
|
||||
$this->addToSendBuffer(self::encodePacketTimed($writer, $evPacket));
|
||||
}
|
||||
if($immediate){
|
||||
$this->flushGamePacketQueue();
|
||||
@@ -564,12 +566,12 @@ class NetworkSession{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{
|
||||
public static function encodePacketTimed(ByteBufferWriter $serializer, ClientboundPacket $packet) : string{
|
||||
$timings = Timings::getEncodeDataPacketTimings($packet);
|
||||
$timings->startTiming();
|
||||
try{
|
||||
$packet->encode($serializer);
|
||||
return $serializer->getBuffer();
|
||||
return $serializer->getData();
|
||||
}finally{
|
||||
$timings->stopTiming();
|
||||
}
|
||||
@@ -591,13 +593,13 @@ class NetworkSession{
|
||||
$syncMode = false;
|
||||
}
|
||||
|
||||
$stream = new BinaryStream();
|
||||
$stream = new ByteBufferWriter();
|
||||
PacketBatch::encodeRaw($stream, $this->sendBuffer);
|
||||
|
||||
if($this->enableCompression){
|
||||
$batch = $this->server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
|
||||
$batch = $this->server->prepareBatch($stream->getData(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
|
||||
}else{
|
||||
$batch = $stream->getBuffer();
|
||||
$batch = $stream->getData();
|
||||
}
|
||||
$this->sendBuffer = [];
|
||||
$ackPromises = $this->sendBufferAckPromises;
|
||||
|
@@ -23,12 +23,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe;
|
||||
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pocketmine\event\server\DataPacketSendEvent;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use function count;
|
||||
use function log;
|
||||
use function spl_object_id;
|
||||
@@ -64,8 +63,10 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
|
||||
|
||||
$totalLength = 0;
|
||||
$packetBuffers = [];
|
||||
$writer = new ByteBufferWriter();
|
||||
foreach($packets as $packet){
|
||||
$buffer = NetworkSession::encodePacketTimed(PacketSerializer::encoder(), $packet);
|
||||
$writer->clear(); //memory reuse let's gooooo
|
||||
$buffer = NetworkSession::encodePacketTimed($writer, $packet);
|
||||
//varint length prefix + packet buffer
|
||||
$totalLength += (((int) log(strlen($buffer), 128)) + 1) + strlen($buffer);
|
||||
$packetBuffers[] = $buffer;
|
||||
@@ -77,9 +78,9 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
|
||||
$threshold = $compressor->getCompressionThreshold();
|
||||
if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){
|
||||
//do not prepare shared batch unless we're sure it will be compressed
|
||||
$stream = new BinaryStream();
|
||||
$stream = new ByteBufferWriter();
|
||||
PacketBatch::encodeRaw($stream, $packetBuffers);
|
||||
$batchBuffer = $stream->getBuffer();
|
||||
$batchBuffer = $stream->getData();
|
||||
|
||||
$batch = $this->server->prepareBatch($batchBuffer, $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
|
||||
foreach($compressorTargets as $target){
|
||||
|
165
src/network/mcpe/auth/AuthJwtHelper.php
Normal file
165
src/network/mcpe/auth/AuthJwtHelper.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\network\mcpe\JwtException;
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\login\JwtBodyRfc7519;
|
||||
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthJwtBody;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
|
||||
use pocketmine\network\mcpe\protocol\types\login\SelfSignedJwtHeader;
|
||||
use function base64_decode;
|
||||
use function time;
|
||||
|
||||
final class AuthJwtHelper{
|
||||
|
||||
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
|
||||
|
||||
private const CLOCK_DRIFT_MAX = 60;
|
||||
|
||||
/**
|
||||
* @throws VerifyLoginException if the token is expired or not yet valid
|
||||
*/
|
||||
private static function checkExpiry(JwtBodyRfc7519 $claims) : void{
|
||||
$time = time();
|
||||
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
|
||||
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
|
||||
}
|
||||
|
||||
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
|
||||
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws VerifyLoginException if errors are encountered
|
||||
*/
|
||||
public static function validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience) : XboxAuthJwtBody{
|
||||
try{
|
||||
if(!JwtUtils::verify($jwt, $signingKeyDer, ec: false)){
|
||||
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
|
||||
}
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
try{
|
||||
[, $claimsArray, ] = JwtUtils::parse($jwt);
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
|
||||
try{
|
||||
//nasty dynamic new for JsonMapper
|
||||
$claims = $mapper->map($claimsArray, new XboxAuthJwtBody());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
if(!isset($claims->iss) || $claims->iss !== $issuer){
|
||||
throw new VerifyLoginException("Invalid JWT issuer");
|
||||
}
|
||||
|
||||
if(!isset($claims->aud) || $claims->aud !== $audience){
|
||||
throw new VerifyLoginException("Invalid JWT audience");
|
||||
}
|
||||
|
||||
self::checkExpiry($claims);
|
||||
|
||||
return $claims;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws VerifyLoginException if errors are encountered
|
||||
*/
|
||||
public static function validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer) : LegacyAuthJwtBody{
|
||||
self::validateSelfSignedToken($jwt, $expectedKeyDer);
|
||||
|
||||
//TODO: this parses the JWT twice and throws away a bunch of parts, optimize this
|
||||
[, $claimsArray, ] = JwtUtils::parse($jwt);
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
try{
|
||||
/** @var LegacyAuthJwtBody $claims */
|
||||
$claims = $mapper->map($claimsArray, new LegacyAuthJwtBody());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
self::checkExpiry($claims);
|
||||
|
||||
return $claims;
|
||||
}
|
||||
|
||||
public static function validateSelfSignedToken(string $jwt, ?string $expectedKeyDer) : void{
|
||||
try{
|
||||
[$headersArray, ] = JwtUtils::parse($jwt);
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
|
||||
try{
|
||||
/** @var SelfSignedJwtHeader $headers */
|
||||
$headers = $mapper->map($headersArray, new SelfSignedJwtHeader());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$headerDerKey = base64_decode($headers->x5u, true);
|
||||
if($headerDerKey === false){
|
||||
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
|
||||
}
|
||||
if($expectedKeyDer !== null && $headerDerKey !== $expectedKeyDer){
|
||||
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
|
||||
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
|
||||
}
|
||||
|
||||
try{
|
||||
if(!JwtUtils::verify($jwt, $headerDerKey, ec: true)){
|
||||
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
|
||||
}
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
164
src/network/mcpe/auth/AuthKeyProvider.php
Normal file
164
src/network/mcpe/auth/AuthKeyProvider.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\network\mcpe\JwtException;
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
|
||||
use pocketmine\promise\Promise;
|
||||
use pocketmine\promise\PromiseResolver;
|
||||
use pocketmine\scheduler\AsyncPool;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function array_keys;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function time;
|
||||
|
||||
class AuthKeyProvider{
|
||||
private const ALLOWED_REFRESH_INTERVAL = 30 * 60; // 30 minutes
|
||||
|
||||
private ?AuthKeyring $keyring = null;
|
||||
|
||||
/** @phpstan-var PromiseResolver<AuthKeyring> */
|
||||
private ?PromiseResolver $resolver = null;
|
||||
|
||||
private int $lastFetch = 0;
|
||||
|
||||
public function __construct(
|
||||
private readonly \Logger $logger,
|
||||
private readonly AsyncPool $asyncPool,
|
||||
private readonly int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL
|
||||
){}
|
||||
|
||||
/**
|
||||
* Fetches the key for the given key ID.
|
||||
* The promise will be resolved with an array of [issuer, pemPublicKey].
|
||||
*
|
||||
* @phpstan-return Promise<array{string, string}>
|
||||
*/
|
||||
public function getKey(string $keyId) : Promise{
|
||||
/** @phpstan-var PromiseResolver<array{string, string}> $resolver */
|
||||
$resolver = new PromiseResolver();
|
||||
|
||||
if(
|
||||
$this->keyring === null || //we haven't fetched keys yet
|
||||
($this->keyring->getKey($keyId) === null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds) //we don't recognise this one & keys might be outdated
|
||||
){
|
||||
//only refresh keys when we see one we don't recognise
|
||||
$this->fetchKeys()->onCompletion(
|
||||
onSuccess: fn(AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId),
|
||||
onFailure: $resolver->reject(...)
|
||||
);
|
||||
}else{
|
||||
$this->resolveKey($resolver, $this->keyring, $keyId);
|
||||
}
|
||||
|
||||
return $resolver->getPromise();
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param PromiseResolver<array{string, string}> $resolver
|
||||
*/
|
||||
private function resolveKey(PromiseResolver $resolver, AuthKeyring $keyring, string $keyId) : void{
|
||||
$key = $keyring->getKey($keyId);
|
||||
if($key === null){
|
||||
$this->logger->debug("Key $keyId not recognised!");
|
||||
$resolver->reject();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug("Key $keyId found in keychain");
|
||||
$resolver->resolve([$keyring->getIssuer(), $key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array<string, AuthServiceKey>|null $keys
|
||||
* @phpstan-param string[]|null $errors
|
||||
*/
|
||||
private function onKeysFetched(?array $keys, string $issuer, ?array $errors) : void{
|
||||
$resolver = $this->resolver;
|
||||
if($resolver === null){
|
||||
throw new AssumptionFailedError("Not expecting this to be called without a resolver present");
|
||||
}
|
||||
if($errors !== null){
|
||||
$this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors));
|
||||
//we might've still succeeded in fetching keys even if there were errors, so don't return
|
||||
}
|
||||
|
||||
if($keys === null){
|
||||
$this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
|
||||
$resolver->reject();
|
||||
}else{
|
||||
$pemKeys = [];
|
||||
foreach($keys as $keyModel){
|
||||
if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){
|
||||
$this->logger->error("Key ID $keyModel->kid doesn't have the expected properties: expected use=sig, kty=RSA, got use=$keyModel->use, kty=$keyModel->kty");
|
||||
continue;
|
||||
}
|
||||
$derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
|
||||
|
||||
//make sure the key is valid
|
||||
try{
|
||||
JwtUtils::parseDerPublicKey($derKey);
|
||||
}catch(JwtException $e){
|
||||
$this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
|
||||
$this->logger->logException($e);
|
||||
continue;
|
||||
}
|
||||
|
||||
//retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads
|
||||
$pemKeys[$keyModel->kid] = $derKey;
|
||||
}
|
||||
|
||||
if(count($keys) === 0){
|
||||
$this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
|
||||
$resolver->reject();
|
||||
}else{
|
||||
$this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys)));
|
||||
$this->keyring = new AuthKeyring($issuer, $pemKeys);
|
||||
$this->lastFetch = time();
|
||||
$resolver->resolve($this->keyring);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-return Promise<AuthKeyring>
|
||||
*/
|
||||
private function fetchKeys() : Promise{
|
||||
if($this->resolver !== null){
|
||||
$this->logger->debug("Key refresh was requested, but it's already in progress");
|
||||
return $this->resolver->getPromise();
|
||||
}
|
||||
|
||||
$this->logger->notice("Fetching new authentication keys");
|
||||
|
||||
/** @phpstan-var PromiseResolver<AuthKeyring> $resolver */
|
||||
$resolver = new PromiseResolver();
|
||||
$this->resolver = $resolver;
|
||||
//TODO: extract this so it can be polyfilled for unit testing
|
||||
$this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...)));
|
||||
return $this->resolver->getPromise();
|
||||
}
|
||||
}
|
45
src/network/mcpe/auth/AuthKeyring.php
Normal file
45
src/network/mcpe/auth/AuthKeyring.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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\auth;
|
||||
|
||||
final class AuthKeyring{
|
||||
|
||||
/**
|
||||
* @param string[] $keys
|
||||
* @phpstan-param array<string, string> $keys
|
||||
*/
|
||||
public function __construct(
|
||||
private string $issuer,
|
||||
private array $keys
|
||||
){}
|
||||
|
||||
public function getIssuer() : string{ return $this->issuer; }
|
||||
|
||||
/**
|
||||
* Returns a (raw) DER public key associated with the given key ID
|
||||
*/
|
||||
public function getKey(string $keyId) : ?string{
|
||||
return $this->keys[$keyId] ?? null;
|
||||
}
|
||||
}
|
209
src/network/mcpe/auth/FetchAuthKeysTask.php
Normal file
209
src/network/mcpe/auth/FetchAuthKeysTask.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceOpenIdConfiguration;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\api\MinecraftServicesDiscovery;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use pocketmine\utils\Internet;
|
||||
use function gettype;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function json_decode;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
class FetchAuthKeysTask extends AsyncTask{
|
||||
private const KEYS_ON_COMPLETION = "completion";
|
||||
|
||||
private const MINECRAFT_SERVICES_DISCOVERY_URL = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
|
||||
private const AUTHORIZATION_SERVICE_URI_FALLBACK = "https://authorization.franchise.minecraft-services.net";
|
||||
private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
|
||||
private const AUTHORIZATION_SERVICE_KEYS_PATH = "/.well-known/keys";
|
||||
|
||||
/** @phpstan-var ?NonThreadSafeValue<array<string, AuthServiceKey>> */
|
||||
private ?NonThreadSafeValue $keys = null;
|
||||
private string $issuer;
|
||||
|
||||
/** @phpstan-var ?NonThreadSafeValue<non-empty-array<string>> */
|
||||
private ?NonThreadSafeValue $errors = null;
|
||||
|
||||
/**
|
||||
* @phpstan-param \Closure(?array<string, AuthServiceKey> $keys, string $issuer, ?string[] $errors) : void $onCompletion
|
||||
*/
|
||||
public function __construct(
|
||||
\Closure $onCompletion
|
||||
){
|
||||
$this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
|
||||
}
|
||||
|
||||
public function onRun() : void{
|
||||
/** @var string[] $errors */
|
||||
$errors = [];
|
||||
|
||||
try{
|
||||
$authServiceUri = $this->getAuthServiceURI();
|
||||
}catch(\RuntimeException $e){
|
||||
$errors[] = $e->getMessage();
|
||||
$authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
|
||||
}
|
||||
|
||||
try {
|
||||
$openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
|
||||
$jwksUri = $openIdConfig->jwks_uri;
|
||||
|
||||
$this->issuer = $openIdConfig->issuer;
|
||||
} catch (\RuntimeException $e) {
|
||||
$errors[] = $e->getMessage();
|
||||
$jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
|
||||
|
||||
$this->issuer = $authServiceUri;
|
||||
}
|
||||
|
||||
try{
|
||||
$this->keys = new NonThreadSafeValue($this->getKeys($jwksUri));
|
||||
}catch(\RuntimeException $e){
|
||||
$errors[] = $e->getMessage();
|
||||
}
|
||||
|
||||
$this->errors = $errors === [] ? null : new NonThreadSafeValue($errors);
|
||||
}
|
||||
|
||||
private function getAuthServiceURI() : string{
|
||||
$result = Internet::getURL(self::MINECRAFT_SERVICES_DISCOVERY_URL);
|
||||
if($result === null || $result->getCode() !== 200){
|
||||
throw new \RuntimeException("Failed to fetch Minecraft services discovery document");
|
||||
}
|
||||
|
||||
try{
|
||||
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw new \RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
if(!is_object($json)){
|
||||
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
try{
|
||||
/** @var MinecraftServicesDiscovery $discovery */
|
||||
$discovery = $mapper->map($json, new MinecraftServicesDiscovery());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
|
||||
}
|
||||
|
||||
private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{
|
||||
$result = Internet::getURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH);
|
||||
if($result === null || $result->getCode() !== 200){
|
||||
throw new \RuntimeException("Failed to fetch OpenID configuration from authorization service");
|
||||
}
|
||||
|
||||
try{
|
||||
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw new \RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
if(!is_object($json)){
|
||||
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
try{
|
||||
/** @var AuthServiceOpenIdConfiguration $configuration */
|
||||
$configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, AuthServiceKey> keys indexed by key ID
|
||||
*/
|
||||
private function getKeys(string $jwksUri) : array{
|
||||
$result = Internet::getURL($jwksUri);
|
||||
if($result === null || $result->getCode() !== 200){
|
||||
return throw new \RuntimeException("Failed to fetch keys from authorization service");
|
||||
}
|
||||
|
||||
try{
|
||||
$json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw new \RuntimeException($e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){
|
||||
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
|
||||
$keys = [];
|
||||
foreach($keysArray as $keyJson){
|
||||
if(!is_array($keyJson)){
|
||||
throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object");
|
||||
}
|
||||
|
||||
try{
|
||||
/** @var AuthServiceKey $key */
|
||||
$key = $mapper->map($keyJson, new AuthServiceKey());
|
||||
$keys[$key->kid] = $key;
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public function onCompletion() : void{
|
||||
/**
|
||||
* @var \Closure $callback
|
||||
* @phpstan-var \Closure(?AuthServiceKey[] $keys, string $issuer, ?string[] $errors) : void $callback
|
||||
*/
|
||||
$callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
|
||||
$callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());
|
||||
}
|
||||
}
|
121
src/network/mcpe/auth/ProcessLegacyLoginTask.php
Normal file
121
src/network/mcpe/auth/ProcessLegacyLoginTask.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\lang\Translatable;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function base64_decode;
|
||||
use function igbinary_serialize;
|
||||
use function igbinary_unserialize;
|
||||
|
||||
class ProcessLegacyLoginTask extends AsyncTask{
|
||||
private const TLS_KEY_ON_COMPLETION = "completion";
|
||||
|
||||
/**
|
||||
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
|
||||
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
|
||||
* it has not yet been deployed.
|
||||
*/
|
||||
public const LEGACY_MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
|
||||
|
||||
private string $chain;
|
||||
|
||||
/**
|
||||
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
|
||||
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
|
||||
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
|
||||
*
|
||||
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
|
||||
*/
|
||||
private NonThreadSafeValue|string|null $error = "Unknown";
|
||||
/** Whether the player has a certificate chain link signed by the given root public key. */
|
||||
private bool $authenticated = false;
|
||||
private ?string $clientPublicKeyDer = null;
|
||||
|
||||
/**
|
||||
* @param string[] $chainJwts
|
||||
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
|
||||
*/
|
||||
public function __construct(
|
||||
array $chainJwts,
|
||||
private string $clientDataJwt,
|
||||
private ?string $rootAuthKeyDer,
|
||||
private bool $authRequired,
|
||||
\Closure $onCompletion
|
||||
){
|
||||
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
|
||||
$this->chain = igbinary_serialize($chainJwts) ?? throw new AssumptionFailedError("This should never fail");
|
||||
}
|
||||
|
||||
public function onRun() : void{
|
||||
try{
|
||||
$this->clientPublicKeyDer = $this->validateChain();
|
||||
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $this->clientPublicKeyDer);
|
||||
$this->error = null;
|
||||
}catch(VerifyLoginException $e){
|
||||
$disconnectMessage = $e->getDisconnectMessage();
|
||||
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateChain() : string{
|
||||
/** @var string[] $chain */
|
||||
$chain = igbinary_unserialize($this->chain);
|
||||
|
||||
$identityPublicKeyDer = null;
|
||||
|
||||
foreach($chain as $jwt){
|
||||
$claims = AuthJwtHelper::validateLegacyAuthToken($jwt, $identityPublicKeyDer);
|
||||
if($this->rootAuthKeyDer !== null && $identityPublicKeyDer === $this->rootAuthKeyDer){
|
||||
$this->authenticated = true; //we're signed into xbox live, according to this root key
|
||||
}
|
||||
if(!isset($claims->identityPublicKey)){
|
||||
throw new VerifyLoginException("Missing identityPublicKey in chain link", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
|
||||
}
|
||||
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
|
||||
if($identityPublicKey === false){
|
||||
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
|
||||
}
|
||||
$identityPublicKeyDer = $identityPublicKey;
|
||||
}
|
||||
|
||||
if($identityPublicKeyDer === null){
|
||||
throw new VerifyLoginException("No authentication chain links provided");
|
||||
}
|
||||
|
||||
return $identityPublicKeyDer;
|
||||
}
|
||||
|
||||
public function onCompletion() : void{
|
||||
/**
|
||||
* @var \Closure $callback
|
||||
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
|
||||
*/
|
||||
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
|
||||
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
|
||||
}
|
||||
}
|
@@ -1,213 +0,0 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\lang\Translatable;
|
||||
use pocketmine\network\mcpe\JwtException;
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody;
|
||||
use pocketmine\network\mcpe\protocol\types\login\JwtHeader;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use function base64_decode;
|
||||
use function igbinary_serialize;
|
||||
use function igbinary_unserialize;
|
||||
use function time;
|
||||
|
||||
class ProcessLoginTask extends AsyncTask{
|
||||
private const TLS_KEY_ON_COMPLETION = "completion";
|
||||
|
||||
/**
|
||||
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
|
||||
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
|
||||
* it has not yet been deployed.
|
||||
*/
|
||||
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
|
||||
|
||||
private const CLOCK_DRIFT_MAX = 60;
|
||||
|
||||
private string $chain;
|
||||
|
||||
/**
|
||||
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
|
||||
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
|
||||
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
|
||||
*
|
||||
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
|
||||
*/
|
||||
private NonThreadSafeValue|string|null $error = "Unknown";
|
||||
/**
|
||||
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
|
||||
* root public key.
|
||||
*/
|
||||
private bool $authenticated = false;
|
||||
private ?string $clientPublicKey = null;
|
||||
|
||||
/**
|
||||
* @param string[] $chainJwts
|
||||
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
|
||||
*/
|
||||
public function __construct(
|
||||
array $chainJwts,
|
||||
private string $clientDataJwt,
|
||||
private bool $authRequired,
|
||||
\Closure $onCompletion
|
||||
){
|
||||
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
|
||||
$this->chain = igbinary_serialize($chainJwts);
|
||||
}
|
||||
|
||||
public function onRun() : void{
|
||||
try{
|
||||
$this->clientPublicKey = $this->validateChain();
|
||||
$this->error = null;
|
||||
}catch(VerifyLoginException $e){
|
||||
$disconnectMessage = $e->getDisconnectMessage();
|
||||
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateChain() : string{
|
||||
/** @var string[] $chain */
|
||||
$chain = igbinary_unserialize($this->chain);
|
||||
|
||||
$currentKey = null;
|
||||
$first = true;
|
||||
|
||||
foreach($chain as $jwt){
|
||||
$this->validateToken($jwt, $currentKey, $first);
|
||||
if($first){
|
||||
$first = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string $clientKey */
|
||||
$clientKey = $currentKey;
|
||||
|
||||
$this->validateToken($this->clientDataJwt, $currentKey);
|
||||
|
||||
return $clientKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws VerifyLoginException if errors are encountered
|
||||
*/
|
||||
private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{
|
||||
try{
|
||||
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
|
||||
try{
|
||||
/** @var JwtHeader $headers */
|
||||
$headers = $mapper->map($headersArray, new JwtHeader());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$headerDerKey = base64_decode($headers->x5u, true);
|
||||
if($headerDerKey === false){
|
||||
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
|
||||
}
|
||||
|
||||
if($currentPublicKey === null){
|
||||
if(!$first){
|
||||
throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
|
||||
}
|
||||
}elseif($headerDerKey !== $currentPublicKey){
|
||||
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
|
||||
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
|
||||
}
|
||||
|
||||
try{
|
||||
$signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey);
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException("Invalid JWT public key: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
try{
|
||||
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
|
||||
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
|
||||
}
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
|
||||
$this->authenticated = true; //we're signed into xbox live
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
$mapper->bRemoveUndefinedAttributes = true;
|
||||
try{
|
||||
/** @var JwtChainLinkBody $claims */
|
||||
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
|
||||
$time = time();
|
||||
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
|
||||
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
|
||||
}
|
||||
|
||||
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
|
||||
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
|
||||
}
|
||||
|
||||
if(isset($claims->identityPublicKey)){
|
||||
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
|
||||
if($identityPublicKey === false){
|
||||
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
|
||||
}
|
||||
try{
|
||||
//verify key format and parameters
|
||||
JwtUtils::parseDerPublicKey($identityPublicKey);
|
||||
}catch(JwtException $e){
|
||||
throw new VerifyLoginException("Invalid identityPublicKey: " . $e->getMessage(), null, 0, $e);
|
||||
}
|
||||
$currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this
|
||||
}
|
||||
}
|
||||
|
||||
public function onCompletion() : void{
|
||||
/**
|
||||
* @var \Closure $callback
|
||||
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
|
||||
*/
|
||||
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
|
||||
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey);
|
||||
}
|
||||
}
|
98
src/network/mcpe/auth/ProcessOpenIdLoginTask.php
Normal file
98
src/network/mcpe/auth/ProcessOpenIdLoginTask.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?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\auth;
|
||||
|
||||
use pocketmine\lang\Translatable;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\thread\NonThreadSafeValue;
|
||||
use function base64_decode;
|
||||
|
||||
class ProcessOpenIdLoginTask extends AsyncTask{
|
||||
private const TLS_KEY_ON_COMPLETION = "completion";
|
||||
|
||||
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
|
||||
|
||||
/**
|
||||
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
|
||||
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
|
||||
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
|
||||
*
|
||||
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
|
||||
*/
|
||||
private NonThreadSafeValue|string|null $error = "Unknown";
|
||||
/**
|
||||
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
|
||||
* root public key.
|
||||
*/
|
||||
private bool $authenticated = false;
|
||||
private ?string $clientPublicKeyDer = null;
|
||||
|
||||
/**
|
||||
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
|
||||
*/
|
||||
public function __construct(
|
||||
private string $jwt,
|
||||
private string $issuer,
|
||||
private string $mojangPublicKeyDer,
|
||||
private string $clientDataJwt,
|
||||
private bool $authRequired,
|
||||
\Closure $onCompletion
|
||||
){
|
||||
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
|
||||
}
|
||||
|
||||
public function onRun() : void{
|
||||
try{
|
||||
$this->clientPublicKeyDer = $this->validateChain();
|
||||
$this->error = null;
|
||||
}catch(VerifyLoginException $e){
|
||||
$disconnectMessage = $e->getDisconnectMessage();
|
||||
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private function validateChain() : string{
|
||||
$claims = AuthJwtHelper::validateOpenIdAuthToken($this->jwt, $this->mojangPublicKeyDer, issuer: $this->issuer, audience: self::MOJANG_AUDIENCE);
|
||||
//validateToken will throw if the JWT is not valid
|
||||
$this->authenticated = true;
|
||||
|
||||
$clientDerKey = base64_decode($claims->cpk, strict: true);
|
||||
if($clientDerKey === false){
|
||||
throw new VerifyLoginException("Invalid client public key: base64 error decoding");
|
||||
}
|
||||
//no further validation needed - OpenSSL will bail if the key is invalid
|
||||
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $clientDerKey);
|
||||
|
||||
return $clientDerKey;
|
||||
}
|
||||
|
||||
public function onCompletion() : void{
|
||||
/**
|
||||
* @var \Closure $callback
|
||||
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
|
||||
*/
|
||||
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
|
||||
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
|
||||
}
|
||||
}
|
6
src/network/mcpe/cache/CraftingDataCache.php
vendored
6
src/network/mcpe/cache/CraftingDataCache.php
vendored
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\cache;
|
||||
|
||||
use pmmp\encoding\BE;
|
||||
use pocketmine\crafting\CraftingManager;
|
||||
use pocketmine\crafting\FurnaceType;
|
||||
use pocketmine\crafting\ShapedRecipe;
|
||||
@@ -41,7 +42,6 @@ use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShaped
|
||||
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\SingletonTrait;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use function array_map;
|
||||
@@ -99,7 +99,7 @@ final class CraftingDataCache{
|
||||
};
|
||||
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPELESS,
|
||||
Binary::writeInt($recipeNetId),
|
||||
BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable
|
||||
array_map($converter->coreRecipeIngredientToNet(...), $recipe->getIngredientList()),
|
||||
array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
|
||||
$nullUUID,
|
||||
@@ -118,7 +118,7 @@ final class CraftingDataCache{
|
||||
}
|
||||
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
|
||||
CraftingDataPacket::ENTRY_SHAPED,
|
||||
Binary::writeInt($recipeNetId),
|
||||
BE::packUnsignedInt($recipeNetId), //TODO: this should probably be changed to something human-readable
|
||||
$inputs,
|
||||
array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
|
||||
$nullUUID,
|
||||
|
@@ -23,6 +23,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\convert;
|
||||
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pocketmine\block\tile\Container;
|
||||
use pocketmine\block\VanillaBlocks;
|
||||
use pocketmine\crafting\ExactRecipeIngredient;
|
||||
use pocketmine\crafting\MetaWildcardRecipeIngredient;
|
||||
@@ -31,12 +34,18 @@ use pocketmine\crafting\TagWildcardRecipeIngredient;
|
||||
use pocketmine\data\bedrock\BedrockDataFiles;
|
||||
use pocketmine\data\bedrock\item\BlockItemIdMap;
|
||||
use pocketmine\data\bedrock\item\ItemTypeNames;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\NbtException;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\Tag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\nbt\UnexpectedTagTypeException;
|
||||
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData;
|
||||
@@ -52,11 +61,13 @@ use pocketmine\utils\SingletonTrait;
|
||||
use pocketmine\world\format\io\GlobalBlockStateHandlers;
|
||||
use pocketmine\world\format\io\GlobalItemDataHandlers;
|
||||
use function get_class;
|
||||
use function hash;
|
||||
|
||||
class TypeConverter{
|
||||
use SingletonTrait;
|
||||
|
||||
private const PM_ID_TAG = "___Id___";
|
||||
private const PM_FULL_NBT_HASH_TAG = "___FullNbtHash___";
|
||||
|
||||
private const RECIPE_INPUT_WILDCARD_META = 0x7fff;
|
||||
|
||||
@@ -197,6 +208,84 @@ class TypeConverter{
|
||||
return new ExactRecipeIngredient($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips unnecessary block actor NBT from items that have it.
|
||||
* This tag can potentially be extremely large, and is not read by the client anyway.
|
||||
*/
|
||||
protected function stripBlockEntityNBT(CompoundTag $tag) : bool{
|
||||
if(($tag->getTag(Item::TAG_BLOCK_ENTITY_TAG)) !== null){
|
||||
//client doesn't use this tag, so it's fine to delete completely
|
||||
$tag->removeTag(Item::TAG_BLOCK_ENTITY_TAG);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips non-viewable data from shulker boxes and similar blocks
|
||||
* The lore for shulker boxes only requires knowing the type & count of items and possibly custom name
|
||||
* We don't need to, and should not allow, sending nested inventories across the network.
|
||||
*/
|
||||
protected function stripContainedItemNonVisualNBT(CompoundTag $tag) : bool{
|
||||
try{
|
||||
$blockEntityInventoryTag = $tag->getListTag(Container::TAG_ITEMS, CompoundTag::class);
|
||||
}catch(UnexpectedTagTypeException){
|
||||
return false;
|
||||
}
|
||||
if($blockEntityInventoryTag !== null && $blockEntityInventoryTag->count() > 0){
|
||||
$stripped = new ListTag();
|
||||
|
||||
foreach($blockEntityInventoryTag as $itemTag){
|
||||
try{
|
||||
$containedItem = Item::nbtDeserialize($itemTag);
|
||||
$customName = $containedItem->getCustomName();
|
||||
$containedItem->clearNamedTag();
|
||||
$containedItem->setCustomName($customName);
|
||||
$stripped->push($containedItem->nbtSerialize());
|
||||
}catch(SavedDataLoadingException){
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$tag->setTag(Container::TAG_ITEMS, $stripped);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash of an item's server-side NBT.
|
||||
* This is baked into an item's network NBT to make sure the client doesn't try to stack items with the same network
|
||||
* NBT but different server-side NBT.
|
||||
*/
|
||||
protected function hashNBT(Tag $tag) : string{
|
||||
$encoded = (new LittleEndianNbtSerializer())->write(new TreeRoot($tag));
|
||||
return hash('sha256', $encoded, binary: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: HACK!
|
||||
* Creates a copy of an item's NBT with non-viewable data stripped.
|
||||
* This is a pretty yucky hack that's mainly needed because of inventories inside blockitems containing blockentity
|
||||
* data. There isn't really a good way to deal with this due to the way tiles currently require a position,
|
||||
* otherwise we could just keep a copy of the tile context and ask it for persistent vs network NBT as needed.
|
||||
* Unfortunately, making this nice will require significant BC breaks, so this will have to do for now.
|
||||
*/
|
||||
protected function cleanupUnnecessaryItemNBT(CompoundTag $original) : CompoundTag{
|
||||
$tag = clone $original;
|
||||
$anythingStripped = false;
|
||||
foreach([
|
||||
$this->stripContainedItemNonVisualNBT($tag),
|
||||
$this->stripBlockEntityNBT($tag)
|
||||
] as $stripped){
|
||||
$anythingStripped = $anythingStripped || $stripped;
|
||||
}
|
||||
|
||||
if($anythingStripped){
|
||||
$tag->setByteArray(self::PM_FULL_NBT_HASH_TAG, $this->hashNBT($original));
|
||||
}
|
||||
return $tag;
|
||||
}
|
||||
|
||||
public function coreItemStackToNet(Item $itemStack) : ItemStack{
|
||||
if($itemStack->isNull()){
|
||||
return ItemStack::null();
|
||||
@@ -205,7 +294,7 @@ class TypeConverter{
|
||||
if($nbt->count() === 0){
|
||||
$nbt = null;
|
||||
}else{
|
||||
$nbt = clone $nbt;
|
||||
$nbt = $this->cleanupUnnecessaryItemNBT($nbt);
|
||||
}
|
||||
|
||||
$idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack);
|
||||
@@ -224,7 +313,7 @@ class TypeConverter{
|
||||
$extraData = $id === $this->shieldRuntimeId ?
|
||||
new ItemStackExtraDataShield($nbt, canPlaceOn: [], canDestroy: [], blockingTick: 0) :
|
||||
new ItemStackExtraData($nbt, canPlaceOn: [], canDestroy: []);
|
||||
$extraDataSerializer = PacketSerializer::encoder();
|
||||
$extraDataSerializer = new ByteBufferWriter();
|
||||
$extraData->write($extraDataSerializer);
|
||||
|
||||
return new ItemStack(
|
||||
@@ -232,7 +321,7 @@ class TypeConverter{
|
||||
$meta,
|
||||
$itemStack->getCount(),
|
||||
$blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID,
|
||||
$extraDataSerializer->getBuffer(),
|
||||
$extraDataSerializer->getData(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,7 +360,7 @@ class TypeConverter{
|
||||
}
|
||||
|
||||
public function deserializeItemStackExtraData(string $extraData, int $id) : ItemStackExtraData{
|
||||
$extraDataDeserializer = PacketSerializer::decoder($extraData, 0);
|
||||
$extraDataDeserializer = new ByteBufferReader($extraData);
|
||||
return $id === $this->shieldRuntimeId ?
|
||||
ItemStackExtraDataShield::read($extraDataDeserializer) :
|
||||
ItemStackExtraData::read($extraDataDeserializer);
|
||||
|
@@ -24,7 +24,7 @@ declare(strict_types=1);
|
||||
namespace pocketmine\network\mcpe\encryption;
|
||||
|
||||
use Crypto\Cipher;
|
||||
use pocketmine\utils\Binary;
|
||||
use pmmp\encoding\LE;
|
||||
use function bin2hex;
|
||||
use function openssl_digest;
|
||||
use function openssl_error_string;
|
||||
@@ -104,7 +104,7 @@ class EncryptionContext{
|
||||
}
|
||||
|
||||
private function calculateChecksum(int $counter, string $payload) : string{
|
||||
$hash = openssl_digest(Binary::writeLLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true);
|
||||
$hash = openssl_digest(LE::packUnsignedLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true);
|
||||
if($hash === false){
|
||||
throw new \RuntimeException("openssl_digest() error: " . openssl_error_string());
|
||||
}
|
||||
|
@@ -760,6 +760,43 @@ class InGamePacketHandler extends PacketHandler{
|
||||
return true; //this packet is useless
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
private function updateSignText(CompoundTag $nbt, string $tagName, bool $frontFace, BaseSign $block, Vector3 $pos) : bool{
|
||||
$textTag = $nbt->getTag($tagName);
|
||||
if(!$textTag instanceof CompoundTag){
|
||||
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textTag) . " for tag \"$tagName\" in sign update data");
|
||||
}
|
||||
$textBlobTag = $textTag->getTag(Sign::TAG_TEXT_BLOB);
|
||||
if(!$textBlobTag instanceof StringTag){
|
||||
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data");
|
||||
}
|
||||
|
||||
try{
|
||||
$text = SignText::fromBlob($textBlobTag->getValue());
|
||||
}catch(\InvalidArgumentException $e){
|
||||
throw PacketHandlingException::wrap($e, "Invalid sign text update");
|
||||
}
|
||||
|
||||
$oldText = $block->getFaceText($frontFace);
|
||||
if($text->getLines() === $oldText->getLines()){
|
||||
return false;
|
||||
}
|
||||
|
||||
try{
|
||||
if(!$block->updateFaceText($this->player, $frontFace, $text)){
|
||||
foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){
|
||||
$this->session->sendDataPacket($updatePacket);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}catch(\UnexpectedValueException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleBlockActorData(BlockActorDataPacket $packet) : bool{
|
||||
$pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
|
||||
if($pos->distanceSquared($this->player->getLocation()) > 10000){
|
||||
@@ -771,29 +808,9 @@ class InGamePacketHandler extends PacketHandler{
|
||||
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
|
||||
|
||||
if($block instanceof BaseSign){
|
||||
$frontTextTag = $nbt->getTag(Sign::TAG_FRONT_TEXT);
|
||||
if(!$frontTextTag instanceof CompoundTag){
|
||||
throw new PacketHandlingException("Invalid tag type " . get_debug_type($frontTextTag) . " for tag \"" . Sign::TAG_FRONT_TEXT . "\" in sign update data");
|
||||
}
|
||||
$textBlobTag = $frontTextTag->getTag(Sign::TAG_TEXT_BLOB);
|
||||
if(!$textBlobTag instanceof StringTag){
|
||||
throw new PacketHandlingException("Invalid tag type " . get_debug_type($textBlobTag) . " for tag \"" . Sign::TAG_TEXT_BLOB . "\" in sign update data");
|
||||
}
|
||||
|
||||
try{
|
||||
$text = SignText::fromBlob($textBlobTag->getValue());
|
||||
}catch(\InvalidArgumentException $e){
|
||||
throw PacketHandlingException::wrap($e, "Invalid sign text update");
|
||||
}
|
||||
|
||||
try{
|
||||
if(!$block->updateText($this->player, $text)){
|
||||
foreach($this->player->getWorld()->createBlockUpdatePackets([$pos]) as $updatePacket){
|
||||
$this->session->sendDataPacket($updatePacket);
|
||||
}
|
||||
}
|
||||
}catch(\UnexpectedValueException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
if(!$this->updateSignText($nbt, Sign::TAG_FRONT_TEXT, true, $block, $pos)){
|
||||
//only one side can be updated at a time
|
||||
$this->updateSignText($nbt, Sign::TAG_BACK_TEXT, false, $block, $pos);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -27,27 +27,35 @@ use pocketmine\entity\InvalidSkinException;
|
||||
use pocketmine\event\player\PlayerPreLoginEvent;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\lang\Translatable;
|
||||
use pocketmine\network\mcpe\auth\ProcessLoginTask;
|
||||
use pocketmine\network\mcpe\auth\ProcessLegacyLoginTask;
|
||||
use pocketmine\network\mcpe\auth\ProcessOpenIdLoginTask;
|
||||
use pocketmine\network\mcpe\JwtException;
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\network\mcpe\NetworkSession;
|
||||
use pocketmine\network\mcpe\protocol\LoginPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
|
||||
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
|
||||
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
|
||||
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper;
|
||||
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain;
|
||||
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
|
||||
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtHeader;
|
||||
use pocketmine\network\PacketHandlingException;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\player\PlayerInfo;
|
||||
use pocketmine\player\XboxLivePlayerInfo;
|
||||
use pocketmine\Server;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
use function chr;
|
||||
use function count;
|
||||
use function gettype;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function json_decode;
|
||||
use function md5;
|
||||
use function ord;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/**
|
||||
@@ -65,15 +73,95 @@ class LoginPacketHandler extends PacketHandler{
|
||||
private \Closure $authCallback
|
||||
){}
|
||||
|
||||
private static function calculateUuidFromXuid(string $xuid) : UuidInterface{
|
||||
$hash = md5("pocket-auth-1-xuid:" . $xuid, binary: true);
|
||||
$hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); // set version to 3
|
||||
$hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); // set variant to RFC 4122
|
||||
|
||||
return Uuid::fromBytes($hash);
|
||||
}
|
||||
|
||||
public function handleLogin(LoginPacket $packet) : bool{
|
||||
$authInfo = $this->parseAuthInfo($packet->authInfoJson);
|
||||
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
|
||||
$extraData = $this->fetchAuthData($jwtChain);
|
||||
|
||||
if(!Player::isValidUserName($extraData->displayName)){
|
||||
if($authInfo->AuthenticationType === AuthenticationType::FULL->value){
|
||||
try{
|
||||
[$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token);
|
||||
}catch(JwtException $e){
|
||||
throw PacketHandlingException::wrap($e, "Error parsing authentication token");
|
||||
}
|
||||
$header = $this->mapXboxTokenHeader($headerArray);
|
||||
$claims = $this->mapXboxTokenBody($claimsArray);
|
||||
|
||||
$legacyUuid = self::calculateUuidFromXuid($claims->xid);
|
||||
$username = $claims->xname;
|
||||
$xuid = $claims->xid;
|
||||
|
||||
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
|
||||
if($authRequired === null){
|
||||
//plugin cancelled
|
||||
return true;
|
||||
}
|
||||
$this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired);
|
||||
|
||||
}elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){
|
||||
try{
|
||||
$chainData = json_decode($authInfo->Certificate, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate chain");
|
||||
}
|
||||
if(!is_object($chainData)){
|
||||
throw new PacketHandlingException("Unexpected type for self-signed certificate chain: " . gettype($chainData) . ", expected object");
|
||||
}
|
||||
try{
|
||||
$chain = $this->defaultJsonMapper()->map($chainData, new LegacyAuthChain());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate chain");
|
||||
}
|
||||
if(count($chain->chain) > 1 || !isset($chain->chain[0])){
|
||||
throw new PacketHandlingException("Expected exactly one certificate in self-signed certificate chain, got " . count($chain->chain));
|
||||
}
|
||||
|
||||
try{
|
||||
[, $claimsArray, ] = JwtUtils::parse($chain->chain[0]);
|
||||
}catch(JwtException $e){
|
||||
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate");
|
||||
}
|
||||
if(!isset($claimsArray["extraData"]) || !is_array($claimsArray["extraData"])){
|
||||
throw new PacketHandlingException("Expected \"extraData\" to be present in self-signed certificate");
|
||||
}
|
||||
|
||||
try{
|
||||
$claims = $this->defaultJsonMapper()->map($claimsArray["extraData"], new LegacyAuthIdentityData());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate extraData");
|
||||
}
|
||||
|
||||
if(!Uuid::isValid($claims->identity)){
|
||||
throw new PacketHandlingException("Invalid UUID string in self-signed certificate: " . $claims->identity);
|
||||
}
|
||||
$legacyUuid = Uuid::fromString($claims->identity);
|
||||
$username = $claims->displayName;
|
||||
$xuid = "";
|
||||
|
||||
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
|
||||
if($authRequired === null){
|
||||
//plugin cancelled
|
||||
return true;
|
||||
}
|
||||
$this->processSelfSignedLogin($chain->chain, $packet->clientDataJwt, $authRequired);
|
||||
}else{
|
||||
throw new PacketHandlingException("Unsupported authentication type: $authInfo->AuthenticationType");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function processLoginCommon(LoginPacket $packet, string $username, UuidInterface $legacyUuid, string $xuid) : ?bool{
|
||||
if(!Player::isValidUserName($username)){
|
||||
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
$clientData = $this->parseClientData($packet->clientDataJwt);
|
||||
@@ -86,32 +174,25 @@ class LoginPacketHandler extends PacketHandler{
|
||||
disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
|
||||
);
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!Uuid::isValid($extraData->identity)){
|
||||
throw new PacketHandlingException("Invalid login UUID");
|
||||
}
|
||||
$uuid = Uuid::fromString($extraData->identity);
|
||||
$arrClientData = (array) $clientData;
|
||||
$arrClientData["TitleID"] = $extraData->titleId;
|
||||
|
||||
if($extraData->XUID !== ""){
|
||||
if($xuid !== ""){
|
||||
$playerInfo = new XboxLivePlayerInfo(
|
||||
$extraData->XUID,
|
||||
$extraData->displayName,
|
||||
$uuid,
|
||||
$xuid,
|
||||
$username,
|
||||
$legacyUuid,
|
||||
$skin,
|
||||
$clientData->LanguageCode,
|
||||
$arrClientData
|
||||
(array) $clientData
|
||||
);
|
||||
}else{
|
||||
$playerInfo = new PlayerInfo(
|
||||
$extraData->displayName,
|
||||
$uuid,
|
||||
$username,
|
||||
$legacyUuid,
|
||||
$skin,
|
||||
$clientData->LanguageCode,
|
||||
$arrClientData
|
||||
(array) $clientData
|
||||
);
|
||||
}
|
||||
($this->playerInfoConsumer)($playerInfo);
|
||||
@@ -144,12 +225,10 @@ class LoginPacketHandler extends PacketHandler{
|
||||
$ev->call();
|
||||
if(!$ev->isAllowed()){
|
||||
$this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired());
|
||||
|
||||
return true;
|
||||
return $ev->isAuthRequired();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,13 +241,10 @@ class LoginPacketHandler extends PacketHandler{
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!is_object($authInfoJson)){
|
||||
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
|
||||
throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper = $this->defaultJsonMapper();
|
||||
try{
|
||||
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
@@ -178,68 +254,31 @@ class LoginPacketHandler extends PacketHandler{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $headerArray
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
|
||||
protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{
|
||||
$mapper = $this->defaultJsonMapper();
|
||||
try{
|
||||
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
|
||||
}catch(\JsonException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(!is_object($jwtChainJson)){
|
||||
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
try{
|
||||
$clientData = $mapper->map($jwtChainJson, new JwtChain());
|
||||
$header = $mapper->map($headerArray, new XboxAuthJwtHeader());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
return $clientData;
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $bodyArray
|
||||
* @throws PacketHandlingException
|
||||
*/
|
||||
protected function fetchAuthData(JwtChain $chain) : AuthenticationData{
|
||||
/** @var AuthenticationData|null $extraData */
|
||||
$extraData = null;
|
||||
foreach($chain->chain as $jwt){
|
||||
//validate every chain element
|
||||
try{
|
||||
[, $claims, ] = JwtUtils::parse($jwt);
|
||||
}catch(JwtException $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if(isset($claims["extraData"])){
|
||||
if($extraData !== null){
|
||||
throw new PacketHandlingException("Found 'extraData' more than once in chainData");
|
||||
}
|
||||
|
||||
if(!is_array($claims["extraData"])){
|
||||
throw new PacketHandlingException("'extraData' key should be an array");
|
||||
}
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
try{
|
||||
/** @var AuthenticationData $extraData */
|
||||
$extraData = $mapper->map($claims["extraData"], new AuthenticationData());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
}
|
||||
protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{
|
||||
$mapper = $this->defaultJsonMapper();
|
||||
try{
|
||||
$header = $mapper->map($bodyArray, new XboxAuthJwtBody());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
if($extraData === null){
|
||||
throw new PacketHandlingException("'extraData' not found in chain data");
|
||||
}
|
||||
return $extraData;
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,11 +291,7 @@ class LoginPacketHandler extends PacketHandler{
|
||||
throw PacketHandlingException::wrap($e);
|
||||
}
|
||||
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper = $this->defaultJsonMapper();
|
||||
try{
|
||||
$clientData = $mapper->map($clientDataClaims, new ClientData());
|
||||
}catch(\JsonMapper_Exception $e){
|
||||
@@ -269,15 +304,37 @@ class LoginPacketHandler extends PacketHandler{
|
||||
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication.
|
||||
* In the future this won't be necessary.
|
||||
*
|
||||
* @param null|string[] $legacyCertificate
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{
|
||||
if($legacyCertificate === null){
|
||||
throw new PacketHandlingException("Legacy certificate cannot be null");
|
||||
}
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
|
||||
protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{
|
||||
$this->session->setHandler(null); //drop packets received during login verification
|
||||
|
||||
$authKeyProvider = $this->server->getAuthKeyProvider();
|
||||
|
||||
$authKeyProvider->getKey($keyId)->onCompletion(
|
||||
function(array $issuerAndKey) use ($token, $clientData, $authRequired) : void{
|
||||
[$issuer, $mojangPublicKeyPem] = $issuerAndKey;
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback));
|
||||
},
|
||||
fn() => ($this->authCallback)(false, $authRequired, "Unrecognized authentication key ID: $keyId", null)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $legacyCertificate
|
||||
*/
|
||||
protected function processSelfSignedLogin(array $legacyCertificate, string $clientDataJwt, bool $authRequired) : void{
|
||||
$this->session->setHandler(null); //drop packets received during login verification
|
||||
|
||||
$this->server->getAsyncPool()->submitTask(new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: null, authRequired: $authRequired, onCompletion: $this->authCallback));
|
||||
}
|
||||
|
||||
private function defaultJsonMapper() : \JsonMapper{
|
||||
$mapper = new \JsonMapper();
|
||||
$mapper->bExceptionOnMissingData = true;
|
||||
$mapper->bExceptionOnUndefinedProperty = true;
|
||||
$mapper->bStrictObjectTypeChecking = true;
|
||||
$mapper->bEnforceMapType = false;
|
||||
return $mapper;
|
||||
}
|
||||
}
|
||||
|
@@ -23,16 +23,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\mcpe\serializer;
|
||||
|
||||
use pmmp\encoding\Byte;
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pmmp\encoding\VarInt;
|
||||
use pocketmine\block\tile\Spawnable;
|
||||
use pocketmine\data\bedrock\BiomeIds;
|
||||
use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
use pocketmine\network\mcpe\convert\BlockTranslator;
|
||||
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
|
||||
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
||||
use pocketmine\network\mcpe\protocol\types\DimensionIds;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\PalettedBlockArray;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
@@ -84,7 +84,7 @@ final class ChunkSerializer{
|
||||
* @phpstan-param DimensionIds::* $dimensionId
|
||||
*/
|
||||
public static function serializeFullChunk(Chunk $chunk, int $dimensionId, BlockTranslator $blockTranslator, ?string $tiles = null) : string{
|
||||
$stream = PacketSerializer::encoder();
|
||||
$stream = new ByteBufferWriter();
|
||||
|
||||
$subChunkCount = self::getSubChunkCount($chunk, $dimensionId);
|
||||
$writtenCount = 0;
|
||||
@@ -100,37 +100,34 @@ final class ChunkSerializer{
|
||||
self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream);
|
||||
}
|
||||
|
||||
$stream->putByte(0); //border block array count
|
||||
Byte::writeUnsigned($stream, 0); //border block array count
|
||||
//Border block entry format: 1 byte (4 bits X, 4 bits Z). These are however useless since they crash the regular client.
|
||||
|
||||
if($tiles !== null){
|
||||
$stream->put($tiles);
|
||||
$stream->writeByteArray($tiles);
|
||||
}else{
|
||||
$stream->put(self::serializeTiles($chunk));
|
||||
$stream->writeByteArray(self::serializeTiles($chunk));
|
||||
}
|
||||
return $stream->getBuffer();
|
||||
return $stream->getData();
|
||||
}
|
||||
|
||||
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{
|
||||
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, ByteBufferWriter $stream, bool $persistentBlockStates) : void{
|
||||
$layers = $subChunk->getBlockLayers();
|
||||
$stream->putByte(8); //version
|
||||
Byte::writeUnsigned($stream, 8); //version
|
||||
|
||||
$stream->putByte(count($layers));
|
||||
Byte::writeUnsigned($stream, count($layers));
|
||||
|
||||
$blockStateDictionary = $blockTranslator->getBlockStateDictionary();
|
||||
|
||||
foreach($layers as $blocks){
|
||||
$bitsPerBlock = $blocks->getBitsPerBlock();
|
||||
$words = $blocks->getWordArray();
|
||||
$stream->putByte(($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1));
|
||||
$stream->put($words);
|
||||
Byte::writeUnsigned($stream, ($bitsPerBlock << 1) | ($persistentBlockStates ? 0 : 1));
|
||||
$stream->writeByteArray($words);
|
||||
$palette = $blocks->getPalette();
|
||||
|
||||
if($bitsPerBlock !== 0){
|
||||
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
|
||||
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
|
||||
//zigzag and just shift directly.
|
||||
$stream->putUnsignedVarInt(count($palette) << 1); //yes, this is intentionally zigzag
|
||||
VarInt::writeSignedInt($stream, count($palette)); //yes, this is intentionally zigzag
|
||||
}
|
||||
if($persistentBlockStates){
|
||||
$nbtSerializer = new NetworkNbtSerializer();
|
||||
@@ -141,46 +138,43 @@ final class ChunkSerializer{
|
||||
$state = $blockTranslator->getFallbackStateData();
|
||||
}
|
||||
|
||||
$stream->put($nbtSerializer->write(new TreeRoot($state->toNbt())));
|
||||
$stream->writeByteArray($nbtSerializer->write(new TreeRoot($state->toNbt())));
|
||||
}
|
||||
}else{
|
||||
//we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of
|
||||
//allocating a temporary array for the mapped palette IDs, especially for small palettes
|
||||
foreach($palette as $p){
|
||||
$stream->put(Binary::writeUnsignedVarInt($blockTranslator->internalIdToNetworkId($p) << 1));
|
||||
VarInt::writeSignedInt($stream, $blockTranslator->internalIdToNetworkId($p));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $stream) : void{
|
||||
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, ByteBufferWriter $stream) : void{
|
||||
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
|
||||
$stream->putByte(($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
|
||||
$stream->put($biomePalette->getWordArray());
|
||||
Byte::writeUnsigned($stream, ($biomePaletteBitsPerBlock << 1) | 1); //the last bit is non-persistence (like for blocks), though it has no effect on biomes since they always use integer IDs
|
||||
$stream->writeByteArray($biomePalette->getWordArray());
|
||||
|
||||
//these LSHIFT by 1 uvarints are optimizations: the client expects zigzag varints here
|
||||
//but since we know they are always unsigned, we can avoid the extra fcall overhead of
|
||||
//zigzag and just shift directly.
|
||||
$biomePaletteArray = $biomePalette->getPalette();
|
||||
if($biomePaletteBitsPerBlock !== 0){
|
||||
$stream->putUnsignedVarInt(count($biomePaletteArray) << 1);
|
||||
VarInt::writeSignedInt($stream, count($biomePaletteArray));
|
||||
}
|
||||
|
||||
foreach($biomePaletteArray as $p){
|
||||
if($biomeIdMap->legacyToString($p) === null){
|
||||
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
|
||||
$p = BiomeIds::OCEAN;
|
||||
}
|
||||
$stream->put(Binary::writeUnsignedVarInt($p << 1));
|
||||
//we would use writeSignedIntArray() here, but the gains of writing in batch are negated by the cost of
|
||||
//allocating a temporary array for the mapped palette IDs, especially for small palettes
|
||||
VarInt::writeSignedInt($stream, $biomeIdMap->legacyToString($p) !== null ? $p : BiomeIds::OCEAN);
|
||||
}
|
||||
}
|
||||
|
||||
public static function serializeTiles(Chunk $chunk) : string{
|
||||
$stream = new BinaryStream();
|
||||
$stream = new ByteBufferWriter();
|
||||
foreach($chunk->getTiles() as $tile){
|
||||
if($tile instanceof Spawnable){
|
||||
$stream->put($tile->getSerializedSpawnCompound()->getEncodedNbt());
|
||||
$stream->writeByteArray($tile->getSerializedSpawnCompound()->getEncodedNbt());
|
||||
}
|
||||
}
|
||||
|
||||
return $stream->getBuffer();
|
||||
return $stream->getData();
|
||||
}
|
||||
}
|
||||
|
@@ -27,16 +27,16 @@ declare(strict_types=1);
|
||||
*/
|
||||
namespace pocketmine\network\query;
|
||||
|
||||
use pmmp\encoding\BE;
|
||||
use pmmp\encoding\Byte;
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pmmp\encoding\DataDecodeException;
|
||||
use pocketmine\network\AdvancedNetworkInterface;
|
||||
use pocketmine\network\RawPacketHandler;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\BinaryDataException;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use function chr;
|
||||
use function hash;
|
||||
use function random_bytes;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
class QueryHandler implements RawPacketHandler{
|
||||
@@ -80,51 +80,53 @@ class QueryHandler implements RawPacketHandler{
|
||||
}
|
||||
|
||||
public static function getTokenString(string $token, string $salt) : int{
|
||||
return Binary::readInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4));
|
||||
return BE::unpackSignedInt(substr(hash("sha512", $salt . ":" . $token, true), 7, 4));
|
||||
}
|
||||
|
||||
public function handle(AdvancedNetworkInterface $interface, string $address, int $port, string $packet) : bool{
|
||||
try{
|
||||
$stream = new BinaryStream($packet);
|
||||
$header = $stream->get(2);
|
||||
$stream = new ByteBufferReader($packet);
|
||||
$header = $stream->readByteArray(2);
|
||||
if($header !== "\xfe\xfd"){ //TODO: have this filtered by the regex filter we installed above
|
||||
return false;
|
||||
}
|
||||
$packetType = $stream->getByte();
|
||||
$sessionID = $stream->getInt();
|
||||
$packetType = Byte::readUnsigned($stream);
|
||||
$sessionID = BE::readUnsignedInt($stream);
|
||||
|
||||
switch($packetType){
|
||||
case self::HANDSHAKE: //Handshake
|
||||
$reply = chr(self::HANDSHAKE);
|
||||
$reply .= Binary::writeInt($sessionID);
|
||||
$reply .= self::getTokenString($this->token, $address) . "\x00";
|
||||
$writer = new ByteBufferWriter();
|
||||
Byte::writeUnsigned($writer, self::HANDSHAKE);
|
||||
BE::writeUnsignedInt($writer, $sessionID);
|
||||
$writer->writeByteArray(self::getTokenString($this->token, $address) . "\x00");
|
||||
|
||||
$interface->sendRawPacket($address, $port, $reply);
|
||||
$interface->sendRawPacket($address, $port, $writer->getData());
|
||||
|
||||
return true;
|
||||
case self::STATISTICS: //Stat
|
||||
$token = $stream->getInt();
|
||||
$token = BE::readUnsignedInt($stream);
|
||||
if($token !== ($t1 = self::getTokenString($this->token, $address)) && $token !== ($t2 = self::getTokenString($this->lastToken, $address))){
|
||||
$this->logger->debug("Bad token $token from $address $port, expected $t1 or $t2");
|
||||
|
||||
return true;
|
||||
}
|
||||
$reply = chr(self::STATISTICS);
|
||||
$reply .= Binary::writeInt($sessionID);
|
||||
$writer = new ByteBufferWriter();
|
||||
Byte::writeUnsigned($writer, self::STATISTICS);
|
||||
BE::writeUnsignedInt($writer, $sessionID);
|
||||
|
||||
$remaining = $stream->getRemaining();
|
||||
if(strlen($remaining) === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01
|
||||
$reply .= $this->server->getQueryInformation()->getLongQuery();
|
||||
$remaining = $stream->getUnreadLength();
|
||||
if($remaining === 4){ //TODO: check this! according to the spec, this should always be here and always be FF FF FF 01
|
||||
$writer->writeByteArray($this->server->getQueryInformation()->getLongQuery());
|
||||
}else{
|
||||
$reply .= $this->server->getQueryInformation()->getShortQuery();
|
||||
$writer->writeByteArray($this->server->getQueryInformation()->getShortQuery());
|
||||
}
|
||||
$interface->sendRawPacket($address, $port, $reply);
|
||||
$interface->sendRawPacket($address, $port, $writer->getData());
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}catch(BinaryDataException $e){
|
||||
}catch(DataDecodeException $e){
|
||||
$this->logger->debug("Bad packet from $address $port: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
@@ -23,11 +23,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\network\query;
|
||||
|
||||
use pmmp\encoding\LE;
|
||||
use pocketmine\player\GameMode;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\plugin\Plugin;
|
||||
use pocketmine\Server;
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\YmlServerProperties;
|
||||
use function array_map;
|
||||
@@ -236,6 +236,6 @@ final class QueryInfo{
|
||||
}
|
||||
|
||||
public function getShortQuery() : string{
|
||||
return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . Binary::writeLShort($this->port) . $this->ip . "\x00");
|
||||
return $this->shortQueryCache ?? ($this->shortQueryCache = $this->serverName . "\x00" . $this->gametype . "\x00" . $this->map . "\x00" . $this->numPlayers . "\x00" . $this->maxPlayers . "\x00" . LE::packUnsignedShort($this->port) . $this->ip . "\x00");
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,7 @@ use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\Human;
|
||||
use pocketmine\entity\Living;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\entity\NeverSavedWithChunkEntity;
|
||||
use pocketmine\entity\object\ItemEntity;
|
||||
use pocketmine\entity\projectile\Arrow;
|
||||
use pocketmine\entity\Skin;
|
||||
@@ -169,7 +170,7 @@ use const PHP_INT_MAX;
|
||||
/**
|
||||
* Main class that handles networking, recovery, and packet sending to the server part
|
||||
*/
|
||||
class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
class Player extends Human implements CommandSender, ChunkListener, IPlayer, NeverSavedWithChunkEntity{
|
||||
use PermissibleDelegateTrait;
|
||||
|
||||
private const MOVES_PER_TICK = 2;
|
||||
@@ -2838,13 +2839,12 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
|
||||
/**
|
||||
* Opens the player's sign editor GUI for the sign at the given position.
|
||||
* TODO: add support for editing the rear side of the sign (not currently supported due to technical limitations)
|
||||
*/
|
||||
public function openSignEditor(Vector3 $position) : void{
|
||||
public function openSignEditor(Vector3 $position, bool $frontFace = true) : void{
|
||||
$block = $this->getWorld()->getBlock($position);
|
||||
if($block instanceof BaseSign){
|
||||
$this->getWorld()->setBlock($position, $block->setEditorEntityRuntimeId($this->getId()));
|
||||
$this->getNetworkSession()->onOpenSignEditor($position, true);
|
||||
$this->getNetworkSession()->onOpenSignEditor($position, $frontFace);
|
||||
}else{
|
||||
throw new \InvalidArgumentException("Block at this position is not a sign");
|
||||
}
|
||||
|
@@ -42,6 +42,7 @@ use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\entity\EntityFactory;
|
||||
use pocketmine\entity\Location;
|
||||
use pocketmine\entity\NeverSavedWithChunkEntity;
|
||||
use pocketmine\entity\object\ExperienceOrb;
|
||||
use pocketmine\entity\object\ItemEntity;
|
||||
use pocketmine\event\block\BlockBreakEvent;
|
||||
@@ -2776,7 +2777,7 @@ class World implements ChunkManager{
|
||||
throw new AssumptionFailedError("Found two different entities sharing entity ID " . $entity->getId());
|
||||
}
|
||||
}
|
||||
if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof Player){
|
||||
if(!EntityFactory::getInstance()->isRegistered($entity::class) && !$entity instanceof NeverSavedWithChunkEntity){
|
||||
//canSaveWithChunk is mutable, so that means it could be toggled after adding the entity and cause a crash
|
||||
//later on. Better we just force all entities to have a save ID, even if it might not be needed.
|
||||
throw new \LogicException("Entity " . $entity::class . " is not registered for a save ID in EntityFactory");
|
||||
|
@@ -23,8 +23,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\format\io;
|
||||
|
||||
use pocketmine\utils\Binary;
|
||||
use pocketmine\utils\BinaryStream;
|
||||
use pmmp\encoding\BE;
|
||||
use pmmp\encoding\Byte;
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pmmp\encoding\ByteBufferWriter;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\PalettedBlockArray;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
@@ -45,15 +47,15 @@ final class FastChunkSerializer{
|
||||
//NOOP
|
||||
}
|
||||
|
||||
private static function serializePalettedArray(BinaryStream $stream, PalettedBlockArray $array) : void{
|
||||
private static function serializePalettedArray(ByteBufferWriter $stream, PalettedBlockArray $array) : void{
|
||||
$wordArray = $array->getWordArray();
|
||||
$palette = $array->getPalette();
|
||||
|
||||
$stream->putByte($array->getBitsPerBlock());
|
||||
$stream->put($wordArray);
|
||||
Byte::writeUnsigned($stream, $array->getBitsPerBlock());
|
||||
$stream->writeByteArray($wordArray);
|
||||
$serialPalette = pack("L*", ...$palette);
|
||||
$stream->putInt(strlen($serialPalette));
|
||||
$stream->put($serialPalette);
|
||||
BE::writeUnsignedInt($stream, strlen($serialPalette));
|
||||
$stream->writeByteArray($serialPalette);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,21 +63,20 @@ final class FastChunkSerializer{
|
||||
* TODO: tiles and entities
|
||||
*/
|
||||
public static function serializeTerrain(Chunk $chunk) : string{
|
||||
$stream = new BinaryStream();
|
||||
$stream->putByte(
|
||||
($chunk->isPopulated() ? self::FLAG_POPULATED : 0)
|
||||
);
|
||||
$stream = new ByteBufferWriter();
|
||||
Byte::writeUnsigned($stream, ($chunk->isPopulated() ? self::FLAG_POPULATED : 0));
|
||||
|
||||
//subchunks
|
||||
$subChunks = $chunk->getSubChunks();
|
||||
$count = count($subChunks);
|
||||
$stream->putByte($count);
|
||||
Byte::writeUnsigned($stream, $count);
|
||||
|
||||
foreach($subChunks as $y => $subChunk){
|
||||
$stream->putByte($y);
|
||||
$stream->putInt($subChunk->getEmptyBlockId());
|
||||
Byte::writeSigned($stream, $y);
|
||||
BE::writeUnsignedInt($stream, $subChunk->getEmptyBlockId());
|
||||
|
||||
$layers = $subChunk->getBlockLayers();
|
||||
$stream->putByte(count($layers));
|
||||
Byte::writeUnsigned($stream, count($layers));
|
||||
foreach($layers as $blocks){
|
||||
self::serializePalettedArray($stream, $blocks);
|
||||
}
|
||||
@@ -83,14 +84,15 @@ final class FastChunkSerializer{
|
||||
|
||||
}
|
||||
|
||||
return $stream->getBuffer();
|
||||
return $stream->getData();
|
||||
}
|
||||
|
||||
private static function deserializePalettedArray(BinaryStream $stream) : PalettedBlockArray{
|
||||
$bitsPerBlock = $stream->getByte();
|
||||
$words = $stream->get(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
|
||||
private static function deserializePalettedArray(ByteBufferReader $stream) : PalettedBlockArray{
|
||||
$bitsPerBlock = Byte::readUnsigned($stream);
|
||||
$words = $stream->readByteArray(PalettedBlockArray::getExpectedWordArraySize($bitsPerBlock));
|
||||
$paletteSize = BE::readUnsignedInt($stream);
|
||||
/** @var int[] $unpackedPalette */
|
||||
$unpackedPalette = unpack("L*", $stream->get($stream->getInt())); //unpack() will never fail here
|
||||
$unpackedPalette = unpack("L*", $stream->readByteArray($paletteSize)); //unpack() will never fail here
|
||||
$palette = array_values($unpackedPalette);
|
||||
|
||||
return PalettedBlockArray::fromData($bitsPerBlock, $words, $palette);
|
||||
@@ -100,20 +102,21 @@ final class FastChunkSerializer{
|
||||
* Deserializes a fast-serialized chunk
|
||||
*/
|
||||
public static function deserializeTerrain(string $data) : Chunk{
|
||||
$stream = new BinaryStream($data);
|
||||
$stream = new ByteBufferReader($data);
|
||||
|
||||
$flags = $stream->getByte();
|
||||
$flags = Byte::readUnsigned($stream);
|
||||
$terrainPopulated = (bool) ($flags & self::FLAG_POPULATED);
|
||||
|
||||
$subChunks = [];
|
||||
|
||||
$count = $stream->getByte();
|
||||
$count = Byte::readUnsigned($stream);
|
||||
for($subCount = 0; $subCount < $count; ++$subCount){
|
||||
$y = Binary::signByte($stream->getByte());
|
||||
$airBlockId = $stream->getInt();
|
||||
$y = Byte::readSigned($stream);
|
||||
//TODO: why the heck are we using big-endian here?
|
||||
$airBlockId = BE::readUnsignedInt($stream);
|
||||
|
||||
$layers = [];
|
||||
for($i = 0, $layerCount = $stream->getByte(); $i < $layerCount; ++$i){
|
||||
for($i = 0, $layerCount = Byte::readUnsigned($stream); $i < $layerCount; ++$i){
|
||||
$layers[] = self::deserializePalettedArray($stream);
|
||||
}
|
||||
$biomeArray = self::deserializePalettedArray($stream);
|
||||
|
@@ -87,12 +87,10 @@ trait LegacyAnvilChunkTrait{
|
||||
}
|
||||
|
||||
$subChunks = [];
|
||||
$subChunksTag = $chunk->getListTag("Sections") ?? [];
|
||||
$subChunksTag = $chunk->getListTag("Sections", CompoundTag::class) ?? [];
|
||||
foreach($subChunksTag as $subChunk){
|
||||
if($subChunk instanceof CompoundTag){
|
||||
$y = $subChunk->getByte("Y");
|
||||
$subChunks[$y] = $this->deserializeSubChunk($subChunk, clone $biomes3d, new \PrefixedLogger($logger, "Subchunk y=$y"));
|
||||
}
|
||||
$y = $subChunk->getByte("Y");
|
||||
$subChunks[$y] = $this->deserializeSubChunk($subChunk, clone $biomes3d, new \PrefixedLogger($logger, "Subchunk y=$y"));
|
||||
}
|
||||
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
|
||||
if(!isset($subChunks[$y])){
|
||||
|
@@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\format\io\region;
|
||||
|
||||
use pocketmine\nbt\NBT;
|
||||
use pocketmine\nbt\tag\ByteArrayTag;
|
||||
use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
@@ -164,21 +163,11 @@ abstract class RegionWorldProvider extends BaseWorldProvider{
|
||||
* @throws CorruptedChunkException
|
||||
*/
|
||||
protected static function getCompoundList(string $context, ListTag $list) : array{
|
||||
if($list->count() === 0){ //empty lists might have wrong types, we don't care
|
||||
return [];
|
||||
}
|
||||
if($list->getTagType() !== NBT::TAG_Compound){
|
||||
$compoundList = $list->cast(CompoundTag::class);
|
||||
if($compoundList === null){
|
||||
throw new CorruptedChunkException("Expected TAG_List<TAG_Compound> for '$context'");
|
||||
}
|
||||
$result = [];
|
||||
foreach($list as $tag){
|
||||
if(!($tag instanceof CompoundTag)){
|
||||
//this should never happen, but it's still possible due to lack of native type safety
|
||||
throw new CorruptedChunkException("Expected TAG_List<TAG_Compound> for '$context'");
|
||||
}
|
||||
$result[] = $tag;
|
||||
}
|
||||
return $result;
|
||||
return $compoundList->getValue();
|
||||
}
|
||||
|
||||
protected static function readFixedSizeByteArray(CompoundTag $chunk, string $tagName, int $length) : string{
|
||||
|
35
src/world/sound/FireworkCrackleSound.php
Normal file
35
src/world/sound/FireworkCrackleSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class FireworkCrackleSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::TWINKLE, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/FireworkExplosionSound.php
Normal file
35
src/world/sound/FireworkExplosionSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class FireworkExplosionSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::BLAST, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/FireworkLargeExplosionSound.php
Normal file
35
src/world/sound/FireworkLargeExplosionSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class FireworkLargeExplosionSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::LARGE_BLAST, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/FireworkLaunchSound.php
Normal file
35
src/world/sound/FireworkLaunchSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class FireworkLaunchSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::LAUNCH, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/TridentHitBlockSound.php
Normal file
35
src/world/sound/TridentHitBlockSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class TridentHitBlockSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT_GROUND, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/TridentHitEntitySound.php
Normal file
35
src/world/sound/TridentHitEntitySound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class TridentHitEntitySound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_HIT, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/TridentThrowSound.php
Normal file
35
src/world/sound/TridentThrowSound.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class TridentThrowSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::ITEM_TRIDENT_THROW, $pos, false)];
|
||||
}
|
||||
}
|
@@ -870,12 +870,6 @@ parameters:
|
||||
count: 1
|
||||
path: ../../../src/network/mcpe/NetworkSession.php
|
||||
|
||||
-
|
||||
message: '#^Property pocketmine\\network\\mcpe\\auth\\ProcessLoginTask\:\:\$chain \(string\) does not accept string\|null\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: ../../../src/network/mcpe/auth/ProcessLoginTask.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#'
|
||||
identifier: argument.type
|
||||
|
@@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\tools\generate_bedrock_data_from_packets;
|
||||
|
||||
use pmmp\encoding\ByteBufferReader;
|
||||
use pocketmine\crafting\json\FurnaceRecipeData;
|
||||
use pocketmine\crafting\json\ItemStackData;
|
||||
use pocketmine\crafting\json\PotionContainerChangeRecipeData;
|
||||
@@ -37,7 +38,6 @@ 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;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\TreeRoot;
|
||||
@@ -52,7 +52,6 @@ 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\inventory\CreativeGroupEntry;
|
||||
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
||||
@@ -191,7 +190,7 @@ class ParserPacketHandler extends PacketHandler{
|
||||
|
||||
$rawExtraData = $itemStack->getRawExtraData();
|
||||
if($rawExtraData !== ""){
|
||||
$decoder = PacketSerializer::decoder($rawExtraData, 0);
|
||||
$decoder = new ByteBufferReader($rawExtraData);
|
||||
$extraData = $itemStringId === ItemTypeNames::SHIELD ? ItemStackExtraDataShield::read($decoder) : ItemStackExtraData::read($decoder);
|
||||
$nbt = $extraData->getNbt();
|
||||
if($nbt !== null && count($nbt) > 0){
|
||||
@@ -554,8 +553,8 @@ class ParserPacketHandler extends PacketHandler{
|
||||
if(!($tag instanceof CompoundTag)){
|
||||
throw new AssumptionFailedError();
|
||||
}
|
||||
$idList = $tag->getTag("idlist");
|
||||
if(!($idList instanceof ListTag) || $idList->getTagType() !== NBT::TAG_Compound){
|
||||
$generic = $tag->getTag("idlist");
|
||||
if(!($generic instanceof ListTag) || ($idList = $generic->cast(CompoundTag::class)) === null){
|
||||
echo $tag . "\n";
|
||||
throw new \RuntimeException("expected TAG_List<TAG_Compound>(\"idlist\") tag inside root TAG_Compound");
|
||||
}
|
||||
@@ -565,9 +564,6 @@ class ParserPacketHandler extends PacketHandler{
|
||||
}
|
||||
echo "updating legacy => string entity ID mapping table\n";
|
||||
$map = [];
|
||||
/**
|
||||
* @var CompoundTag $thing
|
||||
*/
|
||||
foreach($idList as $thing){
|
||||
$map[$thing->getString("id")] = $thing->getInt("rid");
|
||||
}
|
||||
@@ -649,12 +645,13 @@ function main(array $argv) : int{
|
||||
fwrite(STDERR, "Unknown packet on line " . ($lineNum + 1) . ": " . $parts[1]);
|
||||
continue;
|
||||
}
|
||||
$serializer = PacketSerializer::decoder($raw, 0);
|
||||
$serializer = new ByteBufferReader($raw);
|
||||
|
||||
$pk->decode($serializer);
|
||||
$pk->handle($handler);
|
||||
if(!$serializer->feof()){
|
||||
echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getBuffer()) . " bytes): " . bin2hex($serializer->getRemaining()) . "\n";
|
||||
$remaining = strlen($serializer->getData()) - $serializer->getOffset();
|
||||
if($remaining > 0){
|
||||
echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getData()) . " bytes): " . bin2hex($serializer->readByteArray($remaining)) . "\n";
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
Reference in New Issue
Block a user