diff --git a/.github/workflows/team-pr-auto-approve.yml b/.github/workflows/team-pr-auto-approve.yml index a582be328..ad2812383 100644 --- a/.github/workflows/team-pr-auto-approve.yml +++ b/.github/workflows/team-pr-auto-approve.yml @@ -13,30 +13,25 @@ on: - reopened - ready_for_review -permissions: - pull-requests: write - jobs: approve: name: Auto approve runs-on: ubuntu-latest steps: - - name: Check if PR author has write access - id: check-permission - uses: actions-cool/check-user-permission@v2 + - name: Generate access token + id: generate-token + uses: actions/create-github-app-token@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} - require: write - username: ${{ github.event.pull_request.user.login }} - #technically this would be fine for dependabot but generally bots don't count as team members - check-bot: true + app-id: ${{ vars.RESTRICTED_ACTIONS_DISPATCH_ID }} + private-key: ${{ secrets.RESTRICTED_ACTIONS_DISPATCH_KEY }} + owner: ${{ github.repository_owner }} + repositories: RestrictedActions - #TODO: Some way to avoid unnecessary repeated reviews would be nice here - - - name: Approve PR if authorized - if: steps.check-permission.outputs.require-result == 'true' && steps.check-permission.outputs.check-result == 'false' - uses: juliangruber/approve-pull-request-action@v2 + - name: Dispatch restricted action + uses: peter-evans/repository-dispatch@v3 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - number: ${{ github.event.pull_request.number }} + token: ${{ steps.generate-token.outputs.token }} + repository: ${{ github.repository_owner }}/RestrictedActions + event-type: auto_approve_collaborator_pr + client-payload: '{"repo": "${{ github.repository }}", "pull_request_id": "${{ github.event.pull_request.number }}" }' diff --git a/src/block/Flowable.php b/src/block/Flowable.php index 795fe2756..0328bcd74 100644 --- a/src/block/Flowable.php +++ b/src/block/Flowable.php @@ -25,6 +25,7 @@ namespace pocketmine\block; use pocketmine\block\utils\SupportType; use pocketmine\math\AxisAlignedBB; +use pocketmine\math\Vector3; /** * "Flowable" blocks are destroyed if water flows into the same space as the block. These blocks usually don't have any @@ -40,6 +41,11 @@ abstract class Flowable extends Transparent{ return false; } + public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{ + return (!$this->canBeFlowedInto() || !$blockReplace instanceof Liquid) && + parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock); + } + /** * @return AxisAlignedBB[] */ diff --git a/src/block/GlowLichen.php b/src/block/GlowLichen.php index de66ccad7..d30e25395 100644 --- a/src/block/GlowLichen.php +++ b/src/block/GlowLichen.php @@ -24,60 +24,20 @@ declare(strict_types=1); namespace pocketmine\block; use pocketmine\block\utils\BlockEventHelper; +use pocketmine\block\utils\MultiAnySupportTrait; use pocketmine\block\utils\SupportType; -use pocketmine\data\runtime\RuntimeDataDescriber; use pocketmine\item\Fertilizer; use pocketmine\item\Item; use pocketmine\math\AxisAlignedBB; use pocketmine\math\Facing; use pocketmine\math\Vector3; use pocketmine\player\Player; -use pocketmine\world\BlockTransaction; use pocketmine\world\World; -use function array_key_first; use function count; use function shuffle; class GlowLichen extends Transparent{ - - /** @var int[] */ - protected array $faces = []; - - protected function describeBlockOnlyState(RuntimeDataDescriber $w) : void{ - $w->facingFlags($this->faces); - } - - /** @return int[] */ - public function getFaces() : array{ return $this->faces; } - - public function hasFace(int $face) : bool{ - return isset($this->faces[$face]); - } - - /** - * @param int[] $faces - * @return $this - */ - public function setFaces(array $faces) : self{ - $uniqueFaces = []; - foreach($faces as $face){ - Facing::validate($face); - $uniqueFaces[$face] = $face; - } - $this->faces = $uniqueFaces; - return $this; - } - - /** @return $this */ - public function setFace(int $face, bool $value) : self{ - Facing::validate($face); - if($value){ - $this->faces[$face] = $face; - }else{ - unset($this->faces[$face]); - } - return $this; - } + use MultiAnySupportTrait; public function getLightLevel() : int{ return 7; @@ -102,39 +62,11 @@ class GlowLichen extends Transparent{ return true; } - public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ - $this->faces = $blockReplace instanceof GlowLichen ? $blockReplace->faces : []; - $availableFaces = $this->getAvailableFaces(); - - if(count($availableFaces) === 0){ - return false; - } - - $opposite = Facing::opposite($face); - $placedFace = isset($availableFaces[$opposite]) ? $opposite : array_key_first($availableFaces); - $this->faces[$placedFace] = $placedFace; - - return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); - } - - public function onNearbyBlockChange() : void{ - $changed = false; - - foreach($this->faces as $face){ - if($this->getAdjacentSupportType($face) !== SupportType::FULL){ - unset($this->faces[$face]); - $changed = true; - } - } - - if($changed){ - $world = $this->position->getWorld(); - if(count($this->faces) === 0){ - $world->useBreakOn($this->position); - }else{ - $world->setBlock($this->position, $this); - } - } + /** + * @return int[] + */ + protected function getInitialPlaceFaces(Block $blockReplace) : array{ + return $blockReplace instanceof GlowLichen ? $blockReplace->faces : []; } private function getSpreadBlock(Block $replace, int $spreadFace) : ?Block{ @@ -261,17 +193,4 @@ class GlowLichen extends Transparent{ public function getFlammability() : int{ return 100; } - - /** - * @return array $faces - */ - private function getAvailableFaces() : array{ - $faces = []; - foreach(Facing::ALL as $face){ - if(!$this->hasFace($face) && $this->getAdjacentSupportType($face) === SupportType::FULL){ - $faces[$face] = $face; - } - } - return $faces; - } } diff --git a/src/block/Sugarcane.php b/src/block/Sugarcane.php index 97b4aee9c..2da2dc9b9 100644 --- a/src/block/Sugarcane.php +++ b/src/block/Sugarcane.php @@ -36,7 +36,9 @@ use pocketmine\world\Position; class Sugarcane extends Flowable{ use AgeableTrait; - use StaticSupportTrait; + use StaticSupportTrait { + onNearbyBlockChange as onSupportBlockChange; + } public const MAX_AGE = 15; @@ -97,7 +99,13 @@ class Sugarcane extends Flowable{ } public function onRandomTick() : void{ - if(!$this->getSide(Facing::DOWN)->hasSameTypeId($this)){ + $down = $this->getSide(Facing::DOWN); + if(!$down->hasSameTypeId($this)){ + if(!$this->hasNearbyWater($down)){ + $this->position->getWorld()->useBreakOn($this->position, createParticles: true); + return; + } + if($this->age === self::MAX_AGE){ $this->grow($this->position); }else{ @@ -123,4 +131,23 @@ class Sugarcane extends Flowable{ return false; } + + private function hasNearbyWater(Block $down) : bool{ + foreach($down->getHorizontalSides() as $sideBlock){ + $blockId = $sideBlock->getTypeId(); + if($blockId === BlockTypeIds::WATER || $blockId === BlockTypeIds::FROSTED_ICE){ + return true; + } + } + return false; + } + + public function onNearbyBlockChange() : void{ + $down = $this->getSide(Facing::DOWN); + if(!$down->hasSameTypeId($this) && !$this->hasNearbyWater($down)){ + $this->position->getWorld()->useBreakOn($this->position, createParticles: true); + }else{ + $this->onSupportBlockChange(); + } + } } diff --git a/src/block/utils/MultiAnyFacingTrait.php b/src/block/utils/MultiAnyFacingTrait.php new file mode 100644 index 000000000..66f26d980 --- /dev/null +++ b/src/block/utils/MultiAnyFacingTrait.php @@ -0,0 +1,72 @@ +facingFlags($this->faces); + } + + /** @return int[] */ + public function getFaces() : array{ return $this->faces; } + + public function hasFace(int $face) : bool{ + return isset($this->faces[$face]); + } + + /** + * @param int[] $faces + * @return $this + */ + public function setFaces(array $faces) : self{ + $uniqueFaces = []; + foreach($faces as $face){ + Facing::validate($face); + $uniqueFaces[$face] = $face; + } + $this->faces = $uniqueFaces; + return $this; + } + + /** @return $this */ + public function setFace(int $face, bool $value) : self{ + Facing::validate($face); + if($value){ + $this->faces[$face] = $face; + }else{ + unset($this->faces[$face]); + } + return $this; + } +} diff --git a/src/block/utils/MultiAnySupportTrait.php b/src/block/utils/MultiAnySupportTrait.php new file mode 100644 index 000000000..ae1da7bef --- /dev/null +++ b/src/block/utils/MultiAnySupportTrait.php @@ -0,0 +1,96 @@ +faces = $this->getInitialPlaceFaces($blockReplace); + $availableFaces = $this->getAvailableFaces(); + + if(count($availableFaces) === 0){ + return false; + } + + $opposite = Facing::opposite($face); + $placedFace = isset($availableFaces[$opposite]) ? $opposite : array_key_first($availableFaces); + $this->faces[$placedFace] = $placedFace; + + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $changed = false; + + foreach($this->faces as $face){ + if($this->getAdjacentSupportType($face) !== SupportType::FULL){ + unset($this->faces[$face]); + $changed = true; + } + } + + if($changed){ + $world = $this->position->getWorld(); + if(count($this->faces) === 0){ + $world->useBreakOn($this->position); + }else{ + $world->setBlock($this->position, $this); + } + } + } + + /** + * @return array $faces + */ + private function getAvailableFaces() : array{ + $faces = []; + foreach(Facing::ALL as $face){ + if(!$this->hasFace($face) && $this->getAdjacentSupportType($face) === SupportType::FULL){ + $faces[$face] = $face; + } + } + return $faces; + } +} diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index 1268e715b..a6a7e303b 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -64,6 +64,7 @@ use pocketmine\command\defaults\TransferServerCommand; use pocketmine\command\defaults\VanillaCommand; use pocketmine\command\defaults\VersionCommand; use pocketmine\command\defaults\WhitelistCommand; +use pocketmine\command\defaults\XpCommand; use pocketmine\command\utils\CommandStringHelper; use pocketmine\command\utils\InvalidCommandSyntaxException; use pocketmine\lang\KnownTranslationFactory; @@ -128,7 +129,8 @@ class SimpleCommandMap implements CommandMap{ new TitleCommand(), new TransferServerCommand(), new VersionCommand(), - new WhitelistCommand() + new WhitelistCommand(), + new XpCommand(), ]); } diff --git a/src/command/defaults/XpCommand.php b/src/command/defaults/XpCommand.php new file mode 100644 index 000000000..cb0352365 --- /dev/null +++ b/src/command/defaults/XpCommand.php @@ -0,0 +1,89 @@ +setPermissions([ + DefaultPermissionNames::COMMAND_XP_SELF, + DefaultPermissionNames::COMMAND_XP_OTHER + ]); + } + + public function execute(CommandSender $sender, string $commandLabel, array $args){ + if(count($args) < 1){ + throw new InvalidCommandSyntaxException(); + } + + $player = $this->fetchPermittedPlayerTarget($sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_XP_SELF, DefaultPermissionNames::COMMAND_XP_OTHER); + if($player === null){ + return true; + } + + $xpManager = $player->getXpManager(); + if(str_ends_with($args[0], "L")){ + $xpLevelAttr = $player->getAttributeMap()->get(Attribute::EXPERIENCE_LEVEL) ?? throw new AssumptionFailedError(); + $maxXpLevel = (int) $xpLevelAttr->getMaxValue(); + $currentXpLevel = $xpManager->getXpLevel(); + $xpLevels = $this->getInteger($sender, substr($args[0], 0, -1), -$currentXpLevel, $maxXpLevel - $currentXpLevel); + if($xpLevels >= 0){ + $xpManager->addXpLevels($xpLevels, false); + $sender->sendMessage(KnownTranslationFactory::commands_xp_success_levels((string) $xpLevels, $player->getName())); + }else{ + $xpLevels = abs($xpLevels); + $xpManager->subtractXpLevels($xpLevels); + $sender->sendMessage(KnownTranslationFactory::commands_xp_success_negative_levels((string) $xpLevels, $player->getName())); + } + }else{ + $xp = $this->getInteger($sender, $args[0], max: Limits::INT32_MAX); + if($xp < 0){ + $sender->sendMessage(KnownTranslationFactory::commands_xp_failure_widthdrawXp()->prefix(TextFormat::RED)); + }else{ + $xpManager->addXp($xp, false); + $sender->sendMessage(KnownTranslationFactory::commands_xp_success((string) $xp, $player->getName())); + } + } + + return true; + } +} diff --git a/src/permission/DefaultPermissionNames.php b/src/permission/DefaultPermissionNames.php index fab532e28..a916e813c 100644 --- a/src/permission/DefaultPermissionNames.php +++ b/src/permission/DefaultPermissionNames.php @@ -84,6 +84,8 @@ final class DefaultPermissionNames{ public const COMMAND_WHITELIST_LIST = "pocketmine.command.whitelist.list"; public const COMMAND_WHITELIST_RELOAD = "pocketmine.command.whitelist.reload"; public const COMMAND_WHITELIST_REMOVE = "pocketmine.command.whitelist.remove"; + public const COMMAND_XP_OTHER = "pocketmine.command.xp.other"; + public const COMMAND_XP_SELF = "pocketmine.command.xp.self"; public const GROUP_CONSOLE = "pocketmine.group.console"; public const GROUP_OPERATOR = "pocketmine.group.operator"; public const GROUP_USER = "pocketmine.group.user"; diff --git a/src/permission/DefaultPermissions.php b/src/permission/DefaultPermissions.php index c72765af6..1030e9ff8 100644 --- a/src/permission/DefaultPermissions.php +++ b/src/permission/DefaultPermissions.php @@ -112,5 +112,7 @@ abstract class DefaultPermissions{ self::registerPermission(new Permission(Names::COMMAND_WHITELIST_LIST, l10n::pocketmine_permission_command_whitelist_list()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_WHITELIST_RELOAD, l10n::pocketmine_permission_command_whitelist_reload()), [$operatorRoot]); self::registerPermission(new Permission(Names::COMMAND_WHITELIST_REMOVE, l10n::pocketmine_permission_command_whitelist_remove()), [$operatorRoot]); + self::registerPermission(new Permission(Names::COMMAND_XP_OTHER, l10n::pocketmine_permission_command_xp_other()), [$operatorRoot]); + self::registerPermission(new Permission(Names::COMMAND_XP_SELF, l10n::pocketmine_permission_command_xp_self()), [$operatorRoot]); } }