Merge branch 'next-minor' into item-stack-request

This commit is contained in:
Dylan K. Taylor
2022-10-16 16:56:26 +01:00
136 changed files with 1764 additions and 799 deletions

View File

@@ -30,7 +30,7 @@ use pocketmine\scheduler\GarbageCollectionTask;
use pocketmine\timings\Timings;
use pocketmine\utils\Process;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function arsort;
use function count;
use function fclose;

View File

@@ -34,9 +34,10 @@ namespace pocketmine {
use pocketmine\utils\Timezone;
use pocketmine\utils\Utils;
use pocketmine\wizard\SetupWizard;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function defined;
use function extension_loaded;
use function function_exists;
use function getcwd;
use function phpversion;
use function preg_match;
@@ -160,7 +161,7 @@ namespace pocketmine {
if(PHP_DEBUG !== 0){
$logger->warning("This PHP binary was compiled in debug mode. This has a major impact on performance.");
}
if(extension_loaded("xdebug")){
if(extension_loaded("xdebug") && (!function_exists('xdebug_info') || count(xdebug_info('mode')) !== 0)){
$logger->warning("Xdebug extension is enabled. This has a major impact on performance.");
}
if(((int) ini_get('zend.assertions')) !== -1){
@@ -176,10 +177,10 @@ namespace pocketmine {
--------------------------------------- ! WARNING ! ---------------------------------------
You're using PHP 8.0 with JIT enabled. This provides significant performance improvements.
You're using PHP with JIT enabled. This provides significant performance improvements.
HOWEVER, it is EXPERIMENTAL, and has already been seen to cause weird and unexpected bugs.
Proceed with caution.
If you want to report any bugs, make sure to mention that you are using PHP 8.0 with JIT.
If you want to report any bugs, make sure to mention that you have enabled PHP JIT.
To turn off JIT, change `opcache.jit` to `0` in your php.ini file.
-------------------------------------------------------------------------------------------

View File

@@ -31,7 +31,7 @@ use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\command\SimpleCommandMap;
use pocketmine\console\ConsoleCommandSender;
use pocketmine\console\ConsoleReaderThread;
use pocketmine\console\ConsoleReaderChildProcessDaemon;
use pocketmine\crafting\CraftingManager;
use pocketmine\crafting\CraftingManagerFromDataHelper;
use pocketmine\crash\CrashDump;
@@ -88,12 +88,12 @@ use pocketmine\promise\PromiseResolver;
use pocketmine\resourcepacks\ResourcePackManager;
use pocketmine\scheduler\AsyncPool;
use pocketmine\snooze\SleeperHandler;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\stats\SendUsageTask;
use pocketmine\timings\Timings;
use pocketmine\timings\TimingsHandler;
use pocketmine\updater\UpdateChecker;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BroadcastLoggerForwarder;
use pocketmine\utils\Config;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Internet;
@@ -115,7 +115,7 @@ use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
use pocketmine\world\WorldManager;
use Ramsey\Uuid\UuidInterface;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_sum;
use function base64_encode;
use function cli_set_process_title;
@@ -132,6 +132,7 @@ use function get_class;
use function ini_set;
use function is_array;
use function is_dir;
use function is_int;
use function is_object;
use function is_resource;
use function is_string;
@@ -165,7 +166,6 @@ use function zlib_encode;
use const DIRECTORY_SEPARATOR;
use const PHP_EOL;
use const PHP_INT_MAX;
use const PTHREADS_INHERIT_NONE;
use const ZLIB_ENCODING_GZIP;
/**
@@ -225,7 +225,8 @@ class Server{
private MemoryManager $memoryManager;
private ConsoleReaderThread $console;
private ?ConsoleReaderChildProcessDaemon $console = null;
private ?ConsoleCommandSender $consoleSender = null;
private SimpleCommandMap $commandMap;
@@ -570,6 +571,7 @@ class Server{
$playerPos = null;
$spawn = $world->getSpawnLocation();
}
/** @phpstan-var PromiseResolver<Player> $playerPromiseResolver */
$playerPromiseResolver = new PromiseResolver();
$world->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
function() use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{
@@ -607,6 +609,10 @@ class Server{
}
/**
* @deprecated This method's results are unpredictable. The string "Steve" will return the player named "SteveJobs",
* until another player named "SteveJ" joins the server, at which point it will return that player instead. Prefer
* filtering the results of {@link Server::getOnlinePlayers()} yourself.
*
* Returns an online player whose name begins with or equals the given string (case insensitive).
* The closest match will be returned, or null if there are no online matches.
*
@@ -1043,22 +1049,14 @@ class Server{
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET)));
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3)))));
//TODO: move console parts to a separate component
$consoleSender = new ConsoleCommandSender($this, $this->language);
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $consoleSender);
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $consoleSender);
$forwarder = new BroadcastLoggerForwarder($this, $this->logger, $this->language);
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $forwarder);
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $forwarder);
$consoleNotifier = new SleeperNotifier();
$commandBuffer = new \Threaded();
$this->console = new ConsoleReaderThread($commandBuffer, $consoleNotifier);
$this->tickSleeper->addNotifier($consoleNotifier, function() use ($commandBuffer, $consoleSender) : void{
Timings::$serverCommand->startTiming();
while(($line = $commandBuffer->shift()) !== null){
$this->dispatchCommand($consoleSender, (string) $line);
}
Timings::$serverCommand->stopTiming();
});
$this->console->start(PTHREADS_INHERIT_NONE);
//TODO: move console parts to a separate component
if($this->configGroup->getPropertyBool("console.enable-input", true)){
$this->console = new ConsoleReaderChildProcessDaemon($this->logger);
}
$this->tickProcessor();
$this->forceShutdown();
@@ -1511,7 +1509,7 @@ class Server{
$this->configGroup->save();
}
if(isset($this->console)){
if($this->console !== null){
$this->getLogger()->debug("Closing console");
$this->console->quit();
}
@@ -1650,12 +1648,14 @@ class Server{
], 10, [], $postUrlError);
if($reply !== null && is_object($data = json_decode($reply->getBody()))){
if(isset($data->crashId) && isset($data->crashUrl)){
if(isset($data->crashId) && is_int($data->crashId) && isset($data->crashUrl) && is_string($data->crashUrl)){
$reportId = $data->crashId;
$reportUrl = $data->crashUrl;
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId)));
}elseif(isset($data->error)){
}elseif(isset($data->error) && is_string($data->error)){
$this->logger->emergency("Automatic crash report submission failed: $data->error");
}else{
$this->logger->emergency("Invalid JSON response received from crash archive: " . $reply->getBody());
}
}else{
$this->logger->emergency("Failed to communicate with crash archive: $postUrlError");
@@ -1716,7 +1716,7 @@ class Server{
$session = $player->getNetworkSession();
$position = $player->getPosition();
$this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
TextFormat::AQUA . $player->getName() . TextFormat::WHITE,
TextFormat::AQUA . $player->getName() . TextFormat::RESET,
$session->getIp(),
(string) $session->getPort(),
(string) $player->getId(),
@@ -1853,6 +1853,15 @@ class Server{
$this->getMemoryManager()->check();
if($this->console !== null){
Timings::$serverCommand->startTiming();
while(($line = $this->console->readLine()) !== null){
$this->consoleSender ??= new ConsoleCommandSender($this, $this->language);
$this->dispatchCommand($this->consoleSender, $line);
}
Timings::$serverCommand->stopTiming();
}
Timings::$serverTick->stopTiming();
$now = microtime(true);

View File

@@ -31,7 +31,7 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "4.7.3";
public const BASE_VERSION = "4.9.2";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "beta";

View File

@@ -78,25 +78,47 @@ class Block{
$this->position = clone $this->position;
}
/**
* Returns an object containing information about how to identify and store this block type, such as its legacy
* numeric ID(s), tile type (if any), and legacy variant metadata.
*/
public function getIdInfo() : BlockIdentifier{
return $this->idInfo;
}
/**
* Returns the printable English name of the block.
*/
public function getName() : string{
return $this->fallbackName;
}
/**
* @deprecated
*
* Returns the legacy numeric Minecraft block ID.
*/
public function getId() : int{
return $this->idInfo->getBlockId();
}
/**
* @internal
*
* Returns the full blockstate ID of this block. This is a compact way of representing a blockstate used to store
* blocks in chunks at runtime.
*
* This ID can be used to later obtain a copy of this block using {@link BlockFactory::get()}.
*/
public function getFullId() : int{
return ($this->getId() << self::INTERNAL_METADATA_BITS) | $this->getMeta();
}
/**
* Returns the block as an item.
* State information such as facing, powered/unpowered, open/closed, etc., is discarded.
* Type information such as colour, wood type, etc. is preserved.
*/
public function asItem() : Item{
return ItemFactory::getInstance()->get(
$this->idInfo->getItemId(),
@@ -104,6 +126,12 @@ class Block{
);
}
/**
* @deprecated
*
* Returns the legacy Minecraft block meta value. This is a mixed-purpose value, which is used to store different
* things for different blocks.
*/
public function getMeta() : int{
$stateMeta = $this->writeStateToMeta();
assert(($stateMeta & ~$this->getStateBitmask()) === 0);
@@ -116,6 +144,7 @@ class Block{
/**
* Returns a bitmask used to extract state bits from block metadata.
* This is used to remove unwanted information from the legacy meta value when getting the block as an item.
*/
public function getStateBitmask() : int{
return 0;
@@ -143,6 +172,12 @@ class Block{
$this->collisionBoxes = null;
}
/**
* Writes information about the block into the world. This writes the blockstate ID into the chunk, and creates
* and/or removes tiles as necessary.
*
* Note: Do not call this directly. Pass the block to {@link World::setBlock()} instead.
*/
public function writeStateToWorld() : void{
$world = $this->position->getWorld();
$world->getOrLoadChunkAtPosition($this->position)->setFullBlock($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getFullId());
@@ -168,7 +203,7 @@ class Block{
}
/**
* Returns a type ID that identifies this type of block. This does not include information like facing, colour,
* Returns a type ID that identifies this type of block. This does not include information like facing, open/closed,
* powered/unpowered, etc.
*/
public function getTypeId() : int{
@@ -199,22 +234,36 @@ class Block{
return true;
}
/**
* Returns whether this block can be replaced by another block placed in the same position.
*/
public function canBeReplaced() : bool{
return false;
}
/**
* Returns whether this block can replace the given block in the given placement conditions.
* This is used to allow slabs of the same type to combine into double slabs.
*/
public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{
return $blockReplace->canBeReplaced();
}
/**
* Places the Block, using block space and block target, and side. Returns if the block has been placed.
* Generates a block transaction to set all blocks affected by placing this block. Usually this is just the block
* itself, but may be multiple blocks in some cases (such as doors).
*
* @return bool whether the placement should go ahead
*/
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$tx->addBlock($blockReplace->position, $this);
return true;
}
/**
* Called immediately after the block has been placed in the world. Since placement uses a block transaction, some
* things may not be possible until after the transaction has been executed.
*/
public function onPostPlace() : void{
}
@@ -254,7 +303,7 @@ class Block{
/**
* Called when this block is randomly updated due to chunk ticking.
* WARNING: This will not be called if ticksRandomly() does not return true!
* WARNING: This will not be called if {@link Block::ticksRandomly()} does not return true!
*/
public function onRandomTick() : void{
@@ -275,8 +324,7 @@ class Block{
}
/**
* Called when this block is attacked (left-clicked). This is called when a player left-clicks the block to try and
* start to break it in survival mode.
* Called when this block is attacked (left-clicked) by a player attempting to start breaking it in survival.
*
* @return bool if an action took place, prevents starting to break the block if true.
*/
@@ -284,11 +332,19 @@ class Block{
return false;
}
/**
* Returns a multiplier applied to the velocity of entities moving on top of this block. A higher value will make
* the block more slippery (like ice).
*
* @return float 0.0-1.0
*/
public function getFrictionFactor() : float{
return 0.6;
}
/**
* Returns the amount of light emitted by this block.
*
* @return int 0-15
*/
public function getLightLevel() : int{
@@ -331,10 +387,6 @@ class Block{
return false;
}
public function hasEntityCollision() : bool{
return false;
}
/**
* Returns whether entities can climb up this block.
*/
@@ -342,10 +394,6 @@ class Block{
return false;
}
public function addVelocityToEntity(Entity $entity) : ?Vector3{
return null;
}
final public function getPosition() : Position{
return $this->position;
}
@@ -428,6 +476,7 @@ class Block{
/**
* Returns the item that players will equip when middle-clicking on this block.
* If addUserData is true, additional data may be added, such as banner patterns, chest contents, etc.
*/
public function getPickedItem(bool $addUserData = false) : Item{
$item = $this->asItem();
@@ -551,7 +600,7 @@ class Block{
}
/**
* Checks for collision against an AxisAlignedBB
* Returns whether any of the block's collision boxes intersect with the given AxisAlignedBB.
*/
public function collidesWithBB(AxisAlignedBB $bb) : bool{
foreach($this->getCollisionBoxes() as $bb2){
@@ -563,10 +612,21 @@ class Block{
return false;
}
/**
* Returns whether the block has actions to be executed when an entity enters its cell (full cube space).
*
* @see Block::onEntityInside()
*/
public function hasEntityCollision() : bool{
return false;
}
/**
* Called when an entity's bounding box clips inside this block's cell. Note that the entity may not be intersecting
* with the collision box or bounding box.
*
* WARNING: This will not be called if {@link Block::hasEntityCollision()} returns false.
*
* @return bool Whether the block is still the same after the intersection. If it changed (e.g. due to an explosive
* being ignited), this should return false.
*/
@@ -574,6 +634,19 @@ class Block{
return true;
}
/**
* Returns a direction vector describing which way an entity intersecting this block should be pushed.
* This is used by liquids to push entities in liquid currents.
*
* The returned vector is summed with vectors from every other block the entity is intersecting, and normalized to
* produce a final direction vector.
*
* WARNING: This will not be called if {@link Block::hasEntityCollision()} does not return true!
*/
public function addVelocityToEntity(Entity $entity) : ?Vector3{
return null;
}
/**
* Called when an entity lands on this block (usually due to falling).
* @return float|null The new vertical velocity of the entity, or null if unchanged.
@@ -583,6 +656,13 @@ class Block{
}
/**
* Returns an array of collision bounding boxes for this block.
* These are used for:
* - entity movement collision checks (to ensure entities can't clip through blocks)
* - projectile flight paths
* - block placement (to ensure the player can't place blocks inside itself or another entity)
* - anti-cheat checks in plugins
*
* @return AxisAlignedBB[]
*/
final public function getCollisionBoxes() : array{
@@ -613,6 +693,10 @@ class Block{
return [AxisAlignedBB::one()];
}
/**
* Returns the type of support that the block can provide on the given face. This is used to determine whether
* blocks placed on the given face can be supported by this block.
*/
public function getSupportType(int $facing) : SupportType{
return SupportType::FULL();
}
@@ -623,6 +707,10 @@ class Block{
return count($bb) === 1 && $bb[0]->getAverageEdgeLength() >= 1 && $bb[0]->isCube();
}
/**
* Performs a ray trace along the line between the two positions using the block's collision boxes.
* Returns the intersection point closest to pos1, or null if no intersection occurred.
*/
public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{
$bbs = $this->getCollisionBoxes();
if(count($bbs) === 0){

View File

@@ -523,12 +523,6 @@ class BlockFactory{
$this->registerAllMeta(...$leaves);
$this->registerAllMeta(...$allSidedLogs);
static $sandstoneTypes = [
Meta::SANDSTONE_NORMAL => "",
Meta::SANDSTONE_CHISELED => "Chiseled ",
Meta::SANDSTONE_CUT => "Cut ",
Meta::SANDSTONE_SMOOTH => "Smooth "
];
$sandstoneBreakInfo = new BreakInfo(0.8, ToolType::PICKAXE, ToolTier::WOOD()->getHarvestLevel());
$this->registerAllMeta(new Stair(new BID(Ids::RED_SANDSTONE_STAIRS, 0), "Red Sandstone Stairs", $sandstoneBreakInfo));
$this->registerAllMeta(new Stair(new BID(Ids::SMOOTH_RED_SANDSTONE_STAIRS, 0), "Smooth Red Sandstone Stairs", $sandstoneBreakInfo));
@@ -536,7 +530,12 @@ class BlockFactory{
$this->registerAllMeta(new Stair(new BID(Ids::SMOOTH_SANDSTONE_STAIRS, 0), "Smooth Sandstone Stairs", $sandstoneBreakInfo));
$sandstones = [];
$redSandstones = [];
foreach($sandstoneTypes as $variant => $prefix){
foreach([
Meta::SANDSTONE_NORMAL => "",
Meta::SANDSTONE_CHISELED => "Chiseled ",
Meta::SANDSTONE_CUT => "Cut ",
Meta::SANDSTONE_SMOOTH => "Smooth "
] as $variant => $prefix){
$sandstones[] = new Opaque(new BID(Ids::SANDSTONE, $variant), $prefix . "Sandstone", $sandstoneBreakInfo);
$redSandstones[] = new Opaque(new BID(Ids::RED_SANDSTONE, $variant), $prefix . "Red Sandstone", $sandstoneBreakInfo);
}

View File

@@ -72,7 +72,7 @@ class Cactus extends Transparent{
* @return AxisAlignedBB[]
*/
protected function recalculateCollisionBoxes() : array{
static $shrinkSize = 1 / 16;
$shrinkSize = 1 / 16;
return [AxisAlignedBB::one()->contract($shrinkSize, 0, $shrinkSize)->trim(Facing::UP, $shrinkSize)];
}

View File

@@ -34,7 +34,7 @@ use function mt_rand;
class DeadBush extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$this->getSide(Facing::DOWN)->isTransparent()){
if($this->canBeSupportedBy($this->getSide(Facing::DOWN))){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
@@ -42,7 +42,7 @@ class DeadBush extends Flowable{
}
public function onNearbyBlockChange() : void{
if($this->getSide(Facing::DOWN)->isTransparent()){
if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){
$this->position->getWorld()->useBreakOn($this->position);
}
}
@@ -64,4 +64,14 @@ class DeadBush extends Flowable{
public function getFlammability() : int{
return 100;
}
private function canBeSupportedBy(Block $block) : bool{
$blockId = $block->getId();
return $blockId === BlockLegacyIds::SAND
|| $blockId === BlockLegacyIds::PODZOL
|| $blockId === BlockLegacyIds::MYCELIUM
|| $blockId === BlockLegacyIds::DIRT
|| $blockId === BlockLegacyIds::HARDENED_CLAY
|| $blockId === BlockLegacyIds::STAINED_HARDENED_CLAY;
}
}

View File

@@ -26,6 +26,10 @@ namespace pocketmine\block;
use pocketmine\block\utils\SupportType;
use pocketmine\math\AxisAlignedBB;
/**
* "Flowable" blocks are destroyed if water flows into the same space as the block. These blocks usually don't have any
* collision boxes, and can't provide support for other blocks.
*/
abstract class Flowable extends Transparent{
public function canBeFlowedInto() : bool{

View File

@@ -69,7 +69,6 @@ class Lever extends Flowable{
5 => LeverFacing::UP_AXIS_Z(),
6 => LeverFacing::UP_AXIS_X(),
7 => LeverFacing::DOWN_AXIS_Z(),
default => throw new AssumptionFailedError("0x07 mask should make this impossible"), //phpstan doesn't understand :(
};
$this->activated = ($stateMeta & BlockLegacyMetadata::LEVER_FLAG_POWERED) !== 0;

View File

@@ -27,7 +27,7 @@ use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use function mt_rand;
class Melon extends Transparent{
class Melon extends Opaque{
public function getDropsForCompatibleTool(Item $item) : array{
return [

View File

@@ -23,6 +23,10 @@ declare(strict_types=1);
namespace pocketmine\block;
/**
* Opaque blocks do not allow light to pass through. They are usually collidable full-cube blocks.
* Most blocks in Minecraft fall into this category.
*/
class Opaque extends Block{
public function isSolid() : bool{

View File

@@ -43,7 +43,11 @@ class RedMushroom extends Flowable{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$down = $this->getSide(Facing::DOWN);
if(!$down->isTransparent()){
$position = $this->getPosition();
$lightLevel = $position->getWorld()->getFullLightAt($position->x, $position->y, $position->z);
$downId = $down->getId();
//TODO: nylium support
if(($lightLevel <= 12 && !$down->isTransparent()) || $downId === BlockLegacyIds::MYCELIUM || $downId === BlockLegacyIds::PODZOL){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}

View File

@@ -31,6 +31,7 @@ use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\Position;
class Sugarcane extends Flowable{
public const MAX_AGE = 15;
@@ -49,14 +50,23 @@ class Sugarcane extends Flowable{
return 0b1111;
}
private function grow() : bool{
$grew = false;
private function seekToBottom() : Position{
$world = $this->position->getWorld();
$bottom = $this->position;
while(($next = $world->getBlock($bottom->down()))->isSameType($this)){
$bottom = $next->position;
}
return $bottom;
}
private function grow(Position $pos) : bool{
$grew = false;
$world = $pos->getWorld();
for($y = 1; $y < 3; ++$y){
if(!$world->isInWorld($this->position->x, $this->position->y + $y, $this->position->z)){
if(!$world->isInWorld($pos->x, $pos->y + $y, $pos->z)){
break;
}
$b = $world->getBlockAt($this->position->x, $this->position->y + $y, $this->position->z);
$b = $world->getBlockAt($pos->x, $pos->y + $y, $pos->z);
if($b->getId() === BlockLegacyIds::AIR){
$ev = new BlockGrowEvent($b, VanillaBlocks::SUGARCANE());
$ev->call();
@@ -65,12 +75,12 @@ class Sugarcane extends Flowable{
}
$world->setBlock($b->position, $ev->getNewState());
$grew = true;
}else{
}elseif(!$b->isSameType($this)){
break;
}
}
$this->age = 0;
$world->setBlock($this->position, $this);
$world->setBlock($pos, $this);
return $grew;
}
@@ -87,7 +97,7 @@ class Sugarcane extends Flowable{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($item instanceof Fertilizer){
if(!$this->getSide(Facing::DOWN)->isSameType($this) && $this->grow()){
if($this->grow($this->seekToBottom())){
$item->pop();
}
@@ -111,7 +121,7 @@ class Sugarcane extends Flowable{
public function onRandomTick() : void{
if(!$this->getSide(Facing::DOWN)->isSameType($this)){
if($this->age === self::MAX_AGE){
$this->grow();
$this->grow($this->position);
}else{
++$this->age;
$this->position->getWorld()->setBlock($this->position, $this);

View File

@@ -29,6 +29,9 @@ use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing;
use function count;
/**
* Thin blocks behave like glass panes. They connect to full-cube blocks horizontally adjacent to them if possible.
*/
class Thin extends Transparent{
/** @var bool[] facing => dummy */
protected array $connections = [];

View File

@@ -23,6 +23,12 @@ declare(strict_types=1);
namespace pocketmine\block;
/**
* Transparent blocks do not block any light from propagating through them.
*
* Note: This does **not** imply that the block is **visually** transparent. For example, chests allow light to pass
* through, but the player cannot see through them except at the edges.
*/
class Transparent extends Block{
public function isTransparent() : bool{

View File

@@ -25,6 +25,9 @@ namespace pocketmine\block;
use pocketmine\item\Item;
/**
* Represents a block which is unrecognized or not implemented.
*/
class UnknownBlock extends Transparent{
public function __construct(BlockIdentifier $idInfo, BlockBreakInfo $breakInfo){

View File

@@ -585,6 +585,7 @@ final class VanillaBlocks{
/**
* @return Block[]
* @phpstan-return array<string, Block>
*/
public static function getAll() : array{
//phpstan doesn't support generic traits yet :(

View File

@@ -116,7 +116,7 @@ class Vine extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$blockClicked->isSolid() || Facing::axis($face) === Axis::Y){
if(!$blockClicked->isFullCube() || Facing::axis($face) === Axis::Y){
return false;
}

View File

@@ -27,6 +27,7 @@ use pocketmine\block\utils\SignText;
use pocketmine\math\Vector3;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\utils\Binary;
use pocketmine\world\World;
use function array_pad;
use function array_slice;
@@ -42,6 +43,13 @@ use function sprintf;
class Sign extends Spawnable{
public const TAG_TEXT_BLOB = "Text";
public const TAG_TEXT_LINE = "Text%d"; //sprintf()able
public const TAG_TEXT_COLOR = "SignTextColor";
public const TAG_GLOWING_TEXT = "IgnoreLighting";
/**
* This tag is set to indicate that MCPE-117835 has been addressed in whatever version this sign was created.
* @see https://bugs.mojang.com/browse/MCPE-117835
*/
public const TAG_LEGACY_BUG_RESOLVE = "TextIgnoreLegacyBugResolved";
/**
* @return string[]
@@ -111,5 +119,11 @@ class Sign extends Spawnable{
protected function addAdditionalSpawnData(CompoundTag $nbt) : void{
$nbt->setString(self::TAG_TEXT_BLOB, implode("\n", $this->text->getLines()));
//the following are not yet used by the server, but needed to roll back any changes to glowing state or colour
//if the client uses dye on the sign
$nbt->setInt(self::TAG_TEXT_COLOR, Binary::signInt(0xff_00_00_00));
$nbt->setByte(self::TAG_GLOWING_TEXT, 0);
$nbt->setByte(self::TAG_LEGACY_BUG_RESOLVE, 1);
}
}

View File

@@ -39,7 +39,7 @@ class SignText{
private array $lines;
/**
* @param string[]|null $lines index-sensitive; omitting an index will leave it unchanged
* @param string[]|null $lines index-sensitive; keys 0-3 will be used, regardless of array order
*
* @throws \InvalidArgumentException if the array size is greater than 4
* @throws \InvalidArgumentException if invalid keys (out of bounds or string) are found in the array

View File

@@ -27,13 +27,13 @@ declare(strict_types=1);
namespace pocketmine\command;
use pocketmine\command\utils\CommandException;
use pocketmine\console\ConsoleCommandSender;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\permission\PermissionManager;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\timings\TimingsHandler;
use pocketmine\utils\BroadcastLoggerForwarder;
use pocketmine\utils\TextFormat;
use function explode;
use function str_replace;
@@ -227,12 +227,12 @@ abstract class Command{
$result = KnownTranslationFactory::chat_type_admin($source->getName(), $message);
$colored = $result->prefix(TextFormat::GRAY . TextFormat::ITALIC);
if($sendToSource && !($source instanceof ConsoleCommandSender)){
if($sendToSource){
$source->sendMessage($message);
}
foreach($users as $user){
if($user instanceof ConsoleCommandSender){
if($user instanceof BroadcastLoggerForwarder){
$user->sendMessage($result);
}elseif($user !== $source){
$user->sendMessage($colored);

View File

@@ -272,10 +272,11 @@ class SimpleCommandMap implements CommandMap{
}
//These registered commands have absolute priority
$lowerAlias = strtolower($alias);
if(count($targets) > 0){
$this->knownCommands[strtolower($alias)] = new FormattedCommandAlias(strtolower($alias), $targets);
$this->knownCommands[$lowerAlias] = new FormattedCommandAlias($lowerAlias, $targets);
}else{
unset($this->knownCommands[strtolower($alias)]);
unset($this->knownCommands[$lowerAlias]);
}
}

View File

@@ -25,7 +25,7 @@ namespace pocketmine\command\defaults;
use pocketmine\command\CommandSender;
use pocketmine\permission\DefaultPermissionNames;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function date;
class DumpMemoryCommand extends VanillaCommand{

View File

@@ -63,7 +63,7 @@ class GarbageCollectorCommand extends VanillaCommand{
$cyclesCollected = $sender->getServer()->getMemoryManager()->triggerGarbageCollector();
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_header()->format(TextFormat::GREEN . "---- " . TextFormat::WHITE, TextFormat::GREEN . " ----" . TextFormat::WHITE));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_header()->format(TextFormat::GREEN . "---- " . TextFormat::RESET, TextFormat::GREEN . " ----" . TextFormat::RESET));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_chunks(TextFormat::RED . number_format($chunksCollected))->prefix(TextFormat::GOLD));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_gc_entities(TextFormat::RED . number_format($entitiesCollected))->prefix(TextFormat::GOLD));

View File

@@ -75,7 +75,11 @@ class GiveCommand extends VanillaCommand{
if(!isset($args[2])){
$item->setCount($item->getMaxStackSize());
}else{
$item->setCount((int) $args[2]);
$count = $this->getBoundedInt($sender, $args[2], 1, 32767);
if($count === null){
return true;
}
$item->setCount($count);
}
if(isset($args[3])){

View File

@@ -95,7 +95,7 @@ class HelpCommand extends VanillaCommand{
foreach($commands[$pageNumber - 1] as $command){
$description = $command->getDescription();
$descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description;
$sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getName() . ": " . TextFormat::WHITE . $descriptionString);
$sender->sendMessage(TextFormat::DARK_GREEN . "/" . $command->getName() . ": " . TextFormat::RESET . $descriptionString);
}
}
@@ -107,18 +107,18 @@ class HelpCommand extends VanillaCommand{
$description = $cmd->getDescription();
$descriptionString = $description instanceof Translatable ? $lang->translate($description) : $description;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_header($cmd->getName())
->format(TextFormat::YELLOW . "--------- " . TextFormat::WHITE, TextFormat::YELLOW . " ---------"));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::WHITE . $descriptionString)
->format(TextFormat::YELLOW . "--------- " . TextFormat::RESET, TextFormat::YELLOW . " ---------"));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_description(TextFormat::RESET . $descriptionString)
->prefix(TextFormat::GOLD));
$usage = $cmd->getUsage();
$usageString = $usage instanceof Translatable ? $lang->translate($usage) : $usage;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::WHITE . implode("\n" . TextFormat::WHITE, explode("\n", $usageString)))
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_usage(TextFormat::RESET . implode("\n" . TextFormat::RESET, explode("\n", $usageString)))
->prefix(TextFormat::GOLD));
$aliases = $cmd->getAliases();
sort($aliases, SORT_NATURAL);
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::WHITE . implode(", ", $aliases))
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_help_specificCommand_aliases(TextFormat::RESET . implode(", ", $aliases))
->prefix(TextFormat::GOLD));
return true;

View File

@@ -52,7 +52,7 @@ class MeCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
$sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_emote($sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::WHITE . implode(" ", $args)));
$sender->getServer()->broadcastMessage(KnownTranslationFactory::chat_type_emote($sender instanceof Player ? $sender->getDisplayName() : $sender->getName(), TextFormat::RESET . implode(" ", $args)));
return true;
}

View File

@@ -56,7 +56,7 @@ class PluginsCommand extends VanillaCommand{
}, $sender->getServer()->getPluginManager()->getPlugins());
sort($list, SORT_STRING);
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_plugins_success((string) count($list), implode(TextFormat::WHITE . ", ", $list)));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_plugins_success((string) count($list), implode(TextFormat::RESET . ", ", $list)));
return true;
}
}

View File

@@ -52,7 +52,7 @@ class StatusCommand extends VanillaCommand{
$mUsage = Process::getAdvancedMemoryUsage();
$server = $sender->getServer();
$sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::WHITE . "Server status" . TextFormat::GREEN . " ----");
$sender->sendMessage(TextFormat::GREEN . "---- " . TextFormat::RESET . "Server status" . TextFormat::GREEN . " ----");
$time = (int) (microtime(true) - $server->getStartTime());

View File

@@ -35,7 +35,7 @@ use pocketmine\timings\TimingsHandler;
use pocketmine\utils\InternetException;
use pocketmine\utils\InternetRequestResult;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function count;
use function fclose;
use function file_exists;

View File

@@ -32,9 +32,7 @@ use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use function count;
use function function_exists;
use function implode;
use function opcache_get_status;
use function sprintf;
use function stripos;
use function strtolower;
@@ -59,36 +57,31 @@ class VersionCommand extends VanillaCommand{
if(count($args) === 0){
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareName(
VersionInfo::NAME
TextFormat::GREEN . VersionInfo::NAME . TextFormat::RESET
));
$versionColor = VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : TextFormat::GREEN;
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_serverSoftwareVersion(
VersionInfo::VERSION()->getFullVersion(),
VersionInfo::GIT_HASH()
$versionColor . VersionInfo::VERSION()->getFullVersion() . TextFormat::RESET,
TextFormat::GREEN . VersionInfo::GIT_HASH() . TextFormat::RESET
));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_minecraftVersion(
ProtocolInfo::MINECRAFT_VERSION_NETWORK,
(string) ProtocolInfo::CURRENT_PROTOCOL
TextFormat::GREEN . ProtocolInfo::MINECRAFT_VERSION_NETWORK . TextFormat::RESET,
TextFormat::GREEN . ProtocolInfo::CURRENT_PROTOCOL . TextFormat::RESET
));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpVersion(PHP_VERSION));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpVersion(TextFormat::GREEN . PHP_VERSION . TextFormat::RESET));
if(
function_exists('opcache_get_status') &&
($opcacheStatus = opcache_get_status(false)) !== false &&
isset($opcacheStatus["jit"]["on"])
){
$jit = $opcacheStatus["jit"];
if($jit["on"] === true){
$jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitEnabled(
sprintf("CRTO: %s%s%s%s", $jit["opt_flags"] >> 2, $jit["opt_flags"] & 0x03, $jit["kind"], $jit["opt_level"])
);
$jitMode = Utils::getOpcacheJitMode();
if($jitMode !== null){
if($jitMode !== 0){
$jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitEnabled(sprintf("CRTO: %d", $jitMode));
}else{
$jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitDisabled();
}
}else{
$jitStatus = KnownTranslationFactory::pocketmine_command_version_phpJitNotSupported();
}
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpJitStatus($jitStatus));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_operatingSystem(Utils::getOS()));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_phpJitStatus($jitStatus->format(TextFormat::GREEN, TextFormat::RESET)));
$sender->sendMessage(KnownTranslationFactory::pocketmine_command_version_operatingSystem(TextFormat::GREEN . Utils::getOS() . TextFormat::RESET));
}else{
$pluginName = implode(" ", $args);
$exactPlugin = $sender->getServer()->getPluginManager()->getPlugin($pluginName);
@@ -118,7 +111,7 @@ class VersionCommand extends VanillaCommand{
private function describeToSender(Plugin $plugin, CommandSender $sender) : void{
$desc = $plugin->getDescription();
$sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::WHITE . " version " . TextFormat::DARK_GREEN . $desc->getVersion());
$sender->sendMessage(TextFormat::DARK_GREEN . $desc->getName() . TextFormat::RESET . " version " . TextFormat::DARK_GREEN . $desc->getVersion());
if($desc->getDescription() !== ""){
$sender->sendMessage($desc->getDescription());

View File

@@ -30,6 +30,8 @@ use pocketmine\permission\DefaultPermissions;
use pocketmine\permission\PermissibleBase;
use pocketmine\permission\PermissibleDelegateTrait;
use pocketmine\Server;
use pocketmine\utils\Terminal;
use pocketmine\utils\TextFormat;
use function explode;
use function trim;
use const PHP_INT_MAX;
@@ -59,13 +61,12 @@ class ConsoleCommandSender implements CommandSender{
}
public function sendMessage(Translatable|string $message) : void{
$server = $this->getServer();
if($message instanceof Translatable){
$message = $this->getLanguage()->translate($message);
}
foreach(explode("\n", trim($message)) as $line){
$server->getLogger()->info($line);
Terminal::writeLine(TextFormat::GREEN . "Command output | " . TextFormat::addBase(TextFormat::WHITE, $line));
}
}

View File

@@ -23,12 +23,14 @@ declare(strict_types=1);
namespace pocketmine\console;
use pocketmine\utils\Process;
use function cli_set_process_title;
use function count;
use function dirname;
use function feof;
use function fwrite;
use function stream_socket_client;
use const PTHREADS_INHERIT_NONE;
require dirname(__DIR__, 2) . '/vendor/autoload.php';
@@ -43,9 +45,40 @@ $socket = stream_socket_client($argv[1], $errCode, $errMessage, 15.0);
if($socket === false){
throw new \RuntimeException("Failed to connect to server process ($errCode): $errMessage");
}
$consoleReader = new ConsoleReader();
$channel = new \Threaded();
$thread = new class($channel) extends \Thread{
public function __construct(
private \Threaded $channel,
){}
public function run(){
require dirname(__DIR__, 2) . '/vendor/autoload.php';
$channel = $this->channel;
$reader = new ConsoleReader();
while(true){ // @phpstan-ignore-line
$line = $reader->readLine();
if($line !== null){
$channel->synchronized(function() use ($channel, $line) : void{
$channel[] = $line;
$channel->notify();
});
}
}
}
};
$thread->start(PTHREADS_INHERIT_NONE);
while(!feof($socket)){
$line = $consoleReader->readLine();
$line = $channel->synchronized(function() use ($channel) : ?string{
if(count($channel) === 0){
$channel->wait(1_000_000);
}
/** @var string|null $line */
$line = $channel->shift();
return $line;
});
if(@fwrite($socket, ($line ?? "") . "\n") === false){
//Always send even if there's no line, to check if the parent is alive
//If the parent process was terminated forcibly, it won't close the connection properly, so feof() will return
@@ -53,3 +86,8 @@ while(!feof($socket)){
break;
}
}
//For simplicity's sake, we don't bother with a graceful shutdown here.
//The parent process would normally forcibly terminate the child process anyway, so we only reach this point if the
//parent process was terminated forcibly and didn't clean up after itself.
Process::kill(Process::pid(), false);

View File

@@ -23,11 +23,9 @@ declare(strict_types=1);
namespace pocketmine\console;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\thread\Thread;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function base64_encode;
use function fgets;
use function fopen;
@@ -45,36 +43,35 @@ use function trim;
use const PHP_BINARY;
use const STREAM_SHUT_RDWR;
final class ConsoleReaderThread extends Thread{
/**
* This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes
* properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets()
* waiting for a line that might never come (and Windows doesn't support character-based reading either), and
* pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless.
*
* This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill
* the entire server process to make it go away.
*
* To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can
* then brutally murder when the server shuts down, without killing the entire server process.
* Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process
* communication.
*/
final class ConsoleReaderChildProcessDaemon{
private \PrefixedLogger $logger;
/** @var resource */
private $subprocess;
/** @var resource */
private $socket;
public function __construct(
private \Threaded $buffer,
private ?SleeperNotifier $notifier = null
){}
protected function onRun() : void{
$buffer = $this->buffer;
$notifier = $this->notifier;
while(!$this->isKilled){
$this->runSubprocess($buffer, $notifier);
}
\Logger $logger
){
$this->logger = new \PrefixedLogger($logger, "Console Reader Daemon");
$this->prepareSubprocess();
}
/**
* This pile of shit exists because PHP on Windows is broken, and can't handle stream_select() on stdin or pipes
* properly - stdin native triggers stream_select() when a key is pressed, causing it to get stuck in fgets()
* waiting for a line that might never come (and Windows doesn't support character-based reading either), and
* pipes just constantly trigger stream_select() instead of only when data is returned, rendering it useless.
*
* This results in whichever process reads stdin getting stuck on shutdown, which previously forced us to kill
* the entire server process to make it go away.
*
* To get around this problem, we delegate the responsibility of reading stdin to a subprocess, which we can
* then brutally murder when the server shuts down, without killing the entire server process.
* Thankfully, stream_select() actually works properly on sockets, so we can use them for inter-process
* communication.
*/
private function runSubprocess(\Threaded $buffer, ?SleeperNotifier $notifier) : void{
private function prepareSubprocess() : void{
$server = stream_socket_server("tcp://127.0.0.1:0");
if($server === false){
throw new \RuntimeException("Failed to open console reader socket server");
@@ -96,41 +93,43 @@ final class ConsoleReaderThread extends Thread{
throw new AssumptionFailedError("stream_socket_accept() returned false");
}
stream_socket_shutdown($server, STREAM_SHUT_RDWR);
while(!$this->isKilled){
$r = [$client];
$w = null;
$e = null;
if(stream_select($r, $w, $e, 0, 200000) === 1){
$command = fgets($client);
if($command === false){
//subprocess died for some reason; this could be someone killed it manually from outside (e.g.
//mistyped PID) or it might be a ctrl+c signal to this process that the child is handling
//differently (different signal handlers).
//since we have no way to know the difference, we just kill the sub and start a new one.
break;
}
$command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
$command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
if($command === ""){
continue;
}
$buffer[] = $command;
if($notifier !== null){
$notifier->wakeupSleeper();
}
}
}
$this->subprocess = $sub;
$this->socket = $client;
}
private function shutdownSubprocess() : void{
//we have no way to signal to the subprocess to shut down gracefully; besides, Windows sucks, and the subprocess
//gets stuck in a blocking fgets() read because stream_select() is a hunk of junk (hence the separate process in
//the first place).
proc_terminate($sub);
proc_close($sub);
stream_socket_shutdown($client, STREAM_SHUT_RDWR);
proc_terminate($this->subprocess);
proc_close($this->subprocess);
stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
}
public function getThreadName() : string{
return "Console";
public function readLine() : ?string{
$r = [$this->socket];
$w = null;
$e = null;
if(stream_select($r, $w, $e, 0, 0) === 1){
$command = fgets($this->socket);
if($command === false){
$this->logger->debug("Lost connection to subprocess, restarting (maybe the child process was killed from outside?)");
$this->shutdownSubprocess();
$this->prepareSubprocess();
return null;
}
$command = preg_replace("#\\x1b\\x5b([^\\x1b]*\\x7e|[\\x40-\\x50])#", "", trim($command)) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
$command = preg_replace('/[[:cntrl:]]/', '', $command) ?? throw new AssumptionFailedError("This regex is assumed to be valid");
return $command !== "" ? $command : null;
}
return null;
}
public function quit() : void{
$this->shutdownSubprocess();
}
}

View File

@@ -33,7 +33,7 @@ use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function base64_encode;
use function error_get_last;
use function file;
@@ -164,6 +164,8 @@ class CrashDump{
}
$this->data->extensions = $extensions;
$this->data->jit_mode = Utils::getOpcacheJitMode();
if($this->server->getConfigGroup()->getPropertyBool("auto-report.send-phpinfo", true)){
ob_start();
phpinfo();

View File

@@ -66,6 +66,8 @@ final class CrashDumpData implements \JsonSerializable{
*/
public array $extensions = [];
public ?int $jit_mode = null;
public string $phpinfo = "";
public CrashDumpDataGeneral $general;

View File

@@ -74,7 +74,7 @@ final class DyeColorIdMap{
}
public function fromId(int $id) : ?DyeColor{
return $this->idToEnum[$id];
return $this->idToEnum[$id] ?? null;
}
public function fromInvertedId(int $id) : ?DyeColor{

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
final class LegacyBiomeIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
final class LegacyBlockIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
final class LegacyEntityIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
final class LegacyItemIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;

View File

@@ -249,10 +249,8 @@ abstract class Entity{
$this->getWorld()->addEntity($this);
$this->lastUpdate = $this->server->getTick();
(new EntitySpawnEvent($this))->call();
$this->scheduleUpdate();
}
abstract protected function getInitialSizeInfo() : EntitySizeInfo;
@@ -937,6 +935,14 @@ abstract class Entity{
return (new Vector2(-cos(deg2rad($this->location->yaw) - M_PI_2), -sin(deg2rad($this->location->yaw) - M_PI_2)))->normalize();
}
/**
* Called from onUpdate() on the first tick of a new entity. This is called before any movement processing or
* main ticking logic. Use this to fire any events related to spawning the entity.
*/
protected function onFirstUpdate(int $currentTick) : void{
(new EntitySpawnEvent($this))->call();
}
public function onUpdate(int $currentTick) : bool{
if($this->closed){
return false;
@@ -953,6 +959,10 @@ abstract class Entity{
$this->lastUpdate = $currentTick;
if($this->justCreated){
$this->onFirstUpdate($currentTick);
}
if(!$this->isAlive()){
if($this->onDeathUpdate($tickDiff)){
$this->flagForDespawn();

View File

@@ -175,20 +175,6 @@ final class EntityFactory{
}, ['Human']);
}
/**
* @phpstan-param \Closure(World, CompoundTag) : Entity $creationFunc
*/
private static function validateCreationFunc(\Closure $creationFunc) : void{
$sig = new CallbackType(
new ReturnType(Entity::class),
new ParameterType("world", World::class),
new ParameterType("nbt", CompoundTag::class)
);
if(!$sig->isSatisfiedBy($creationFunc)){
throw new \TypeError("Declaration of callable `" . CallbackType::createFromCallable($creationFunc) . "` must be compatible with `" . $sig . "`");
}
}
/**
* Registers an entity type into the index.
*
@@ -207,7 +193,11 @@ final class EntityFactory{
throw new \InvalidArgumentException("At least one save name must be provided");
}
Utils::testValidInstance($className, Entity::class);
self::validateCreationFunc($creationFunc);
Utils::validateCallableSignature(new CallbackType(
new ReturnType(Entity::class),
new ParameterType("world", World::class),
new ParameterType("nbt", CompoundTag::class)
), $creationFunc);
foreach($saveNames as $name){
$this->creationFuncs[$name] = $creationFunc;
@@ -227,7 +217,7 @@ final class EntityFactory{
*/
public function createFromData(World $world, CompoundTag $nbt) : ?Entity{
try{
$saveId = $nbt->getTag("id") ?? $nbt->getTag("identifier");
$saveId = $nbt->getTag("identifier") ?? $nbt->getTag("id");
$func = null;
if($saveId instanceof StringTag){
$func = $this->creationFuncs[$saveId->getValue()] ?? null;
@@ -248,7 +238,7 @@ final class EntityFactory{
public function injectSaveId(string $class, CompoundTag $saveData) : void{
if(isset($this->saveNames[$class])){
$saveData->setTag("id", new StringTag($this->saveNames[$class]));
$saveData->setTag("identifier", new StringTag($this->saveNames[$class]));
}else{
throw new \InvalidArgumentException("Entity $class is not registered");
}

View File

@@ -237,11 +237,10 @@ class ExperienceManager{
}
public function onPickupXp(int $xpValue) : void{
static $mainHandIndex = -1;
static $offHandIndex = -2;
$mainHandIndex = -1;
$offHandIndex = -2;
//TODO: replace this with a more generic equipment getting/setting interface
/** @var Durable[] $equipment */
$equipment = [];
if(($item = $this->entity->getInventory()->getItemInHand()) instanceof Durable && $item->hasEnchantment(VanillaEnchantments::MENDING())){

View File

@@ -56,6 +56,7 @@ use pocketmine\network\mcpe\protocol\types\entity\EntityMetadataProperties;
use pocketmine\player\Player;
use pocketmine\timings\Timings;
use pocketmine\utils\Binary;
use pocketmine\world\sound\BurpSound;
use pocketmine\world\sound\EntityLandSound;
use pocketmine\world\sound\EntityLongFallSound;
use pocketmine\world\sound\EntityShortFallSound;
@@ -255,8 +256,7 @@ abstract class Living extends Entity{
$size = $this->getInitialSizeInfo();
if($this->isSwimming() || $this->isGliding()){
$width = $size->getWidth();
//we don't actually know an appropriate eye height for a swimming mob, but 2/3 should be good enough.
$this->setSize((new EntitySizeInfo($width, $width, $width * 2 / 3))->scale($this->getScale()));
$this->setSize((new EntitySizeInfo($width, $width, $width * 0.9))->scale($this->getScale()));
}else{
$this->setSize($size->scale($this->getScale()));
}
@@ -293,6 +293,10 @@ abstract class Living extends Entity{
return $nbt;
}
/**
* @deprecated This function always returns true, no matter whether the target is in the line of sight or not.
* @see VoxelRayTrace::inDirection() for a more generalized method of ray-tracing to a target.
*/
public function hasLineOfSight(Entity $entity) : bool{
//TODO: head height
return true;
@@ -320,6 +324,9 @@ abstract class Living extends Entity{
foreach($consumable->getAdditionalEffects() as $effect){
$this->effectManager->add($effect);
}
if($consumable instanceof FoodSource){
$this->broadcastSound(new BurpSound());
}
$consumable->onConsume($this);
}

View File

@@ -38,7 +38,6 @@ class Effect{
* @param Translatable|string $name Translation key used for effect name
* @param Color $color Color of bubbles given by this effect
* @param bool $bad Whether the effect is harmful
* @param int $defaultDuration
* @param bool $hasBubbles Whether the effect has potion bubbles. Some do not (e.g. Instant Damage has its own particles instead of bubbles)
*/
public function __construct(

View File

@@ -33,5 +33,8 @@ class HealthBoostEffect extends Effect{
public function remove(Living $entity, EffectInstance $instance) : void{
$entity->setMaxHealth($entity->getMaxHealth() - 4 * $instance->getEffectLevel());
if($entity->getHealth() > $entity->getMaxHealth()){
$entity->setHealth($entity->getMaxHealth());
}
}
}

View File

@@ -33,7 +33,7 @@ class InstantDamageEffect extends InstantEffect{
public function applyEffect(Living $entity, EffectInstance $instance, float $potency = 1.0, ?Entity $source = null) : void{
//TODO: add particles (witch spell)
$damage = (4 << $instance->getAmplifier()) * $potency;
$damage = (6 << $instance->getAmplifier()) * $potency;
if($source !== null){
$sourceOwner = $source->getOwningEntity();
if($sourceOwner !== null){

View File

@@ -30,7 +30,7 @@ use pocketmine\event\entity\EntityRegainHealthEvent;
class RegenerationEffect extends Effect{
public function canTick(EffectInstance $instance) : bool{
if(($interval = (40 >> $instance->getAmplifier())) > 0){
if(($interval = (50 >> $instance->getAmplifier())) > 0){
return ($instance->getDuration() % $interval) === 0;
}
return true;

View File

@@ -101,6 +101,7 @@ final class VanillaEffects{
/**
* @return Effect[]
* @phpstan-return array<string, Effect>
*/
public static function getAll() : array{
//phpstan doesn't support generic traits yet :(

View File

@@ -117,7 +117,6 @@ class FallingBlock extends Entity{
$block = $world->getBlock($pos);
if(!$block->canBeReplaced() || !$world->isInWorld($pos->getFloorX(), $pos->getFloorY(), $pos->getFloorZ()) || ($this->onGround && abs($this->location->y - $this->location->getFloorY()) > 0.001)){
//FIXME: anvils are supposed to destroy torches
$world->dropItem($this->location, $this->block->asItem());
}else{
$ev = new EntityBlockChangeEvent($this, $block, $blockTarget ?? $this->block);

View File

@@ -92,8 +92,11 @@ class ItemEntity extends Entity{
$this->pickupDelay = $nbt->getShort("PickupDelay", $this->pickupDelay);
$this->owner = $nbt->getString("Owner", $this->owner);
$this->thrower = $nbt->getString("Thrower", $this->thrower);
}
(new ItemSpawnEvent($this))->call();
protected function onFirstUpdate(int $currentTick) : void{
(new ItemSpawnEvent($this))->call(); //this must be called before EntitySpawnEvent, to maintain backwards compatibility
parent::onFirstUpdate($currentTick);
}
protected function entityBaseTick(int $tickDiff = 1) : bool{
@@ -186,6 +189,10 @@ class ItemEntity extends Entity{
return true;
}
public function canSaveWithChunk() : bool{
return !$this->item->isNull() && parent::canSaveWithChunk();
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setTag("Item", $this->item->nbtSerialize());

View File

@@ -98,8 +98,8 @@ class SplashPotion extends Throwable{
if($hasEffects){
if(!$this->willLinger()){
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){
if($entity instanceof Living && $entity->isAlive()){
foreach($this->getWorld()->getCollidingEntities($this->boundingBox->expandedCopy(4.125, 2.125, 4.125), $this) as $entity){
if($entity instanceof Living){
$distanceSquared = $entity->getEyePos()->distanceSquared($this->location);
if($distanceSquared > 16){ //4 blocks
continue;

View File

@@ -72,7 +72,7 @@ class BlockBreakEvent extends BlockEvent implements Cancellable{
* Returns the item used to destroy the block.
*/
public function getItem() : Item{
return $this->item;
return clone $this->item;
}
/**

View File

@@ -65,7 +65,7 @@ class BlockPlaceEvent extends BlockEvent implements Cancellable{
* Gets the item in hand
*/
public function getItem() : Item{
return $this->item;
return clone $this->item;
}
public function getBlockReplaced() : Block{

View File

@@ -69,7 +69,7 @@ class PlayerInteractEvent extends PlayerEvent implements Cancellable{
}
public function getItem() : Item{
return $this->item;
return clone $this->item;
}
public function getBlock() : Block{

View File

@@ -60,6 +60,6 @@ class PlayerItemHeldEvent extends PlayerEvent implements Cancellable{
* Returns the item in the slot that the player is trying to equip.
*/
public function getItem() : Item{
return $this->item;
return clone $this->item;
}
}

View File

@@ -47,7 +47,7 @@ class PlayerItemUseEvent extends PlayerEvent implements Cancellable{
* Returns the item used.
*/
public function getItem() : Item{
return $this->item;
return clone $this->item;
}
/**

View File

@@ -76,11 +76,13 @@ abstract class BaseInventory implements Inventory{
/**
* @param Item[] $items
* @phpstan-param array<int, Item> $items
*/
abstract protected function internalSetContents(array $items) : void;
/**
* @param Item[] $items
* @phpstan-param array<int, Item> $items
*/
public function setContents(array $items) : void{
if(count($items) > $this->getSize()){
@@ -132,6 +134,7 @@ abstract class BaseInventory implements Inventory{
return $slots;
}
public function first(Item $item, bool $exact = false) : int{
$count = $exact ? $item->getCount() : max(1, $item->getCount());
$checkDamage = $exact || !$item->hasAnyDamageValue();

View File

@@ -26,7 +26,7 @@ namespace pocketmine\inventory;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
use function json_decode;

View File

@@ -24,8 +24,6 @@ declare(strict_types=1);
namespace pocketmine\inventory;
use pocketmine\item\Item;
use pocketmine\player\Player;
use function count;
/**
* An inventory which is backed by another inventory, and acts as a proxy to that inventory.
@@ -37,16 +35,25 @@ class DelegateInventory extends BaseInventory{
private Inventory $backingInventory
){
parent::__construct();
$weakThis = \WeakReference::create($this);
$this->backingInventory->getListeners()->add($this->inventoryListener = new CallbackInventoryListener(
function(Inventory $unused, int $slot, Item $oldItem) : void{
$this->onSlotChange($slot, $oldItem);
static function(Inventory $unused, int $slot, Item $oldItem) use ($weakThis) : void{
if(($strongThis = $weakThis->get()) !== null){
$strongThis->onSlotChange($slot, $oldItem);
}
},
function(Inventory $unused, array $oldContents) : void{
$this->onContentChange($oldContents);
static function(Inventory $unused, array $oldContents) use ($weakThis) : void{
if(($strongThis = $weakThis->get()) !== null){
$strongThis->onContentChange($oldContents);
}
}
));
}
public function __destruct(){
$this->backingInventory->getListeners()->remove($this->inventoryListener);
}
public function getSize() : int{
return $this->backingInventory->getSize();
}
@@ -66,12 +73,4 @@ class DelegateInventory extends BaseInventory{
protected function internalSetContents(array $items) : void{
$this->backingInventory->setContents($items);
}
public function onClose(Player $who) : void{
parent::onClose($who);
if(count($this->getViewers()) === 0 && count($this->getListeners()->toArray()) === 1){
$this->backingInventory->getListeners()->remove($this->inventoryListener);
$this->inventoryListener = CallbackInventoryListener::onAnyChange(static function() : void{}); //break cyclic reference
}
}
}

View File

@@ -47,12 +47,20 @@ interface Inventory{
public function setItem(int $index, Item $item) : void;
/**
* Returns an array of all the itemstacks in the inventory, indexed by their slot number.
* Empty slots are not included unless includeEmpty is true.
*
* @return Item[]
* @phpstan-return array<int, Item>
*/
public function getContents(bool $includeEmpty = false) : array;
/**
* Sets the contents of the inventory. Non-numeric offsets or offsets larger than the size of the inventory are
* ignored.
*
* @param Item[] $items
* @phpstan-param array<int, Item> $items
*/
public function setContents(array $items) : void;
@@ -85,8 +93,10 @@ interface Inventory{
/**
* Will return all the Items that has the same id and metadata (if not null).
* Won't check amount
* The returned array is indexed by slot number.
*
* @return Item[]
* @phpstan-return array<int, Item>
*/
public function all(Item $item) : array;

View File

@@ -58,6 +58,7 @@ class SimpleInventory extends BaseInventory{
/**
* @return Item[]
* @phpstan-return array<int, Item>
*/
public function getContents(bool $includeEmpty = false) : array{
$contents = [];

View File

@@ -42,6 +42,9 @@ class DropItemAction extends InventoryAction{
if($this->targetItem->isNull()){
throw new TransactionValidationException("Cannot drop an empty itemstack");
}
if($this->targetItem->getCount() > $this->targetItem->getMaxStackSize()){
throw new TransactionValidationException("Target item exceeds item type max stack size");
}
}
public function onPreExecute(Player $source) : bool{

View File

@@ -70,6 +70,12 @@ class SlotChangeAction extends InventoryAction{
if(!$this->inventory->getItem($this->inventorySlot)->equalsExact($this->sourceItem)){
throw new TransactionValidationException("Slot does not contain expected original item");
}
if($this->targetItem->getCount() > $this->targetItem->getMaxStackSize()){
throw new TransactionValidationException("Target item exceeds item type max stack size");
}
if($this->targetItem->getCount() > $this->inventory->getMaxStackSize()){
throw new TransactionValidationException("Target item exceeds inventory max stack size");
}
}
/**

View File

@@ -26,7 +26,7 @@ namespace pocketmine\item;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function explode;
use function file_get_contents;
use function is_array;
@@ -108,8 +108,9 @@ final class LegacyStringToItemParser{
throw new LegacyStringToItemParserException("Unable to parse \"" . $b[1] . "\" from \"" . $input . "\" as a valid meta value");
}
if(isset($this->map[strtolower($b[0])])){
$item = $this->itemFactory->get($this->map[strtolower($b[0])], $meta);
$id = strtolower($b[0]);
if(isset($this->map[$id])){
$item = $this->itemFactory->get($this->map[$id], $meta);
}else{
throw new LegacyStringToItemParserException("Unable to resolve \"" . $input . "\" to a valid item");
}

View File

@@ -384,6 +384,7 @@ final class VanillaItems{
/**
* @return Item[]
* @phpstan-return array<string, Item>
*/
public static function getAll() : array{
//phpstan doesn't support generic traits yet :(

View File

@@ -24,9 +24,10 @@ declare(strict_types=1);
namespace pocketmine\lang;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_filter;
use function array_map;
use function count;
use function explode;
use function file_exists;
use function is_dir;
@@ -67,10 +68,14 @@ class Language{
$result = [];
foreach($files as $file){
$code = explode(".", $file)[0];
$strings = self::loadLang($path, $code);
if(isset($strings["language.name"])){
$result[$code] = $strings["language.name"];
try{
$code = explode(".", $file)[0];
$strings = self::loadLang($path, $code);
if(isset($strings["language.name"])){
$result[$code] = $strings["language.name"];
}
}catch(LanguageNotFoundException $e){
// no-op
}
}
@@ -124,7 +129,10 @@ class Language{
protected static function loadLang(string $path, string $languageCode) : array{
$file = Path::join($path, $languageCode . ".ini");
if(file_exists($file)){
return array_map('\stripcslashes', Utils::assumeNotFalse(parse_ini_file($file, false, INI_SCANNER_RAW), "Missing or inaccessible required resource files"));
$strings = array_map('stripcslashes', Utils::assumeNotFalse(parse_ini_file($file, false, INI_SCANNER_RAW), "Missing or inaccessible required resource files"));
if(count($strings) > 0){
return $strings;
}
}
throw new LanguageNotFoundException("Language \"$languageCode\" not found");

View File

@@ -345,8 +345,10 @@ class InventoryManager{
public function onClientRemoveWindow(int $id) : void{
if($id === $this->lastInventoryNetworkId){
$this->remove($id);
$this->player->removeCurrentWindow();
if(isset($this->windowMap[$id]) && $id !== $this->pendingCloseWindowId){
$this->remove($id);
$this->player->removeCurrentWindow();
}
}else{
$this->session->getLogger()->debug("Attempted to close inventory with network ID $id, but current is $this->lastInventoryNetworkId");
}

View File

@@ -56,6 +56,7 @@ use pocketmine\network\mcpe\handler\LoginPacketHandler;
use pocketmine\network\mcpe\handler\PacketHandler;
use pocketmine\network\mcpe\handler\PreSpawnPacketHandler;
use pocketmine\network\mcpe\handler\ResourcePacksPacketHandler;
use pocketmine\network\mcpe\handler\SessionStartPacketHandler;
use pocketmine\network\mcpe\handler\SpawnResponsePacketHandler;
use pocketmine\network\mcpe\protocol\AvailableCommandsPacket;
use pocketmine\network\mcpe\protocol\ChunkRadiusUpdatedPacket;
@@ -87,6 +88,7 @@ use pocketmine\network\mcpe\protocol\SetTimePacket;
use pocketmine\network\mcpe\protocol\SetTitlePacket;
use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
use pocketmine\network\mcpe\protocol\TextPacket;
use pocketmine\network\mcpe\protocol\ToastRequestPacket;
use pocketmine\network\mcpe\protocol\TransferPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\command\CommandData;
@@ -164,6 +166,7 @@ class NetworkSession{
*/
private \SplQueue $compressedQueue;
private bool $forceAsyncCompression = true;
private bool $enableCompression = false; //disabled until handshake completed
private PacketSerializerContext $packetSerializerContext;
@@ -196,17 +199,10 @@ class NetworkSession{
$this->connectTime = time();
$this->setHandler(new LoginPacketHandler(
$this->setHandler(new SessionStartPacketHandler(
$this->server,
$this,
function(PlayerInfo $info) : void{
$this->info = $info;
$this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET);
$this->logger->setPrefix($this->getLogPrefix());
},
function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
$this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey);
}
fn() => $this->onSessionStartSuccess()
));
$this->manager->add($this);
@@ -221,6 +217,24 @@ class NetworkSession{
return $this->logger;
}
private function onSessionStartSuccess() : void{
$this->logger->debug("Session start handshake completed, awaiting login packet");
$this->flushSendBuffer(true);
$this->enableCompression = true;
$this->setHandler(new LoginPacketHandler(
$this->server,
$this,
function(PlayerInfo $info) : void{
$this->info = $info;
$this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET);
$this->logger->setPrefix($this->getLogPrefix());
},
function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
$this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey);
}
));
}
protected function createPlayer() : void{
$this->server->createPlayer($this, $this->info, $this->authenticated, $this->cachedOfflinePlayerData)->onCompletion(
\Closure::fromCallable([$this, 'onPlayerCreated']),
@@ -335,18 +349,22 @@ class NetworkSession{
}
}
Timings::$playerNetworkReceiveDecompress->startTiming();
try{
$stream = new PacketBatch($this->compressor->decompress($payload));
}catch(DecompressionException $e){
$this->logger->debug("Failed to decompress packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Compressed packet batch decode error");
}finally{
Timings::$playerNetworkReceiveDecompress->stopTiming();
if($this->enableCompression){
Timings::$playerNetworkReceiveDecompress->startTiming();
try{
$decompressed = $this->compressor->decompress($payload);
}catch(DecompressionException $e){
$this->logger->debug("Failed to decompress packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Compressed packet batch decode error");
}finally{
Timings::$playerNetworkReceiveDecompress->stopTiming();
}
}else{
$decompressed = $payload;
}
try{
foreach($stream->getPackets($this->packetPool, $this->packetSerializerContext, 500) as [$packet, $buffer]){
foreach((new PacketBatch($decompressed))->getPackets($this->packetPool, $this->packetSerializerContext, 500) as [$packet, $buffer]){
if($packet === null){
$this->logger->debug("Unknown packet: " . base64_encode($buffer));
throw new PacketHandlingException("Unknown packet received");
@@ -451,7 +469,14 @@ class NetworkSession{
}elseif($this->forceAsyncCompression){
$syncMode = false;
}
$promise = $this->server->prepareBatch(PacketBatch::fromPackets($this->packetSerializerContext, ...$this->sendBuffer), $this->compressor, $syncMode);
$batch = PacketBatch::fromPackets($this->packetSerializerContext, ...$this->sendBuffer);
if($this->enableCompression){
$promise = $this->server->prepareBatch($batch, $this->compressor, $syncMode);
}else{
$promise = new CompressBatchPromise();
$promise->resolve($batch->getBuffer());
}
$this->sendBuffer = [];
$this->queueCompressedNoBufferFlush($promise, $immediate);
}
@@ -1076,6 +1101,10 @@ class NetworkSession{
$this->sendDataPacket(EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER));
}
public function onToastNotification(string $title, string $body) : void{
$this->sendDataPacket(ToastRequestPacket::create($title, $body));
}
public function tick() : void{
if($this->info === null){
if(time() >= $this->connectTime + 10){

View File

@@ -43,8 +43,6 @@ class ChunkCache implements ChunkListener{
/**
* Fetches the ChunkCache instance for the given world. This lazily creates cache systems as needed.
*
* @return ChunkCache
*/
public static function getInstance(World $world, Compressor $compressor) : self{
$worldId = spl_object_id($world);

View File

@@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\utils\SingletonTrait;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
class StaticPacketCache{

View File

@@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
use function is_array;
use function is_bool;

View File

@@ -28,7 +28,7 @@ use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_key_exists;
use function file_get_contents;
use function is_array;

View File

@@ -32,7 +32,7 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
/**

View File

@@ -43,7 +43,9 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset;
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
@@ -110,7 +112,7 @@ class TypeConverter{
public function coreItemStackToRecipeIngredient(Item $itemStack) : RecipeIngredient{
if($itemStack->isNull()){
return new RecipeIngredient(0, 0, 0);
return new RecipeIngredient(null, 0);
}
if($itemStack->hasAnyDamageValue()){
[$id, ] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), 0);
@@ -118,15 +120,25 @@ class TypeConverter{
}else{
[$id, $meta] = ItemTranslator::getInstance()->toNetworkId($itemStack->getId(), $itemStack->getMeta());
}
return new RecipeIngredient($id, $meta, $itemStack->getCount());
return new RecipeIngredient(new IntIdMetaItemDescriptor($id, $meta), $itemStack->getCount());
}
public function recipeIngredientToCoreItemStack(RecipeIngredient $ingredient) : Item{
if($ingredient->getId() === 0){
$descriptor = $ingredient->getDescriptor();
if($descriptor === null){
return VanillaItems::AIR();
}
[$id, $meta] = ItemTranslator::getInstance()->fromNetworkIdWithWildcardHandling($ingredient->getId(), $ingredient->getMeta());
return ItemFactory::getInstance()->get($id, $meta, $ingredient->getCount());
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());
}
throw new \LogicException("Unsupported conversion of recipe ingredient to core item stack");
}
public function coreItemStackToNet(Item $itemStack) : ItemStack{

View File

@@ -51,7 +51,6 @@ use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ActorEventPacket;
use pocketmine\network\mcpe\protocol\ActorPickRequestPacket;
use pocketmine\network\mcpe\protocol\AdventureSettingsPacket;
use pocketmine\network\mcpe\protocol\AnimatePacket;
use pocketmine\network\mcpe\protocol\BlockActorDataPacket;
use pocketmine\network\mcpe\protocol\BlockPickRequestPacket;
@@ -687,10 +686,6 @@ class InGamePacketHandler extends PacketHandler{
return true; //this is a broken useless packet, so we don't use it
}
public function handleAdventureSettings(AdventureSettingsPacket $packet) : bool{
return true; //no longer used, but the client still sends it for flight changes
}
public function handleBlockActorData(BlockActorDataPacket $packet) : bool{
$pos = new Vector3($packet->blockPosition->getX(), $packet->blockPosition->getY(), $packet->blockPosition->getZ());
if($pos->distanceSquared($this->player->getLocation()) > 10000){

View File

@@ -30,6 +30,7 @@ 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;
use pocketmine\network\mcpe\protocol\RequestChunkRadiusPacket;
use pocketmine\network\mcpe\protocol\StartGamePacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
@@ -62,6 +63,7 @@ class PreSpawnPacketHandler extends PacketHandler{
$location = $this->player->getLocation();
$world = $location->getWorld();
$this->session->getLogger()->debug("Preparing StartGamePacket");
$levelSettings = new LevelSettings();
$levelSettings->seed = -1;
$levelSettings->spawnSettings = new SpawnSettings(SpawnSettings::BIOME_TYPE_DEFAULT, "", DimensionIds::OVERWORLD); //TODO: implement this properly
@@ -105,22 +107,41 @@ class PreSpawnPacketHandler extends PacketHandler{
GlobalItemTypeDictionary::getInstance()->getDictionary()->getEntries(),
));
$this->session->getLogger()->debug("Sending actor identifiers");
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getAvailableActorIdentifiers());
$this->session->getLogger()->debug("Sending biome definitions");
$this->session->sendDataPacket(StaticPacketCache::getInstance()->getBiomeDefs());
$this->session->getLogger()->debug("Sending attributes");
$this->session->syncAttributes($this->player, $this->player->getAttributeMap()->getAll());
$this->session->getLogger()->debug("Sending available commands");
$this->session->syncAvailableCommands();
$this->session->getLogger()->debug("Sending abilities");
$this->session->syncAbilities($this->player);
$this->session->syncAdventureSettings();
$this->session->getLogger()->debug("Sending effects");
foreach($this->player->getEffects()->all() as $effect){
$this->session->onEntityEffectAdded($this->player, $effect, false);
}
$this->session->getLogger()->debug("Sending actor metadata");
$this->player->sendData([$this->player]);
$this->session->getLogger()->debug("Sending inventory");
$this->inventoryManager->syncAll();
$this->inventoryManager->syncCreative();
$this->inventoryManager->syncSelectedHotbarSlot();
$this->session->getLogger()->debug("Sending creative inventory data");
$this->inventoryManager->syncCreative();
$this->session->getLogger()->debug("Sending crafting data");
$this->session->sendDataPacket(CraftingDataCache::getInstance()->getCache($this->server->getCraftingManager()));
$this->session->getLogger()->debug("Sending player list");
$this->session->syncPlayerList($this->server->getOnlinePlayers());
}
@@ -129,4 +150,10 @@ class PreSpawnPacketHandler extends PacketHandler{
return true;
}
public function handlePlayerAuthInput(PlayerAuthInputPacket $packet) : bool{
//the client will send this every tick once we start sending chunks, but we don't handle it in this stage
//this is very spammy so we filter it out
return true;
}
}

View File

@@ -65,9 +65,19 @@ class ResourcePacksPacketHandler extends PacketHandler{
){}
public function setUp() : void{
$resourcePackEntries = array_map(static function(ResourcePack $pack) : ResourcePackInfoEntry{
$resourcePackEntries = array_map(function(ResourcePack $pack) : ResourcePackInfoEntry{
//TODO: more stuff
return new ResourcePackInfoEntry($pack->getPackId(), $pack->getPackVersion(), $pack->getPackSize(), "", "", "", false);
$encryptionKey = $this->resourcePackManager->getPackEncryptionKey($pack->getPackId());
return new ResourcePackInfoEntry(
$pack->getPackId(),
$pack->getPackVersion(),
$pack->getPackSize(),
$encryptionKey ?? "",
"",
$pack->getPackId(),
false
);
}, $this->resourcePackManager->getResourceStack());
//TODO: support forcing server packs
$this->session->sendDataPacket(ResourcePacksInfoPacket::create($resourcePackEntries, [], $this->resourcePackManager->resourcePacksRequired(), false, false));

View File

@@ -0,0 +1,76 @@
<?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\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{
/**
* @phpstan-param \Closure() : void $onSuccess
*/
public function __construct(
private Server $server,
private NetworkSession $session,
private \Closure $onSuccess
){}
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
);
return true;
}
//TODO: we're filling in the defaults to get pre-1.19.30 behaviour back for now, but we should explore the new options in the future
$this->session->sendDataPacket(NetworkSettingsPacket::create(
NetworkSettingsPacket::COMPRESS_EVERYTHING,
CompressionAlgorithm::ZLIB,
false,
0,
0
));
($this->onSuccess)();
return true;
}
protected function isCompatibleProtocol(int $protocolVersion) : bool{
return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL;
}
}

View File

@@ -58,7 +58,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
* Sometimes this gets changed when the MCPE-layer protocol gets broken to the point where old and new can't
* communicate. It's important that we check this to avoid catastrophes.
*/
private const MCPE_RAKNET_PROTOCOL_VERSION = 10;
private const MCPE_RAKNET_PROTOCOL_VERSION = 11;
private const MCPE_RAKNET_PACKET_ID = "\xfe";

View File

@@ -47,7 +47,6 @@ final class RakLibThreadCrashInfo{
return new self(null, $info["message"], $info["file"], $info["line"]);
}
/** @return string|null */
public function getClass() : ?string{ return $this->class; }
public function getMessage() : string{ return $this->message; }

View File

@@ -418,6 +418,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
return $this->lastPlayed - $this->firstPlayed > 1; // microtime(true) - microtime(true) may have less than one millisecond difference
}
/**
* Sets whether the player is allowed to toggle flight mode.
*
* If set to false, the player will be locked in its current flight mode (flying/not flying), and attempts by the
* player to enter or exit flight mode will be prevented.
*
* Note: Setting this to false DOES NOT change whether the player is currently flying. Use
* {@link Player::setFlying()} for that purpose.
*/
public function setAllowFlight(bool $value) : void{
if($this->allowFlight !== $value){
$this->allowFlight = $value;
@@ -425,10 +434,24 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
}
/**
* Returns whether the player is allowed to toggle its flight state.
*
* If false, the player is locked in its current flight mode (flying/not flying), and attempts by the player to
* enter or exit flight mode will be prevented.
*/
public function getAllowFlight() : bool{
return $this->allowFlight;
}
/**
* Sets whether the player's movement may be obstructed by blocks with collision boxes.
* If set to false, the player can move through any block unobstructed.
*
* Note: Enabling flight mode in conjunction with this is recommended. A non-flying player will simply fall through
* the ground into the void.
* @see Player::setFlying()
*/
public function setHasBlockCollision(bool $value) : void{
if($this->blockCollision !== $value){
$this->blockCollision = $value;
@@ -436,6 +459,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
}
/**
* Returns whether blocks may obstruct the player's movement.
* If false, the player can move through any block unobstructed.
*/
public function hasBlockCollision() : bool{
return $this->blockCollision;
}
@@ -1021,7 +1048,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function internalSetGameMode(GameMode $gameMode) : void{
$this->gamemode = $gameMode;
$this->allowFlight = $this->isCreative();
$this->allowFlight = $this->gamemode->equals(GameMode::CREATIVE());
$this->hungerManager->setEnabled($this->isSurvival());
if($this->isSpectator()){
@@ -1316,6 +1343,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->lastUpdate = $currentTick;
if($this->justCreated){
$this->onFirstUpdate($currentTick);
}
if(!$this->isAlive() && $this->spawned){
$this->onDeathUpdate($tickDiff);
return true;
@@ -1986,6 +2017,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->getNetworkSession()->onTip($message);
}
/**
* Sends a toast message to the player, or queue to send it if a toast message is already shown.
*/
public function sendToastNotification(string $title, string $body) : void{
$this->getNetworkSession()->onToastNotification($title, $body);
}
/**
* Sends a Form to the player, or queue to send it if a form is already open.
*

View File

@@ -33,7 +33,7 @@ use pocketmine\Server;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Config;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function count;
use function dirname;
use function fclose;

View File

@@ -40,7 +40,7 @@ use pocketmine\Server;
use pocketmine\timings\TimingsHandler;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_diff_key;
use function array_key_exists;
use function array_keys;
@@ -75,6 +75,9 @@ class PluginManager{
/** @var Plugin[] */
protected $enabledPlugins = [];
/** @var array<string, array<string, true>> */
private array $pluginDependents = [];
private bool $loadPluginsGuard = false;
/**
@@ -453,6 +456,15 @@ class PluginManager{
if($plugin->isEnabled()){ //the plugin may have disabled itself during onEnable()
$this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
foreach($plugin->getDescription()->getDepend() as $dependency){
$this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
}
foreach($plugin->getDescription()->getSoftDepend() as $dependency){
if(isset($this->plugins[$dependency])){
$this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
}
}
(new PluginEnableEvent($plugin))->call();
return true;
@@ -472,8 +484,19 @@ class PluginManager{
}
public function disablePlugins() : void{
foreach($this->getPlugins() as $plugin){
$this->disablePlugin($plugin);
while(count($this->enabledPlugins) > 0){
foreach($this->enabledPlugins as $plugin){
if(!$plugin->isEnabled()){
continue; //in case a plugin disabled another plugin
}
$name = $plugin->getDescription()->getName();
if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){
$this->server->getLogger()->debug("Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(", ", array_keys($this->pluginDependents[$name])));
continue;
}
$this->disablePlugin($plugin);
}
}
}
@@ -483,6 +506,12 @@ class PluginManager{
(new PluginDisableEvent($plugin))->call();
unset($this->enabledPlugins[$plugin->getDescription()->getName()]);
foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){
unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
if(count($this->pluginDependents[$dependency]) === 0){
unset($this->pluginDependents[$dependency]);
}
}
$plugin->onEnableStateChange(false);
$plugin->getScheduler()->shutdown();

View File

@@ -23,12 +23,14 @@ declare(strict_types=1);
namespace pocketmine\resourcepacks;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\utils\Config;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_keys;
use function copy;
use function count;
use function file_exists;
use function file_get_contents;
use function gettype;
use function is_array;
use function is_dir;
@@ -49,6 +51,12 @@ class ResourcePackManager{
/** @var ResourcePack[] */
private array $uuidList = [];
/**
* @var string[]
* @phpstan-var array<string, string>
*/
private array $encryptionKeys = [];
/**
* @param string $path Path to resource-packs directory.
*/
@@ -105,7 +113,19 @@ class ResourcePackManager{
if($newPack instanceof ResourcePack){
$this->resourcePacks[] = $newPack;
$this->uuidList[strtolower($newPack->getPackId())] = $newPack;
$index = strtolower($newPack->getPackId());
$this->uuidList[$index] = $newPack;
$keyPath = Path::join($this->path, $pack . ".key");
if(file_exists($keyPath)){
try{
$this->encryptionKeys[$index] = ErrorToExceptionHandler::trapAndRemoveFalse(
fn() => file_get_contents($keyPath)
);
}catch(\ErrorException $e){
throw new ResourcePackException("Could not read encryption key file: " . $e->getMessage(), 0, $e);
}
}
}else{
throw new ResourcePackException("Format not recognized");
}
@@ -153,4 +173,11 @@ class ResourcePackManager{
public function getPackIdList() : array{
return array_keys($this->uuidList);
}
/**
* Returns the key with which the pack was encrypted, or null if the pack has no key.
*/
public function getPackEncryptionKey(string $id) : ?string{
return $this->encryptionKeys[strtolower($id)] ?? null;
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\scheduler;
use pocketmine\MemoryManager;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
/**
* Task used to dump memory from AsyncWorkers

View File

@@ -69,6 +69,9 @@ class TaskHandler{
return $this->nextRun;
}
/**
* @internal
*/
public function setNextRun(int $ticks) : void{
$this->nextRun = $ticks;
}
@@ -103,11 +106,17 @@ class TaskHandler{
}
}
/**
* @internal
*/
public function remove() : void{
$this->cancelled = true;
$this->task->setHandler(null);
}
/**
* @internal
*/
public function run() : void{
$this->timings->startTiming();
try{

View File

@@ -56,7 +56,9 @@ abstract class Worker extends \Worker{
$this->isKilled = true;
if(!$this->isShutdown()){
while($this->unstack() !== null);
$this->synchronized(function() : void{
while($this->unstack() !== null);
});
$this->notify();
$this->shutdown();
}

View File

@@ -0,0 +1,79 @@
<?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\utils;
use pocketmine\command\CommandSender;
use pocketmine\lang\Language;
use pocketmine\lang\Translatable;
use pocketmine\permission\PermissibleBase;
use pocketmine\permission\PermissibleDelegateTrait;
use pocketmine\Server;
/**
* Forwards any messages it receives via sendMessage() to the given logger. Used for forwarding chat messages and
* command audit log messages to the server log file.
*
* Unfortunately, broadcast subscribers are currently required to implement CommandSender, so this class has to include
* a lot of useless methods.
*/
final class BroadcastLoggerForwarder implements CommandSender{
use PermissibleDelegateTrait;
public function __construct(
private Server $server, //annoying useless dependency
private \Logger $logger,
private Language $language
){
//this doesn't need any permissions
$this->perm = new PermissibleBase([]);
}
public function getLanguage() : Language{
return $this->language;
}
public function sendMessage(Translatable|string $message) : void{
if($message instanceof Translatable){
$this->logger->info($this->language->translate($message));
}else{
$this->logger->info($message);
}
}
public function getServer() : Server{
return $this->server;
}
public function getName() : string{
return "Broadcast Logger Forwarder";
}
public function getScreenLineHeight() : int{
return PHP_INT_MAX;
}
public function setScreenLineHeight(?int $height) : void{
//NOOP
}
}

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\utils;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function array_change_key_case;
use function array_fill_keys;
use function array_keys;

View File

@@ -48,6 +48,7 @@ trait EnumTrait{
* This is overridden to change the return typehint.
*
* @return self[]
* @phpstan-return array<string, self>
*/
public static function getAll() : array{
//phpstan doesn't support generic traits yet :(

View File

@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace pocketmine\utils;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function copy;
use function dirname;
use function fclose;

View File

@@ -191,7 +191,7 @@ class MainLogger extends \AttachableThreadedLogger implements \BufferedLogger{
$threadName = (new \ReflectionClass($thread))->getShortName() . " thread";
}
$message = sprintf($this->format, $time->format("H:i:s.v"), $color, $threadName, $prefix, TextFormat::clean($message, false));
$message = sprintf($this->format, $time->format("H:i:s.v"), $color, $threadName, $prefix, TextFormat::addBase($color, TextFormat::clean($message, false)));
if(!Terminal::isInit()){
Terminal::init($this->useFormattingCodes); //lazy-init colour codes because we don't know if they've been registered on this thread

View File

@@ -29,7 +29,10 @@ use function mb_strtoupper;
use function preg_match;
trait RegistryTrait{
/** @var object[] */
/**
* @var object[]
* @phpstan-var array<string, object>
*/
private static $members = null;
private static function verifyName(string $name) : void{
@@ -107,6 +110,7 @@ trait RegistryTrait{
/**
* @return object[]
* @phpstan-return array<string, object>
*/
private static function _registryGetAll() : array{
self::checkInit();

View File

@@ -158,6 +158,31 @@ abstract class TextFormat{
return self::preg_replace('/' . preg_quote($placeholder, "/") . '([0-9a-gk-or])/u', TextFormat::ESCAPE . '$1', $string);
}
/**
* Adds base formatting to the string. The given format codes will be inserted directly after any RESET (§r) codes.
*
* This is useful for log messages, where a RESET code should return to the log message's original colour (e.g.
* blue for NOTICE), rather than whatever the terminal's base text colour is (usually some off-white colour).
*
* Example behaviour:
* - Base format "§c" (red) + "Hello" (no format) = "§r§cHello"
* - Base format "§c" + "Hello §rWorld" = "§r§cHello §r§cWorld"
*
* Note: Adding base formatting to the output string a second time will result in a combination of formats from both
* calls. This is not by design, but simply a consequence of the way the function is implemented.
*/
public static function addBase(string $baseFormat, string $string) : string{
$baseFormatParts = self::tokenize($baseFormat);
foreach($baseFormatParts as $part){
if(!isset(self::FORMATS[$part]) && !isset(self::COLORS[$part])){
throw new \InvalidArgumentException("Unexpected base format token \"$part\", expected only color and format tokens");
}
}
$baseFormat = self::RESET . $baseFormat;
return $baseFormat . str_replace(TextFormat::RESET, $baseFormat, $string);
}
/**
* Returns an HTML-formatted string with colors/markup
*/

View File

@@ -69,6 +69,7 @@ use function mb_check_encoding;
use function ob_end_clean;
use function ob_get_contents;
use function ob_start;
use function opcache_get_status;
use function ord;
use function php_uname;
use function phpversion;
@@ -108,6 +109,7 @@ final class Utils{
private static ?string $os = null;
private static ?UuidInterface $serverUniqueId = null;
private static ?int $cpuCores = null;
/**
* Returns a readable identifier for the given Closure, including file and line.
@@ -295,14 +297,11 @@ final class Utils{
}
public static function getCoreCount(bool $recalculate = false) : int{
static $processors = 0;
if($processors > 0 && !$recalculate){
return $processors;
}else{
$processors = 0;
if(self::$cpuCores !== null && !$recalculate){
return self::$cpuCores;
}
$processors = 0;
switch(Utils::getOS()){
case Utils::OS_LINUX:
case Utils::OS_ANDROID:
@@ -326,7 +325,7 @@ final class Utils{
$processors = (int) getenv("NUMBER_OF_PROCESSORS");
break;
}
return $processors;
return self::$cpuCores = $processors;
}
/**
@@ -365,12 +364,6 @@ final class Utils{
$ord -= 0x100;
}
$hash = 31 * $hash + $ord;
while($hash > 0x7FFFFFFF){
$hash -= 0x100000000;
}
while($hash < -0x80000000){
$hash += 0x100000000;
}
$hash &= 0xFFFFFFFF;
}
return $hash;
@@ -486,8 +479,8 @@ final class Utils{
*/
public static function currentTrace(int $skipFrames = 0) : array{
++$skipFrames; //omit this frame from trace, in addition to other skipped frames
if(function_exists("xdebug_get_function_stack")){
$trace = array_reverse(xdebug_get_function_stack());
if(function_exists("xdebug_get_function_stack") && count($trace = @xdebug_get_function_stack()) !== 0){
$trace = array_reverse($trace);
}else{
$e = new \Exception();
$trace = $e->getTrace();
@@ -634,4 +627,30 @@ final class Utils{
public static function checkLocationNotInfOrNaN(Location $location) : void{
self::checkVector3NotInfOrNaN($location);
}
/**
* Returns an integer describing the current OPcache JIT setting.
* @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit
*/
public static function getOpcacheJitMode() : ?int{
if(
function_exists('opcache_get_status') &&
($opcacheStatus = opcache_get_status(false)) !== false &&
isset($opcacheStatus["jit"]["on"])
){
$jit = $opcacheStatus["jit"];
if($jit["on"] === true){
return (($jit["opt_flags"] >> 2) * 1000) +
(($jit["opt_flags"] & 0x03) * 100) +
($jit["kind"] * 10) +
$jit["opt_level"];
}
//jit available, but disabled
return 0;
}
//jit not available
return null;
}
}

View File

@@ -39,7 +39,7 @@ use pocketmine\utils\Internet;
use pocketmine\utils\InternetException;
use pocketmine\utils\Utils;
use pocketmine\VersionInfo;
use Webmozart\PathUtil\Path;
use Symfony\Component\Filesystem\Path;
use function fgets;
use function sleep;
use function strtolower;

View File

@@ -195,7 +195,7 @@ class Explosion{
}
$entity->attack($ev);
$entity->setMotion($motion->multiply($impact));
$entity->setMotion($entity->getMotion()->addVector($motion->multiply($impact)));
}
}

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