Merge branch 'minor-next' into hot-events-optimisation

This commit is contained in:
Dylan K. Taylor
2023-08-01 17:01:52 +01:00
818 changed files with 38074 additions and 70531 deletions

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\NetworkSession;
use function count;
use function spl_object_id;
@ -95,10 +96,13 @@ class NetworkSessionManager{
/**
* Terminates all connected sessions with the given reason.
*
* @param Translatable|string $reason Shown in the server log - this should be a short one-line message
* @param Translatable|string|null $disconnectScreenMessage Shown on the player's disconnection screen (null will use the reason)
*/
public function close(string $reason = "") : void{
public function close(Translatable|string $reason = "", Translatable|string|null $disconnectScreenMessage = null) : void{
foreach($this->sessions as $session){
$session->disconnect($reason);
$session->disconnect($reason, $disconnectScreenMessage);
}
$this->sessions = [];
}

View File

@ -25,39 +25,30 @@ namespace pocketmine\network\mcpe;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\LevelChunkPacket;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\network\mcpe\protocol\types\ChunkPosition;
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;
class ChunkRequestTask extends AsyncTask{
private const TLS_KEY_PROMISE = "promise";
private const TLS_KEY_ERROR_HOOK = "errorHook";
/** @var string */
protected $chunk;
/** @var int */
protected $chunkX;
/** @var int */
protected $chunkZ;
/** @var Compressor */
protected $compressor;
protected string $chunk;
protected int $chunkX;
protected int $chunkZ;
/** @phpstan-var NonThreadSafeValue<Compressor> */
protected NonThreadSafeValue $compressor;
private string $tiles;
/**
* @phpstan-param (\Closure() : void)|null $onError
*/
public function __construct(int $chunkX, int $chunkZ, Chunk $chunk, CompressBatchPromise $promise, Compressor $compressor, ?\Closure $onError = null){
$this->compressor = $compressor;
public function __construct(int $chunkX, int $chunkZ, Chunk $chunk, CompressBatchPromise $promise, Compressor $compressor){
$this->compressor = new NonThreadSafeValue($compressor);
$this->chunk = FastChunkSerializer::serializeTerrain($chunk);
$this->chunkX = $chunkX;
@ -65,29 +56,18 @@ class ChunkRequestTask extends AsyncTask{
$this->tiles = ChunkSerializer::serializeTiles($chunk);
$this->storeLocal(self::TLS_KEY_PROMISE, $promise);
$this->storeLocal(self::TLS_KEY_ERROR_HOOK, $onError);
}
public function onRun() : void{
$chunk = FastChunkSerializer::deserializeTerrain($this->chunk);
$subCount = ChunkSerializer::getSubChunkCount($chunk) + ChunkSerializer::LOWER_PADDING_SIZE;
$encoderContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
$payload = ChunkSerializer::serializeFullChunk($chunk, RuntimeBlockMapping::getInstance(), $encoderContext, $this->tiles);
$subCount = ChunkSerializer::getSubChunkCount($chunk);
$converter = TypeConverter::getInstance();
$encoderContext = new PacketSerializerContext($converter->getItemTypeDictionary());
$payload = ChunkSerializer::serializeFullChunk($chunk, $converter->getBlockTranslator(), $encoderContext, $this->tiles);
$stream = new BinaryStream();
PacketBatch::encodePackets($stream, $encoderContext, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $subCount, false, null, $payload)]);
$this->setResult($this->compressor->compress($stream->getBuffer()));
}
public function onError() : void{
/**
* @var \Closure|null $hook
* @phpstan-var (\Closure() : void)|null $hook
*/
$hook = $this->fetchLocal(self::TLS_KEY_ERROR_HOOK);
if($hook !== null){
$hook();
}
$this->setResult($this->compressor->deserialize()->compress($stream->getBuffer()));
}
public function onCompletion() : void{

View File

@ -26,30 +26,28 @@ namespace pocketmine\network\mcpe;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\BlockInventory;
use pocketmine\block\inventory\BrewingStandInventory;
use pocketmine\block\inventory\CartographyTableInventory;
use pocketmine\block\inventory\CraftingTableInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\inventory\FurnaceInventory;
use pocketmine\block\inventory\HopperInventory;
use pocketmine\block\inventory\LoomInventory;
use pocketmine\block\inventory\SmithingTableInventory;
use pocketmine\block\inventory\StonecutterInventory;
use pocketmine\crafting\FurnaceType;
use pocketmine\inventory\CreativeInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\SlotChangeAction;
use pocketmine\inventory\transaction\InventoryTransaction;
use pocketmine\item\Item;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\cache\CreativeInventoryCache;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
use pocketmine\network\mcpe\protocol\ContainerOpenPacket;
use pocketmine\network\mcpe\protocol\ContainerSetDataPacket;
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\InventoryContentPacket;
use pocketmine\network\mcpe\protocol\InventorySlotPacket;
use pocketmine\network\mcpe\protocol\MobEquipmentPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
@ -110,7 +108,7 @@ class InventoryManager{
private NetworkSession $session
){
$this->containerOpenCallbacks = new ObjectSet();
$this->containerOpenCallbacks->add(\Closure::fromCallable([self::class, 'createContainerOpen']));
$this->containerOpenCallbacks->add(self::createContainerOpen(...));
$this->add(ContainerIds::INVENTORY, $this->player->getInventory());
$this->add(ContainerIds::OFFHAND, $this->player->getOffHandInventory());
@ -118,9 +116,7 @@ class InventoryManager{
$this->addComplex(UIInventorySlotOffset::CURSOR, $this->player->getCursorInventory());
$this->addComplex(UIInventorySlotOffset::CRAFTING2X2_INPUT, $this->player->getCraftingGrid());
$this->player->getInventory()->getHeldItemIndexChangeListeners()->add(function() : void{
$this->syncSelectedHotbarSlot();
});
$this->player->getInventory()->getHeldItemIndexChangeListeners()->add($this->syncSelectedHotbarSlot(...));
}
private function associateIdWithInventory(int $id, Inventory $inventory) : void{
@ -205,11 +201,13 @@ class InventoryManager{
if($entry === null){
return null;
}
$inventory = $entry->getInventory();
$coreSlotId = $entry->mapNetToCore($netSlotId);
return $coreSlotId !== null ? [$entry->getInventory(), $coreSlotId] : null;
return $coreSlotId !== null && $inventory->slotExists($coreSlotId) ? [$inventory, $coreSlotId] : null;
}
if(isset($this->networkIdToInventoryMap[$windowId])){
return [$this->networkIdToInventoryMap[$windowId], $netSlotId];
$inventory = $this->networkIdToInventoryMap[$windowId] ?? null;
if($inventory !== null && $inventory->slotExists($netSlotId)){
return [$inventory, $netSlotId];
}
return null;
}
@ -219,10 +217,11 @@ class InventoryManager{
}
public function addTransactionPredictedSlotChanges(InventoryTransaction $tx) : void{
$typeConverter = $this->session->getTypeConverter();
foreach($tx->getActions() as $action){
if($action instanceof SlotChangeAction){
//TODO: ItemStackRequestExecutor can probably build these predictions with much lower overhead
$itemStack = TypeConverter::getInstance()->coreItemStackToNet($action->getTargetItem());
$itemStack = $typeConverter->coreItemStackToNet($action->getTargetItem());
$this->addPredictedSlotChange($action->getInventory(), $action->getSlot(), $itemStack);
}
}
@ -295,6 +294,8 @@ class InventoryManager{
$inventory instanceof LoomInventory => UIInventorySlotOffset::LOOM,
$inventory instanceof StonecutterInventory => [UIInventorySlotOffset::STONE_CUTTER_INPUT => StonecutterInventory::SLOT_INPUT],
$inventory instanceof CraftingTableInventory => UIInventorySlotOffset::CRAFTING3X3_INPUT,
$inventory instanceof CartographyTableInventory => UIInventorySlotOffset::CARTOGRAPHY_TABLE,
$inventory instanceof SmithingTableInventory => UIInventorySlotOffset::SMITHING_TABLE,
default => null,
};
}
@ -349,6 +350,8 @@ class InventoryManager{
$inv instanceof HopperInventory => WindowTypes::HOPPER,
$inv instanceof CraftingTableInventory => WindowTypes::WORKBENCH,
$inv instanceof StonecutterInventory => WindowTypes::STONECUTTER,
$inv instanceof CartographyTableInventory => WindowTypes::CARTOGRAPHY,
$inv instanceof SmithingTableInventory => WindowTypes::SMITHING_TABLE,
default => WindowTypes::CONTAINER
};
return [ContainerOpenPacket::blockInv($id, $windowType, $blockPosition)];
@ -413,7 +416,7 @@ class InventoryManager{
//is cleared before removal.
return;
}
$currentItem = TypeConverter::getInstance()->coreItemStackToNet($inventory->getItem($slot));
$currentItem = $this->session->getTypeConverter()->coreItemStackToNet($inventory->getItem($slot));
$clientSideItem = $inventoryEntry->predictions[$slot] ?? null;
if($clientSideItem === null || !$clientSideItem->equals($currentItem)){
//no prediction or incorrect - do not associate this with the currently active itemstack request
@ -489,7 +492,7 @@ class InventoryManager{
$entry->predictions = [];
$entry->pendingSyncs = [];
$contents = [];
$typeConverter = TypeConverter::getInstance();
$typeConverter = $this->session->getTypeConverter();
foreach($inventory->getContents(true) as $slot => $item){
$itemStack = $typeConverter->coreItemStackToNet($item);
$info = $this->trackItemStack($entry, $slot, $itemStack, null);
@ -524,7 +527,7 @@ class InventoryManager{
}
public function syncMismatchedPredictedSlotChanges() : void{
$typeConverter = TypeConverter::getInstance();
$typeConverter = $this->session->getTypeConverter();
foreach($this->inventories as $entry){
$inventory = $entry->inventory;
foreach($entry->predictions as $slot => $expectedItem){
@ -587,7 +590,7 @@ class InventoryManager{
$this->session->sendDataPacket(MobEquipmentPacket::create(
$this->player->getId(),
new ItemStackWrapper($itemStackInfo->getStackId(), TypeConverter::getInstance()->coreItemStackToNet($playerInventory->getItemInHand())),
new ItemStackWrapper($itemStackInfo->getStackId(), $this->session->getTypeConverter()->coreItemStackToNet($playerInventory->getItemInHand())),
$selected,
$selected,
ContainerIds::INVENTORY
@ -597,16 +600,7 @@ class InventoryManager{
}
public function syncCreative() : void{
$typeConverter = TypeConverter::getInstance();
$entries = [];
if(!$this->player->isSpectator()){
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
foreach(CreativeInventory::getInstance()->getAll() as $k => $item){
$entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item));
}
}
$this->session->sendDataPacket(CreativeContentPacket::create($entries));
$this->session->sendDataPacket(CreativeInventoryCache::getInstance()->getCache($this->player->getCreativeInventory()));
}
private function newItemStackId() : int{

View File

@ -40,7 +40,7 @@ final class InventoryManagerEntry{
public array $itemStackInfos = [];
/**
* @var int[]
* @var ItemStack[]
* @phpstan-var array<int, ItemStack>
*/
public array $pendingSyncs = [];

View File

@ -23,28 +23,26 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\Sequence;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function base64_decode;
use function base64_encode;
use function bin2hex;
use function chr;
use function count;
use function explode;
use function gmp_export;
use function gmp_import;
use function gmp_init;
use function gmp_strval;
use function is_array;
use function json_decode;
use function json_encode;
use function json_last_error_msg;
use function ltrim;
use function openssl_error_string;
use function openssl_pkey_get_details;
use function openssl_pkey_get_public;
use function openssl_sign;
use function openssl_verify;
use function ord;
use function preg_match;
use function rtrim;
use function sprintf;
@ -54,13 +52,19 @@ use function str_replace;
use function str_split;
use function strlen;
use function strtr;
use const GMP_BIG_ENDIAN;
use const GMP_MSW_FIRST;
use function substr;
use const JSON_THROW_ON_ERROR;
use const OPENSSL_ALGO_SHA384;
use const STR_PAD_LEFT;
final class JwtUtils{
public const BEDROCK_SIGNING_KEY_CURVE_NAME = "secp384r1";
private const ASN1_INTEGER_TAG = "\x02";
private const ASN1_SEQUENCE_TAG = "\x30";
private const SIGNATURE_PART_LENGTH = 48;
private const SIGNATURE_ALGORITHM = OPENSSL_ALGO_SHA384;
/**
* @return string[]
@ -97,30 +101,84 @@ final class JwtUtils{
return [$header, $body, $signature];
}
private static function signaturePartToAsn1(string $part) : string{
if(strlen($part) !== self::SIGNATURE_PART_LENGTH){
throw new JwtException("R and S for a SHA384 signature must each be exactly 48 bytes, but have " . strlen($part) . " bytes");
}
$part = ltrim($part, "\x00");
if(ord($part[0]) >= 128){
//ASN.1 integers with a leading 1 bit are considered negative - add a leading 0 byte to prevent this
//ECDSA signature R and S values are always positive
$part = "\x00" . $part;
}
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
return self::ASN1_INTEGER_TAG . chr(strlen($part)) . $part;
}
private static function rawSignatureToDer(string $rawSignature) : string{
if(strlen($rawSignature) !== self::SIGNATURE_PART_LENGTH * 2){
throw new JwtException("JWT signature has unexpected length, expected 96, got " . strlen($rawSignature));
}
[$rString, $sString] = str_split($rawSignature, self::SIGNATURE_PART_LENGTH);
$sequence = self::signaturePartToAsn1($rString) . self::signaturePartToAsn1($sString);
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
return self::ASN1_SEQUENCE_TAG . chr(strlen($sequence)) . $sequence;
}
private static function signaturePartFromAsn1(BinaryStream $stream) : string{
$prefix = $stream->get(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();
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);
return str_pad(ltrim($part, "\x00"), self::SIGNATURE_PART_LENGTH, "\x00", STR_PAD_LEFT);
}
private static function rawSignatureFromDer(string $derSignature) : string{
if($derSignature[0] !== self::ASN1_SEQUENCE_TAG){
throw new \InvalidArgumentException("Invalid DER signature, expected ASN.1 SEQUENCE tag, got " . bin2hex($derSignature[0]));
}
//we can assume the length is 1 byte here - if it were larger than 127, more complex logic would be needed
$length = ord($derSignature[1]);
$parts = substr($derSignature, 2, $length);
if(strlen($parts) !== $length){
throw new \InvalidArgumentException("Invalid DER signature, expected $length sequence bytes, got " . strlen($parts));
}
$stream = new BinaryStream($parts);
$rRaw = self::signaturePartFromAsn1($stream);
$sRaw = self::signaturePartFromAsn1($stream);
if(!$stream->feof()){
throw new \InvalidArgumentException("Invalid DER signature, unexpected trailing sequence data");
}
return $rRaw . $sRaw;
}
/**
* @throws JwtException
*/
public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{
[$header, $body, $signature] = self::split($jwt);
$plainSignature = self::b64UrlDecode($signature);
if(strlen($plainSignature) !== 96){
throw new JwtException("JWT signature has unexpected length, expected 96, got " . strlen($plainSignature));
}
[$rString, $sString] = str_split($plainSignature, 48);
$convert = fn(string $str) => gmp_strval(gmp_import($str, 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST), 10);
$sequence = new Sequence(
new Integer($convert($rString)),
new Integer($convert($sString))
);
$rawSignature = self::b64UrlDecode($signature);
$derSignature = self::rawSignatureToDer($rawSignature);
$v = openssl_verify(
$header . '.' . $body,
$sequence->getBinary(),
$derSignature,
$signingKey,
OPENSSL_ALGO_SHA384
self::SIGNATURE_ALGORITHM
);
switch($v){
case 0: return false;
@ -139,33 +197,13 @@ final class JwtUtils{
openssl_sign(
$jwtBody,
$rawDerSig,
$derSignature,
$signingKey,
OPENSSL_ALGO_SHA384
self::SIGNATURE_ALGORITHM
);
try{
$asnObject = Sequence::fromBinary($rawDerSig);
}catch(ParserException $e){
throw new AssumptionFailedError("Failed to parse OpenSSL signature: " . $e->getMessage(), 0, $e);
}
if(count($asnObject) !== 2){
throw new AssumptionFailedError("OpenSSL produced invalid signature, expected exactly 2 parts");
}
[$r, $s] = [$asnObject[0], $asnObject[1]];
if(!($r instanceof Integer) || !($s instanceof Integer)){
throw new AssumptionFailedError("OpenSSL produced invalid signature, expected 2 INTEGER parts");
}
$rString = $r->getContent();
$sString = $s->getContent();
$toBinary = fn($str) => str_pad(
gmp_export(gmp_init($str, 10), 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST),
48,
"\x00",
STR_PAD_LEFT
);
$jwtSig = JwtUtils::b64UrlEncode($toBinary($rString) . $toBinary($sString));
$rawSignature = self::rawSignatureFromDer($derSignature);
$jwtSig = self::b64UrlEncode($rawSignature);
return "$jwtBody.$jwtSig";
}
@ -203,6 +241,17 @@ final class JwtUtils{
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;
}
}

View File

@ -30,7 +30,6 @@ use pocketmine\event\server\DataPacketReceiveEvent;
use pocketmine\event\server\DataPacketSendEvent;
use pocketmine\form\Form;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\Translatable;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
@ -39,7 +38,6 @@ use pocketmine\network\mcpe\cache\ChunkCache;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\network\mcpe\compression\DecompressionException;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\encryption\DecryptionException;
use pocketmine\network\mcpe\encryption\EncryptionContext;
@ -60,11 +58,13 @@ use pocketmine\network\mcpe\protocol\DisconnectPacket;
use pocketmine\network\mcpe\protocol\ModalFormRequestPacket;
use pocketmine\network\mcpe\protocol\MovePlayerPacket;
use pocketmine\network\mcpe\protocol\NetworkChunkPublisherUpdatePacket;
use pocketmine\network\mcpe\protocol\OpenSignPacket;
use pocketmine\network\mcpe\protocol\Packet;
use pocketmine\network\mcpe\protocol\PacketDecodeException;
use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\PlayerListPacket;
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\serializer\PacketSerializerContext;
@ -83,6 +83,7 @@ use pocketmine\network\mcpe\protocol\types\AbilitiesLayer;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\command\CommandData;
use pocketmine\network\mcpe\protocol\types\command\CommandEnum;
use pocketmine\network\mcpe\protocol\types\command\CommandOverload;
use pocketmine\network\mcpe\protocol\types\command\CommandParameter;
use pocketmine\network\mcpe\protocol\types\command\CommandPermissions;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
@ -106,7 +107,6 @@ use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\world\Position;
use function array_map;
use function array_values;
@ -114,8 +114,11 @@ use function base64_encode;
use function bin2hex;
use function count;
use function get_class;
use function implode;
use function in_array;
use function json_encode;
use function random_bytes;
use function str_split;
use function strcasecmp;
use function strlen;
use function strtolower;
@ -178,6 +181,7 @@ class NetworkSession{
private PacketBroadcaster $broadcaster,
private EntityEventBroadcaster $entityEventBroadcaster,
private Compressor $compressor,
private TypeConverter $typeConverter,
private string $ip,
private int $port
){
@ -192,13 +196,12 @@ class NetworkSession{
$this->gamePacketLimiter = new PacketRateLimiter("Game Packets", self::INCOMING_GAME_PACKETS_PER_TICK, self::INCOMING_GAME_PACKETS_BUFFER_TICKS);
$this->setHandler(new SessionStartPacketHandler(
$this->server,
$this,
fn() => $this->onSessionStartSuccess()
$this->onSessionStartSuccess(...)
));
$this->manager->add($this);
$this->logger->info("Session opened");
$this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_open()));
}
private function getLogPrefix() : string{
@ -218,20 +221,22 @@ class NetworkSession{
$this,
function(PlayerInfo $info) : void{
$this->info = $info;
$this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET);
$this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_playerName(TextFormat::AQUA . $info->getUsername() . TextFormat::RESET)));
$this->logger->setPrefix($this->getLogPrefix());
$this->manager->markLoginReceived($this);
},
function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
$this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey);
}
$this->setAuthenticationStatus(...)
));
}
protected function createPlayer() : void{
$this->server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion(
\Closure::fromCallable([$this, 'onPlayerCreated']),
fn() => $this->disconnect("Player creation failed") //TODO: this should never actually occur... right?
$this->onPlayerCreated(...),
function() : void{
//TODO: this should never actually occur... right?
$this->logger->error("Failed to create player");
$this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_internal());
}
);
}
@ -464,8 +469,11 @@ class NetworkSession{
if($ev->isCancelled()){
return false;
}
$packets = $ev->getPackets();
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder($this->packetSerializerContext), $packet));
foreach($packets as $evPacket){
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder($this->packetSerializerContext), $evPacket));
}
if($immediate){
$this->flushSendBuffer(true);
}
@ -512,7 +520,7 @@ class NetworkSession{
PacketBatch::encodeRaw($stream, $this->sendBuffer);
if($this->enableCompression){
$promise = $this->server->prepareBatch(new PacketBatch($stream->getBuffer()), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
$promise = $this->server->prepareBatch($stream->getBuffer(), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
}else{
$promise = new CompressBatchPromise();
$promise->resolve($stream->getBuffer());
@ -535,6 +543,8 @@ class NetworkSession{
return $this->compressor;
}
public function getTypeConverter() : TypeConverter{ return $this->typeConverter; }
public function queueCompressed(CompressBatchPromise $payload, bool $immediate = false) : void{
Timings::$playerNetworkSend->startTiming();
try{
@ -595,7 +605,7 @@ class NetworkSession{
/**
* @phpstan-param \Closure() : void $func
*/
private function tryDisconnect(\Closure $func, string $reason) : void{
private function tryDisconnect(\Closure $func, Translatable|string $reason) : void{
if($this->connected && !$this->disconnectGuard){
$this->disconnectGuard = true;
$func();
@ -608,7 +618,8 @@ class NetworkSession{
$this->disposeHooks->clear();
$this->setHandler(null);
$this->connected = false;
$this->logger->info("Session closed due to $reason");
$this->logger->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_network_session_close($reason)));
}
}
@ -620,13 +631,25 @@ class NetworkSession{
$this->invManager = null;
}
private function sendDisconnectPacket(Translatable|string $message) : void{
if($message instanceof Translatable){
$translated = $this->server->getLanguage()->translate($message);
}else{
$translated = $message;
}
$this->sendDataPacket(DisconnectPacket::create($translated));
}
/**
* Disconnects the session, destroying the associated player (if it exists).
*
* @param Translatable|string $reason Shown in the server log - this should be a short one-line message
* @param Translatable|string|null $disconnectScreenMessage Shown on the player's disconnection screen (null will use the reason)
*/
public function disconnect(string $reason, bool $notify = true) : void{
$this->tryDisconnect(function() use ($reason, $notify) : void{
public function disconnect(Translatable|string $reason, Translatable|string|null $disconnectScreenMessage = null, bool $notify = true) : void{
$this->tryDisconnect(function() use ($reason, $disconnectScreenMessage, $notify) : void{
if($notify){
$this->sendDataPacket(DisconnectPacket::create($reason));
$this->sendDisconnectPacket($disconnectScreenMessage ?? $reason);
}
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
@ -634,10 +657,24 @@ class NetworkSession{
}, $reason);
}
public function disconnectWithError(Translatable|string $reason) : void{
$this->disconnect(KnownTranslationFactory::pocketmine_disconnect_error($reason, implode("-", str_split(bin2hex(random_bytes(6)), 4))));
}
public function disconnectIncompatibleProtocol(int $protocolVersion) : void{
$this->tryDisconnect(
function() use ($protocolVersion) : void{
$this->sendDataPacket(PlayStatusPacket::create($protocolVersion < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
},
KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $protocolVersion)
);
}
/**
* Instructs the remote client to connect to a different server.
*/
public function transfer(string $ip, int $port, string $reason = "transfer") : void{
public function transfer(string $ip, int $port, Translatable|string|null $reason = null) : void{
$reason ??= KnownTranslationFactory::pocketmine_disconnect_transfer();
$this->tryDisconnect(function() use ($ip, $port, $reason) : void{
$this->sendDataPacket(TransferPacket::create($ip, $port), true);
if($this->player !== null){
@ -649,9 +686,9 @@ class NetworkSession{
/**
* Called by the Player when it is closed (for example due to getting kicked).
*/
public function onPlayerDestroyed(string $reason) : void{
$this->tryDisconnect(function() use ($reason) : void{
$this->sendDataPacket(DisconnectPacket::create($reason));
public function onPlayerDestroyed(Translatable|string $reason, Translatable|string $disconnectScreenMessage) : void{
$this->tryDisconnect(function() use ($disconnectScreenMessage) : void{
$this->sendDisconnectPacket($disconnectScreenMessage);
}, $reason);
}
@ -659,7 +696,7 @@ class NetworkSession{
* Called by the network interface to close the session when the client disconnects without server input, for
* example in a timeout condition or voluntary client disconnect.
*/
public function onClientDisconnect(string $reason) : void{
public function onClientDisconnect(Translatable|string $reason) : void{
$this->tryDisconnect(function() use ($reason) : void{
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
@ -667,7 +704,7 @@ class NetworkSession{
}, $reason);
}
private function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
private function setAuthenticationStatus(bool $authenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPubKey) : void{
if(!$this->connected){
return;
}
@ -680,7 +717,7 @@ class NetworkSession{
}
if($error !== null){
$this->disconnect($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_invalidSession($this->server->getLanguage()->translateString($error))));
$this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_invalidSession($error));
return;
}
@ -689,7 +726,7 @@ class NetworkSession{
if(!$this->authenticated){
if($authRequired){
$this->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_NOTAUTHENTICATED);
$this->disconnect("Not authenticated", KnownTranslationFactory::disconnectionScreen_notAuthenticated());
return;
}
if($this->info instanceof XboxLivePlayerInfo){
@ -723,14 +760,14 @@ class NetworkSession{
if($kickForXUIDMismatch($info instanceof XboxLivePlayerInfo ? $info->getXuid() : "")){
return;
}
$ev = new PlayerDuplicateLoginEvent($this, $existingSession);
$ev = new PlayerDuplicateLoginEvent($this, $existingSession, KnownTranslationFactory::disconnectionScreen_loggedinOtherLocation(), null);
$ev->call();
if($ev->isCancelled()){
$this->disconnect($ev->getDisconnectMessage());
$this->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
return;
}
$existingSession->disconnect($ev->getDisconnectMessage());
$existingSession->disconnect($ev->getDisconnectReason(), $ev->getDisconnectScreenMessage());
}
}
@ -755,9 +792,7 @@ class NetworkSession{
$this->cipher = EncryptionContext::fakeGCM($encryptionKey);
$this->setHandler(new HandshakePacketHandler(function() : void{
$this->onServerLoginSuccess();
}));
$this->setHandler(new HandshakePacketHandler($this->onServerLoginSuccess(...)));
$this->logger->debug("Enabled encryption");
}));
}else{
@ -778,7 +813,7 @@ class NetworkSession{
private function beginSpawnSequence() : void{
$this->setHandler(new PreSpawnPacketHandler($this->server, $this->player, $this, $this->invManager));
$this->player->setImmobile(); //TODO: HACK: fix client-side falling pre-spawn
$this->player->setNoClientPredictions(); //TODO: HACK: fix client-side falling pre-spawn
$this->logger->debug("Waiting for chunk radius request");
}
@ -786,14 +821,12 @@ class NetworkSession{
public function notifyTerrainReady() : void{
$this->logger->debug("Sending spawn notification, waiting for spawn response");
$this->sendDataPacket(PlayStatusPacket::create(PlayStatusPacket::PLAYER_SPAWN));
$this->setHandler(new SpawnResponsePacketHandler(function() : void{
$this->onClientSpawnResponse();
}));
$this->setHandler(new SpawnResponsePacketHandler($this->onClientSpawnResponse(...)));
}
private function onClientSpawnResponse() : void{
$this->logger->debug("Received spawn response, entering in-game phase");
$this->player->setImmobile(false); //TODO: HACK: we set this during the spawn sequence to prevent the client sending junk movements
$this->player->setNoClientPredictions(false); //TODO: HACK: we set this during the spawn sequence to prevent the client sending junk movements
$this->player->doFirstSpawn();
$this->forceAsyncCompression = false;
$this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager));
@ -857,7 +890,7 @@ class NetworkSession{
}
public function syncGameMode(GameMode $mode, bool $isRollback = false) : void{
$this->sendDataPacket(SetPlayerGameTypePacket::create(TypeConverter::getInstance()->coreGameModeToProtocol($mode)));
$this->sendDataPacket(SetPlayerGameTypePacket::create($this->typeConverter->coreGameModeToProtocol($mode)));
if($this->player !== null){
$this->syncAbilities($this->player);
$this->syncAdventureSettings(); //TODO: we might be able to do this with the abilities packet alone
@ -891,14 +924,26 @@ class NetworkSession{
AbilitiesLayer::ABILITY_PRIVILEGED_BUILDER => false,
];
$layers = [
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
];
if(!$for->hasBlockCollision()){
//TODO: HACK! In 1.19.80, the client starts falling in our faux spectator mode when it clips into a
//block. We can't seem to prevent this short of forcing the player to always fly when block collision is
//disabled. Also, for some reason the client always reads flight state from this layer if present, even
//though the player isn't in spectator mode.
$layers[] = new AbilitiesLayer(AbilitiesLayer::LAYER_SPECTATOR, [
AbilitiesLayer::ABILITY_FLYING => true,
], null, null);
}
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
$isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
$isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
$for->getId(),
[
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
]
$layers
)));
}
@ -942,8 +987,9 @@ class NetworkSession{
0,
$aliasObj,
[
[CommandParameter::standard("args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0, true)]
]
new CommandOverload(chaining: false, parameters: [CommandParameter::standard("args", AvailableCommandsPacket::ARG_TYPE_RAWTEXT, 0, true)])
],
chainedSubCommandData: []
);
$commandData[$command->getLabel()] = $data;
@ -952,26 +998,39 @@ class NetworkSession{
$this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));
}
/**
* @return string[][]
* @phpstan-return array{string, string[]}
*/
public function prepareClientTranslatableMessage(Translatable $message) : array{
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$language = $this->player->getLanguage();
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
return [$language->translateString($message->getText(), $parameters, "pocketmine."), $parameters];
}
public function onChatMessage(Translatable|string $message) : void{
if($message instanceof Translatable){
$language = $this->player->getLanguage();
if(!$this->server->isLanguageForced()){
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
$this->sendDataPacket(TextPacket::translation($language->translateString($message->getText(), $parameters, "pocketmine."), $parameters));
$this->sendDataPacket(TextPacket::translation(...$this->prepareClientTranslatableMessage($message)));
}else{
$this->sendDataPacket(TextPacket::raw($language->translate($message)));
$this->sendDataPacket(TextPacket::raw($this->player->getLanguage()->translate($message)));
}
}else{
$this->sendDataPacket(TextPacket::raw($message));
}
}
/**
* @param string[] $parameters
*/
public function onJukeboxPopup(string $key, array $parameters) : void{
$this->sendDataPacket(TextPacket::jukeboxPopup($key, $parameters));
public function onJukeboxPopup(Translatable|string $message) : void{
$parameters = [];
if($message instanceof Translatable){
if(!$this->server->isLanguageForced()){
[$message, $parameters] = $this->prepareClientTranslatableMessage($message);
}else{
$message = $this->player->getLanguage()->translate($message);
}
}
$this->sendDataPacket(TextPacket::jukeboxPopup($message, $parameters));
}
public function onPopup(string $message) : void{
@ -992,8 +1051,6 @@ class NetworkSession{
* @phpstan-param \Closure() : void $onCompletion
*/
public function startUsingChunk(int $chunkX, int $chunkZ, \Closure $onCompletion) : void{
Utils::validateCallableSignature(function() : void{}, $onCompletion);
$world = $this->player->getLocation()->getWorld();
ChunkCache::getInstance($world, $this->compressor)->request($chunkX, $chunkZ)->onResolve(
@ -1056,12 +1113,12 @@ class NetworkSession{
*/
public function syncPlayerList(array $players) : void{
$this->sendDataPacket(PlayerListPacket::add(array_map(function(Player $player) : PlayerListEntry{
return PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), SkinAdapterSingleton::get()->toSkinData($player->getSkin()), $player->getXuid());
return PlayerListEntry::createAdditionEntry($player->getUniqueId(), $player->getId(), $player->getDisplayName(), TypeConverter::getInstance()->getSkinAdapter()->toSkinData($player->getSkin()), $player->getXuid());
}, $players)));
}
public function onPlayerAdded(Player $p) : void{
$this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), SkinAdapterSingleton::get()->toSkinData($p->getSkin()), $p->getXuid())]));
$this->sendDataPacket(PlayerListPacket::add([PlayerListEntry::createAdditionEntry($p->getUniqueId(), $p->getId(), $p->getDisplayName(), TypeConverter::getInstance()->getSkinAdapter()->toSkinData($p->getSkin()), $p->getXuid())]));
}
public function onPlayerRemoved(Player $p) : void{
@ -1098,6 +1155,10 @@ class NetworkSession{
$this->sendDataPacket(ToastRequestPacket::create($title, $body));
}
public function onOpenSignEditor(Vector3 $signPosition, bool $frontSide) : void{
$this->sendDataPacket(OpenSignPacket::create(BlockPosition::fromVector3($signPosition), $frontSide));
}
public function tick() : void{
if(!$this->isConnected()){
$this->dispose();
@ -1106,7 +1167,7 @@ class NetworkSession{
if($this->info === null){
if(time() >= $this->connectTime + 10){
$this->disconnect("Login timeout");
$this->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_loginTimeout());
}
return;

View File

@ -51,7 +51,8 @@ use const SORT_NUMERIC;
final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
public function __construct(
private StandardPacketBroadcaster $broadcaster
private PacketBroadcaster $broadcaster,
private TypeConverter $typeConverter
){}
/**
@ -103,7 +104,7 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
$inv = $mob->getInventory();
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
$mob->getId(),
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItemInHand())),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItemInHand())),
$inv->getHeldItemIndex(),
$inv->getHeldItemIndex(),
ContainerIds::INVENTORY
@ -114,7 +115,7 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
$inv = $mob->getOffHandInventory();
$this->sendDataPacket($recipients, MobEquipmentPacket::create(
$mob->getId(),
ItemStackWrapper::legacy(TypeConverter::getInstance()->coreItemStackToNet($inv->getItem(0))),
ItemStackWrapper::legacy($this->typeConverter->coreItemStackToNet($inv->getItem(0))),
0,
0,
ContainerIds::OFFHAND
@ -123,7 +124,7 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
public function onMobArmorChange(array $recipients, Living $mob) : void{
$inv = $mob->getArmorInventory();
$converter = TypeConverter::getInstance();
$converter = $this->typeConverter;
$this->sendDataPacket($recipients, MobArmorEquipmentPacket::create(
$mob->getId(),
ItemStackWrapper::legacy($converter->coreItemStackToNet($inv->getHelmet())),
@ -138,6 +139,6 @@ final class StandardEntityEventBroadcaster implements EntityEventBroadcaster{
}
public function onEmote(array $recipients, Human $from, string $emoteId) : void{
$this->sendDataPacket($recipients, EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER));
$this->sendDataPacket($recipients, EmotePacket::create($from->getId(), $emoteId, "", "", EmotePacket::FLAG_SERVER | EmotePacket::FLAG_MUTE_ANNOUNCEMENT));
}
}

View File

@ -86,7 +86,7 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{
PacketBatch::encodeRaw($stream, $packetBuffers);
$batchBuffer = $stream->getBuffer();
$promise = $this->server->prepareBatch(new PacketBatch($batchBuffer), $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
$promise = $this->server->prepareBatch($batchBuffer, $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
foreach($compressorTargets as $target){
$target->queueCompressed($promise);
}

View File

@ -23,22 +23,38 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationKeys;
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 openssl_error_string;
use function time;
class ProcessLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V";
/**
* Old Mojang root auth key. This was used since the introduction of Xbox Live authentication in 0.15.0.
* This key is expected to be replaced by the key below in the future, but this has not yet happened as of
* 2023-07-01.
* Ideally we would place a time expiry on this key, but since Mojang have not given a hard date for the key change,
* and one bad guess has already caused a major outage, we can't do this.
* TODO: This needs to be removed as soon as the new key is deployed by Mojang's authentication servers.
*/
public const MOJANG_OLD_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8ELkixyLcwlZryUQcu1TvPOmI2B7vX83ndnWRUaXm74wFfa5f/lwQNTfrLVHa2PmenpGI6JhIMUJaWZrjmMj90NoKNFSNBuKdm8rYiXsfaz3K36x/1U26HpG0ZxK/V1V";
/**
* 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;
@ -48,8 +64,10 @@ class ProcessLoginTask extends AsyncTask{
* 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 ?string $error = "Unknown";
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.
@ -59,7 +77,7 @@ class ProcessLoginTask extends AsyncTask{
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPublicKey) : void $onCompletion
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
@ -76,7 +94,8 @@ class ProcessLoginTask extends AsyncTask{
$this->clientPublicKey = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$this->error = $e->getMessage();
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
@ -109,7 +128,8 @@ class ProcessLoginTask extends AsyncTask{
try{
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
@ -121,37 +141,40 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtHeader $headers */
$headers = $mapper->map($headersArray, new JwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e);
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
//TODO: we shouldn't be showing internal information like this to the client
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){
if(!$first){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY);
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(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
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: " . openssl_error_string());
//TODO: we shouldn't be showing this internal information to the client
throw new VerifyLoginException("Invalid JWT public key: " . $e->getMessage(), null, 0, $e);
}
try{
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
throw new VerifyLoginException(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), 0, $e);
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY || $headers->x5u === self::MOJANG_OLD_ROOT_PUBLIC_KEY){
$this->authenticated = true; //we're signed into xbox live
}
@ -164,16 +187,16 @@ class ProcessLoginTask extends AsyncTask{
/** @var JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), 0, $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(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY);
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(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE);
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
if(isset($claims->identityPublicKey)){
@ -181,6 +204,12 @@ class ProcessLoginTask extends AsyncTask{
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
}
}
@ -188,9 +217,9 @@ class ProcessLoginTask extends AsyncTask{
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, ?string, ?string) : void $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, $this->clientPublicKey);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey);
}
}

View File

@ -23,6 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\Translatable;
class VerifyLoginException extends \RuntimeException{
private Translatable|string $disconnectMessage;
public function __construct(string $message, Translatable|string|null $disconnectMessage = null, int $code = 0, ?\Throwable $previous = null){
parent::__construct($message, $code, $previous);
$this->disconnectMessage = $disconnectMessage ?? $message;
}
public function getDisconnectMessage() : Translatable|string{ return $this->disconnectMessage; }
}

View File

@ -118,14 +118,7 @@ class ChunkCache implements ChunkListener{
$chunkZ,
$chunk,
$this->caches[$chunkHash],
$this->compressor,
function() use ($chunkHash, $chunkX, $chunkZ) : void{
$this->world->getLogger()->error("Failed preparing chunk $chunkX $chunkZ, retrying");
if(isset($this->caches[$chunkHash])){
$this->restartPendingRequest($chunkX, $chunkZ);
}
}
$this->compressor
)
);

View File

@ -28,17 +28,14 @@ use pocketmine\crafting\FurnaceType;
use pocketmine\crafting\ShapedRecipe;
use pocketmine\crafting\ShapelessRecipe;
use pocketmine\crafting\ShapelessRecipeType;
use pocketmine\item\Item;
use pocketmine\network\mcpe\convert\ItemTranslator;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\recipe\CraftingRecipeBlockName;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe as ProtocolFurnaceRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipeBlockName;
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
use pocketmine\network\mcpe\protocol\types\recipe\PotionContainerChangeRecipe as ProtocolPotionContainerChangeRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\PotionTypeRecipe as ProtocolPotionTypeRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe as ProtocolShapedRecipe;
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe as ProtocolShapelessRecipe;
use pocketmine\timings\Timings;
@ -87,17 +84,15 @@ final class CraftingDataCache{
$typeTag = match($recipe->getType()->id()){
ShapelessRecipeType::CRAFTING()->id() => CraftingRecipeBlockName::CRAFTING_TABLE,
ShapelessRecipeType::STONECUTTER()->id() => CraftingRecipeBlockName::STONECUTTER,
ShapelessRecipeType::CARTOGRAPHY()->id() => CraftingRecipeBlockName::CARTOGRAPHY_TABLE,
ShapelessRecipeType::SMITHING()->id() => CraftingRecipeBlockName::SMITHING_TABLE,
default => throw new AssumptionFailedError("Unreachable"),
};
$recipesWithTypeIds[] = new ProtocolShapelessRecipe(
CraftingDataPacket::ENTRY_SHAPELESS,
Binary::writeInt($index),
array_map(function(Item $item) use ($converter) : RecipeIngredient{
return $converter->coreItemStackToRecipeIngredient($item);
}, $recipe->getIngredientList()),
array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item);
}, $recipe->getResults()),
array_map($converter->coreRecipeIngredientToNet(...), $recipe->getIngredientList()),
array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
$nullUUID,
$typeTag,
50,
@ -108,16 +103,14 @@ final class CraftingDataCache{
for($row = 0, $height = $recipe->getHeight(); $row < $height; ++$row){
for($column = 0, $width = $recipe->getWidth(); $column < $width; ++$column){
$inputs[$row][$column] = $converter->coreItemStackToRecipeIngredient($recipe->getIngredient($column, $row));
$inputs[$row][$column] = $converter->coreRecipeIngredientToNet($recipe->getIngredient($column, $row));
}
}
$recipesWithTypeIds[] = $r = new ProtocolShapedRecipe(
CraftingDataPacket::ENTRY_SHAPED,
Binary::writeInt($index),
$inputs,
array_map(function(Item $item) use ($converter) : ItemStack{
return $converter->coreItemStackToNet($item);
}, $recipe->getResults()),
array_map($converter->coreItemStackToNet(...), $recipe->getResults()),
$nullUUID,
CraftingRecipeBlockName::CRAFTING_TABLE,
50,
@ -136,7 +129,10 @@ final class CraftingDataCache{
default => throw new AssumptionFailedError("Unreachable"),
};
foreach($manager->getFurnaceRecipeManager($furnaceType)->getAll() as $recipe){
$input = $converter->coreItemStackToNet($recipe->getInput());
$input = $converter->coreRecipeIngredientToNet($recipe->getInput())->getDescriptor();
if(!$input instanceof IntIdMetaItemDescriptor){
throw new AssumptionFailedError();
}
$recipesWithTypeIds[] = new ProtocolFurnaceRecipe(
CraftingDataPacket::ENTRY_FURNACE_DATA,
$input->getId(),
@ -148,35 +144,37 @@ final class CraftingDataCache{
}
$potionTypeRecipes = [];
foreach($manager->getPotionTypeRecipes() as $recipes){
foreach($recipes as $recipe){
$input = $converter->coreItemStackToNet($recipe->getInput());
$ingredient = $converter->coreItemStackToNet($recipe->getIngredient());
$output = $converter->coreItemStackToNet($recipe->getOutput());
$potionTypeRecipes[] = new ProtocolPotionTypeRecipe(
$input->getId(),
$input->getMeta(),
$ingredient->getId(),
$ingredient->getMeta(),
$output->getId(),
$output->getMeta()
);
foreach($manager->getPotionTypeRecipes() as $recipe){
$input = $converter->coreRecipeIngredientToNet($recipe->getInput())->getDescriptor();
$ingredient = $converter->coreRecipeIngredientToNet($recipe->getIngredient())->getDescriptor();
if(!$input instanceof IntIdMetaItemDescriptor || !$ingredient instanceof IntIdMetaItemDescriptor){
throw new AssumptionFailedError();
}
$output = $converter->coreItemStackToNet($recipe->getOutput());
$potionTypeRecipes[] = new ProtocolPotionTypeRecipe(
$input->getId(),
$input->getMeta(),
$ingredient->getId(),
$ingredient->getMeta(),
$output->getId(),
$output->getMeta()
);
}
$potionContainerChangeRecipes = [];
$itemTranslator = ItemTranslator::getInstance();
foreach($manager->getPotionContainerChangeRecipes() as $recipes){
foreach($recipes as $recipe){
$input = $itemTranslator->toNetworkId($recipe->getInputItemId(), 0);
$ingredient = $itemTranslator->toNetworkId($recipe->getIngredient()->getId(), 0);
$output = $itemTranslator->toNetworkId($recipe->getOutputItemId(), 0);
$potionContainerChangeRecipes[] = new ProtocolPotionContainerChangeRecipe(
$input[0],
$ingredient[0],
$output[0]
);
$itemTypeDictionary = TypeConverter::getInstance()->getItemTypeDictionary();
foreach($manager->getPotionContainerChangeRecipes() as $recipe){
$input = $itemTypeDictionary->fromStringId($recipe->getInputItemId());
$ingredient = $converter->coreRecipeIngredientToNet($recipe->getIngredient())->getDescriptor();
if(!$ingredient instanceof IntIdMetaItemDescriptor){
throw new AssumptionFailedError();
}
$output = $itemTypeDictionary->fromStringId($recipe->getOutputItemId());
$potionContainerChangeRecipes[] = new ProtocolPotionContainerChangeRecipe(
$input,
$ingredient->getId(),
$output
);
}
Timings::$craftingDataCacheRebuild->stopTiming();

View File

@ -0,0 +1,69 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\inventory\CreativeInventory;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
use pocketmine\utils\SingletonTrait;
use function spl_object_id;
final class CreativeInventoryCache{
use SingletonTrait;
/**
* @var CreativeContentPacket[]
* @phpstan-var array<int, CreativeContentPacket>
*/
private array $caches = [];
public function getCache(CreativeInventory $inventory) : CreativeContentPacket{
$id = spl_object_id($inventory);
if(!isset($this->caches[$id])){
$inventory->getDestructorCallbacks()->add(function() use ($id) : void{
unset($this->caches[$id]);
});
$inventory->getContentChangedCallbacks()->add(function() use ($id) : void{
unset($this->caches[$id]);
});
$this->caches[$id] = $this->buildCreativeInventoryCache($inventory);
}
return $this->caches[$id];
}
/**
* Rebuild the cache for the given inventory.
*/
private function buildCreativeInventoryCache(CreativeInventory $inventory) : CreativeContentPacket{
$entries = [];
$typeConverter = TypeConverter::getInstance();
//creative inventory may have holes if items were unregistered - ensure network IDs used are always consistent
foreach($inventory->getAll() as $k => $item){
$entries[] = new CreativeContentEntry($k, $typeConverter->coreItemStackToNet($item));
}
return CreativeContentPacket::create($entries);
}
}

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\compression;
use pocketmine\utils\Utils;
use function array_push;
class CompressBatchPromise{
@ -42,9 +41,6 @@ class CompressBatchPromise{
*/
public function onResolve(\Closure ...$callbacks) : void{
$this->checkCancelled();
foreach($callbacks as $callback){
Utils::validateCallableSignature(function(CompressBatchPromise $promise) : void{}, $callback);
}
if($this->result !== null){
foreach($callbacks as $callback){
$callback($this);

View File

@ -24,21 +24,26 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\compression;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
class CompressBatchTask extends AsyncTask{
private const TLS_KEY_PROMISE = "promise";
/** @phpstan-var NonThreadSafeValue<Compressor> */
private NonThreadSafeValue $compressor;
public function __construct(
private string $data,
CompressBatchPromise $promise,
private Compressor $compressor
Compressor $compressor
){
$this->compressor = new NonThreadSafeValue($compressor);
$this->storeLocal(self::TLS_KEY_PROMISE, $promise);
}
public function onRun() : void{
$this->setResult($this->compressor->compress($this->data));
$this->setResult($this->compressor->deserialize()->compress($this->data));
}
public function onCompletion() : void{

View File

@ -0,0 +1,200 @@
<?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\convert;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\block\BlockTypeNames;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\utils\Utils;
use function array_key_first;
use function array_map;
use function count;
use function get_debug_type;
use function is_array;
use function is_int;
use function is_string;
use function json_decode;
use const JSON_THROW_ON_ERROR;
/**
* Handles translation of network block runtime IDs into blockstate data, and vice versa
*/
final class BlockStateDictionary{
/**
* @var int[][]|int[]
* @phpstan-var array<string, array<string, int>|int>
*/
private array $stateDataToStateIdLookup = [];
/**
* @var int[][]|null
* @phpstan-var array<string, array<int, int>|int>|null
*/
private ?array $idMetaToStateIdLookupCache = null;
/**
* @param BlockStateDictionaryEntry[] $states
*
* @phpstan-param list<BlockStateDictionaryEntry> $states
*/
public function __construct(
private array $states
){
$table = [];
foreach($this->states as $stateId => $stateNbt){
$table[$stateNbt->getStateName()][$stateNbt->getRawStateProperties()] = $stateId;
}
//setup fast path for stateless blocks
foreach(Utils::stringifyKeys($table) as $name => $stateIds){
if(count($stateIds) === 1){
$this->stateDataToStateIdLookup[$name] = $stateIds[array_key_first($stateIds)];
}else{
$this->stateDataToStateIdLookup[$name] = $stateIds;
}
}
}
/**
* @return int[][]
* @phpstan-return array<string, array<int, int>|int>
*/
private function getIdMetaToStateIdLookup() : array{
if($this->idMetaToStateIdLookupCache === null){
$table = [];
//TODO: if we ever allow mutating the dictionary, this would need to be rebuilt on modification
foreach($this->states as $i => $state){
$table[$state->getStateName()][$state->getMeta()] = $i;
}
$this->idMetaToStateIdLookupCache = [];
foreach(Utils::stringifyKeys($table) as $name => $metaToStateId){
//if only one meta value exists
if(count($metaToStateId) === 1){
$this->idMetaToStateIdLookupCache[$name] = $metaToStateId[array_key_first($metaToStateId)];
}else{
$this->idMetaToStateIdLookupCache[$name] = $metaToStateId;
}
}
}
return $this->idMetaToStateIdLookupCache;
}
public function generateDataFromStateId(int $networkRuntimeId) : ?BlockStateData{
return ($this->states[$networkRuntimeId] ?? null)?->generateStateData();
}
/**
* Searches for the appropriate state ID which matches the given blockstate NBT.
* Returns null if there were no matches.
*/
public function lookupStateIdFromData(BlockStateData $data) : ?int{
$name = $data->getName();
$lookup = $this->stateDataToStateIdLookup[$name] ?? null;
return match(true){
$lookup === null => null,
is_int($lookup) => $lookup,
is_array($lookup) => $lookup[BlockStateDictionaryEntry::encodeStateProperties($data->getStates())] ?? null
};
}
/**
* Returns the blockstate meta value associated with the given blockstate runtime ID.
* This is used for serializing crafting recipe inputs.
*/
public function getMetaFromStateId(int $networkRuntimeId) : ?int{
return ($this->states[$networkRuntimeId] ?? null)?->getMeta();
}
/**
* Returns the blockstate data associated with the given block ID and meta value.
* This is used for deserializing crafting recipe inputs.
*/
public function lookupStateIdFromIdMeta(string $id, int $meta) : ?int{
$metas = $this->getIdMetaToStateIdLookup()[$id] ?? null;
return match(true){
$metas === null => null,
is_int($metas) => $metas,
is_array($metas) => $metas[$meta] ?? null
};
}
/**
* Returns an array mapping runtime ID => blockstate data.
* @return BlockStateDictionaryEntry[]
* @phpstan-return array<int, BlockStateDictionaryEntry>
*/
public function getStates() : array{ return $this->states; }
/**
* @return BlockStateData[]
* @phpstan-return list<BlockStateData>
*
* @throws NbtDataException
*/
public static function loadPaletteFromString(string $blockPaletteContents) : array{
return array_map(
fn(TreeRoot $root) => BlockStateData::fromNbt($root->mustGetCompoundTag()),
(new NetworkNbtSerializer())->readMultiple($blockPaletteContents)
);
}
public static function loadFromString(string $blockPaletteContents, string $metaMapContents) : self{
$metaMap = json_decode($metaMapContents, flags: JSON_THROW_ON_ERROR);
if(!is_array($metaMap)){
throw new \InvalidArgumentException("Invalid metaMap, expected array for root type, got " . get_debug_type($metaMap));
}
$entries = [];
$uniqueNames = [];
//this hack allows the internal cache index to use interned strings which are already available in the
//core code anyway, saving around 40 KB of memory
foreach((new \ReflectionClass(BlockTypeNames::class))->getConstants() as $value){
if(is_string($value)){
$uniqueNames[$value] = $value;
}
}
foreach(self::loadPaletteFromString($blockPaletteContents) as $i => $state){
$meta = $metaMap[$i] ?? null;
if($meta === null){
throw new \InvalidArgumentException("Missing associated meta value for state $i (" . $state->toNbt() . ")");
}
if(!is_int($meta)){
throw new \InvalidArgumentException("Invalid metaMap offset $i, expected int, got " . get_debug_type($meta));
}
$uniqueName = $uniqueNames[$state->getName()] ??= $state->getName();
$entries[$i] = new BlockStateDictionaryEntry($uniqueName, $state->getStates(), $meta);
}
return new self($entries);
}
}

View File

@ -0,0 +1,95 @@
<?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\convert;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\nbt\LittleEndianNbtSerializer;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\Tag;
use pocketmine\nbt\TreeRoot;
use function count;
use function ksort;
use const SORT_STRING;
final class BlockStateDictionaryEntry{
/**
* @var string[]
* @phpstan-var array<string, string>
*/
private static array $uniqueRawStates = [];
private string $rawStateProperties;
/**
* @param Tag[] $stateProperties
*/
public function __construct(
private string $stateName,
array $stateProperties,
private int $meta
){
$rawStateProperties = self::encodeStateProperties($stateProperties);
$this->rawStateProperties = self::$uniqueRawStates[$rawStateProperties] ??= $rawStateProperties;
}
public function getStateName() : string{ return $this->stateName; }
public function getRawStateProperties() : string{ return $this->rawStateProperties; }
public function generateStateData() : BlockStateData{
return new BlockStateData(
$this->stateName,
self::decodeStateProperties($this->rawStateProperties),
BlockStateData::CURRENT_VERSION
);
}
public function getMeta() : int{ return $this->meta; }
/**
* @return Tag[]
*/
public static function decodeStateProperties(string $rawProperties) : array{
if($rawProperties === ""){
return [];
}
return (new LittleEndianNbtSerializer())->read($rawProperties)->mustGetCompoundTag()->getValue();
}
/**
* @param Tag[] $properties
*/
public static function encodeStateProperties(array $properties) : string{
if(count($properties) === 0){
return "";
}
//TODO: make a more efficient encoding - NBT will do for now, but it's not very compact
ksort($properties, SORT_STRING);
$tag = new CompoundTag();
foreach($properties as $k => $v){
$tag->setTag($k, $v);
}
return (new LittleEndianNbtSerializer())->write(new TreeRoot($tag));
}
}

View File

@ -0,0 +1,91 @@
<?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\convert;
use pocketmine\data\bedrock\block\BlockStateData;
use pocketmine\data\bedrock\block\BlockStateSerializeException;
use pocketmine\data\bedrock\block\BlockStateSerializer;
use pocketmine\data\bedrock\block\BlockTypeNames;
use pocketmine\utils\AssumptionFailedError;
/**
* @internal
*/
final class BlockTranslator{
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $networkIdCache = [];
/** Used when a blockstate can't be correctly serialized (e.g. because it's unknown) */
private BlockStateData $fallbackStateData;
private int $fallbackStateId;
public function __construct(
private BlockStateDictionary $blockStateDictionary,
private BlockStateSerializer $blockStateSerializer
){
$this->fallbackStateData = BlockStateData::current(BlockTypeNames::INFO_UPDATE, []);
$this->fallbackStateId = $this->blockStateDictionary->lookupStateIdFromData($this->fallbackStateData) ??
throw new AssumptionFailedError(BlockTypeNames::INFO_UPDATE . " should always exist");
}
public function internalIdToNetworkId(int $internalStateId) : int{
if(isset($this->networkIdCache[$internalStateId])){
return $this->networkIdCache[$internalStateId];
}
try{
$blockStateData = $this->blockStateSerializer->serialize($internalStateId);
$networkId = $this->blockStateDictionary->lookupStateIdFromData($blockStateData);
if($networkId === null){
throw new AssumptionFailedError("Unmapped blockstate returned by blockstate serializer: " . $blockStateData->toNbt());
}
}catch(BlockStateSerializeException){
//TODO: this will swallow any error caused by invalid block properties; this is not ideal, but it should be
//covered by unit tests, so this is probably a safe assumption.
$networkId = $this->fallbackStateId;
}
return $this->networkIdCache[$internalStateId] = $networkId;
}
/**
* Looks up the network state data associated with the given internal state ID.
*/
public function internalIdToNetworkStateData(int $internalStateId) : BlockStateData{
//we don't directly use the blockstate serializer here - we can't assume that the network blockstate NBT is the
//same as the disk blockstate NBT, in case we decide to have different world version than network version (or in
//case someone wants to implement multi version).
$networkRuntimeId = $this->internalIdToNetworkId($internalStateId);
return $this->blockStateDictionary->generateDataFromStateId($networkRuntimeId) ?? throw new AssumptionFailedError("We just looked up this state ID, so it must exist");
}
public function getBlockStateDictionary() : BlockStateDictionary{ return $this->blockStateDictionary; }
public function getFallbackStateData() : BlockStateData{ return $this->fallbackStateData; }
}

View File

@ -23,183 +23,104 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\LegacyItemIdToStringIdMap;
use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\data\bedrock\item\ItemDeserializer;
use pocketmine\data\bedrock\item\ItemSerializer;
use pocketmine\data\bedrock\item\ItemTypeDeserializeException;
use pocketmine\data\bedrock\item\ItemTypeSerializeException;
use pocketmine\data\bedrock\item\SavedItemData;
use pocketmine\item\Item;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use function array_key_exists;
use function is_array;
use function is_numeric;
use function is_string;
use function json_decode;
/**
* This class handles translation between network item ID+metadata to PocketMine-MP internal ID+metadata and vice versa.
*/
final class ItemTranslator{
use SingletonTrait;
public const NO_BLOCK_RUNTIME_ID = 0; //this is technically a valid block runtime ID, but is used to represent "no block" (derp mojang)
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $simpleCoreToNetMapping = [];
/**
* @var int[]
* @phpstan-var array<int, int>
*/
private array $simpleNetToCoreMapping = [];
/**
* runtimeId = array[internalId][metadata]
* @var int[][]
* @phpstan-var array<int, array<int, int>>
*/
private array $complexCoreToNetMapping = [];
/**
* [internalId, metadata] = array[runtimeId]
* @var int[][]
* @phpstan-var array<int, array{int, int}>
*/
private array $complexNetToCoreMapping = [];
private static function make() : self{
$data = Filesystem::fileGetContents(BedrockDataFiles::R16_TO_CURRENT_ITEM_MAP_JSON);
$json = json_decode($data, true);
if(!is_array($json) || !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){
throw new AssumptionFailedError("Invalid item table format");
}
$legacyStringToIntMap = LegacyItemIdToStringIdMap::getInstance();
/** @phpstan-var array<string, int> $simpleMappings */
$simpleMappings = [];
foreach($json["simple"] as $oldId => $newId){
if(!is_string($oldId) || !is_string($newId)){
throw new AssumptionFailedError("Invalid item table format");
}
$intId = $legacyStringToIntMap->stringToLegacy($oldId);
if($intId === null){
//new item without a fixed legacy ID - we can't handle this right now
continue;
}
$simpleMappings[$newId] = $intId;
}
foreach(Utils::stringifyKeys($legacyStringToIntMap->getStringToLegacyMap()) as $stringId => $intId){
if(isset($simpleMappings[$stringId])){
throw new \UnexpectedValueException("Old ID $stringId collides with new ID");
}
$simpleMappings[$stringId] = $intId;
}
/** @phpstan-var array<string, array{int, int}> $complexMappings */
$complexMappings = [];
foreach($json["complex"] as $oldId => $map){
if(!is_string($oldId) || !is_array($map)){
throw new AssumptionFailedError("Invalid item table format");
}
foreach($map as $meta => $newId){
if(!is_numeric($meta) || !is_string($newId)){
throw new AssumptionFailedError("Invalid item table format");
}
$intId = $legacyStringToIntMap->stringToLegacy($oldId);
if($intId === null){
//new item without a fixed legacy ID - we can't handle this right now
continue;
}
$complexMappings[$newId] = [$intId, (int) $meta];
}
}
return new self(GlobalItemTypeDictionary::getInstance()->getDictionary(), $simpleMappings, $complexMappings);
}
/**
* @param int[] $simpleMappings
* @param int[][] $complexMappings
* @phpstan-param array<string, int> $simpleMappings
* @phpstan-param array<string, array<int, int>> $complexMappings
*/
public function __construct(ItemTypeDictionary $dictionary, array $simpleMappings, array $complexMappings){
foreach($dictionary->getEntries() as $entry){
$stringId = $entry->getStringId();
$netId = $entry->getNumericId();
if(isset($complexMappings[$stringId])){
[$id, $meta] = $complexMappings[$stringId];
$this->complexCoreToNetMapping[$id][$meta] = $netId;
$this->complexNetToCoreMapping[$netId] = [$id, $meta];
}elseif(isset($simpleMappings[$stringId])){
$this->simpleCoreToNetMapping[$simpleMappings[$stringId]] = $netId;
$this->simpleNetToCoreMapping[$netId] = $simpleMappings[$stringId];
}else{
//not all items have a legacy mapping - for now, we only support the ones that do
continue;
}
}
}
public function __construct(
private ItemTypeDictionary $itemTypeDictionary,
private BlockStateDictionary $blockStateDictionary,
private ItemSerializer $itemSerializer,
private ItemDeserializer $itemDeserializer,
private BlockItemIdMap $blockItemIdMap
){}
/**
* @return int[]|null
* @phpstan-return array{int, int}|null
* @phpstan-return array{int, int, ?int}|null
*/
public function toNetworkIdQuiet(int $internalId, int $internalMeta) : ?array{
if($internalMeta === -1){
$internalMeta = 0x7fff;
public function toNetworkIdQuiet(Item $item) : ?array{
try{
return $this->toNetworkId($item);
}catch(ItemTypeSerializeException){
return null;
}
if(isset($this->complexCoreToNetMapping[$internalId][$internalMeta])){
return [$this->complexCoreToNetMapping[$internalId][$internalMeta], 0];
}
if(array_key_exists($internalId, $this->simpleCoreToNetMapping)){
return [$this->simpleCoreToNetMapping[$internalId], $internalMeta];
}
return null;
}
/**
* @return int[]
* @phpstan-return array{int, int}
* @phpstan-return array{int, int, ?int}
*
* @throws ItemTypeSerializeException
*/
public function toNetworkId(int $internalId, int $internalMeta) : array{
return $this->toNetworkIdQuiet($internalId, $internalMeta) ??
throw new \InvalidArgumentException("Unmapped ID/metadata combination $internalId:$internalMeta");
}
public function toNetworkId(Item $item) : array{
//TODO: we should probably come up with a cache for this
/**
* @phpstan-param-out bool $isComplexMapping
* @return int[]
* @phpstan-return array{int, int}
* @throws TypeConversionException
*/
public function fromNetworkId(int $networkId, int $networkMeta, ?bool &$isComplexMapping = null) : array{
if(isset($this->complexNetToCoreMapping[$networkId])){
if($networkMeta !== 0){
throw new TypeConversionException("Unexpected non-zero network meta on complex item mapping");
$itemData = $this->itemSerializer->serializeType($item);
$numericId = $this->itemTypeDictionary->fromStringId($itemData->getName());
$blockStateData = $itemData->getBlock();
if($blockStateData !== null){
$blockRuntimeId = $this->blockStateDictionary->lookupStateIdFromData($blockStateData);
if($blockRuntimeId === null){
throw new AssumptionFailedError("Unmapped blockstate returned by blockstate serializer: " . $blockStateData->toNbt());
}
$isComplexMapping = true;
return $this->complexNetToCoreMapping[$networkId];
}else{
$blockRuntimeId = null;
}
$isComplexMapping = false;
if(isset($this->simpleNetToCoreMapping[$networkId])){
return [$this->simpleNetToCoreMapping[$networkId], $networkMeta];
}
throw new TypeConversionException("Unmapped network ID/metadata combination $networkId:$networkMeta");
return [$numericId, $itemData->getMeta(), $blockRuntimeId];
}
/**
* @throws ItemTypeSerializeException
*/
public function toNetworkNbt(Item $item) : CompoundTag{
//TODO: this relies on the assumption that network item NBT is the same as disk item NBT, which may not always
//be true - if we stick on an older world version while updating network version, this could be a problem (and
//may be a problem for multi version implementations)
return $this->itemSerializer->serializeStack($item)->toNbt();
}
/**
* @return int[]
* @phpstan-return array{int, int}
* @throws TypeConversionException
*/
public function fromNetworkIdWithWildcardHandling(int $networkId, int $networkMeta) : array{
$isComplexMapping = false;
if($networkMeta !== 0x7fff){
return $this->fromNetworkId($networkId, $networkMeta);
public function fromNetworkId(int $networkId, int $networkMeta, int $networkBlockRuntimeId) : Item{
try{
$stringId = $this->itemTypeDictionary->fromIntId($networkId);
}catch(\InvalidArgumentException $e){
//TODO: a quiet version of fromIntId() would be better than catching InvalidArgumentException
throw TypeConversionException::wrap($e, "Invalid network itemstack ID $networkId");
}
$blockStateData = null;
if($this->blockItemIdMap->lookupBlockId($stringId) !== null){
$blockStateData = $this->blockStateDictionary->generateDataFromStateId($networkBlockRuntimeId);
if($blockStateData === null){
throw new TypeConversionException("Blockstate runtimeID $networkBlockRuntimeId does not correspond to any known blockstate");
}
}elseif($networkBlockRuntimeId !== self::NO_BLOCK_RUNTIME_ID){
throw new TypeConversionException("Item $stringId is not a blockitem, but runtime ID $networkBlockRuntimeId was provided");
}
try{
return $this->itemDeserializer->deserializeType(new SavedItemData($stringId, $networkMeta, $blockStateData));
}catch(ItemTypeDeserializeException $e){
throw TypeConversionException::wrap($e, "Invalid network itemstack data");
}
[$id, $meta] = $this->fromNetworkId($networkId, 0, $isComplexMapping);
return [$id, $isComplexMapping ? $meta : -1];
}
}

View File

@ -23,23 +23,18 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function json_decode;
final class GlobalItemTypeDictionary{
use SingletonTrait;
final class ItemTypeDictionaryFromDataHelper{
private static function make() : self{
$data = Filesystem::fileGetContents(BedrockDataFiles::REQUIRED_ITEM_LIST_JSON);
public static function loadFromString(string $data) : ItemTypeDictionary{
$table = json_decode($data, true);
if(!is_array($table)){
throw new AssumptionFailedError("Invalid item list format");
@ -52,12 +47,6 @@ final class GlobalItemTypeDictionary{
}
$params[] = new ItemTypeEntry($name, $entry["runtime_id"], $entry["component_based"]);
}
return new self(new ItemTypeDictionary($params));
return new ItemTypeDictionary($params);
}
public function __construct(
private ItemTypeDictionary $dictionary
){}
public function getDictionary() : ItemTypeDictionary{ return $this->dictionary; }
}

View File

@ -1,50 +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\convert;
use pocketmine\nbt\tag\CompoundTag;
final class R12ToCurrentBlockMapEntry{
public function __construct(
private string $id,
private int $meta,
private CompoundTag $blockState
){}
public function getId() : string{
return $this->id;
}
public function getMeta() : int{
return $this->meta;
}
public function getBlockState() : CompoundTag{
return $this->blockState;
}
public function __toString(){
return "id=$this->id, meta=$this->meta, nbt=$this->blockState";
}
}

View File

@ -1,139 +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\convert;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
/**
* @internal
*/
final class RuntimeBlockMapping{
use SingletonTrait;
/** @var int[] */
private array $legacyToRuntimeMap = [];
/** @var int[] */
private array $runtimeToLegacyMap = [];
/** @var CompoundTag[] */
private array $bedrockKnownStates;
private static function make() : self{
return new self(
BedrockDataFiles::CANONICAL_BLOCK_STATES_NBT,
BedrockDataFiles::R12_TO_CURRENT_BLOCK_MAP_BIN
);
}
public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){
$stream = new BinaryStream(Filesystem::fileGetContents($canonicalBlockStatesFile));
$list = [];
$nbtReader = new NetworkNbtSerializer();
while(!$stream->feof()){
$offset = $stream->getOffset();
$blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
$list[] = $blockState;
}
$this->bedrockKnownStates = $list;
$this->setupLegacyMappings($r12ToCurrentBlockMapFile);
}
private function setupLegacyMappings(string $r12ToCurrentBlockMapFile) : void{
$legacyIdMap = LegacyBlockIdToStringIdMap::getInstance();
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
$legacyStateMap = [];
$legacyStateMapReader = new BinaryStream(Filesystem::fileGetContents($r12ToCurrentBlockMapFile));
$nbtReader = new NetworkNbtSerializer();
while(!$legacyStateMapReader->feof()){
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt());
$meta = $legacyStateMapReader->getLShort();
$offset = $legacyStateMapReader->getOffset();
$state = $nbtReader->read($legacyStateMapReader->getBuffer(), $offset)->mustGetCompoundTag();
$legacyStateMapReader->setOffset($offset);
$legacyStateMap[] = new R12ToCurrentBlockMapEntry($id, $meta, $state);
}
/**
* @var int[][] $idToStatesMap string id -> int[] list of candidate state indices
*/
$idToStatesMap = [];
foreach($this->bedrockKnownStates as $k => $state){
$idToStatesMap[$state->getString("name")][] = $k;
}
foreach($legacyStateMap as $pair){
$id = $legacyIdMap->stringToLegacy($pair->getId());
if($id === null){
throw new \RuntimeException("No legacy ID matches " . $pair->getId());
}
$data = $pair->getMeta();
if($data > 15){
//we can't handle metadata with more than 4 bits
continue;
}
$mappedState = $pair->getBlockState();
$mappedName = $mappedState->getString("name");
if(!isset($idToStatesMap[$mappedName])){
throw new \RuntimeException("Mapped new state does not appear in network table");
}
foreach($idToStatesMap[$mappedName] as $k){
$networkState = $this->bedrockKnownStates[$k];
if($mappedState->equals($networkState)){
$this->registerMapping($k, $id, $data);
continue 2;
}
}
throw new \RuntimeException("Mapped new state does not appear in network table");
}
}
public function toRuntimeId(int $internalStateId) : int{
return $this->legacyToRuntimeMap[$internalStateId] ?? $this->legacyToRuntimeMap[BlockLegacyIds::INFO_UPDATE << Block::INTERNAL_METADATA_BITS];
}
public function fromRuntimeId(int $runtimeId) : int{
return $this->runtimeToLegacyMap[$runtimeId];
}
private function registerMapping(int $staticRuntimeId, int $legacyId, int $legacyMeta) : void{
$this->legacyToRuntimeMap[($legacyId << Block::INTERNAL_METADATA_BITS) | $legacyMeta] = $staticRuntimeId;
$this->runtimeToLegacyMap[$staticRuntimeId] = ($legacyId << Block::INTERNAL_METADATA_BITS) | $legacyMeta;
}
/**
* @return CompoundTag[]
*/
public function getBedrockKnownStates() : array{
return $this->bedrockKnownStates;
}
}

View File

@ -1,42 +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\convert;
/**
* Accessor for SkinAdapter
*/
class SkinAdapterSingleton{
private static ?SkinAdapter $skinAdapter = null;
public static function get() : SkinAdapter{
if(self::$skinAdapter === null){
self::$skinAdapter = new LegacySkinAdapter();
}
return self::$skinAdapter;
}
public static function set(SkinAdapter $adapter) : void{
self::$skinAdapter = $adapter;
}
}

View File

@ -23,37 +23,82 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\block\BlockLegacyIds;
use pocketmine\item\Durable;
use pocketmine\block\VanillaBlocks;
use pocketmine\crafting\ExactRecipeIngredient;
use pocketmine\crafting\MetaWildcardRecipeIngredient;
use pocketmine\crafting\RecipeIngredient;
use pocketmine\crafting\TagWildcardRecipeIngredient;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\item\BlockItemIdMap;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
use pocketmine\item\ItemIds;
use pocketmine\item\VanillaItems;
use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\types\GameMode as ProtocolGameMode;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient as ProtocolRecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
use pocketmine\network\mcpe\protocol\types\recipe\TagItemDescriptor;
use pocketmine\player\GameMode;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\world\format\io\GlobalBlockStateHandlers;
use pocketmine\world\format\io\GlobalItemDataHandlers;
use function get_class;
class TypeConverter{
use SingletonTrait;
private const DAMAGE_TAG = "Damage"; //TAG_Int
private const DAMAGE_TAG_CONFLICT_RESOLUTION = "___Damage_ProtocolCollisionResolution___";
private const PM_ID_TAG = "___Id___";
private const PM_META_TAG = "___Meta___";
private const RECIPE_INPUT_WILDCARD_META = 0x7fff;
private BlockItemIdMap $blockItemIdMap;
private BlockTranslator $blockTranslator;
private ItemTranslator $itemTranslator;
private ItemTypeDictionary $itemTypeDictionary;
private int $shieldRuntimeId;
private SkinAdapter $skinAdapter;
public function __construct(){
//TODO: inject stuff via constructor
$this->shieldRuntimeId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromStringId("minecraft:shield");
$this->blockItemIdMap = BlockItemIdMap::getInstance();
$canonicalBlockStatesRaw = Filesystem::fileGetContents(BedrockDataFiles::CANONICAL_BLOCK_STATES_NBT);
$metaMappingRaw = Filesystem::fileGetContents(BedrockDataFiles::BLOCK_STATE_META_MAP_JSON);
$this->blockTranslator = new BlockTranslator(
BlockStateDictionary::loadFromString($canonicalBlockStatesRaw, $metaMappingRaw),
GlobalBlockStateHandlers::getSerializer()
);
$this->itemTypeDictionary = ItemTypeDictionaryFromDataHelper::loadFromString(Filesystem::fileGetContents(BedrockDataFiles::REQUIRED_ITEM_LIST_JSON));
$this->shieldRuntimeId = $this->itemTypeDictionary->fromStringId("minecraft:shield");
$this->itemTranslator = new ItemTranslator(
$this->itemTypeDictionary,
$this->blockTranslator->getBlockStateDictionary(),
GlobalItemDataHandlers::getSerializer(),
GlobalItemDataHandlers::getDeserializer(),
$this->blockItemIdMap
);
$this->skinAdapter = new LegacySkinAdapter();
}
public function getBlockTranslator() : BlockTranslator{ return $this->blockTranslator; }
public function getItemTypeDictionary() : ItemTypeDictionary{ return $this->itemTypeDictionary; }
public function getItemTranslator() : ItemTranslator{ return $this->itemTranslator; }
public function getSkinAdapter() : SkinAdapter{ return $this->skinAdapter; }
public function setSkinAdapter(SkinAdapter $skinAdapter) : void{
$this->skinAdapter = $skinAdapter;
}
/**
@ -76,14 +121,6 @@ class TypeConverter{
}
}
public function protocolGameModeName(GameMode $gameMode) : string{
switch($gameMode->id()){
case GameMode::SURVIVAL()->id(): return "Survival";
case GameMode::ADVENTURE()->id(): return "Adventure";
default: return "Creative";
}
}
public function protocolGameModeToCore(int $gameMode) : ?GameMode{
switch($gameMode){
case ProtocolGameMode::SURVIVAL:
@ -100,97 +137,101 @@ class TypeConverter{
}
}
public function coreItemStackToRecipeIngredient(Item $itemStack) : RecipeIngredient{
if($itemStack->isNull()){
return new RecipeIngredient(null, 0);
public function coreRecipeIngredientToNet(?RecipeIngredient $ingredient) : ProtocolRecipeIngredient{
if($ingredient === null){
return new ProtocolRecipeIngredient(null, 0);
}
if($itemStack->hasAnyDamageValue()){
[$id, ] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), 0);
$meta = 0x7fff;
if($ingredient instanceof MetaWildcardRecipeIngredient){
$id = $this->itemTypeDictionary->fromStringId($ingredient->getItemId());
$meta = self::RECIPE_INPUT_WILDCARD_META;
$descriptor = new IntIdMetaItemDescriptor($id, $meta);
}elseif($ingredient instanceof ExactRecipeIngredient){
$item = $ingredient->getItem();
[$id, $meta, $blockRuntimeId] = $this->itemTranslator->toNetworkId($item);
if($blockRuntimeId !== null){
$meta = $this->blockTranslator->getBlockStateDictionary()->getMetaFromStateId($blockRuntimeId);
if($meta === null){
throw new AssumptionFailedError("Every block state should have an associated meta value");
}
}
$descriptor = new IntIdMetaItemDescriptor($id, $meta);
}elseif($ingredient instanceof TagWildcardRecipeIngredient){
$descriptor = new TagItemDescriptor($ingredient->getTagName());
}else{
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), $itemStack->getMeta());
throw new \LogicException("Unsupported recipe ingredient type " . get_class($ingredient) . ", only " . ExactRecipeIngredient::class . " and " . MetaWildcardRecipeIngredient::class . " are supported");
}
return new RecipeIngredient(new IntIdMetaItemDescriptor($id, $meta), $itemStack->getCount());
return new ProtocolRecipeIngredient($descriptor, 1);
}
public function recipeIngredientToCoreItemStack(RecipeIngredient $ingredient) : Item{
public function netRecipeIngredientToCore(ProtocolRecipeIngredient $ingredient) : ?RecipeIngredient{
$descriptor = $ingredient->getDescriptor();
if($descriptor === null){
return VanillaItems::AIR();
}
if($descriptor instanceof IntIdMetaItemDescriptor){
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($descriptor->getId(), $descriptor->getMeta());
return ItemFactory::getInstance()->get($id, $meta, $ingredient->getCount());
}
if($descriptor instanceof StringIdMetaItemDescriptor){
$intId = GlobalItemTypeDictionary::getInstance()->getDictionary()->fromStringId($descriptor->getId());
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($intId, $descriptor->getMeta());
return ItemFactory::getInstance()->get($id, $meta, $ingredient->getCount());
return null;
}
throw new \LogicException("Unsupported conversion of recipe ingredient to core item stack");
if($descriptor instanceof TagItemDescriptor){
return new TagWildcardRecipeIngredient($descriptor->getTag());
}
if($descriptor instanceof IntIdMetaItemDescriptor){
$stringId = $this->itemTypeDictionary->fromIntId($descriptor->getId());
$meta = $descriptor->getMeta();
}elseif($descriptor instanceof StringIdMetaItemDescriptor){
$stringId = $descriptor->getId();
$meta = $descriptor->getMeta();
}else{
throw new \LogicException("Unsupported conversion of recipe ingredient to core item stack");
}
if($meta === self::RECIPE_INPUT_WILDCARD_META){
return new MetaWildcardRecipeIngredient($stringId);
}
$blockRuntimeId = null;
if(($blockId = $this->blockItemIdMap->lookupBlockId($stringId)) !== null){
$blockRuntimeId = $this->blockTranslator->getBlockStateDictionary()->lookupStateIdFromIdMeta($blockId, $meta);
if($blockRuntimeId !== null){
$meta = 0;
}
}
$result = $this->itemTranslator->fromNetworkId(
$this->itemTypeDictionary->fromStringId($stringId),
$meta,
$blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID
);
return new ExactRecipeIngredient($result);
}
public function coreItemStackToNet(Item $itemStack) : ItemStack{
if($itemStack->isNull()){
return ItemStack::null();
}
$nbt = null;
if($itemStack->hasNamedTag()){
$nbt = clone $itemStack->getNamedTag();
$nbt = $itemStack->getNamedTag();
if($nbt->count() === 0){
$nbt = null;
}else{
$nbt = clone $nbt;
}
$isBlockItem = $itemStack->getId() < 256;
$idMeta = ItemTranslator::getInstance()->toNetworkIdQuiet($itemStack->getId(), $itemStack->getMeta());
$idMeta = $this->itemTranslator->toNetworkIdQuiet($itemStack);
if($idMeta === null){
//Display unmapped items as INFO_UPDATE, but stick something in their NBT to make sure they don't stack with
//other unmapped items.
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId(ItemIds::INFO_UPDATE, 0);
[$id, $meta, $blockRuntimeId] = $this->itemTranslator->toNetworkId(VanillaBlocks::INFO_UPDATE()->asItem());
if($nbt === null){
$nbt = new CompoundTag();
}
$nbt->setInt(self::PM_ID_TAG, $itemStack->getId());
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
$nbt->setLong(self::PM_ID_TAG, $itemStack->getStateId());
}else{
[$id, $meta] = $idMeta;
if($itemStack instanceof Durable && $itemStack->getDamage() > 0){
if($nbt !== null){
if(($existing = $nbt->getTag(self::DAMAGE_TAG)) !== null){
$nbt->removeTag(self::DAMAGE_TAG);
$nbt->setTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION, $existing);
}
}else{
$nbt = new CompoundTag();
}
$nbt->setInt(self::DAMAGE_TAG, $itemStack->getDamage());
$meta = 0;
}elseif($isBlockItem && $itemStack->getMeta() !== 0){
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
if($nbt === null){
$nbt = new CompoundTag();
}
$nbt->setInt(self::PM_META_TAG, $itemStack->getMeta());
$meta = 0;
}
}
$blockRuntimeId = 0;
if($isBlockItem){
$block = $itemStack->getBlock();
if($block->getId() !== BlockLegacyIds::AIR){
$blockRuntimeId = RuntimeBlockMapping::getInstance()->toRuntimeId($block->getFullId());
}
[$id, $meta, $blockRuntimeId] = $idMeta;
}
return new ItemStack(
$id,
$meta,
$itemStack->getCount(),
$blockRuntimeId,
$blockRuntimeId ?? ItemTranslator::NO_BLOCK_RUNTIME_ID,
$nbt,
[],
[],
@ -207,48 +248,21 @@ class TypeConverter{
}
$compound = $itemStack->getNbt();
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkId($itemStack->getId(), $itemStack->getMeta());
$itemResult = $this->itemTranslator->fromNetworkId($itemStack->getId(), $itemStack->getMeta(), $itemStack->getBlockRuntimeId());
if($compound !== null){
$compound = clone $compound;
if(($idTag = $compound->getTag(self::PM_ID_TAG)) instanceof IntTag){
$id = $idTag->getValue();
$compound->removeTag(self::PM_ID_TAG);
}
if(($damageTag = $compound->getTag(self::DAMAGE_TAG)) instanceof IntTag){
$meta = $damageTag->getValue();
$compound->removeTag(self::DAMAGE_TAG);
if(($conflicted = $compound->getTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION)) !== null){
$compound->removeTag(self::DAMAGE_TAG_CONFLICT_RESOLUTION);
$compound->setTag(self::DAMAGE_TAG, $conflicted);
}
}elseif(($metaTag = $compound->getTag(self::PM_META_TAG)) instanceof IntTag){
//TODO HACK: This foul-smelling code ensures that we can correctly deserialize an item when the
//client sends it back to us, because as of 1.16.220, blockitems quietly discard their metadata
//client-side. Aside from being very annoying, this also breaks various server-side behaviours.
$meta = $metaTag->getValue();
$compound->removeTag(self::PM_META_TAG);
}
if($compound->count() === 0){
$compound = null;
}
}
if($id < -0x8000 || $id >= 0x7fff){
throw new TypeConversionException("Item ID must be in range " . -0x8000 . " ... " . 0x7fff . " (received $id)");
}
if($meta < 0 || $meta >= 0x7fff){ //this meta value may have been restored from the NBT
throw new TypeConversionException("Item meta must be in range 0 ... " . 0x7fff . " (received $meta)");
}
try{
return ItemFactory::getInstance()->get(
$id,
$meta,
$itemStack->getCount(),
$compound
);
}catch(NbtException $e){
throw TypeConversionException::wrap($e, "Bad itemstack NBT data");
$itemResult->setCount($itemStack->getCount());
if($compound !== null){
try{
$itemResult->setNamedTag($compound);
}catch(NbtException $e){
throw TypeConversionException::wrap($e, "Bad itemstack NBT data");
}
}
return $itemResult;
}
}

View File

@ -34,8 +34,7 @@ use function substr;
class EncryptionContext{
private const CHECKSUM_ALGO = "sha256";
/** @var bool */
public static $ENABLED = true;
public static bool $ENABLED = true;
private string $key;

View File

@ -33,6 +33,7 @@ use function hex2bin;
use function openssl_digest;
use function openssl_error_string;
use function openssl_pkey_derive;
use function openssl_pkey_get_details;
use function str_pad;
use const STR_PAD_LEFT;
@ -42,7 +43,20 @@ final class EncryptionUtils{
//NOOP
}
private static function validateKey(\OpenSSLAsymmetricKey $key) : void{
$keyDetails = Utils::assumeNotFalse(openssl_pkey_get_details($key));
if(!isset($keyDetails["ec"]["curve_name"])){
throw new \InvalidArgumentException("Key must be an EC key");
}
$curveName = $keyDetails["ec"]["curve_name"];
if($curveName !== JwtUtils::BEDROCK_SIGNING_KEY_CURVE_NAME){
throw new \InvalidArgumentException("Key must belong to the " . JwtUtils::BEDROCK_SIGNING_KEY_CURVE_NAME . " elliptic curve, got $curveName");
}
}
public static function generateSharedSecret(\OpenSSLAsymmetricKey $localPriv, \OpenSSLAsymmetricKey $remotePub) : \GMP{
self::validateKey($localPriv);
self::validateKey($remotePub);
$hexSecret = openssl_pkey_derive($remotePub, $localPriv, 48);
if($hexSecret === false){
throw new \InvalidArgumentException("Failed to derive shared secret: " . openssl_error_string());

View File

@ -32,7 +32,6 @@ use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\RespawnPacket;
use pocketmine\network\mcpe\protocol\types\PlayerAction;
use pocketmine\player\Player;
use function array_map;
class DeathPacketHandler extends PacketHandler{
public function __construct(
@ -54,9 +53,7 @@ class DeathPacketHandler extends PacketHandler{
if($this->deathMessage instanceof Translatable){
$language = $this->player->getLanguage();
if(!$this->player->getServer()->isLanguageForced()){
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $this->deathMessage->getParameters());
$message = $language->translateString($this->deathMessage->getText(), $parameters, "pocketmine.");
[$message, $parameters] = $this->session->prepareClientTranslatableMessage($this->deathMessage);
}else{
$message = $language->translate($this->deathMessage);
}

View File

@ -45,8 +45,6 @@ use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
@ -113,9 +111,9 @@ use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\world\format\Chunk;
use function array_push;
use function base64_encode;
use function count;
use function fmod;
use function get_debug_type;
use function implode;
use function in_array;
use function is_bool;
@ -136,18 +134,15 @@ use const JSON_THROW_ON_ERROR;
class InGamePacketHandler extends PacketHandler{
private const MAX_FORM_RESPONSE_DEPTH = 2; //modal/simple will be 1, custom forms 2 - they will never contain anything other than string|int|float|bool|null
/** @var float */
protected $lastRightClickTime = 0.0;
/** @var UseItemTransactionData|null */
protected $lastRightClickData = null;
protected float $lastRightClickTime = 0.0;
protected ?UseItemTransactionData $lastRightClickData = null;
protected ?Vector3 $lastPlayerAuthInputPosition = null;
protected ?float $lastPlayerAuthInputYaw = null;
protected ?float $lastPlayerAuthInputPitch = null;
protected ?int $lastPlayerAuthInputFlags = null;
/** @var bool */
public $forceMoveSync = false;
public bool $forceMoveSync = false;
protected ?string $lastRequestedFullSkinId = null;
@ -241,6 +236,9 @@ class InGamePacketHandler extends PacketHandler{
if($packet->hasFlag(PlayerAuthInputFlags::START_JUMPING)){
$this->player->jump();
}
if($packet->hasFlag(PlayerAuthInputFlags::MISSED_SWING)){
$this->player->missSwing();
}
}
if(!$this->forceMoveSync && $hasMoved){
@ -328,6 +326,9 @@ class InGamePacketHandler extends PacketHandler{
if(count($packet->trData->getActions()) > 50){
throw new PacketHandlingException("Too many actions in inventory transaction");
}
if(count($packet->requestChangedSlots) > 10){
throw new PacketHandlingException("Too many slot sync requests in inventory transaction");
}
$this->inventoryManager->setCurrentItemStackRequestId($packet->requestId);
$this->inventoryManager->addRawPredictedSlotChanges($packet->trData->getActions());
@ -347,6 +348,21 @@ class InGamePacketHandler extends PacketHandler{
}
$this->inventoryManager->syncMismatchedPredictedSlotChanges();
//requestChangedSlots asks the server to always send out the contents of the specified slots, even if they
//haven't changed. Handling these is necessary to ensure the client inventory stays in sync if the server
//rejects the transaction. The most common example of this is equipping armor by right-click, which doesn't send
//a legacy prediction action for the destination armor slot.
foreach($packet->requestChangedSlots as $containerInfo){
foreach($containerInfo->getChangedSlotIndexes() as $netSlot){
[$windowId, $slot] = ItemStackContainerIdTranslator::translate($containerInfo->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $netSlot);
$inventoryAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slot);
if($inventoryAndSlot !== null){ //trigger the normal slot sync logic
$this->inventoryManager->onSlotChange($inventoryAndSlot[0], $inventoryAndSlot[1]);
}
}
}
$this->inventoryManager->setCurrentItemStackRequestId(null);
return $result;
}
@ -428,7 +444,7 @@ class InGamePacketHandler extends PacketHandler{
if($sourceSlotItem->getCount() < $droppedCount){
return false;
}
$serverItemStack = TypeConverter::getInstance()->coreItemStackToNet($sourceSlotItem);
$serverItemStack = $this->session->getTypeConverter()->coreItemStackToNet($sourceSlotItem);
//because the client doesn't tell us the expected itemstack ID, we have to deep-compare our known
//itemstack info with the one the client sent. This is costly, but we don't have any other option :(
if(!$serverItemStack->equals($clientItemStack)){
@ -730,27 +746,32 @@ class InGamePacketHandler extends PacketHandler{
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
if($block instanceof BaseSign){
if(($textBlobTag = $nbt->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
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);
}
return true;
$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");
}
$this->session->getLogger()->debug("Invalid sign update data: " . base64_encode($packet->nbt->getEncodedNbt()));
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);
}
return true;
}
return false;
@ -761,7 +782,7 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleSetPlayerGameType(SetPlayerGameTypePacket $packet) : bool{
$gameMode = TypeConverter::getInstance()->protocolGameModeToCore($packet->gamemode);
$gameMode = $this->session->getTypeConverter()->protocolGameModeToCore($packet->gamemode);
if($gameMode === null || !$gameMode->equals($this->player->getGamemode())){
//Set this back to default. TODO: handle this properly
$this->session->syncGameMode($this->player->getGamemode(), true);
@ -823,7 +844,7 @@ class InGamePacketHandler extends PacketHandler{
$this->session->getLogger()->debug("Processing skin change request");
try{
$skin = SkinAdapterSingleton::get()->fromSkinData($packet->skin);
$skin = $this->session->getTypeConverter()->getSkinAdapter()->fromSkinData($packet->skin);
}catch(InvalidSkinException $e){
throw PacketHandlingException::wrap($e, "Invalid skin in PlayerSkinPacket");
}

View File

@ -33,15 +33,21 @@ final class ItemStackContainerIdTranslator{
//NOOP
}
public static function translate(int $containerInterfaceId, int $currentWindowId) : int{
/**
* @return int[]
* @phpstan-return array{int, int}
* @throws PacketHandlingException
*/
public static function translate(int $containerInterfaceId, int $currentWindowId, int $slotId) : array{
return match($containerInterfaceId){
ContainerUIIds::ARMOR => ContainerIds::ARMOR,
ContainerUIIds::ARMOR => [ContainerIds::ARMOR, $slotId],
ContainerUIIds::HOTBAR,
ContainerUIIds::INVENTORY,
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => ContainerIds::INVENTORY,
ContainerUIIds::COMBINED_HOTBAR_AND_INVENTORY => [ContainerIds::INVENTORY, $slotId],
ContainerUIIds::OFFHAND => ContainerIds::OFFHAND,
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70 (though this doesn't really matter since the offhand has only 1 slot anyway)
ContainerUIIds::OFFHAND => [ContainerIds::OFFHAND, 0],
ContainerUIIds::ANVIL_INPUT,
ContainerUIIds::ANVIL_MATERIAL,
@ -64,11 +70,12 @@ final class ItemStackContainerIdTranslator{
ContainerUIIds::MATERIAL_REDUCER_OUTPUT,
ContainerUIIds::SMITHING_TABLE_INPUT,
ContainerUIIds::SMITHING_TABLE_MATERIAL,
ContainerUIIds::SMITHING_TABLE_TEMPLATE,
ContainerUIIds::STONECUTTER_INPUT,
ContainerUIIds::TRADE2_INGREDIENT1,
ContainerUIIds::TRADE2_INGREDIENT2,
ContainerUIIds::TRADE_INGREDIENT1,
ContainerUIIds::TRADE_INGREDIENT2 => ContainerIds::UI,
ContainerUIIds::TRADE_INGREDIENT2 => [ContainerIds::UI, $slotId],
ContainerUIIds::BARREL,
ContainerUIIds::BLAST_FURNACE_INGREDIENT,
@ -78,9 +85,10 @@ final class ItemStackContainerIdTranslator{
ContainerUIIds::FURNACE_FUEL,
ContainerUIIds::FURNACE_INGREDIENT,
ContainerUIIds::FURNACE_RESULT,
ContainerUIIds::HORSE_EQUIP,
ContainerUIIds::LEVEL_ENTITY, //chest
ContainerUIIds::SHULKER_BOX,
ContainerUIIds::SMOKER_INGREDIENT => $currentWindowId,
ContainerUIIds::SMOKER_INGREDIENT => [$currentWindowId, $slotId],
//all preview slots are ignored, since the client shouldn't be modifying those directly

View File

@ -23,8 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\crafting\CraftingGrid;
use pocketmine\inventory\CreativeInventory;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
@ -112,12 +110,7 @@ class ItemStackRequestExecutor{
* @throws ItemStackRequestProcessException
*/
protected function getBuilderInventoryAndSlot(ItemStackRequestSlotInfo $info) : array{
$windowId = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId());
$slotId = $info->getSlotId();
if($info->getContainerId() === ContainerUIIds::OFFHAND && $slotId === 1){
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70
$slotId = 0;
}
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($info->getContainerId(), $this->inventoryManager->getCurrentWindowId(), $info->getSlotId());
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
if($windowAndSlot === null){
throw new ItemStackRequestProcessException("No open inventory matches container UI ID: " . $info->getContainerId() . ", slot ID: " . $info->getSlotId());
@ -246,13 +239,11 @@ class ItemStackRequestExecutor{
$this->specialTransaction = new CraftingTransaction($this->player, $craftingManager, [], $recipe, $repetitions);
$currentWindow = $this->player->getCurrentWindow();
if($currentWindow !== null && !($currentWindow instanceof CraftingGrid)){
throw new ItemStackRequestProcessException("Player's current window is not a crafting grid");
}
$craftingGrid = $currentWindow ?? $this->player->getCraftingGrid();
$craftingResults = $recipe->getResultsFor($craftingGrid);
//TODO: Since the system assumes that crafting can only be done in the crafting grid, we have to give it a
//crafting grid to make the API happy. No implementation of getResultsFor() actually uses the crafting grid
//right now, so this will work, but this will become a problem in the future for things like shulker boxes and
//custom crafting recipes.
$craftingResults = $recipe->getResultsFor($this->player->getCraftingGrid());
foreach($craftingResults as $k => $craftingResult){
$craftingResult->setCount($craftingResult->getCount() * $repetitions);
$this->craftingResults[$k] = $craftingResult;
@ -335,7 +326,7 @@ class ItemStackRequestExecutor{
$this->builder->addAction(new DestroyItemAction($destroyed));
}elseif($action instanceof CreativeCreateStackRequestAction){
$item = CreativeInventory::getInstance()->getItem($action->getCreativeItemId());
$item = $this->player->getCreativeInventory()->getItem($action->getCreativeItemId());
if($item === null){
throw new ItemStackRequestProcessException("No such creative item index: " . $action->getCreativeItemId());
}

View File

@ -53,11 +53,7 @@ final class ItemStackResponseBuilder{
* @phpstan-return array{Inventory, int}
*/
private function getInventoryAndSlot(int $containerInterfaceId, int $slotId) : ?array{
if($containerInterfaceId === ContainerUIIds::OFFHAND && $slotId === 1){
//TODO: HACK! The client sends an incorrect slot ID for the offhand as of 1.19.70
$slotId = 0;
}
$windowId = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId());
[$windowId, $slotId] = ItemStackContainerIdTranslator::translate($containerInterfaceId, $this->inventoryManager->getCurrentWindowId(), $slotId);
$windowAndSlot = $this->inventoryManager->locateWindowAndSlot($windowId, $slotId);
if($windowAndSlot === null){
return null;

View File

@ -25,9 +25,9 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\auth\ProcessLoginTask;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
@ -50,7 +50,7 @@ use function is_array;
class LoginPacketHandler extends PacketHandler{
/**
* @phpstan-param \Closure(PlayerInfo) : void $playerInfoConsumer
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void $authCallback
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPubKey) : void $authCallback
*/
public function __construct(
private Server $server,
@ -63,7 +63,7 @@ class LoginPacketHandler extends PacketHandler{
$extraData = $this->fetchAuthData($packet->chainDataJwt);
if(!Player::isValidUserName($extraData->displayName)){
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDNAME);
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
return true;
}
@ -71,10 +71,10 @@ class LoginPacketHandler extends PacketHandler{
$clientData = $this->parseClientData($packet->clientDataJwt);
try{
$skin = SkinAdapterSingleton::get()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
$skin = $this->session->getTypeConverter()->getSkinAdapter()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
}catch(\InvalidArgumentException | InvalidSkinException $e){
$this->session->getLogger()->debug("Invalid skin: " . $e->getMessage());
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDSKIN);
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidSkin());
return true;
}
@ -110,18 +110,27 @@ class LoginPacketHandler extends PacketHandler{
$this->server->requiresAuthentication()
);
if($this->server->getNetwork()->getValidConnectionCount() > $this->server->getMaxPlayers()){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_FULL, KnownTranslationKeys::DISCONNECTIONSCREEN_SERVERFULL);
$ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_FULL, KnownTranslationFactory::disconnectionScreen_serverFull());
}
if(!$this->server->isWhitelisted($playerInfo->getUsername())){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_SERVER_WHITELISTED, "Server is whitelisted");
$ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_SERVER_WHITELISTED, KnownTranslationFactory::pocketmine_disconnect_whitelisted());
}
if($this->server->getNameBans()->isBanned($playerInfo->getUsername()) || $this->server->getIPBans()->isBanned($this->session->getIp())){
$ev->setKickReason(PlayerPreLoginEvent::KICK_REASON_BANNED, "You are banned");
$banMessage = null;
if(($banEntry = $this->server->getNameBans()->getEntry($playerInfo->getUsername())) !== null){
$banReason = $banEntry->getReason();
$banMessage = $banReason === "" ? KnownTranslationFactory::pocketmine_disconnect_ban_noReason() : KnownTranslationFactory::pocketmine_disconnect_ban($banReason);
}elseif(($banEntry = $this->server->getIPBans()->getEntry($this->session->getIp())) !== null){
$banReason = $banEntry->getReason();
$banMessage = KnownTranslationFactory::pocketmine_disconnect_ban($banReason !== "" ? $banReason : KnownTranslationFactory::pocketmine_disconnect_ban_ip());
}
if($banMessage !== null){
$ev->setKickFlag(PlayerPreLoginEvent::KICK_FLAG_BANNED, $banMessage);
}
$ev->call();
if(!$ev->isAllowed()){
$this->session->disconnect($ev->getFinalKickMessage());
$this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
return true;
}

View File

@ -26,8 +26,6 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\cache\CraftingDataCache;
use pocketmine\network\mcpe\cache\StaticPacketCache;
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
@ -39,6 +37,7 @@ use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\network\mcpe\protocol\types\DimensionIds;
use pocketmine\network\mcpe\protocol\types\Experiments;
use pocketmine\network\mcpe\protocol\types\LevelSettings;
use pocketmine\network\mcpe\protocol\types\NetworkPermissions;
use pocketmine\network\mcpe\protocol\types\PlayerMovementSettings;
use pocketmine\network\mcpe\protocol\types\PlayerMovementType;
use pocketmine\network\mcpe\protocol\types\SpawnSettings;
@ -66,11 +65,13 @@ class PreSpawnPacketHandler extends PacketHandler{
$location = $this->player->getLocation();
$world = $location->getWorld();
$typeConverter = $this->session->getTypeConverter();
$this->session->getLogger()->debug("Preparing StartGamePacket");
$levelSettings = new LevelSettings();
$levelSettings->seed = -1;
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
$levelSettings->worldGamemode = TypeConverter::getInstance()->coreGameModeToProtocol($this->server->getGamemode());
$levelSettings->worldGamemode = $typeConverter->coreGameModeToProtocol($this->server->getGamemode());
$levelSettings->difficulty = $world->getDifficulty();
$levelSettings->spawnPosition = BlockPosition::fromVector3($world->getSpawnLocation());
$levelSettings->hasAchievementsDisabled = true;
@ -87,7 +88,7 @@ class PreSpawnPacketHandler extends PacketHandler{
$this->session->sendDataPacket(StartGamePacket::create(
$this->player->getId(),
$this->player->getId(),
TypeConverter::getInstance()->coreGameModeToProtocol($this->player->getGamemode()),
$typeConverter->coreGameModeToProtocol($this->player->getGamemode()),
$this->player->getOffsetPosition($location),
$location->pitch,
$location->yaw,
@ -105,9 +106,11 @@ class PreSpawnPacketHandler extends PacketHandler{
sprintf("%s %s", VersionInfo::NAME, VersionInfo::VERSION()->getFullVersion(true)),
Uuid::fromString(Uuid::NIL),
false,
false,
new NetworkPermissions(disableClientSounds: true),
[],
0,
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
$typeConverter->getItemTypeDictionary()->getEntries(),
));
$this->session->getLogger()->debug("Sending actor identifiers");

View File

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\ResourcePackChunkDataPacket;
@ -86,14 +86,14 @@ class ResourcePacksPacketHandler extends PacketHandler{
private function disconnectWithError(string $error) : void{
$this->session->getLogger()->error("Error downloading resource packs: " . $error);
$this->session->disconnect(KnownTranslationKeys::DISCONNECTIONSCREEN_RESOURCEPACK);
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_resourcePack());
}
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
switch($packet->status){
case ResourcePackClientResponsePacket::STATUS_REFUSED:
//TODO: add lang strings for this
$this->session->disconnect("You must accept resource packs to join this server.", true);
$this->session->disconnect("Refused resource packs", "You must accept resource packs to join this server.", true);
break;
case ResourcePackClientResponsePacket::STATUS_SEND_PACKS:
foreach($packet->packIds as $uuid){

View File

@ -23,14 +23,11 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\NetworkSettingsPacket;
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\RequestNetworkSettingsPacket;
use pocketmine\network\mcpe\protocol\types\CompressionAlgorithm;
use pocketmine\Server;
final class SessionStartPacketHandler extends PacketHandler{
@ -38,7 +35,6 @@ final class SessionStartPacketHandler extends PacketHandler{
* @phpstan-param \Closure() : void $onSuccess
*/
public function __construct(
private Server $server,
private NetworkSession $session,
private \Closure $onSuccess
){}
@ -46,13 +42,7 @@ final class SessionStartPacketHandler extends PacketHandler{
public function handleRequestNetworkSettings(RequestNetworkSettingsPacket $packet) : bool{
$protocolVersion = $packet->getProtocolVersion();
if(!$this->isCompatibleProtocol($protocolVersion)){
$this->session->sendDataPacket(PlayStatusPacket::create($protocolVersion < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
$this->session->disconnect(
$this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $protocolVersion)),
false
);
$this->session->disconnectIncompatibleProtocol($protocolVersion);
return true;
}

View File

@ -23,10 +23,14 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\raklib;
use pmmp\thread\ThreadSafeArray;
use raklib\server\ipc\InterThreadChannelReader;
final class PthreadsChannelReader implements InterThreadChannelReader{
public function __construct(private \Threaded $buffer){}
/**
* @phpstan-param ThreadSafeArray<int, string> $buffer
*/
public function __construct(private ThreadSafeArray $buffer){}
public function read() : ?string{
return $this->buffer->shift();

View File

@ -23,10 +23,14 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\raklib;
use pmmp\thread\ThreadSafeArray;
use raklib\server\ipc\InterThreadChannelWriter;
final class PthreadsChannelWriter implements InterThreadChannelWriter{
public function __construct(private \Threaded $buffer){}
/**
* @phpstan-param ThreadSafeArray<int, string> $buffer
*/
public function __construct(private ThreadSafeArray $buffer){}
public function write(string $str) : void{
$this->buffer[] = $str;

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\raklib;
use pmmp\thread\ThreadSafeArray;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\mcpe\compression\ZlibCompressor;
use pocketmine\network\mcpe\convert\TypeConverter;
@ -35,10 +37,12 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\network\Network;
use pocketmine\network\NetworkInterfaceStartException;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\GameMode;
use pocketmine\Server;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\thread\ThreadCrashException;
use pocketmine\timings\Timings;
use pocketmine\utils\Utils;
use raklib\generic\DisconnectReason;
use raklib\generic\SocketException;
use raklib\protocol\EncapsulatedPacket;
use raklib\protocol\PacketReliability;
@ -48,10 +52,8 @@ use raklib\server\ServerEventListener;
use raklib\utils\InternetAddress;
use function addcslashes;
use function base64_encode;
use function bin2hex;
use function implode;
use function mt_rand;
use function random_bytes;
use function rtrim;
use function substr;
use const PHP_INT_MAX;
@ -77,24 +79,45 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
private RakLibToUserThreadMessageReceiver $eventReceiver;
private UserToRakLibThreadMessageSender $interface;
private SleeperNotifier $sleeper;
private int $sleeperNotifierId;
private PacketBroadcaster $packetBroadcaster;
private EntityEventBroadcaster $entityEventBroadcaster;
private PacketSerializerContext $packetSerializerContext;
private TypeConverter $typeConverter;
public function __construct(Server $server, string $ip, int $port, bool $ipV6, PacketBroadcaster $packetBroadcaster, EntityEventBroadcaster $entityEventBroadcaster, PacketSerializerContext $packetSerializerContext){
public function __construct(
Server $server,
string $ip,
int $port,
bool $ipV6,
PacketBroadcaster $packetBroadcaster,
EntityEventBroadcaster $entityEventBroadcaster,
PacketSerializerContext $packetSerializerContext,
TypeConverter $typeConverter
){
$this->server = $server;
$this->packetBroadcaster = $packetBroadcaster;
$this->packetSerializerContext = $packetSerializerContext;
$this->entityEventBroadcaster = $entityEventBroadcaster;
$this->typeConverter = $typeConverter;
$this->rakServerId = mt_rand(0, PHP_INT_MAX);
$this->sleeper = new SleeperNotifier();
$sleeperEntry = $this->server->getTickSleeper()->addNotifier(function() : void{
Timings::$connection->startTiming();
try{
while($this->eventReceiver->handle($this));
}finally{
Timings::$connection->stopTiming();
}
});
$this->sleeperNotifierId = $sleeperEntry->getNotifierId();
$mainToThreadBuffer = new \Threaded();
$threadToMainBuffer = new \Threaded();
/** @phpstan-var ThreadSafeArray<int, string> $mainToThreadBuffer */
$mainToThreadBuffer = new ThreadSafeArray();
/** @phpstan-var ThreadSafeArray<int, string> $threadToMainBuffer */
$threadToMainBuffer = new ThreadSafeArray();
$this->rakLib = new RakLibServer(
$this->server->getLogger(),
@ -104,7 +127,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$this->rakServerId,
$this->server->getConfigGroup()->getPropertyInt("network.max-mtu-size", 1492),
self::MCPE_RAKNET_PROTOCOL_VERSION,
$this->sleeper
$sleeperEntry
);
$this->eventReceiver = new RakLibToUserThreadMessageReceiver(
new PthreadsChannelReader($threadToMainBuffer)
@ -115,14 +138,6 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
}
public function start() : void{
$this->server->getTickSleeper()->addNotifier($this->sleeper, function() : void{
Timings::$connection->startTiming();
try{
while($this->eventReceiver->handle($this));
}finally{
Timings::$connection->stopTiming();
}
});
$this->server->getLogger()->debug("Waiting for RakLib to start...");
try{
$this->rakLib->startAndWait();
@ -140,17 +155,22 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
if(!$this->rakLib->isRunning()){
$e = $this->rakLib->getCrashInfo();
if($e !== null){
throw new \RuntimeException("RakLib crashed: " . $e->makePrettyMessage());
throw new ThreadCrashException("RakLib crashed", $e);
}
throw new \Exception("RakLib Thread crashed without crash information");
}
}
public function onClientDisconnect(int $sessionId, string $reason) : void{
public function onClientDisconnect(int $sessionId, int $reason) : void{
if(isset($this->sessions[$sessionId])){
$session = $this->sessions[$sessionId];
unset($this->sessions[$sessionId]);
$session->onClientDisconnect($reason);
$session->onClientDisconnect(match($reason){
DisconnectReason::CLIENT_DISCONNECT => KnownTranslationFactory::pocketmine_disconnect_clientDisconnect(),
DisconnectReason::PEER_TIMEOUT => KnownTranslationFactory::pocketmine_disconnect_error_timeout(),
DisconnectReason::CLIENT_RECONNECT => KnownTranslationFactory::pocketmine_disconnect_clientReconnect(),
default => "Unknown RakLib disconnect reason (ID $reason)"
});
}
}
@ -162,7 +182,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
}
public function shutdown() : void{
$this->server->getTickSleeper()->removeNotifier($this->sleeper);
$this->server->getTickSleeper()->removeNotifier($this->sleeperNotifierId);
$this->rakLib->quit();
}
@ -176,6 +196,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$this->packetBroadcaster,
$this->entityEventBroadcaster,
ZlibCompressor::getInstance(), //TODO: this shouldn't be hardcoded, but we might need the RakNet protocol version to select it
$this->typeConverter,
$address,
$port
);
@ -196,14 +217,12 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
try{
$session->handleEncoded($buf);
}catch(PacketHandlingException $e){
$errorId = bin2hex(random_bytes(6));
$logger = $session->getLogger();
$logger->error("Bad packet (error ID $errorId): " . $e->getMessage());
$logger->error("Bad packet: " . $e->getMessage());
//intentionally doesn't use logException, we don't want spammy packet error traces to appear in release mode
$logger->debug(implode("\n", Utils::printableExceptionInfo($e)));
$session->disconnect("Packet processing error (Error ID: $errorId)");
$session->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_badPacket());
$this->interface->blockAddress($address, 5);
}catch(\Throwable $e){
//record the name of the player who caused the crash, to make it easier to find the reproducing steps
@ -250,7 +269,11 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$info->getMaxPlayerCount(),
$this->rakServerId,
$this->server->getName(),
TypeConverter::getInstance()->protocolGameModeName($this->server->getGamemode())
match($this->server->getGamemode()){
GameMode::SURVIVAL() => "Survival",
GameMode::ADVENTURE() => "Adventure",
default => "Creative"
}
]) . ";"
);
}

View File

@ -23,161 +23,93 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\raklib;
use pocketmine\snooze\SleeperNotifier;
use pmmp\thread\Thread as NativeThread;
use pmmp\thread\ThreadSafeArray;
use pocketmine\snooze\SleeperHandlerEntry;
use pocketmine\thread\log\ThreadSafeLogger;
use pocketmine\thread\NonThreadSafeValue;
use pocketmine\thread\Thread;
use raklib\generic\Socket;
use pocketmine\thread\ThreadCrashException;
use raklib\generic\SocketException;
use raklib\server\ipc\RakLibToUserThreadMessageSender;
use raklib\server\ipc\UserToRakLibThreadMessageReceiver;
use raklib\server\Server;
use raklib\server\ServerSocket;
use raklib\server\SimpleProtocolAcceptor;
use raklib\utils\ExceptionTraceCleaner;
use raklib\utils\InternetAddress;
use function error_get_last;
use function gc_enable;
use function ini_set;
use function register_shutdown_function;
use const PTHREADS_INHERIT_NONE;
class RakLibServer extends Thread{
private InternetAddress $address;
/** @var \ThreadedLogger */
protected $logger;
/** @var bool */
protected $cleanShutdown = false;
/** @var bool */
protected $ready = false;
/** @var \Threaded */
protected $mainToThreadBuffer;
/** @var \Threaded */
protected $threadToMainBuffer;
/** @var string */
protected $mainPath;
/** @var int */
protected $serverId;
/** @var int */
protected $maxMtuSize;
private int $protocolVersion;
/** @var SleeperNotifier */
protected $mainThreadNotifier;
/** @var RakLibThreadCrashInfo|null */
public $crashInfo = null;
public function __construct(
\ThreadedLogger $logger,
\Threaded $mainToThreadBuffer,
\Threaded $threadToMainBuffer,
InternetAddress $address,
int $serverId,
int $maxMtuSize,
int $protocolVersion,
SleeperNotifier $sleeper
){
$this->address = $address;
$this->serverId = $serverId;
$this->maxMtuSize = $maxMtuSize;
$this->logger = $logger;
$this->mainToThreadBuffer = $mainToThreadBuffer;
$this->threadToMainBuffer = $threadToMainBuffer;
$this->mainPath = \pocketmine\PATH;
$this->protocolVersion = $protocolVersion;
$this->mainThreadNotifier = $sleeper;
}
protected bool $ready = false;
protected string $mainPath;
/** @phpstan-var NonThreadSafeValue<InternetAddress> */
protected NonThreadSafeValue $address;
/**
* @return void
* @phpstan-param ThreadSafeArray<int, string> $mainToThreadBuffer
* @phpstan-param ThreadSafeArray<int, string> $threadToMainBuffer
*/
public function shutdownHandler(){
if($this->cleanShutdown !== true && $this->crashInfo === null){
$error = error_get_last();
if($error !== null){
$this->logger->emergency("Fatal error: " . $error["message"] . " in " . $error["file"] . " on line " . $error["line"]);
$this->setCrashInfo(RakLibThreadCrashInfo::fromLastErrorInfo($error));
}else{
$this->logger->emergency("RakLib shutdown unexpectedly");
}
}
public function __construct(
protected ThreadSafeLogger $logger,
protected ThreadSafeArray $mainToThreadBuffer,
protected ThreadSafeArray $threadToMainBuffer,
InternetAddress $address,
protected int $serverId,
protected int $maxMtuSize,
protected int $protocolVersion,
protected SleeperHandlerEntry $sleeperEntry
){
$this->mainPath = \pocketmine\PATH;
$this->address = new NonThreadSafeValue($address);
}
public function getCrashInfo() : ?RakLibThreadCrashInfo{
return $this->crashInfo;
}
private function setCrashInfo(RakLibThreadCrashInfo $info) : void{
$this->synchronized(function(RakLibThreadCrashInfo $info) : void{
$this->crashInfo = $info;
$this->notify();
}, $info);
}
public function startAndWait(int $options = PTHREADS_INHERIT_NONE) : void{
public function startAndWait(int $options = NativeThread::INHERIT_NONE) : void{
$this->start($options);
$this->synchronized(function() : void{
while(!$this->ready && $this->crashInfo === null){
while(!$this->ready && $this->getCrashInfo() === null){
$this->wait();
}
$crashInfo = $this->crashInfo;
$crashInfo = $this->getCrashInfo();
if($crashInfo !== null){
if($crashInfo->getClass() === SocketException::class){
if($crashInfo->getType() === SocketException::class){
throw new SocketException($crashInfo->getMessage());
}
throw new \RuntimeException("RakLib failed to start: " . $crashInfo->makePrettyMessage());
throw new ThreadCrashException("RakLib failed to start", $crashInfo);
}
});
}
protected function onRun() : void{
try{
gc_enable();
ini_set("display_errors", '1');
ini_set("display_startup_errors", '1');
gc_enable();
ini_set("display_errors", '1');
ini_set("display_startup_errors", '1');
register_shutdown_function([$this, "shutdownHandler"]);
try{
$socket = new Socket($this->address);
}catch(SocketException $e){
$this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
return;
}
$manager = new Server(
$this->serverId,
$this->logger,
$socket,
$this->maxMtuSize,
new SimpleProtocolAcceptor($this->protocolVersion),
new UserToRakLibThreadMessageReceiver(new PthreadsChannelReader($this->mainToThreadBuffer)),
new RakLibToUserThreadMessageSender(new SnoozeAwarePthreadsChannelWriter($this->threadToMainBuffer, $this->mainThreadNotifier)),
new ExceptionTraceCleaner($this->mainPath)
);
$this->synchronized(function() : void{
$this->ready = true;
$this->notify();
});
while(!$this->isKilled){
$manager->tickProcessor();
}
$manager->waitShutdown();
$this->cleanShutdown = true;
}catch(\Throwable $e){
$this->setCrashInfo(RakLibThreadCrashInfo::fromThrowable($e));
$this->logger->logException($e);
$socket = new ServerSocket($this->address->deserialize());
$manager = new Server(
$this->serverId,
$this->logger,
$socket,
$this->maxMtuSize,
new SimpleProtocolAcceptor($this->protocolVersion),
new UserToRakLibThreadMessageReceiver(new PthreadsChannelReader($this->mainToThreadBuffer)),
new RakLibToUserThreadMessageSender(new SnoozeAwarePthreadsChannelWriter($this->threadToMainBuffer, $this->sleeperEntry->createNotifier())),
new ExceptionTraceCleaner($this->mainPath)
);
$this->synchronized(function() : void{
$this->ready = true;
$this->notify();
});
while(!$this->isKilled){
$manager->tickProcessor();
}
$manager->waitShutdown();
}
protected function onUncaughtException(\Throwable $e) : void{
parent::onUncaughtException($e);
$this->logger->logException($e);
}
public function getThreadName() : string{

View File

@ -1,61 +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\raklib;
use pocketmine\utils\Filesystem;
use function get_class;
use function sprintf;
final class RakLibThreadCrashInfo{
public function __construct(
private ?string $class,
private string $message,
private string $file,
private int $line
){}
public static function fromThrowable(\Throwable $e) : self{
return new self(get_class($e), $e->getMessage(), $e->getFile(), $e->getLine());
}
/**
* @phpstan-param array{message: string, file: string, line: int} $info
*/
public static function fromLastErrorInfo(array $info) : self{
return new self(null, $info["message"], $info["file"], $info["line"]);
}
public function getClass() : ?string{ return $this->class; }
public function getMessage() : string{ return $this->message; }
public function getFile() : string{ return $this->file; }
public function getLine() : int{ return $this->line; }
public function makePrettyMessage() : string{
return sprintf("%s: \"%s\" in %s on line %d", $this->class ?? "Fatal error", $this->message, Filesystem::cleanPath($this->file), $this->line);
}
}

View File

@ -23,12 +23,16 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\raklib;
use pmmp\thread\ThreadSafeArray;
use pocketmine\snooze\SleeperNotifier;
use raklib\server\ipc\InterThreadChannelWriter;
final class SnoozeAwarePthreadsChannelWriter implements InterThreadChannelWriter{
/**
* @phpstan-param ThreadSafeArray<int, string> $buffer
*/
public function __construct(
private \Threaded $buffer,
private ThreadSafeArray $buffer,
private SleeperNotifier $notifier
){}

View File

@ -27,7 +27,7 @@ use pocketmine\block\tile\Spawnable;
use pocketmine\data\bedrock\BiomeIds;
use pocketmine\data\bedrock\LegacyBiomeIdToStringIdMap;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\convert\RuntimeBlockMapping;
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\serializer\PacketSerializerContext;
@ -36,13 +36,9 @@ use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\PalettedBlockArray;
use pocketmine\world\format\SubChunk;
use function chr;
use function count;
use function str_repeat;
final class ChunkSerializer{
public const LOWER_PADDING_SIZE = 4;
private function __construct(){
//NOOP
}
@ -62,23 +58,20 @@ final class ChunkSerializer{
return 0;
}
public static function serializeFullChunk(Chunk $chunk, RuntimeBlockMapping $blockMapper, PacketSerializerContext $encoderContext, ?string $tiles = null) : string{
public static function serializeFullChunk(Chunk $chunk, BlockTranslator $blockTranslator, PacketSerializerContext $encoderContext, ?string $tiles = null) : string{
$stream = PacketSerializer::encoder($encoderContext);
//TODO: HACK! fill in fake subchunks to make up for the new negative space client-side
for($y = 0; $y < self::LOWER_PADDING_SIZE; $y++){
$stream->putByte(8); //subchunk version 8
$stream->putByte(0); //0 layers - client will treat this as all-air
}
$subChunkCount = self::getSubChunkCount($chunk);
for($y = Chunk::MIN_SUBCHUNK_INDEX, $writtenCount = 0; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){
self::serializeSubChunk($chunk->getSubChunk($y), $blockMapper, $stream, false);
$writtenCount = 0;
for($y = Chunk::MIN_SUBCHUNK_INDEX; $writtenCount < $subChunkCount; ++$y, ++$writtenCount){
self::serializeSubChunk($chunk->getSubChunk($y), $blockTranslator, $stream, false);
}
//TODO: right now we don't support 3D natively, so we just 3Dify our 2D biomes so they fill the column
$encodedBiomePalette = self::serializeBiomesAsPalette($chunk);
$stream->put(str_repeat($encodedBiomePalette, 24));
$biomeIdMap = LegacyBiomeIdToStringIdMap::getInstance();
//all biomes must always be written :(
for($y = Chunk::MIN_SUBCHUNK_INDEX; $y <= Chunk::MAX_SUBCHUNK_INDEX; ++$y){
self::serializeBiomePalette($chunk->getSubChunk($y)->getBiomeArray(), $biomeIdMap, $stream);
}
$stream->putByte(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.
@ -91,12 +84,14 @@ final class ChunkSerializer{
return $stream->getBuffer();
}
public static function serializeSubChunk(SubChunk $subChunk, RuntimeBlockMapping $blockMapper, PacketSerializer $stream, bool $persistentBlockStates) : void{
public static function serializeSubChunk(SubChunk $subChunk, BlockTranslator $blockTranslator, PacketSerializer $stream, bool $persistentBlockStates) : void{
$layers = $subChunk->getBlockLayers();
$stream->putByte(8); //version
$stream->putByte(count($layers));
$blockStateDictionary = $blockTranslator->getBlockStateDictionary();
foreach($layers as $blocks){
$bitsPerBlock = $blocks->getBitsPerBlock();
$words = $blocks->getWordArray();
@ -113,16 +108,44 @@ final class ChunkSerializer{
if($persistentBlockStates){
$nbtSerializer = new NetworkNbtSerializer();
foreach($palette as $p){
$stream->put($nbtSerializer->write(new TreeRoot($blockMapper->getBedrockKnownStates()[$blockMapper->toRuntimeId($p)])));
//TODO: introduce a binary cache for this
$state = $blockStateDictionary->generateDataFromStateId($blockTranslator->internalIdToNetworkId($p));
if($state === null){
$state = $blockTranslator->getFallbackStateData();
}
$stream->put($nbtSerializer->write(new TreeRoot($state->toNbt())));
}
}else{
foreach($palette as $p){
$stream->put(Binary::writeUnsignedVarInt($blockMapper->toRuntimeId($p) << 1));
$stream->put(Binary::writeUnsignedVarInt($blockTranslator->internalIdToNetworkId($p) << 1));
}
}
}
}
private static function serializeBiomePalette(PalettedBlockArray $biomePalette, LegacyBiomeIdToStringIdMap $biomeIdMap, PacketSerializer $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());
//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);
}
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));
}
}
public static function serializeTiles(Chunk $chunk) : string{
$stream = new BinaryStream();
foreach($chunk->getTiles() as $tile){
@ -133,39 +156,4 @@ final class ChunkSerializer{
return $stream->getBuffer();
}
private static function serializeBiomesAsPalette(Chunk $chunk) : string{
$biomeIdMap = LegacyBiomeIdToStringIdMap::getInstance();
$biomePalette = new PalettedBlockArray($chunk->getBiomeId(0, 0));
for($x = 0; $x < 16; ++$x){
for($z = 0; $z < 16; ++$z){
$biomeId = $chunk->getBiomeId($x, $z);
if($biomeIdMap->legacyToString($biomeId) === null){
//make sure we aren't sending bogus biomes - the 1.18.0 client crashes if we do this
$biomeId = BiomeIds::OCEAN;
}
for($y = 0; $y < 16; ++$y){
$biomePalette->set($x, $y, $z, $biomeId);
}
}
}
$biomePaletteBitsPerBlock = $biomePalette->getBitsPerBlock();
$encodedBiomePalette =
chr(($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
$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){
$encodedBiomePalette .= Binary::writeUnsignedVarInt(count($biomePaletteArray) << 1);
}
foreach($biomePaletteArray as $p){
$encodedBiomePalette .= Binary::writeUnsignedVarInt($p << 1);
}
return $encodedBiomePalette;
}
}

View File

@ -29,6 +29,7 @@ use pocketmine\plugin\Plugin;
use pocketmine\Server;
use pocketmine\utils\Binary;
use pocketmine\utils\Utils;
use function array_map;
use function chr;
use function count;
use function str_replace;
@ -41,7 +42,7 @@ final class QueryInfo{
private bool $listPlugins;
/** @var Plugin[] */
private array $plugins;
/** @var Player[] */
/** @var string[] */
private array $players;
private string $gametype;
@ -67,7 +68,7 @@ final class QueryInfo{
$this->serverName = $server->getMotd();
$this->listPlugins = $server->getConfigGroup()->getPropertyBool("settings.query-plugins", true);
$this->plugins = $server->getPluginManager()->getPlugins();
$this->players = $server->getOnlinePlayers();
$this->players = array_map(fn(Player $p) => $p->getName(), $server->getOnlinePlayers());
$this->gametype = ($server->getGamemode()->equals(GameMode::SURVIVAL()) || $server->getGamemode()->equals(GameMode::ADVENTURE())) ? "SMP" : "CMP";
$this->version = $server->getVersion();
@ -122,17 +123,17 @@ final class QueryInfo{
}
/**
* @return Player[]
* @return string[]
*/
public function getPlayerList() : array{
return $this->players;
}
/**
* @param Player[] $players
* @param string[] $players
*/
public function setPlayerList(array $players) : void{
Utils::validateArrayValueType($players, function(Player $_) : void{});
Utils::validateArrayValueType($players, function(string $_) : void{});
$this->players = $players;
$this->destroyCache();
}
@ -226,7 +227,7 @@ final class QueryInfo{
$query .= "\x00\x01player_\x00\x00";
foreach($this->players as $player){
$query .= $player->getName() . "\x00";
$query .= $player . "\x00";
}
$query .= "\x00";