Merge branch 'next-minor' into next-major

This commit is contained in:
Dylan K. Taylor 2022-09-24 18:12:38 +01:00
commit 9295afe8b9
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
13 changed files with 174 additions and 46 deletions

@ -1 +1 @@
Subproject commit cf79c0172283a0a26f2c3695af260d100e0fdabd Subproject commit 50062b5861235fbb9fe6e5d5b30684f4cb464470

View File

@ -55,7 +55,7 @@
"webmozart/path-util": "^2.3" "webmozart/path-util": "^2.3"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "1.8.5", "phpstan/phpstan": "1.8.6",
"phpstan/phpstan-phpunit": "^1.1.0", "phpstan/phpstan-phpunit": "^1.1.0",
"phpstan/phpstan-strict-rules": "^1.2.0", "phpstan/phpstan-strict-rules": "^1.2.0",
"phpunit/phpunit": "^9.2" "phpunit/phpunit": "^9.2"

28
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "284e89a86c5b7fdb4d97ec5516666b36", "content-hash": "5a893a5cf9cd6e4c8f70b332cbde4a2d",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -1560,16 +1560,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "1.8.5", "version": "1.8.6",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c386ab2741e64cc9e21729f891b28b2b10fe6618",
"reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1599,7 +1599,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan/issues", "issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/1.8.5" "source": "https://github.com/phpstan/phpstan/tree/1.8.6"
}, },
"funding": [ "funding": [
{ {
@ -1615,7 +1615,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-09-07T16:05:32+00:00" "time": "2022-09-23T09:54:39+00:00"
}, },
{ {
"name": "phpstan/phpstan-phpunit", "name": "phpstan/phpstan-phpunit",
@ -1671,21 +1671,21 @@
}, },
{ {
"name": "phpstan/phpstan-strict-rules", "name": "phpstan/phpstan-strict-rules",
"version": "1.4.3", "version": "1.4.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git", "url": "https://github.com/phpstan/phpstan-strict-rules.git",
"reference": "431b3d6e8040075de196680cd5bc95735987b4ae" "reference": "23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/431b3d6e8040075de196680cd5bc95735987b4ae", "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6",
"reference": "431b3d6e8040075de196680cd5bc95735987b4ae", "reference": "23e5f377ee6395a1a04842d3d6ed4bd25e7b44a6",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.2 || ^8.0", "php": "^7.2 || ^8.0",
"phpstan/phpstan": "^1.8.3" "phpstan/phpstan": "^1.8.6"
}, },
"require-dev": { "require-dev": {
"nikic/php-parser": "^4.13.0", "nikic/php-parser": "^4.13.0",
@ -1713,9 +1713,9 @@
"description": "Extra strict and opinionated rules for PHPStan", "description": "Extra strict and opinionated rules for PHPStan",
"support": { "support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues", "issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.4.3" "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.4.4"
}, },
"time": "2022-08-26T15:05:46+00:00" "time": "2022-09-21T11:38:17+00:00"
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",

View File

@ -76,21 +76,38 @@ class Block{
$this->position = clone $this->position; $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{ public function getIdInfo() : BlockIdentifier{
return $this->idInfo; return $this->idInfo;
} }
/**
* Returns the printable English name of the block.
*/
public function getName() : string{ public function getName() : string{
return $this->fallbackName; return $this->fallbackName;
} }
/** /**
* @internal * @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::fromStateId()}.
*/ */
public function getStateId() : int{ public function getStateId() : int{
return ($this->getTypeId() << self::INTERNAL_STATE_DATA_BITS) | $this->computeStateData(); return ($this->getTypeId() << self::INTERNAL_STATE_DATA_BITS) | $this->computeStateData();
} }
/**
* 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{ public function asItem() : Item{
return new ItemBlock($this); return new ItemBlock($this);
} }
@ -191,6 +208,12 @@ class Block{
return $this; return $this;
} }
/**
* 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{ public function writeStateToWorld() : void{
$world = $this->position->getWorld(); $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->getStateId()); $world->getOrLoadChunkAtPosition($this->position)->setFullBlock($this->position->x & Chunk::COORD_MASK, $this->position->y, $this->position->z & Chunk::COORD_MASK, $this->getStateId());
@ -216,7 +239,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. * powered/unpowered, etc.
*/ */
public function getTypeId() : int{ public function getTypeId() : int{
@ -263,22 +286,36 @@ class Block{
return true; return true;
} }
/**
* Returns whether this block can be replaced by another block placed in the same position.
*/
public function canBeReplaced() : bool{ public function canBeReplaced() : bool{
return false; 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{ public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{
return $blockReplace->canBeReplaced(); 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{ public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$tx->addBlock($blockReplace->position, $this); $tx->addBlock($blockReplace->position, $this);
return true; 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{ public function onPostPlace() : void{
} }
@ -320,7 +357,7 @@ class Block{
/** /**
* Called when this block is randomly updated due to chunk ticking. * 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{ public function onRandomTick() : void{
@ -343,8 +380,7 @@ class Block{
} }
/** /**
* Called when this block is attacked (left-clicked). This is called when a player left-clicks the block to try and * Called when this block is attacked (left-clicked) by a player attempting to start breaking it in survival.
* start to break it in survival mode.
* *
* @return bool if an action took place, prevents starting to break the block if true. * @return bool if an action took place, prevents starting to break the block if true.
*/ */
@ -352,11 +388,19 @@ class Block{
return false; 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{ public function getFrictionFactor() : float{
return 0.6; return 0.6;
} }
/** /**
* Returns the amount of light emitted by this block.
*
* @return int 0-15 * @return int 0-15
*/ */
public function getLightLevel() : int{ public function getLightLevel() : int{
@ -399,10 +443,6 @@ class Block{
return false; return false;
} }
public function hasEntityCollision() : bool{
return false;
}
/** /**
* Returns whether entities can climb up this block. * Returns whether entities can climb up this block.
*/ */
@ -410,10 +450,6 @@ class Block{
return false; return false;
} }
public function addVelocityToEntity(Entity $entity) : ?Vector3{
return null;
}
final public function getPosition() : Position{ final public function getPosition() : Position{
return $this->position; return $this->position;
} }
@ -496,6 +532,7 @@ class Block{
/** /**
* Returns the item that players will equip when middle-clicking on this 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{ public function getPickedItem(bool $addUserData = false) : Item{
$item = $this->asItem(); $item = $this->asItem();
@ -623,7 +660,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{ public function collidesWithBB(AxisAlignedBB $bb) : bool{
foreach($this->getCollisionBoxes() as $bb2){ foreach($this->getCollisionBoxes() as $bb2){
@ -635,10 +672,21 @@ class Block{
return false; 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 * 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. * 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 * @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. * being ignited), this should return false.
*/ */
@ -646,6 +694,19 @@ class Block{
return true; 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). * 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. * @return float|null The new vertical velocity of the entity, or null if unchanged.
@ -662,6 +723,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[] * @return AxisAlignedBB[]
*/ */
final public function getCollisionBoxes() : array{ final public function getCollisionBoxes() : array{
@ -692,6 +760,10 @@ class Block{
return [AxisAlignedBB::one()]; 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{ public function getSupportType(int $facing) : SupportType{
return SupportType::FULL(); return SupportType::FULL();
} }
@ -702,6 +774,10 @@ class Block{
return count($bb) === 1 && $bb[0]->getAverageEdgeLength() >= 1 && $bb[0]->isCube(); 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{ public function calculateIntercept(Vector3 $pos1, Vector3 $pos2) : ?RayTraceResult{
$bbs = $this->getCollisionBoxes(); $bbs = $this->getCollisionBoxes();
if(count($bbs) === 0){ if(count($bbs) === 0){

View File

@ -26,6 +26,10 @@ namespace pocketmine\block;
use pocketmine\block\utils\SupportType; use pocketmine\block\utils\SupportType;
use pocketmine\math\AxisAlignedBB; 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{ abstract class Flowable extends Transparent{
public function canBeFlowedInto() : bool{ public function canBeFlowedInto() : bool{

View File

@ -23,6 +23,10 @@ declare(strict_types=1);
namespace pocketmine\block; 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{ class Opaque extends Block{
public function isSolid() : bool{ public function isSolid() : bool{

View File

@ -29,6 +29,9 @@ use pocketmine\math\AxisAlignedBB;
use pocketmine\math\Facing; use pocketmine\math\Facing;
use function count; 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{ class Thin extends Transparent{
/** @var bool[] facing => dummy */ /** @var bool[] facing => dummy */
protected array $connections = []; protected array $connections = [];

View File

@ -23,6 +23,12 @@ declare(strict_types=1);
namespace pocketmine\block; 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{ class Transparent extends Block{
public function isTransparent() : bool{ public function isTransparent() : bool{

View File

@ -27,6 +27,9 @@ use pocketmine\data\runtime\RuntimeDataReader;
use pocketmine\data\runtime\RuntimeDataWriter; use pocketmine\data\runtime\RuntimeDataWriter;
use pocketmine\item\Item; use pocketmine\item\Item;
/**
* Represents a block which is unrecognized or not implemented.
*/
class UnknownBlock extends Transparent{ class UnknownBlock extends Transparent{
private int $stateData; private int $stateData;

View File

@ -175,20 +175,6 @@ final class EntityFactory{
}, ['Human']); }, ['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. * 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"); throw new \InvalidArgumentException("At least one save name must be provided");
} }
Utils::testValidInstance($className, Entity::class); 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){ foreach($saveNames as $name){
$this->creationFuncs[$name] = $creationFunc; $this->creationFuncs[$name] = $creationFunc;

View File

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

View File

@ -88,6 +88,7 @@ use pocketmine\network\mcpe\protocol\SetTimePacket;
use pocketmine\network\mcpe\protocol\SetTitlePacket; use pocketmine\network\mcpe\protocol\SetTitlePacket;
use pocketmine\network\mcpe\protocol\TakeItemActorPacket; use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
use pocketmine\network\mcpe\protocol\TextPacket; use pocketmine\network\mcpe\protocol\TextPacket;
use pocketmine\network\mcpe\protocol\ToastRequestPacket;
use pocketmine\network\mcpe\protocol\TransferPacket; use pocketmine\network\mcpe\protocol\TransferPacket;
use pocketmine\network\mcpe\protocol\types\BlockPosition; use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\command\CommandData; use pocketmine\network\mcpe\protocol\types\command\CommandData;
@ -1100,6 +1101,10 @@ class NetworkSession{
$this->sendDataPacket(EmotePacket::create($from->getId(), $emoteId, EmotePacket::FLAG_SERVER)); $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{ public function tick() : void{
if($this->info === null){ if($this->info === null){
if(time() >= $this->connectTime + 10){ if(time() >= $this->connectTime + 10){

View File

@ -420,6 +420,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 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{ public function setAllowFlight(bool $value) : void{
if($this->allowFlight !== $value){ if($this->allowFlight !== $value){
$this->allowFlight = $value; $this->allowFlight = $value;
@ -427,10 +436,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{ public function getAllowFlight() : bool{
return $this->allowFlight; 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{ public function setHasBlockCollision(bool $value) : void{
if($this->blockCollision !== $value){ if($this->blockCollision !== $value){
$this->blockCollision = $value; $this->blockCollision = $value;
@ -438,6 +461,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{ public function hasBlockCollision() : bool{
return $this->blockCollision; return $this->blockCollision;
} }
@ -1023,7 +1050,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
protected function internalSetGameMode(GameMode $gameMode) : void{ protected function internalSetGameMode(GameMode $gameMode) : void{
$this->gamemode = $gameMode; $this->gamemode = $gameMode;
$this->allowFlight = $this->isCreative(); $this->allowFlight = $this->gamemode->equals(GameMode::CREATIVE());
$this->hungerManager->setEnabled($this->isSurvival()); $this->hungerManager->setEnabled($this->isSurvival());
if($this->isSpectator()){ if($this->isSpectator()){
@ -1992,6 +2019,13 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->getNetworkSession()->onTip($message); $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. * Sends a Form to the player, or queue to send it if a form is already open.
* *