mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-06 20:07:09 +00:00
Merge 'minor-next' into 'major-next'
Automatic merge performed by: https://github.com/pmmp/RestrictedActions/actions/runs/15288247521
This commit is contained in:
commit
d5a1007c80
@ -786,8 +786,9 @@ final class BlockTypeIds{
|
||||
public const RESIN_BRICKS = 10756;
|
||||
public const RESIN_CLUMP = 10757;
|
||||
public const CHISELED_RESIN_BRICKS = 10758;
|
||||
public const RESPAWN_ANCHOR = 10759;
|
||||
|
||||
public const FIRST_UNUSED_BLOCK_ID = 10759;
|
||||
public const FIRST_UNUSED_BLOCK_ID = 10760;
|
||||
|
||||
private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID;
|
||||
|
||||
|
123
src/block/RespawnAnchor.php
Normal file
123
src/block/RespawnAnchor.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block;
|
||||
|
||||
use pocketmine\data\runtime\RuntimeDataDescriber;
|
||||
use pocketmine\event\block\BlockPreExplodeEvent;
|
||||
use pocketmine\event\player\PlayerRespawnAnchorUseEvent;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\ItemTypeIds;
|
||||
use pocketmine\lang\KnownTranslationFactory;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\world\Explosion;
|
||||
use pocketmine\world\Position;
|
||||
use pocketmine\world\sound\RespawnAnchorChargeSound;
|
||||
use pocketmine\world\sound\RespawnAnchorSetSpawnSound;
|
||||
|
||||
final class RespawnAnchor extends Opaque{
|
||||
private const MIN_CHARGES = 0;
|
||||
private const MAX_CHARGES = 4;
|
||||
|
||||
private int $charges = self::MIN_CHARGES;
|
||||
|
||||
protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{
|
||||
$w->boundedIntAuto(self::MIN_CHARGES, self::MAX_CHARGES, $this->charges);
|
||||
}
|
||||
|
||||
public function getCharges() : int{
|
||||
return $this->charges;
|
||||
}
|
||||
|
||||
/** @return $this */
|
||||
public function setCharges(int $charges) : self{
|
||||
if($charges < self::MIN_CHARGES || $charges > self::MAX_CHARGES){
|
||||
throw new \InvalidArgumentException("Charges must be between " . self::MIN_CHARGES . " and " . self::MAX_CHARGES . ", given: $charges");
|
||||
}
|
||||
$this->charges = $charges;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLightLevel() : int{
|
||||
return $this->charges > 0 ? ($this->charges * 4) - 1 : 0;
|
||||
}
|
||||
|
||||
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
|
||||
if($item->getTypeId() === ItemTypeIds::fromBlockTypeId(BlockTypeIds::GLOWSTONE) && $this->charges < self::MAX_CHARGES){
|
||||
$this->position->getWorld()->setBlock($this->position, $this->setCharges($this->charges + 1));
|
||||
$this->position->getWorld()->addSound($this->position, new RespawnAnchorChargeSound());
|
||||
return true;
|
||||
}
|
||||
|
||||
if($this->charges > self::MIN_CHARGES){
|
||||
if($player === null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$ev = new PlayerRespawnAnchorUseEvent($player, $this, PlayerRespawnAnchorUseEvent::ACTION_EXPLODE);
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return false;
|
||||
}
|
||||
|
||||
switch($ev->getAction()){
|
||||
case PlayerRespawnAnchorUseEvent::ACTION_EXPLODE:
|
||||
$this->explode($player);
|
||||
return false;
|
||||
|
||||
case PlayerRespawnAnchorUseEvent::ACTION_SET_SPAWN:
|
||||
if($player->getSpawn() !== null && $player->getSpawn()->equals($this->position)){
|
||||
return true;
|
||||
}
|
||||
|
||||
$player->setSpawn($this->position);
|
||||
$this->position->getWorld()->addSound($this->position, new RespawnAnchorSetSpawnSound());
|
||||
$player->sendMessage(KnownTranslationFactory::tile_respawn_anchor_respawnSet()->prefix(TextFormat::GRAY));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function explode(?Player $player) : void{
|
||||
$ev = new BlockPreExplodeEvent($this, 5, $player);
|
||||
$ev->setIncendiary(true);
|
||||
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return;
|
||||
}
|
||||
|
||||
$this->position->getWorld()->setBlock($this->position, VanillaBlocks::AIR());
|
||||
|
||||
$explosion = new Explosion(Position::fromObject($this->position->add(0.5, 0.5, 0.5), $this->position->getWorld()), $ev->getRadius(), $this);
|
||||
$explosion->setFireChance($ev->getFireChance());
|
||||
|
||||
if($ev->isBlockBreaking()){
|
||||
$explosion->explodeA();
|
||||
}
|
||||
$explosion->explodeB();
|
||||
}
|
||||
}
|
@ -694,6 +694,7 @@ use function strtolower;
|
||||
* @method static Stair RESIN_BRICK_STAIRS()
|
||||
* @method static Wall RESIN_BRICK_WALL()
|
||||
* @method static ResinClump RESIN_CLUMP()
|
||||
* @method static RespawnAnchor RESPAWN_ANCHOR()
|
||||
* @method static DoublePlant ROSE_BUSH()
|
||||
* @method static Sand SAND()
|
||||
* @method static Opaque SANDSTONE()
|
||||
@ -1647,6 +1648,8 @@ final class VanillaBlocks{
|
||||
self::register("warped_roots", fn(BID $id) => new NetherRoots($id, "Warped Roots", $netherRootsInfo));
|
||||
|
||||
self::register("chain", fn(BID $id) => new Chain($id, "Chain", new Info(BreakInfo::pickaxe(5.0, ToolTier::WOOD, 30.0))));
|
||||
|
||||
self::register("respawn_anchor", fn(BID $id) => new RespawnAnchor($id, "Respawn Anchor", new Info(BreakInfo::pickaxe(50.0, ToolTier::DIAMOND, 6000.0))));
|
||||
}
|
||||
|
||||
private static function registerBlocksR17() : void{
|
||||
|
@ -122,6 +122,7 @@ use pocketmine\block\RedstoneRepeater;
|
||||
use pocketmine\block\RedstoneTorch;
|
||||
use pocketmine\block\RedstoneWire;
|
||||
use pocketmine\block\ResinClump;
|
||||
use pocketmine\block\RespawnAnchor;
|
||||
use pocketmine\block\RuntimeBlockStateRegistry;
|
||||
use pocketmine\block\Sapling;
|
||||
use pocketmine\block\SeaPickle;
|
||||
@ -1753,6 +1754,10 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
|
||||
return Writer::create(Ids::RESIN_CLUMP)
|
||||
->writeFacingFlags($block->getFaces());
|
||||
});
|
||||
$this->map(Blocks::RESPAWN_ANCHOR(), function(RespawnAnchor $block) : Writer{
|
||||
return Writer::create(Ids::RESPAWN_ANCHOR)
|
||||
->writeInt(StateNames::RESPAWN_ANCHOR_CHARGE, $block->getCharges());
|
||||
});
|
||||
$this->map(Blocks::ROSE_BUSH(), fn(DoublePlant $block) => Helper::encodeDoublePlant($block, Writer::create(Ids::ROSE_BUSH)));
|
||||
$this->mapSlab(Blocks::SANDSTONE_SLAB(), Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB);
|
||||
$this->mapStairs(Blocks::SANDSTONE_STAIRS(), Ids::SANDSTONE_STAIRS);
|
||||
|
@ -1717,6 +1717,10 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
|
||||
$this->mapStairs(Ids::RESIN_BRICK_STAIRS, fn() => Blocks::RESIN_BRICK_STAIRS());
|
||||
$this->map(Ids::RESIN_BRICK_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::RESIN_BRICK_WALL(), $in));
|
||||
$this->map(Ids::RESIN_CLUMP, fn(Reader $in) => Blocks::RESIN_CLUMP()->setFaces($in->readFacingFlags()));
|
||||
$this->map(Ids::RESPAWN_ANCHOR, function(Reader $in) : Block{
|
||||
return Blocks::RESPAWN_ANCHOR()
|
||||
->setCharges($in->readBoundedInt(StateNames::RESPAWN_ANCHOR_CHARGE, 0, 4));
|
||||
});
|
||||
$this->mapSlab(Ids::SANDSTONE_SLAB, Ids::SANDSTONE_DOUBLE_SLAB, fn() => Blocks::SANDSTONE_SLAB());
|
||||
$this->mapStairs(Ids::SANDSTONE_STAIRS, fn() => Blocks::SANDSTONE_STAIRS());
|
||||
$this->map(Ids::SANDSTONE_WALL, fn(Reader $in) => Helper::decodeWall(Blocks::SANDSTONE_WALL(), $in));
|
||||
|
@ -129,7 +129,7 @@ class EndCrystal extends Entity implements Explosive{
|
||||
$ev = new EntityPreExplodeEvent($this, 6);
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
$explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this);
|
||||
$explosion = new Explosion($this->getPosition(), $ev->getRadius(), $this, $ev->getFireChance());
|
||||
if($ev->isBlockBreaking()){
|
||||
$explosion->explodeA();
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ class PrimedTNT extends Entity implements Explosive{
|
||||
$ev->call();
|
||||
if(!$ev->isCancelled()){
|
||||
//TODO: deal with underwater TNT (underwater TNT treats water as if it has a blast resistance of 0)
|
||||
$explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this);
|
||||
$explosion = new Explosion(Position::fromObject($this->location->add(0, $this->size->getHeight() / 2, 0), $this->getWorld()), $ev->getRadius(), $this, $ev->getFireChance());
|
||||
if($ev->isBlockBreaking()){
|
||||
$explosion->explodeA();
|
||||
}
|
||||
|
122
src/event/block/BlockExplodeEvent.php
Normal file
122
src/event/block/BlockExplodeEvent.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\block;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\Position;
|
||||
|
||||
/**
|
||||
* Called when a block explodes, after explosion impact has been calculated.
|
||||
*
|
||||
* @see BlockPreExplodeEvent
|
||||
*/
|
||||
class BlockExplodeEvent extends BlockEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
/**
|
||||
* @param Block[] $blocks
|
||||
* @param Block[] $ignitions
|
||||
*/
|
||||
public function __construct(
|
||||
Block $block,
|
||||
private Position $position,
|
||||
private array $blocks,
|
||||
private float $yield,
|
||||
private array $ignitions
|
||||
){
|
||||
parent::__construct($block);
|
||||
|
||||
Utils::checkFloatNotInfOrNaN("yield", $yield);
|
||||
if($yield < 0.0 || $yield > 100.0){
|
||||
throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0");
|
||||
}
|
||||
}
|
||||
|
||||
public function getPosition() : Position{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the percentage chance of drops from each block destroyed by the explosion.
|
||||
*
|
||||
* @return float 0-100
|
||||
*/
|
||||
public function getYield() : float{
|
||||
return $this->yield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the percentage chance of drops from each block destroyed by the explosion.
|
||||
*
|
||||
* @param float $yield 0-100
|
||||
*/
|
||||
public function setYield(float $yield) : void{
|
||||
Utils::checkFloatNotInfOrNaN("yield", $yield);
|
||||
if($yield < 0.0 || $yield > 100.0){
|
||||
throw new \InvalidArgumentException("Yield must be in range 0.0 - 100.0");
|
||||
}
|
||||
$this->yield = $yield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of blocks destroyed by the explosion.
|
||||
*
|
||||
* @return Block[]
|
||||
*/
|
||||
public function getAffectedBlocks() : array{
|
||||
return $this->blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the blocks destroyed by the explosion.
|
||||
*
|
||||
* @param Block[] $blocks
|
||||
*/
|
||||
public function setAffectedBlocks(array $blocks) : void{
|
||||
Utils::validateArrayValueType($blocks, fn(Block $block) => null);
|
||||
$this->blocks = $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of affected blocks that will be replaced by fire.
|
||||
*
|
||||
* @return Block[]
|
||||
*/
|
||||
public function getIgnitions() : array{
|
||||
return $this->ignitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of blocks that will be replaced by fire.
|
||||
*
|
||||
* @param Block[] $ignitions
|
||||
*/
|
||||
public function setIgnitions(array $ignitions) : void{
|
||||
Utils::validateArrayValueType($ignitions, fn(Block $block) => null);
|
||||
$this->ignitions = $ignitions;
|
||||
}
|
||||
}
|
129
src/event/block/BlockPreExplodeEvent.php
Normal file
129
src/event/block/BlockPreExplodeEvent.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\block;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\Explosion;
|
||||
|
||||
/**
|
||||
* Called when a block wants to explode, before the explosion impact is calculated.
|
||||
* This allows changing the explosion force, fire chance and whether it will destroy blocks.
|
||||
*
|
||||
* @see BlockExplodeEvent
|
||||
*/
|
||||
class BlockPreExplodeEvent extends BlockEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
private bool $blockBreaking = true;
|
||||
|
||||
public function __construct(
|
||||
Block $block,
|
||||
private float $radius,
|
||||
private readonly ?Player $player = null,
|
||||
private float $fireChance = 0.0
|
||||
){
|
||||
Utils::checkFloatNotInfOrNaN("radius", $radius);
|
||||
if($radius <= 0){
|
||||
throw new \InvalidArgumentException("Explosion radius must be positive");
|
||||
}
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
|
||||
}
|
||||
parent::__construct($block);
|
||||
}
|
||||
|
||||
public function getRadius() : float{
|
||||
return $this->radius;
|
||||
}
|
||||
|
||||
public function setRadius(float $radius) : void{
|
||||
Utils::checkFloatNotInfOrNaN("radius", $radius);
|
||||
if($radius <= 0){
|
||||
throw new \InvalidArgumentException("Explosion radius must be positive");
|
||||
}
|
||||
$this->radius = $radius;
|
||||
}
|
||||
|
||||
public function isBlockBreaking() : bool{
|
||||
return $this->blockBreaking;
|
||||
}
|
||||
|
||||
public function setBlockBreaking(bool $affectsBlocks) : void{
|
||||
$this->blockBreaking = $affectsBlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the explosion will create a fire.
|
||||
*/
|
||||
public function isIncendiary() : bool{
|
||||
return $this->fireChance > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the explosion will create a fire by filling fireChance with default values.
|
||||
*
|
||||
* If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0).
|
||||
*/
|
||||
public function setIncendiary(bool $incendiary) : void{
|
||||
if(!$incendiary){
|
||||
$this->fireChance = 0;
|
||||
}elseif($this->fireChance <= 0){
|
||||
$this->fireChance = Explosion::DEFAULT_FIRE_CHANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a chance between 0 and 1 of creating a fire.
|
||||
*/
|
||||
public function getFireChance() : float{
|
||||
return $this->fireChance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a chance between 0 and 1 of creating a fire.
|
||||
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
|
||||
*
|
||||
* @param float $fireChance 0 ... 1
|
||||
*/
|
||||
public function setFireChance(float $fireChance) : void{
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
|
||||
}
|
||||
$this->fireChance = $fireChance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player who triggered the block explosion.
|
||||
* Returns null if the block was exploded by other means.
|
||||
*/
|
||||
public function getPlayer() : ?Player{
|
||||
return $this->player;
|
||||
}
|
||||
}
|
@ -43,13 +43,15 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
|
||||
|
||||
/**
|
||||
* @param Block[] $blocks
|
||||
* @param float $yield 0-100
|
||||
* @param float $yield 0-100
|
||||
* @param Block[] $ignitions
|
||||
*/
|
||||
public function __construct(
|
||||
Entity $entity,
|
||||
protected Position $position,
|
||||
protected array $blocks,
|
||||
protected float $yield
|
||||
protected float $yield,
|
||||
private array $ignitions
|
||||
){
|
||||
$this->entity = $entity;
|
||||
if($yield < 0.0 || $yield > 100.0){
|
||||
@ -98,4 +100,23 @@ class EntityExplodeEvent extends EntityEvent implements Cancellable{
|
||||
}
|
||||
$this->yield = $yield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of blocks that will be replaced by fire.
|
||||
*
|
||||
* @param Block[] $ignitions
|
||||
*/
|
||||
public function setIgnitions(array $ignitions) : void{
|
||||
Utils::validateArrayValueType($ignitions, fn(Block $block) => null);
|
||||
$this->ignitions = $ignitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of affected blocks that will be replaced by fire.
|
||||
*
|
||||
* @return Block[]
|
||||
*/
|
||||
public function getIgnitions() : array{
|
||||
return $this->ignitions;
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,8 @@ namespace pocketmine\event\entity;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\Explosion;
|
||||
|
||||
/**
|
||||
* Called when an entity decides to explode, before the explosion's impact is calculated.
|
||||
@ -42,11 +44,16 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{
|
||||
|
||||
public function __construct(
|
||||
Entity $entity,
|
||||
protected float $radius
|
||||
protected float $radius,
|
||||
private float $fireChance = 0.0,
|
||||
){
|
||||
if($radius <= 0){
|
||||
throw new \InvalidArgumentException("Explosion radius must be positive");
|
||||
}
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be between 0 and 1.");
|
||||
}
|
||||
$this->entity = $entity;
|
||||
}
|
||||
|
||||
@ -61,6 +68,47 @@ class EntityPreExplodeEvent extends EntityEvent implements Cancellable{
|
||||
$this->radius = $radius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the explosion will create a fire.
|
||||
*/
|
||||
public function isIncendiary() : bool{
|
||||
return $this->fireChance > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the explosion will create a fire by filling fireChance with default values.
|
||||
*
|
||||
* If $incendiary is true, the fire chance will be filled only if explosion isn't currently creating a fire (if fire chance is 0).
|
||||
*/
|
||||
public function setIncendiary(bool $incendiary) : void{
|
||||
if(!$incendiary){
|
||||
$this->fireChance = 0;
|
||||
}elseif($this->fireChance <= 0){
|
||||
$this->fireChance = Explosion::DEFAULT_FIRE_CHANCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a chance between 0 and 1 of creating a fire.
|
||||
*/
|
||||
public function getFireChance() : float{
|
||||
return $this->fireChance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a chance between 0 and 1 of creating a fire.
|
||||
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
|
||||
*
|
||||
* @param float $fireChance 0 ... 1
|
||||
*/
|
||||
public function setFireChance(float $fireChance) : void{
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be between 0 and 1.");
|
||||
}
|
||||
$this->fireChance = $fireChance;
|
||||
}
|
||||
|
||||
public function isBlockBreaking() : bool{
|
||||
return $this->blockBreaking;
|
||||
}
|
||||
|
56
src/event/player/PlayerRespawnAnchorUseEvent.php
Normal file
56
src/event/player/PlayerRespawnAnchorUseEvent.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\event\player;
|
||||
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\event\Cancellable;
|
||||
use pocketmine\event\CancellableTrait;
|
||||
use pocketmine\player\Player;
|
||||
|
||||
class PlayerRespawnAnchorUseEvent extends PlayerEvent implements Cancellable{
|
||||
use CancellableTrait;
|
||||
|
||||
public const ACTION_EXPLODE = 0;
|
||||
public const ACTION_SET_SPAWN = 1;
|
||||
|
||||
public function __construct(
|
||||
Player $player,
|
||||
protected Block $block,
|
||||
private int $action = self::ACTION_EXPLODE
|
||||
){
|
||||
$this->player = $player;
|
||||
}
|
||||
|
||||
public function getBlock() : Block{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function getAction() : int{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
public function setAction(int $action) : void{
|
||||
$this->action = $action;
|
||||
}
|
||||
}
|
@ -993,6 +993,7 @@ final class StringToItemParser extends StringToTParser{
|
||||
$result->registerBlock("resin_brick_wall", fn() => Blocks::RESIN_BRICK_WALL());
|
||||
$result->registerBlock("resin_bricks", fn() => Blocks::RESIN_BRICKS());
|
||||
$result->registerBlock("resin_clump", fn() => Blocks::RESIN_CLUMP());
|
||||
$result->registerBlock("respawn_anchor", fn() => Blocks::RESPAWN_ANCHOR());
|
||||
$result->registerBlock("rooted_dirt", fn() => Blocks::DIRT()->setDirtType(DirtType::ROOTED));
|
||||
$result->registerBlock("rose", fn() => Blocks::POPPY());
|
||||
$result->registerBlock("rose_bush", fn() => Blocks::ROSE_BUSH());
|
||||
|
@ -27,6 +27,7 @@ use DateTimeImmutable;
|
||||
use pocketmine\block\BaseSign;
|
||||
use pocketmine\block\Bed;
|
||||
use pocketmine\block\BlockTypeTags;
|
||||
use pocketmine\block\RespawnAnchor;
|
||||
use pocketmine\block\UnknownBlock;
|
||||
use pocketmine\block\VanillaBlocks;
|
||||
use pocketmine\command\CommandSender;
|
||||
@ -137,6 +138,7 @@ use pocketmine\world\sound\EntityAttackNoDamageSound;
|
||||
use pocketmine\world\sound\EntityAttackSound;
|
||||
use pocketmine\world\sound\FireExtinguishSound;
|
||||
use pocketmine\world\sound\ItemBreakSound;
|
||||
use pocketmine\world\sound\RespawnAnchorDepleteSound;
|
||||
use pocketmine\world\sound\Sound;
|
||||
use pocketmine\world\World;
|
||||
use pocketmine\YmlServerProperties;
|
||||
@ -2543,6 +2545,21 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
|
||||
}
|
||||
$this->logger->debug("Respawn position located, completing respawn");
|
||||
$ev = new PlayerRespawnEvent($this, $safeSpawn);
|
||||
$spawnPosition = $ev->getRespawnPosition();
|
||||
$spawnBlock = $spawnPosition->getWorld()->getBlock($spawnPosition);
|
||||
if($spawnBlock instanceof RespawnAnchor){
|
||||
if($spawnBlock->getCharges() > 0){
|
||||
$spawnPosition->getWorld()->setBlock($spawnPosition, $spawnBlock->setCharges($spawnBlock->getCharges() - 1));
|
||||
$spawnPosition->getWorld()->addSound($spawnPosition, new RespawnAnchorDepleteSound());
|
||||
}else{
|
||||
$defaultSpawn = $this->server->getWorldManager()->getDefaultWorld()?->getSpawnLocation();
|
||||
if($defaultSpawn !== null){
|
||||
$this->setSpawn($defaultSpawn);
|
||||
$ev->setRespawnPosition($defaultSpawn);
|
||||
$this->sendMessage(KnownTranslationFactory::tile_respawn_anchor_notValid()->prefix(TextFormat::GRAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
$ev->call();
|
||||
|
||||
$realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld());
|
||||
|
@ -26,16 +26,20 @@ namespace pocketmine\world;
|
||||
use pocketmine\block\Block;
|
||||
use pocketmine\block\RuntimeBlockStateRegistry;
|
||||
use pocketmine\block\TNT;
|
||||
use pocketmine\block\utils\SupportType;
|
||||
use pocketmine\block\VanillaBlocks;
|
||||
use pocketmine\entity\Entity;
|
||||
use pocketmine\event\block\BlockExplodeEvent;
|
||||
use pocketmine\event\entity\EntityDamageByBlockEvent;
|
||||
use pocketmine\event\entity\EntityDamageByEntityEvent;
|
||||
use pocketmine\event\entity\EntityDamageEvent;
|
||||
use pocketmine\event\entity\EntityExplodeEvent;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\math\AxisAlignedBB;
|
||||
use pocketmine\math\Facing;
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\Utils;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
use pocketmine\world\particle\HugeExplodeSeedParticle;
|
||||
use pocketmine\world\sound\ExplodeSound;
|
||||
@ -48,25 +52,36 @@ use function mt_rand;
|
||||
use function sqrt;
|
||||
|
||||
class Explosion{
|
||||
public const DEFAULT_FIRE_CHANCE = 1.0 / 3.0;
|
||||
|
||||
private int $rays = 16;
|
||||
public World $world;
|
||||
|
||||
/** @var Block[] */
|
||||
/**
|
||||
* @var Block[]
|
||||
* @phpstan-var array<int, Block>
|
||||
*/
|
||||
public array $affectedBlocks = [];
|
||||
public float $stepLen = 0.3;
|
||||
/** @var Block[] */
|
||||
private array $fireIgnitions = [];
|
||||
|
||||
private SubChunkExplorer $subChunkExplorer;
|
||||
|
||||
public function __construct(
|
||||
public Position $source,
|
||||
public float $radius,
|
||||
private Entity|Block|null $what = null
|
||||
private Entity|Block|null $what = null,
|
||||
private float $fireChance = 0.0
|
||||
){
|
||||
if(!$this->source->isValid()){
|
||||
throw new \InvalidArgumentException("Position does not have a valid world");
|
||||
}
|
||||
$this->world = $this->source->getWorld();
|
||||
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
|
||||
}
|
||||
if($radius <= 0){
|
||||
throw new \InvalidArgumentException("Explosion radius must be greater than 0, got $radius");
|
||||
}
|
||||
@ -85,6 +100,7 @@ class Explosion{
|
||||
$blockFactory = RuntimeBlockStateRegistry::getInstance();
|
||||
|
||||
$mRays = $this->rays - 1;
|
||||
$incendiary = $this->fireChance > 0;
|
||||
for($i = 0; $i < $this->rays; ++$i){
|
||||
for($j = 0; $j < $this->rays; ++$j){
|
||||
for($k = 0; $k < $this->rays; ++$k){
|
||||
@ -127,7 +143,12 @@ class Explosion{
|
||||
$_block = $this->world->getBlockAt($vBlockX, $vBlockY, $vBlockZ, true, false);
|
||||
foreach($_block->getAffectedBlocks() as $_affectedBlock){
|
||||
$_affectedBlockPos = $_affectedBlock->getPosition();
|
||||
$this->affectedBlocks[World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z)] = $_affectedBlock;
|
||||
$posHash = World::blockHash($_affectedBlockPos->x, $_affectedBlockPos->y, $_affectedBlockPos->z);
|
||||
$this->affectedBlocks[$posHash] = $_affectedBlock;
|
||||
|
||||
if($incendiary && Utils::getRandomFloat() <= $this->fireChance){
|
||||
$this->fireIgnitions[$posHash] = $_affectedBlock;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -150,13 +171,32 @@ class Explosion{
|
||||
$yield = min(100, (1 / $this->radius) * 100);
|
||||
|
||||
if($this->what instanceof Entity){
|
||||
$ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield);
|
||||
$ev = new EntityExplodeEvent($this->what, $this->source, $this->affectedBlocks, $yield, $this->fireIgnitions);
|
||||
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return false;
|
||||
}
|
||||
|
||||
$yield = $ev->getYield();
|
||||
$this->affectedBlocks = $ev->getBlockList();
|
||||
$this->fireIgnitions = $ev->getIgnitions();
|
||||
}elseif($this->what instanceof Block){
|
||||
$ev = new BlockExplodeEvent(
|
||||
$this->what,
|
||||
$this->source,
|
||||
$this->affectedBlocks,
|
||||
$yield,
|
||||
$this->fireIgnitions,
|
||||
);
|
||||
|
||||
$ev->call();
|
||||
if($ev->isCancelled()){
|
||||
return false;
|
||||
}else{
|
||||
$yield = $ev->getYield();
|
||||
$this->affectedBlocks = $ev->getBlockList();
|
||||
$this->affectedBlocks = $ev->getAffectedBlocks();
|
||||
$this->fireIgnitions = $ev->getIgnitions();
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,8 +238,9 @@ class Explosion{
|
||||
|
||||
$air = VanillaItems::AIR();
|
||||
$airBlock = VanillaBlocks::AIR();
|
||||
$fireBlock = VanillaBlocks::FIRE();
|
||||
|
||||
foreach($this->affectedBlocks as $block){
|
||||
foreach($this->affectedBlocks as $hash => $block){
|
||||
$pos = $block->getPosition();
|
||||
if($block instanceof TNT){
|
||||
$block->ignite(mt_rand(10, 30));
|
||||
@ -212,7 +253,13 @@ class Explosion{
|
||||
if(($t = $this->world->getTileAt($pos->x, $pos->y, $pos->z)) !== null){
|
||||
$t->onBlockDestroyed(); //needed to create drops for inventories
|
||||
}
|
||||
$this->world->setBlockAt($pos->x, $pos->y, $pos->z, $airBlock);
|
||||
$targetBlock =
|
||||
isset($this->fireIgnitions[$hash]) &&
|
||||
$block->getSide(Facing::DOWN)->getSupportType(Facing::UP) === SupportType::FULL ?
|
||||
$fireBlock :
|
||||
$airBlock;
|
||||
|
||||
$this->world->setBlockAt($pos->x, $pos->y, $pos->z, $targetBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,4 +268,18 @@ class Explosion{
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a chance between 0 and 1 of creating a fire.
|
||||
* For example, if the chance is 1/3, then that amount of affected blocks will be ignited.
|
||||
*
|
||||
* @param float $fireChance 0 ... 1
|
||||
*/
|
||||
public function setFireChance(float $fireChance) : void{
|
||||
Utils::checkFloatNotInfOrNaN("fireChance", $fireChance);
|
||||
if($fireChance < 0.0 || $fireChance > 1.0){
|
||||
throw new \InvalidArgumentException("Fire chance must be a number between 0 and 1.");
|
||||
}
|
||||
$this->fireChance = $fireChance;
|
||||
}
|
||||
}
|
||||
|
@ -93,9 +93,11 @@ use pocketmine\world\format\io\GlobalBlockStateHandlers;
|
||||
use pocketmine\world\format\io\WritableWorldProvider;
|
||||
use pocketmine\world\format\LightArray;
|
||||
use pocketmine\world\format\SubChunk;
|
||||
use pocketmine\world\generator\executor\AsyncGeneratorExecutor;
|
||||
use pocketmine\world\generator\executor\GeneratorExecutor;
|
||||
use pocketmine\world\generator\executor\GeneratorExecutorSetupParameters;
|
||||
use pocketmine\world\generator\executor\SyncGeneratorExecutor;
|
||||
use pocketmine\world\generator\GeneratorManager;
|
||||
use pocketmine\world\generator\GeneratorRegisterTask;
|
||||
use pocketmine\world\generator\GeneratorUnregisterTask;
|
||||
use pocketmine\world\generator\PopulationTask;
|
||||
use pocketmine\world\light\BlockLightUpdate;
|
||||
use pocketmine\world\light\LightPopulationTask;
|
||||
@ -336,11 +338,7 @@ class World implements ChunkManager{
|
||||
*/
|
||||
private array $chunkPopulationRequestQueueIndex = [];
|
||||
|
||||
/**
|
||||
* @var true[]
|
||||
* @phpstan-var array<int, true>
|
||||
*/
|
||||
private array $generatorRegisteredWorkers = [];
|
||||
private readonly GeneratorExecutor $generatorExecutor;
|
||||
|
||||
private bool $autoSave = true;
|
||||
|
||||
@ -360,9 +358,6 @@ class World implements ChunkManager{
|
||||
|
||||
private bool $doingTick = false;
|
||||
|
||||
/** @phpstan-var class-string<generator\Generator> */
|
||||
private string $generator;
|
||||
|
||||
private bool $unloaded = false;
|
||||
/**
|
||||
* @var \Closure[]
|
||||
@ -498,7 +493,23 @@ class World implements ChunkManager{
|
||||
$generator = GeneratorManager::getInstance()->getGenerator($this->provider->getWorldData()->getGenerator()) ??
|
||||
throw new AssumptionFailedError("WorldManager should already have checked that the generator exists");
|
||||
$generator->validateGeneratorOptions($this->provider->getWorldData()->getGeneratorOptions());
|
||||
$this->generator = $generator->getGeneratorClass();
|
||||
|
||||
$executorSetupParameters = new GeneratorExecutorSetupParameters(
|
||||
worldMinY: $this->minY,
|
||||
worldMaxY: $this->maxY,
|
||||
generatorSeed: $this->getSeed(),
|
||||
generatorClass: $generator->getGeneratorClass(),
|
||||
generatorSettings: $this->provider->getWorldData()->getGeneratorOptions()
|
||||
);
|
||||
$this->generatorExecutor = $generator->isFast() ?
|
||||
new SyncGeneratorExecutor($executorSetupParameters) :
|
||||
new AsyncGeneratorExecutor(
|
||||
$this->logger,
|
||||
$this->workerPool,
|
||||
$executorSetupParameters,
|
||||
$this->worldId
|
||||
);
|
||||
|
||||
$this->chunkPopulationRequestQueue = new \SplQueue();
|
||||
$this->addOnUnloadCallback(function() : void{
|
||||
$this->logger->debug("Cancelling unfulfilled generation requests");
|
||||
@ -534,17 +545,6 @@ class World implements ChunkManager{
|
||||
$this->initRandomTickBlocksFromConfig($cfg);
|
||||
|
||||
$this->timings = new WorldTimings($this);
|
||||
|
||||
$this->workerPool->addWorkerStartHook($workerStartHook = function(int $workerId) : void{
|
||||
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
|
||||
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
|
||||
unset($this->generatorRegisteredWorkers[$workerId]);
|
||||
}
|
||||
});
|
||||
$workerPool = $this->workerPool;
|
||||
$this->addOnUnloadCallback(static function() use ($workerPool, $workerStartHook) : void{
|
||||
$workerPool->removeWorkerStartHook($workerStartHook);
|
||||
});
|
||||
}
|
||||
|
||||
private function initRandomTickBlocksFromConfig(ServerConfigGroup $cfg) : void{
|
||||
@ -585,21 +585,6 @@ class World implements ChunkManager{
|
||||
return $this->tickRateTime;
|
||||
}
|
||||
|
||||
public function registerGeneratorToWorker(int $worker) : void{
|
||||
$this->logger->debug("Registering generator on worker $worker");
|
||||
$this->workerPool->submitTaskToWorker(new GeneratorRegisterTask($this, $this->generator, $this->provider->getWorldData()->getGeneratorOptions()), $worker);
|
||||
$this->generatorRegisteredWorkers[$worker] = true;
|
||||
}
|
||||
|
||||
public function unregisterGenerator() : void{
|
||||
foreach($this->workerPool->getRunningWorkers() as $i){
|
||||
if(isset($this->generatorRegisteredWorkers[$i])){
|
||||
$this->workerPool->submitTaskToWorker(new GeneratorUnregisterTask($this), $i);
|
||||
}
|
||||
}
|
||||
$this->generatorRegisteredWorkers = [];
|
||||
}
|
||||
|
||||
public function getServer() : Server{
|
||||
return $this->server;
|
||||
}
|
||||
@ -657,7 +642,7 @@ class World implements ChunkManager{
|
||||
|
||||
$this->save();
|
||||
|
||||
$this->unregisterGenerator();
|
||||
$this->generatorExecutor->shutdown();
|
||||
|
||||
$this->provider->close();
|
||||
$this->blockCache = [];
|
||||
@ -3467,8 +3452,8 @@ class World implements ChunkManager{
|
||||
|
||||
$centerChunk = $this->loadChunk($chunkX, $chunkZ);
|
||||
$adjacentChunks = $this->getAdjacentChunks($chunkX, $chunkZ);
|
||||
$task = new PopulationTask(
|
||||
$this->worldId,
|
||||
|
||||
$this->generatorExecutor->populate(
|
||||
$chunkX,
|
||||
$chunkZ,
|
||||
$centerChunk,
|
||||
@ -3481,15 +3466,6 @@ class World implements ChunkManager{
|
||||
$this->generateChunkCallback($chunkPopulationLockId, $chunkX, $chunkZ, $centerChunk, $adjacentChunks, $temporaryChunkLoader);
|
||||
}
|
||||
);
|
||||
$workerId = $this->workerPool->selectWorker();
|
||||
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
|
||||
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
|
||||
unset($this->generatorRegisteredWorkers[$workerId]);
|
||||
}
|
||||
if(!isset($this->generatorRegisteredWorkers[$workerId])){
|
||||
$this->registerGeneratorToWorker($workerId);
|
||||
}
|
||||
$this->workerPool->submitTaskToWorker($task, $workerId);
|
||||
|
||||
return $resolver->getPromise();
|
||||
}finally{
|
||||
|
@ -50,7 +50,7 @@ final class GeneratorManager{
|
||||
}catch(InvalidGeneratorOptionsException $e){
|
||||
return $e;
|
||||
}
|
||||
});
|
||||
}, fast: true);
|
||||
$this->addGenerator(Normal::class, "normal", fn() => null);
|
||||
$this->addAlias("normal", "default");
|
||||
$this->addGenerator(Nether::class, "nether", fn() => null);
|
||||
@ -62,6 +62,7 @@ final class GeneratorManager{
|
||||
* @param string $name Alias for this generator type that can be written in configs
|
||||
* @param \Closure $presetValidator Callback to validate generator options for new worlds
|
||||
* @param bool $overwrite Whether to force overwriting any existing registered generator with the same name
|
||||
* @param bool $fast Whether this generator is fast enough to run without async tasks
|
||||
*
|
||||
* @phpstan-param \Closure(string) : ?InvalidGeneratorOptionsException $presetValidator
|
||||
*
|
||||
@ -69,7 +70,7 @@ final class GeneratorManager{
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false) : void{
|
||||
public function addGenerator(string $class, string $name, \Closure $presetValidator, bool $overwrite = false, bool $fast = false) : void{
|
||||
Utils::testValidInstance($class, Generator::class);
|
||||
|
||||
$name = strtolower($name);
|
||||
@ -77,7 +78,7 @@ final class GeneratorManager{
|
||||
throw new \InvalidArgumentException("Alias \"$name\" is already assigned");
|
||||
}
|
||||
|
||||
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator);
|
||||
$this->list[$name] = new GeneratorManagerEntry($class, $presetValidator, $fast);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,12 +31,15 @@ final class GeneratorManagerEntry{
|
||||
*/
|
||||
public function __construct(
|
||||
private string $generatorClass,
|
||||
private \Closure $presetValidator
|
||||
private \Closure $presetValidator,
|
||||
private readonly bool $fast
|
||||
){}
|
||||
|
||||
/** @phpstan-return class-string<Generator> */
|
||||
public function getGeneratorClass() : string{ return $this->generatorClass; }
|
||||
|
||||
public function isFast() : bool{ return $this->fast; }
|
||||
|
||||
/**
|
||||
* @throws InvalidGeneratorOptionsException
|
||||
*/
|
||||
|
@ -27,11 +27,18 @@ use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\format\io\FastChunkSerializer;
|
||||
use pocketmine\world\generator\executor\ThreadLocalGeneratorContext;
|
||||
use function array_map;
|
||||
use function igbinary_serialize;
|
||||
use function igbinary_unserialize;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* TODO: this should be moved to the executor namespace, but plugins have unfortunately used it directly due to the
|
||||
* difficulty of regenerating chunks. This should be addressed in the future.
|
||||
* For the remainder of PM5, we can't relocate this class.
|
||||
*
|
||||
* @phpstan-type OnCompletion \Closure(Chunk $centerChunk, array<int, Chunk> $adjacentChunks) : void
|
||||
*/
|
||||
class PopulationTask extends AsyncTask{
|
||||
|
106
src/world/generator/executor/AsyncGeneratorExecutor.php
Normal file
106
src/world/generator/executor/AsyncGeneratorExecutor.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\scheduler\AsyncPool;
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\generator\PopulationTask;
|
||||
use function array_key_exists;
|
||||
|
||||
final class AsyncGeneratorExecutor implements GeneratorExecutor{
|
||||
private static int $nextAsyncContextId = 1;
|
||||
|
||||
private readonly \Logger $logger;
|
||||
|
||||
/** @phpstan-var \Closure(int) : void */
|
||||
private readonly \Closure $workerStartHook;
|
||||
|
||||
private readonly int $asyncContextId;
|
||||
|
||||
/**
|
||||
* @var true[]
|
||||
* @phpstan-var array<int, true>
|
||||
*/
|
||||
private array $generatorRegisteredWorkers = [];
|
||||
|
||||
public function __construct(
|
||||
\Logger $logger,
|
||||
private readonly AsyncPool $workerPool,
|
||||
private readonly GeneratorExecutorSetupParameters $setupParameters,
|
||||
int $asyncContextId = null
|
||||
){
|
||||
$this->logger = new \PrefixedLogger($logger, "AsyncGeneratorExecutor");
|
||||
|
||||
//TODO: we only allow setting this for PM5 because of PopulationTask uses in plugins
|
||||
$this->asyncContextId = $asyncContextId ?? self::$nextAsyncContextId++;
|
||||
|
||||
$this->workerStartHook = function(int $workerId) : void{
|
||||
if(array_key_exists($workerId, $this->generatorRegisteredWorkers)){
|
||||
$this->logger->debug("Worker $workerId with previously registered generator restarted, flagging as unregistered");
|
||||
unset($this->generatorRegisteredWorkers[$workerId]);
|
||||
}
|
||||
};
|
||||
$this->workerPool->addWorkerStartHook($this->workerStartHook);
|
||||
}
|
||||
|
||||
private function registerGeneratorToWorker(int $worker) : void{
|
||||
$this->logger->debug("Registering generator on worker $worker");
|
||||
$this->workerPool->submitTaskToWorker(new AsyncGeneratorRegisterTask($this->setupParameters, $this->asyncContextId), $worker);
|
||||
$this->generatorRegisteredWorkers[$worker] = true;
|
||||
}
|
||||
|
||||
private function unregisterGenerator() : void{
|
||||
foreach($this->workerPool->getRunningWorkers() as $i){
|
||||
if(isset($this->generatorRegisteredWorkers[$i])){
|
||||
$this->workerPool->submitTaskToWorker(new AsyncGeneratorUnregisterTask($this->asyncContextId), $i);
|
||||
}
|
||||
}
|
||||
$this->generatorRegisteredWorkers = [];
|
||||
}
|
||||
|
||||
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{
|
||||
$task = new PopulationTask(
|
||||
$this->asyncContextId,
|
||||
$chunkX,
|
||||
$chunkZ,
|
||||
$centerChunk,
|
||||
$adjacentChunks,
|
||||
$onCompletion
|
||||
);
|
||||
$workerId = $this->workerPool->selectWorker();
|
||||
if(!isset($this->workerPool->getRunningWorkers()[$workerId]) && isset($this->generatorRegisteredWorkers[$workerId])){
|
||||
$this->logger->debug("Selected worker $workerId previously had generator registered, but is now offline");
|
||||
unset($this->generatorRegisteredWorkers[$workerId]);
|
||||
}
|
||||
if(!isset($this->generatorRegisteredWorkers[$workerId])){
|
||||
$this->registerGeneratorToWorker($workerId);
|
||||
}
|
||||
$this->workerPool->submitTaskToWorker($task, $workerId);
|
||||
}
|
||||
|
||||
public function shutdown() : void{
|
||||
$this->unregisterGenerator();
|
||||
$this->workerPool->removeWorkerStartHook($this->workerStartHook);
|
||||
}
|
||||
}
|
@ -21,37 +21,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator;
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class GeneratorRegisterTask extends AsyncTask{
|
||||
public int $seed;
|
||||
public int $worldId;
|
||||
public int $worldMinY;
|
||||
public int $worldMaxY;
|
||||
class AsyncGeneratorRegisterTask extends AsyncTask{
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<Generator> $generatorClass
|
||||
*/
|
||||
public function __construct(
|
||||
World $world,
|
||||
public string $generatorClass,
|
||||
public string $generatorSettings
|
||||
){
|
||||
$this->seed = $world->getSeed();
|
||||
$this->worldId = $world->getId();
|
||||
$this->worldMinY = $world->getMinY();
|
||||
$this->worldMaxY = $world->getMaxY();
|
||||
}
|
||||
private readonly GeneratorExecutorSetupParameters $setupParameters,
|
||||
private readonly int $contextId
|
||||
){}
|
||||
|
||||
public function onRun() : void{
|
||||
/**
|
||||
* @var Generator $generator
|
||||
* @see Generator::__construct()
|
||||
*/
|
||||
$generator = new $this->generatorClass($this->seed, $this->generatorSettings);
|
||||
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $this->worldMinY, $this->worldMaxY), $this->worldId);
|
||||
$setupParameters = $this->setupParameters;
|
||||
$generator = $setupParameters->createGenerator();
|
||||
ThreadLocalGeneratorContext::register(new ThreadLocalGeneratorContext($generator, $setupParameters->worldMinY, $setupParameters->worldMaxY), $this->contextId);
|
||||
}
|
||||
}
|
@ -21,19 +21,16 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator;
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\world\World;
|
||||
|
||||
class GeneratorUnregisterTask extends AsyncTask{
|
||||
public int $worldId;
|
||||
|
||||
public function __construct(World $world){
|
||||
$this->worldId = $world->getId();
|
||||
}
|
||||
class AsyncGeneratorUnregisterTask extends AsyncTask{
|
||||
public function __construct(
|
||||
private readonly int $contextId
|
||||
){}
|
||||
|
||||
public function onRun() : void{
|
||||
ThreadLocalGeneratorContext::unregister($this->worldId);
|
||||
ThreadLocalGeneratorContext::unregister($this->contextId);
|
||||
}
|
||||
}
|
38
src/world/generator/executor/GeneratorExecutor.php
Normal file
38
src/world/generator/executor/GeneratorExecutor.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\world\format\Chunk;
|
||||
|
||||
interface GeneratorExecutor{
|
||||
/**
|
||||
* @param Chunk[]|null[] $adjacentChunks
|
||||
* @phpstan-param array<int, Chunk|null> $adjacentChunks
|
||||
* @phpstan-param \Closure(Chunk $centerChunk, array<int, Chunk> $adjacentChunks) : void $onCompletion
|
||||
*/
|
||||
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void;
|
||||
|
||||
public function shutdown() : void;
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pmmp\thread\ThreadSafe;
|
||||
use pocketmine\world\generator\Generator;
|
||||
|
||||
final class GeneratorExecutorSetupParameters extends ThreadSafe{
|
||||
|
||||
/**
|
||||
* @phpstan-param class-string<covariant \pocketmine\world\generator\Generator> $generatorClass
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $worldMinY,
|
||||
public readonly int $worldMaxY,
|
||||
public readonly int $generatorSeed,
|
||||
public readonly string $generatorClass,
|
||||
public readonly string $generatorSettings,
|
||||
){}
|
||||
|
||||
public function createGenerator() : Generator{
|
||||
/**
|
||||
* @var Generator $generator
|
||||
* @see Generator::__construct()
|
||||
*/
|
||||
$generator = new $this->generatorClass($this->generatorSeed, $this->generatorSettings);
|
||||
return $generator;
|
||||
}
|
||||
}
|
61
src/world/generator/executor/SyncGeneratorExecutor.php
Normal file
61
src/world/generator/executor/SyncGeneratorExecutor.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\world\format\Chunk;
|
||||
use pocketmine\world\generator\Generator;
|
||||
use pocketmine\world\generator\PopulationUtils;
|
||||
|
||||
final class SyncGeneratorExecutor implements GeneratorExecutor{
|
||||
|
||||
private readonly Generator $generator;
|
||||
private readonly int $worldMinY;
|
||||
private readonly int $worldMaxY;
|
||||
|
||||
public function __construct(
|
||||
GeneratorExecutorSetupParameters $setupParameters
|
||||
){
|
||||
$this->generator = $setupParameters->createGenerator();
|
||||
$this->worldMinY = $setupParameters->worldMinY;
|
||||
$this->worldMaxY = $setupParameters->worldMaxY;
|
||||
}
|
||||
|
||||
public function populate(int $chunkX, int $chunkZ, ?Chunk $centerChunk, array $adjacentChunks, \Closure $onCompletion) : void{
|
||||
[$centerChunk, $adjacentChunks] = PopulationUtils::populateChunkWithAdjacents(
|
||||
$this->worldMinY,
|
||||
$this->worldMaxY,
|
||||
$this->generator,
|
||||
$chunkX,
|
||||
$chunkZ,
|
||||
$centerChunk,
|
||||
$adjacentChunks
|
||||
);
|
||||
|
||||
$onCompletion($centerChunk, $adjacentChunks);
|
||||
}
|
||||
|
||||
public function shutdown() : void{
|
||||
//NOOP
|
||||
}
|
||||
}
|
@ -21,7 +21,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\generator;
|
||||
namespace pocketmine\world\generator\executor;
|
||||
|
||||
use pocketmine\world\generator\Generator;
|
||||
|
||||
/**
|
||||
* Manages thread-local caches for generators and the things needed to support them
|
35
src/world/sound/RespawnAnchorChargeSound.php
Normal file
35
src/world/sound/RespawnAnchorChargeSound.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class RespawnAnchorChargeSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::RESPAWN_ANCHOR_CHARGE, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/RespawnAnchorDepleteSound.php
Normal file
35
src/world/sound/RespawnAnchorDepleteSound.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class RespawnAnchorDepleteSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::RESPAWN_ANCHOR_DEPLETE, $pos, false)];
|
||||
}
|
||||
}
|
35
src/world/sound/RespawnAnchorSetSpawnSound.php
Normal file
35
src/world/sound/RespawnAnchorSetSpawnSound.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
*
|
||||
* ____ _ _ __ __ _ __ __ ____
|
||||
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
||||
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
||||
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
||||
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* @author PocketMine Team
|
||||
* @link http://www.pocketmine.net/
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\world\sound;
|
||||
|
||||
use pocketmine\math\Vector3;
|
||||
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
|
||||
|
||||
class RespawnAnchorSetSpawnSound implements Sound{
|
||||
|
||||
public function encode(Vector3 $pos) : array{
|
||||
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::RESPAWN_ANCHOR_SET_SPAWN, $pos, false)];
|
||||
}
|
||||
}
|
@ -1272,18 +1272,18 @@ parameters:
|
||||
count: 1
|
||||
path: ../../../src/world/format/io/region/RegionLoader.php
|
||||
|
||||
-
|
||||
message: '#^Dynamic new is not allowed\.$#'
|
||||
identifier: pocketmine.new.dynamic
|
||||
count: 1
|
||||
path: ../../../src/world/generator/GeneratorRegisterTask.php
|
||||
|
||||
-
|
||||
message: '#^Method pocketmine\\world\\generator\\biome\\BiomeSelector\:\:pickBiome\(\) should return pocketmine\\world\\biome\\Biome but returns pocketmine\\world\\biome\\Biome\|null\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: ../../../src/world/generator/biome/BiomeSelector.php
|
||||
|
||||
-
|
||||
message: '#^Dynamic new is not allowed\.$#'
|
||||
identifier: pocketmine.new.dynamic
|
||||
count: 1
|
||||
path: ../../../src/world/generator/executor/GeneratorExecutorSetupParameters.php
|
||||
|
||||
-
|
||||
message: '#^Cannot call method getBiomeId\(\) on pocketmine\\world\\format\\Chunk\|null\.$#'
|
||||
identifier: method.nonObject
|
||||
|
@ -615,6 +615,7 @@
|
||||
"RESIN_BRICK_STAIRS": 8,
|
||||
"RESIN_BRICK_WALL": 162,
|
||||
"RESIN_CLUMP": 64,
|
||||
"RESPAWN_ANCHOR": 5,
|
||||
"ROSE_BUSH": 2,
|
||||
"SAND": 1,
|
||||
"SANDSTONE": 1,
|
||||
|
Loading…
x
Reference in New Issue
Block a user