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

This commit is contained in:
Dylan K. Taylor
2023-03-14 22:25:49 +00:00
143 changed files with 58227 additions and 1278 deletions

View File

@@ -37,4 +37,6 @@ define('pocketmine\PATH', dirname(__DIR__) . '/');
define('pocketmine\RESOURCE_PATH', dirname(__DIR__) . '/resources/');
define('pocketmine\BEDROCK_DATA_PATH', dirname(__DIR__) . '/vendor/pocketmine/bedrock-data/');
define('pocketmine\LOCALE_DATA_PATH', dirname(__DIR__) . '/vendor/pocketmine/locale-data/');
define('pocketmine\BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH', dirname(__DIR__) . '/vendor/pocketmine/bedrock-block-upgrade-schema/');
define('pocketmine\BEDROCK_ITEM_UPGRADE_SCHEMA_PATH', dirname(__DIR__) . '/vendor/pocketmine/bedrock-item-upgrade-schema/');
define('pocketmine\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__) . '/vendor/autoload.php');

View File

@@ -49,6 +49,7 @@ use function ini_get;
use function ini_set;
use function intdiv;
use function is_array;
use function is_float;
use function is_object;
use function is_resource;
use function is_string;
@@ -69,6 +70,10 @@ use const JSON_UNESCAPED_SLASHES;
use const SORT_NUMERIC;
class MemoryManager{
private const DEFAULT_CHECK_RATE = Server::TARGET_TICKS_PER_SECOND;
private const DEFAULT_CONTINUOUS_TRIGGER_RATE = Server::TARGET_TICKS_PER_SECOND * 2;
private const DEFAULT_TICKS_PER_GC = 30 * 60 * Server::TARGET_TICKS_PER_SECOND;
private int $memoryLimit;
private int $globalMemoryLimit;
private int $checkRate;
@@ -113,20 +118,12 @@ class MemoryManager{
if($m <= 0){
$defaultMemory = 0;
}else{
switch(mb_strtoupper($matches[2])){
case "K":
$defaultMemory = intdiv($m, 1024);
break;
case "M":
$defaultMemory = $m;
break;
case "G":
$defaultMemory = $m * 1024;
break;
default:
$defaultMemory = $m;
break;
}
$defaultMemory = match(mb_strtoupper($matches[2])){
"K" => intdiv($m, 1024),
"M" => $m,
"G" => $m * 1024,
default => $m,
};
}
}
@@ -139,11 +136,11 @@ class MemoryManager{
}
$this->globalMemoryLimit = $config->getPropertyInt("memory.global-limit", 0) * 1024 * 1024;
$this->checkRate = $config->getPropertyInt("memory.check-rate", 20);
$this->checkRate = $config->getPropertyInt("memory.check-rate", self::DEFAULT_CHECK_RATE);
$this->continuousTrigger = $config->getPropertyBool("memory.continuous-trigger", true);
$this->continuousTriggerRate = $config->getPropertyInt("memory.continuous-trigger-rate", 30);
$this->continuousTriggerRate = $config->getPropertyInt("memory.continuous-trigger-rate", self::DEFAULT_CONTINUOUS_TRIGGER_RATE);
$this->garbageCollectionPeriod = $config->getPropertyInt("memory.garbage-collection.period", 36000);
$this->garbageCollectionPeriod = $config->getPropertyInt("memory.garbage-collection.period", self::DEFAULT_TICKS_PER_GC);
$this->garbageCollectionTrigger = $config->getPropertyBool("memory.garbage-collection.low-memory-trigger", true);
$this->garbageCollectionAsync = $config->getPropertyBool("memory.garbage-collection.collect-async-worker", true);
@@ -523,6 +520,8 @@ class MemoryManager{
$data = "(string) len(" . strlen($from) . ") " . substr(Utils::printable($from), 0, $maxStringSize);
}elseif(is_resource($from)){
$data = "(resource) " . print_r($from, true);
}elseif(is_float($from)){
$data = "(float) $from";
}else{
$data = $from;
}

View File

@@ -122,7 +122,7 @@ namespace pocketmine {
if(substr_count($pthreads_version, ".") < 2){
$pthreads_version = "0.$pthreads_version";
}
if(version_compare($pthreads_version, "4.0.0") < 0 || version_compare($pthreads_version, "5.0.0") > 0){
if(version_compare($pthreads_version, "4.0.0") < 0 || version_compare($pthreads_version, "5.0.0") >= 0){
$messages[] = "pthreads ^4.0.0 is required, while you have $pthreads_version.";
}
}

View File

@@ -49,10 +49,7 @@ use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException;
use pocketmine\lang\Translatable;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\CompressBatchTask;
use pocketmine\network\mcpe\compression\Compressor;
@@ -72,9 +69,13 @@ use pocketmine\network\query\QueryInfo;
use pocketmine\network\upnp\UPnPNetworkInterface;
use pocketmine\permission\BanList;
use pocketmine\permission\DefaultPermissions;
use pocketmine\player\DatFilePlayerDataProvider;
use pocketmine\player\GameMode;
use pocketmine\player\OfflinePlayer;
use pocketmine\player\Player;
use pocketmine\player\PlayerDataLoadException;
use pocketmine\player\PlayerDataProvider;
use pocketmine\player\PlayerDataSaveException;
use pocketmine\player\PlayerInfo;
use pocketmine\plugin\PharPluginLoader;
use pocketmine\plugin\Plugin;
@@ -105,17 +106,18 @@ use pocketmine\utils\SignalHandler;
use pocketmine\utils\Terminal;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\WorldProviderManager;
use pocketmine\world\format\io\WritableWorldProviderManagerEntry;
use pocketmine\world\generator\Generator;
use pocketmine\world\generator\GeneratorManager;
use pocketmine\world\generator\InvalidGeneratorOptionsException;
use pocketmine\world\Position;
use pocketmine\world\World;
use pocketmine\world\WorldCreationOptions;
use pocketmine\world\WorldManager;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Filesystem\Path;
use function array_fill;
use function array_sum;
use function base64_encode;
use function cli_set_process_title;
@@ -124,7 +126,6 @@ use function count;
use function date;
use function fclose;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function filemtime;
use function fopen;
@@ -161,12 +162,9 @@ use function time;
use function touch;
use function trim;
use function yaml_parse;
use function zlib_decode;
use function zlib_encode;
use const DIRECTORY_SEPARATOR;
use const PHP_EOL;
use const PHP_INT_MAX;
use const ZLIB_ENCODING_GZIP;
/**
* The class that manages everything
@@ -184,9 +182,30 @@ class Server{
public const DEFAULT_PORT_IPV6 = 19133;
public const DEFAULT_MAX_VIEW_DISTANCE = 16;
/**
* Worlds, network, commands and most other things are polled this many times per second on average.
* Between ticks, the server will sleep to ensure that the average tick rate is maintained.
* It may wake up between ticks if a Snooze notification source is triggered (e.g. to process network packets).
*/
public const TARGET_TICKS_PER_SECOND = 20;
/**
* The average time between ticks, in seconds.
*/
public const TARGET_SECONDS_PER_TICK = 1 / self::TARGET_TICKS_PER_SECOND;
public const TARGET_NANOSECONDS_PER_TICK = 1_000_000_000 / self::TARGET_TICKS_PER_SECOND;
/**
* The TPS threshold below which the server will generate log warnings.
*/
private const TPS_OVERLOAD_WARNING_THRESHOLD = self::TARGET_TICKS_PER_SECOND * 0.6;
private const TICKS_PER_WORLD_CACHE_CLEAR = 5 * self::TARGET_TICKS_PER_SECOND;
private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND;
private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND;
private static ?Server $instance = null;
private SleeperHandler $tickSleeper;
private TimeTrackingSleeperHandler $tickSleeper;
private BanList $banByName;
@@ -202,7 +221,7 @@ class Server{
private PluginManager $pluginManager;
private float $profilingTickRate = 20;
private float $profilingTickRate = self::TARGET_TICKS_PER_SECOND;
private UpdateChecker $updater;
@@ -212,10 +231,10 @@ class Server{
private int $tickCounter = 0;
private float $nextTick = 0;
/** @var float[] */
private array $tickAverage = [20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20];
private array $tickAverage;
/** @var float[] */
private array $useAverage = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
private float $currentTPS = 20;
private array $useAverage;
private float $currentTPS = self::TARGET_TICKS_PER_SECOND;
private float $currentUse = 0;
private float $startTime;
@@ -251,6 +270,8 @@ class Server{
private string $dataPath;
private string $pluginPath;
private PlayerDataProvider $playerDataProvider;
/**
* @var string[]
* @phpstan-var array<string, string>
@@ -484,49 +505,22 @@ class Server{
return $result;
}
private function getPlayerDataPath(string $username) : string{
return Path::join($this->getDataPath(), 'players', strtolower($username) . '.dat');
}
/**
* Returns whether the server has stored any saved data for this player.
*/
public function hasOfflinePlayerData(string $name) : bool{
return file_exists($this->getPlayerDataPath($name));
}
private function handleCorruptedPlayerData(string $name) : void{
$path = $this->getPlayerDataPath($name);
rename($path, $path . '.bak');
$this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
return $this->playerDataProvider->hasData($name);
}
public function getOfflinePlayerData(string $name) : ?CompoundTag{
return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{
$name = strtolower($name);
$path = $this->getPlayerDataPath($name);
if(file_exists($path)){
$contents = @file_get_contents($path);
if($contents === false){
throw new \RuntimeException("Failed to read player data file \"$path\" (permission denied?)");
}
$decompressed = @zlib_decode($contents);
if($decompressed === false){
$this->logger->debug("Failed to decompress raw player data for \"$name\"");
$this->handleCorruptedPlayerData($name);
return null;
}
try{
return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag();
}catch(NbtDataException $e){ //corrupt data
$this->logger->debug("Failed to decode NBT data for \"$name\": " . $e->getMessage());
$this->handleCorruptedPlayerData($name);
return null;
}
try{
return $this->playerDataProvider->loadData($name);
}catch(PlayerDataLoadException $e){
$this->logger->debug("Failed to load player data for $name: " . $e->getMessage());
$this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
return null;
}
return null;
});
}
@@ -540,11 +534,9 @@ class Server{
if(!$ev->isCancelled()){
Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
$nbt = new BigEndianNbtSerializer();
$contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly");
try{
Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents);
}catch(\RuntimeException $e){
$this->playerDataProvider->saveData($name, $ev->getSaveData());
}catch(PlayerDataSaveException $e){
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
$this->logger->logException($e);
}
@@ -560,51 +552,47 @@ class Server{
$ev->call();
$class = $ev->getPlayerClass();
if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString("Level", ""))) !== null){
if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString(Player::TAG_LEVEL, ""))) !== null){
$playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
$spawn = $playerPos->asVector3();
}else{
$world = $this->worldManager->getDefaultWorld();
if($world === null){
throw new AssumptionFailedError("Default world should always be loaded");
}
$playerPos = null;
$spawn = $world->getSpawnLocation();
}
/** @phpstan-var PromiseResolver<Player> $playerPromiseResolver */
$playerPromiseResolver = new PromiseResolver();
$world->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
function() use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{
if(!$session->isConnected()){
$playerPromiseResolver->reject();
return;
}
/* Stick with the original spawn at the time of generation request, even if it changed since then.
* This is because we know for sure that that chunk will be generated, but the one at the new location
* might not be, and it would be much more complex to go back and redo the whole thing.
*
* TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the
* provided position. If this assumption is broken, we'll start seeing crashes in here.
*/
/**
* @see Player::__construct()
* @var Player $player
*/
$player = new $class($this, $session, $playerInfo, $authenticated, $playerPos ?? Location::fromObject($world->getSafeSpawn($spawn), $world), $offlinePlayerData);
if(!$player->hasPlayedBefore()){
$player->onGround = true; //TODO: this hack is needed for new players in-air ticks - they don't get detected as on-ground until they move
}
$playerPromiseResolver->resolve($player);
},
static function() use ($playerPromiseResolver, $session) : void{
if($session->isConnected()){
$session->disconnect("Spawn terrain generation failed");
}
$playerPromiseResolver->reject();
$createPlayer = function(Location $location) use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $offlinePlayerData) : void{
$player = new $class($this, $session, $playerInfo, $authenticated, $location, $offlinePlayerData);
if(!$player->hasPlayedBefore()){
$player->onGround = true; //TODO: this hack is needed for new players in-air ticks - they don't get detected as on-ground until they move
}
);
$playerPromiseResolver->resolve($player);
};
if($playerPos === null){ //new player or no valid position due to world not being loaded
$world->requestSafeSpawn()->onCompletion(
function(Position $spawn) use ($createPlayer, $playerPromiseResolver, $session, $world) : void{
if(!$session->isConnected()){
$playerPromiseResolver->reject();
return;
}
$createPlayer(Location::fromObject($spawn, $world));
},
function() use ($playerPromiseResolver, $session) : void{
if($session->isConnected()){
//TODO: this needs to be localized - this might be reached if the spawn world was unloaded while the player was logging in
$session->disconnect("Failed to find a safe spawn location");
}
$playerPromiseResolver->reject();
}
);
}else{ //returning player with a valid position - safe spawn not required
$createPlayer($playerPos);
}
return $playerPromiseResolver->getPromise();
}
@@ -780,8 +768,11 @@ class Server{
}
self::$instance = $this;
$this->startTime = microtime(true);
$this->tickAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, self::TARGET_TICKS_PER_SECOND);
$this->useAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, 0);
$this->tickSleeper = new SleeperHandler();
Timings::init();
$this->tickSleeper = new TimeTrackingSleeperHandler(Timings::$serverInterrupts);
$this->signalHandler = new SignalHandler(function() : void{
$this->logger->info("Received signal interrupt, stopping the server");
@@ -806,7 +797,7 @@ class Server{
$this->logger->info("Loading server configuration");
$pocketmineYmlPath = Path::join($this->dataPath, "pocketmine.yml");
if(!file_exists($pocketmineYmlPath)){
$content = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml")), "Missing required resource file");
$content = Filesystem::fileGetContents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml"));
if(VersionInfo::IS_DEVELOPMENT_BUILD){
$content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content);
}
@@ -900,6 +891,9 @@ class Server{
if($this->configGroup->getPropertyInt("network.batch-threshold", 256) >= 0){
$netCompressionThreshold = $this->configGroup->getPropertyInt("network.batch-threshold", 256);
}
if($netCompressionThreshold < 0){
$netCompressionThreshold = null;
}
$netCompressionLevel = $this->configGroup->getPropertyInt("network.compression-level", 6);
if($netCompressionLevel < 1 || $netCompressionLevel > 9){
@@ -961,15 +955,14 @@ class Server{
)));
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_license($this->getName())));
Timings::init();
TimingsHandler::setEnabled($this->configGroup->getPropertyBool("settings.enable-profiling", false));
$this->profilingTickRate = $this->configGroup->getPropertyInt("settings.profile-report-trigger", 20);
$this->profilingTickRate = $this->configGroup->getPropertyInt("settings.profile-report-trigger", self::TARGET_TICKS_PER_SECOND);
DefaultPermissions::registerCorePermissions();
$this->commandMap = new SimpleCommandMap($this);
$this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes.json"));
$this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\RESOURCE_PATH, "legacy_recipes.json"));
$this->resourceManager = new ResourcePackManager(Path::join($this->getDataPath(), "resource_packs"), $this->logger);
@@ -979,7 +972,7 @@ class Server{
copy(Path::join(\pocketmine\RESOURCE_PATH, 'plugin_list.yml'), $graylistFile);
}
try{
$pluginGraylist = PluginGraylist::fromArray(yaml_parse(file_get_contents($graylistFile)));
$pluginGraylist = PluginGraylist::fromArray(yaml_parse(Filesystem::fileGetContents($graylistFile)));
}catch(\InvalidArgumentException $e){
$this->logger->emergency("Failed to load $graylistFile: " . $e->getMessage());
$this->forceShutdownExit();
@@ -1001,12 +994,14 @@ class Server{
$this->worldManager = new WorldManager($this, Path::join($this->dataPath, "worlds"), $providerManager);
$this->worldManager->setAutoSave($this->configGroup->getConfigBool("auto-save", $this->worldManager->getAutoSave()));
$this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt("ticks-per.autosave", 6000));
$this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt("ticks-per.autosave", $this->worldManager->getAutoSaveInterval()));
$this->updater = new UpdateChecker($this, $this->configGroup->getPropertyString("auto-updater.host", "update.pmmp.io"));
$this->queryInfo = new QueryInfo($this);
$this->playerDataProvider = new DatFilePlayerDataProvider(Path::join($this->dataPath, "players"));
register_shutdown_function([$this, "crashDump"]);
$loadErrorCount = 0;
@@ -1039,7 +1034,7 @@ class Server{
}
if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){
$this->sendUsageTicker = 6000;
$this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
$this->sendUsage(SendUsageTask::TYPE_OPEN);
}
@@ -1386,14 +1381,16 @@ class Server{
*
* @param bool|null $sync Compression on the main thread (true) or workers (false). Default is automatic (null).
*/
public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null) : CompressBatchPromise{
public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null, ?TimingsHandler $timings = null) : CompressBatchPromise{
$timings ??= Timings::$playerNetworkSendCompress;
try{
Timings::$playerNetworkSendCompress->startTiming();
$timings->startTiming();
$buffer = $stream->getBuffer();
if($sync === null){
$sync = !($this->networkCompressionAsync && $compressor->willCompress($buffer));
$threshold = $compressor->getCompressionThreshold();
$sync = !$this->networkCompressionAsync || $threshold === null || strlen($stream->getBuffer()) < $threshold;
}
$promise = new CompressBatchPromise();
@@ -1406,7 +1403,7 @@ class Server{
return $promise;
}finally{
Timings::$playerNetworkSendCompress->stopTiming();
$timings->stopTiming();
}
}
@@ -1821,11 +1818,11 @@ class Server{
$this->network->tick();
Timings::$connection->stopTiming();
if(($this->tickCounter % 20) === 0){
if(($this->tickCounter % self::TARGET_TICKS_PER_SECOND) === 0){
if($this->doTitleTick){
$this->titleTick();
}
$this->currentTPS = 20;
$this->currentTPS = self::TARGET_TICKS_PER_SECOND;
$this->currentUse = 0;
$queryRegenerateEvent = new QueryRegenerateEvent(new QueryInfo($this));
@@ -1837,18 +1834,18 @@ class Server{
}
if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
$this->sendUsageTicker = 6000;
$this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
$this->sendUsage(SendUsageTask::TYPE_STATUS);
}
if(($this->tickCounter % 100) === 0){
if(($this->tickCounter % self::TICKS_PER_WORLD_CACHE_CLEAR) === 0){
foreach($this->worldManager->getWorlds() as $world){
$world->clearCache();
}
}
if($this->getTicksPerSecondAverage() < 12){
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
}
if(($this->tickCounter % self::TICKS_PER_TPS_OVERLOAD_WARNING) === 0 && $this->getTicksPerSecondAverage() < self::TPS_OVERLOAD_WARNING_THRESHOLD){
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
}
$this->getMemoryManager()->check();
@@ -1865,19 +1862,21 @@ class Server{
Timings::$serverTick->stopTiming();
$now = microtime(true);
$this->currentTPS = min(20, 1 / max(0.001, $now - $tickTime));
$this->currentUse = min(1, ($now - $tickTime) / 0.05);
$totalTickTimeSeconds = $now - $tickTime + ($this->tickSleeper->getNotificationProcessingTime() / 1_000_000_000);
$this->currentTPS = min(self::TARGET_TICKS_PER_SECOND, 1 / max(0.001, $totalTickTimeSeconds));
$this->currentUse = min(1, $totalTickTimeSeconds / self::TARGET_SECONDS_PER_TICK);
TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate);
$idx = $this->tickCounter % 20;
$idx = $this->tickCounter % self::TARGET_TICKS_PER_SECOND;
$this->tickAverage[$idx] = $this->currentTPS;
$this->useAverage[$idx] = $this->currentUse;
$this->tickSleeper->resetNotificationProcessingTime();
if(($this->nextTick - $tickTime) < -1){
$this->nextTick = $tickTime;
}else{
$this->nextTick += 0.05;
$this->nextTick += self::TARGET_SECONDS_PER_TICK;
}
}
}

View File

@@ -0,0 +1,64 @@
<?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;
use pocketmine\snooze\SleeperHandler;
use pocketmine\timings\TimingsHandler;
use function hrtime;
/**
* Custom Snooze sleeper handler which captures notification processing time.
* @internal
*/
final class TimeTrackingSleeperHandler extends SleeperHandler{
private int $notificationProcessingTimeNs = 0;
public function __construct(
private TimingsHandler $timings
){
parent::__construct();
}
/**
* Returns the time in nanoseconds spent processing notifications since the last reset.
*/
public function getNotificationProcessingTime() : int{ return $this->notificationProcessingTimeNs; }
/**
* Resets the notification processing time tracker to zero.
*/
public function resetNotificationProcessingTime() : void{ $this->notificationProcessingTimeNs = 0; }
public function processNotifications() : void{
$startTime = hrtime(true);
$this->timings->startTiming();
try{
parent::processNotifications();
}finally{
$this->notificationProcessingTimeNs += hrtime(true) - $startTime;
$this->timings->stopTiming();
}
}
}

View File

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

View File

@@ -112,7 +112,14 @@ abstract class BaseBanner extends Transparent{
return SupportType::NONE();
}
private function canBeSupportedBy(Block $block) : bool{
return $block->isSolid();
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$this->canBeSupportedBy($blockReplace->getSide($this->getSupportingFace()))){
return false;
}
if($item instanceof ItemBanner){
$this->color = $item->getColor();
$this->setPatterns($item->getPatterns());
@@ -124,7 +131,7 @@ abstract class BaseBanner extends Transparent{
abstract protected function getSupportingFace() : int;
public function onNearbyBlockChange() : void{
if($this->getSide($this->getSupportingFace())->getId() === BlockLegacyIds::AIR){
if(!$this->canBeSupportedBy($this->getSide($this->getSupportingFace()))){
$this->position->getWorld()->useBreakOn($this->position);
}
}

View File

@@ -114,14 +114,13 @@ final class Bell extends Transparent{
return $this;
}
private function canBeSupportedBy(Block $block) : bool{
//TODO: this isn't the actual logic, but it's the closest approximation we can support for now
return $block->isSolid();
private function canBeSupportedBy(Block $block, int $face) : bool{
return !$block->getSupportType($face)->equals(SupportType::NONE());
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($face === Facing::UP){
if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->down()))){
if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->down()), Facing::UP)){
return false;
}
if($player !== null){
@@ -129,18 +128,18 @@ final class Bell extends Transparent{
}
$this->setAttachmentType(BellAttachmentType::FLOOR());
}elseif($face === Facing::DOWN){
if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->up()))){
if(!$this->canBeSupportedBy($tx->fetchBlock($this->position->up()), Facing::DOWN)){
return false;
}
$this->setAttachmentType(BellAttachmentType::CEILING());
}else{
$this->setFacing($face);
if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide(Facing::opposite($face))))){
if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide(Facing::opposite($face))), $face)){
$this->setAttachmentType(BellAttachmentType::ONE_WALL());
}else{
return false;
}
if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide($face)))){
if($this->canBeSupportedBy($tx->fetchBlock($this->position->getSide($face)), Facing::opposite($face))){
$this->setAttachmentType(BellAttachmentType::TWO_WALLS());
}
}
@@ -149,10 +148,10 @@ final class Bell extends Transparent{
public function onNearbyBlockChange() : void{
if(
($this->attachmentType->equals(BellAttachmentType::CEILING()) && !$this->canBeSupportedBy($this->getSide(Facing::UP))) ||
($this->attachmentType->equals(BellAttachmentType::FLOOR()) && !$this->canBeSupportedBy($this->getSide(Facing::DOWN))) ||
($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) && !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing)))) ||
($this->attachmentType->equals(BellAttachmentType::TWO_WALLS()) && (!$this->canBeSupportedBy($this->getSide($this->facing)) || !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing)))))
($this->attachmentType->equals(BellAttachmentType::CEILING()) && !$this->canBeSupportedBy($this->getSide(Facing::UP), Facing::DOWN)) ||
($this->attachmentType->equals(BellAttachmentType::FLOOR()) && !$this->canBeSupportedBy($this->getSide(Facing::DOWN), Facing::UP)) ||
($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) && !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing)), $this->facing)) ||
($this->attachmentType->equals(BellAttachmentType::TWO_WALLS()) && (!$this->canBeSupportedBy($this->getSide($this->facing), Facing::opposite($this->facing)) || !$this->canBeSupportedBy($this->getSide(Facing::opposite($this->facing)), $this->facing)))
){
$this->position->getWorld()->useBreakOn($this->position);
}
@@ -161,21 +160,20 @@ final class Bell extends Transparent{
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($player !== null){
$faceHit = Facing::opposite($player->getHorizontalFacing());
if($this->attachmentType->equals(BellAttachmentType::CEILING())){
$this->ring($faceHit);
}
if($this->attachmentType->equals(BellAttachmentType::FLOOR()) && Facing::axis($faceHit) === Facing::axis($this->facing)){
$this->ring($faceHit);
}
if(
($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) || $this->attachmentType->equals(BellAttachmentType::TWO_WALLS())) &&
($faceHit === Facing::rotateY($this->facing, false) || $faceHit === Facing::rotateY($this->facing, true))
$this->attachmentType->equals(BellAttachmentType::CEILING()) ||
($this->attachmentType->equals(BellAttachmentType::FLOOR()) && Facing::axis($faceHit) === Facing::axis($this->facing)) ||
(
($this->attachmentType->equals(BellAttachmentType::ONE_WALL()) || $this->attachmentType->equals(BellAttachmentType::TWO_WALLS())) &&
($faceHit === Facing::rotateY($this->facing, false) || $faceHit === Facing::rotateY($this->facing, true))
)
){
$this->ring($faceHit);
return true;
}
}
return true;
return false;
}
public function ring(int $faceHit) : void{

View File

@@ -253,6 +253,14 @@ class Block{
* 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).
*
* @param BlockTransaction $tx Blocks to be set should be added to this transaction (do not modify thr world directly)
* @param Item $item Item used to place the block
* @param Block $blockReplace Block expected to be replaced
* @param Block $blockClicked Block that was clicked using the item
* @param int $face Face of the clicked block which was clicked
* @param Vector3 $clickVector Exact position inside the clicked block where the click occurred, relative to the block's position
* @param Player|null $player Player who placed the block, or null if it was not a player
*
* @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{

View File

@@ -61,7 +61,7 @@ abstract class Button extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($this->canBeSupportedBy($blockClicked, $face)){
if($this->canBeSupportedBy($blockReplace->getSide(Facing::opposite($face)), $face)){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}

View File

@@ -175,7 +175,7 @@ class ItemFrame extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($face === Facing::DOWN || $face === Facing::UP || !$blockClicked->isSolid()){
if($face === Facing::DOWN || $face === Facing::UP || !$blockReplace->getSide(Facing::opposite($face))->isSolid()){
return false;
}

View File

@@ -26,6 +26,7 @@ namespace pocketmine\block;
use pocketmine\block\tile\Jukebox as JukeboxTile;
use pocketmine\item\Item;
use pocketmine\item\Record;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\sound\RecordSound;
@@ -44,7 +45,7 @@ class Jukebox extends Opaque{
if($this->record !== null){
$this->ejectRecord();
}elseif($item instanceof Record){
$player->sendJukeboxPopup("record.nowPlaying", [$player->getLanguage()->translate($item->getRecordType()->getTranslatableName())]);
$player->sendJukeboxPopup(KnownTranslationKeys::RECORD_NOWPLAYING, [$player->getLanguage()->translate($item->getRecordType()->getTranslatableName())]);
$this->insertRecord($item->pop());
}
}

View File

@@ -70,7 +70,7 @@ class Ladder extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($this->canBeSupportedBy($blockClicked, $face) && Facing::axis($face) !== Axis::Y){
if($this->canBeSupportedBy($blockReplace->getSide(Facing::opposite($face)), $face) && Facing::axis($face) !== Axis::Y){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}

View File

@@ -95,7 +95,7 @@ class Lever extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$this->canBeSupportedBy($blockClicked, $face)){
if(!$this->canBeSupportedBy($blockReplace->getSide(Facing::opposite($face)), $face)){
return false;
}

View File

@@ -45,7 +45,7 @@ abstract class PressurePlate extends Transparent{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($this->canBeSupportedBy($blockClicked)){
if($this->canBeSupportedBy($blockReplace->getSide(Facing::DOWN))){
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
return false;
@@ -55,5 +55,11 @@ abstract class PressurePlate extends Transparent{
return !$block->getSupportType(Facing::UP)->equals(SupportType::NONE());
}
public function onNearbyBlockChange() : void{
if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){
$this->position->getWorld()->useBreakOn($this->position);
}
}
//TODO
}

View File

@@ -41,7 +41,7 @@ class Thin extends Transparent{
foreach(Facing::HORIZONTAL as $facing){
$side = $this->getSide($facing);
if($side instanceof Thin || $side->isFullCube()){
if($side instanceof Thin || $side instanceof Wall || $side->isFullCube()){
$this->connections[$facing] = true;
}else{
unset($this->connections[$facing]);

View File

@@ -65,7 +65,6 @@ class Torch extends Flowable{
}
public function onNearbyBlockChange() : void{
$below = $this->getSide(Facing::DOWN);
$face = Facing::opposite($this->facing);
if(!$this->canBeSupportedBy($this->getSide($face), $this->facing)){
@@ -74,10 +73,7 @@ class Torch extends Flowable{
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($blockClicked->canBeReplaced() && $this->canBeSupportedBy($blockClicked->getSide(Facing::DOWN), Facing::UP)){
$this->facing = Facing::UP;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}elseif($face !== Facing::DOWN && $this->canBeSupportedBy($blockClicked, $face)){
if($face !== Facing::DOWN && $this->canBeSupportedBy($blockReplace->getSide(Facing::opposite($face)), $face)){
$this->facing = $face;
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}else{

View File

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

View File

@@ -39,7 +39,7 @@ class Wall extends Transparent{
foreach(Facing::HORIZONTAL as $facing){
$block = $this->getSide($facing);
if($block instanceof static || $block instanceof FenceGate || ($block->isSolid() && !$block->isTransparent())){
if($block instanceof static || $block instanceof FenceGate || $block instanceof Thin || ($block->isSolid() && !$block->isTransparent())){
$this->connections[$facing] = $facing;
}else{
unset($this->connections[$facing]);

View File

@@ -113,7 +113,7 @@ final class WallCoralFan extends BaseCoral{
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
$axis = Facing::axis($face);
if(($axis !== Axis::X && $axis !== Axis::Z) || !$this->canBeSupportedBy($blockClicked, $face)){
if(($axis !== Axis::X && $axis !== Axis::Z) || !$this->canBeSupportedBy($blockReplace->getSide(Facing::opposite($face)), $face)){
return false;
}
$this->facing = $face;

View File

@@ -39,19 +39,23 @@ class WaterLily extends Flowable{
return [AxisAlignedBB::one()->contract(1 / 16, 0, 1 / 16)->trim(Facing::UP, 63 / 64)];
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if($blockClicked instanceof Water){
$up = $blockClicked->getSide(Facing::UP);
if($up->canBeReplaced()){
return parent::place($tx, $item, $up, $blockClicked, $face, $clickVector, $player);
}
}
public function canBePlacedAt(Block $blockReplace, Vector3 $clickVector, int $face, bool $isClickedBlock) : bool{
return !$blockReplace instanceof Water && parent::canBePlacedAt($blockReplace, $clickVector, $face, $isClickedBlock);
}
return false;
private function canBeSupportedBy(Block $block) : bool{
return $block instanceof Water;
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$this->canBeSupportedBy($blockReplace->getSide(Facing::DOWN))){
return false;
}
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
public function onNearbyBlockChange() : void{
if(!($this->getSide(Facing::DOWN) instanceof Water)){
if(!$this->canBeSupportedBy($this->getSide(Facing::DOWN))){
$this->position->getWorld()->useBreakOn($this->position);
}
}

View File

@@ -52,7 +52,7 @@ trait ContainerTrait{
$newContents = [];
/** @var CompoundTag $itemNBT */
foreach($inventoryTag as $itemNBT){
$newContents[$itemNBT->getByte("Slot")] = Item::nbtDeserialize($itemNBT);
$newContents[$itemNBT->getByte(Item::TAG_SLOT)] = Item::nbtDeserialize($itemNBT);
}
$inventory->setContents($newContents);

View File

@@ -30,7 +30,7 @@ use function array_slice;
use function count;
use function explode;
use function is_int;
use function strpos;
use function str_contains;
class SignText{
public const LINE_COUNT = 4;
@@ -54,7 +54,7 @@ class SignText{
foreach($lines as $k => $line){
$this->checkLineIndex($k);
Utils::checkUTF8($line);
if(strpos($line, "\n") !== false){
if(str_contains($line, "\n")){
throw new \InvalidArgumentException("Line must not contain newlines");
}
//TODO: add length checks

View File

@@ -72,8 +72,8 @@ use pocketmine\utils\TextFormat;
use function array_shift;
use function count;
use function implode;
use function str_contains;
use function strcasecmp;
use function strpos;
use function strtolower;
use function trim;
@@ -238,7 +238,7 @@ class SimpleCommandMap implements CommandMap{
$values = $this->server->getCommandAliases();
foreach($values as $alias => $commandStrings){
if(strpos($alias, ":") !== false){
if(str_contains($alias, ":")){
$this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_illegal($alias)));
continue;
}

View File

@@ -33,7 +33,6 @@ use pocketmine\item\LegacyStringToItemParserException;
use pocketmine\item\StringToItemParser;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use function count;
use function implode;
@@ -59,23 +58,9 @@ class ClearCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
if(isset($args[0])){
$target = $sender->getServer()->getPlayerByPrefix($args[0]);
if($target === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}
if($target !== $sender && !$this->testPermission($sender, DefaultPermissionNames::COMMAND_CLEAR_OTHER)){
return true;
}
}elseif($sender instanceof Player){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_CLEAR_SELF)){
return true;
}
$target = $sender;
}else{
throw new InvalidCommandSyntaxException();
$target = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_CLEAR_SELF, DefaultPermissionNames::COMMAND_CLEAR_OTHER);
if($target === null){
return true;
}
$targetItem = null;

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\command\defaults;
use pocketmine\command\CommandSender;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use Symfony\Component\Filesystem\Path;
use function date;
@@ -33,7 +34,7 @@ class DumpMemoryCommand extends VanillaCommand{
public function __construct(string $name){
parent::__construct(
$name,
"Dumps the memory",
KnownTranslationFactory::pocketmine_command_dumpmemory_description(),
"/$name [path]"
);
$this->setPermission(DefaultPermissionNames::COMMAND_DUMPMEMORY);

View File

@@ -32,6 +32,7 @@ use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\Limits;
use pocketmine\utils\TextFormat;
use function count;
use function implode;
use function strtolower;
class EffectCommand extends VanillaCommand{
@@ -42,7 +43,10 @@ class EffectCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_effect_description(),
KnownTranslationFactory::commands_effect_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_EFFECT);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_EFFECT_SELF,
DefaultPermissionNames::COMMAND_EFFECT_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -54,10 +58,8 @@ class EffectCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
$player = $sender->getServer()->getPlayerByPrefix($args[0]);
$player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_EFFECT_SELF, DefaultPermissionNames::COMMAND_EFFECT_OTHER);
if($player === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}
$effectManager = $player->getEffects();

View File

@@ -29,8 +29,8 @@ use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\StringToEnchantmentParser;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\TextFormat;
use function count;
use function implode;
class EnchantCommand extends VanillaCommand{
@@ -40,7 +40,10 @@ class EnchantCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_enchant_description(),
KnownTranslationFactory::commands_enchant_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_ENCHANT);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_ENCHANT_SELF,
DefaultPermissionNames::COMMAND_ENCHANT_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -52,10 +55,8 @@ class EnchantCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
$player = $sender->getServer()->getPlayerByPrefix($args[0]);
$player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_ENCHANT_SELF, DefaultPermissionNames::COMMAND_ENCHANT_OTHER);
if($player === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}

View File

@@ -29,9 +29,8 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\GameMode;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use function count;
use function implode;
class GamemodeCommand extends VanillaCommand{
@@ -41,7 +40,10 @@ class GamemodeCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_gamemode_description(),
KnownTranslationFactory::commands_gamemode_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_GAMEMODE);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_GAMEMODE_SELF,
DefaultPermissionNames::COMMAND_GAMEMODE_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -59,17 +61,9 @@ class GamemodeCommand extends VanillaCommand{
return true;
}
if(isset($args[1])){
$target = $sender->getServer()->getPlayerByPrefix($args[1]);
if($target === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}
}elseif($sender instanceof Player){
$target = $sender;
}else{
throw new InvalidCommandSyntaxException();
$target = $this->fetchPermittedPlayerTarget($sender, $args[1] ?? null, DefaultPermissionNames::COMMAND_GAMEMODE_SELF, DefaultPermissionNames::COMMAND_GAMEMODE_OTHER);
if($target === null){
return true;
}
if($target->getGamemode()->equals($gameMode)){

View File

@@ -47,7 +47,10 @@ class GiveCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_give_description(),
KnownTranslationFactory::pocketmine_command_give_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_GIVE);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_GIVE_SELF,
DefaultPermissionNames::COMMAND_GIVE_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -59,9 +62,8 @@ class GiveCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
$player = $sender->getServer()->getPlayerByPrefix($args[0]);
$player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_GIVE_SELF, DefaultPermissionNames::COMMAND_GIVE_OTHER);
if($player === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}

View File

@@ -29,8 +29,6 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\event\entity\EntityDamageEvent;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use function count;
use function implode;
@@ -55,32 +53,16 @@ class KillCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
if(count($args) === 1){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_KILL_OTHER)){
return true;
}
$player = $sender->getServer()->getPlayerByPrefix($args[0]);
if($player instanceof Player){
$player->attack(new EntityDamageEvent($player, EntityDamageEvent::CAUSE_SUICIDE, 1000));
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kill_successful($player->getName()));
}else{
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
}
$player = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_KILL_SELF, DefaultPermissionNames::COMMAND_KILL_OTHER);
if($player === null){
return true;
}
if($sender instanceof Player){
if(!$this->testPermission($sender, DefaultPermissionNames::COMMAND_KILL_SELF)){
return true;
}
$sender->attack(new EntityDamageEvent($sender, EntityDamageEvent::CAUSE_SUICIDE, 1000));
$player->attack(new EntityDamageEvent($player, EntityDamageEvent::CAUSE_SUICIDE, 1000));
if($player === $sender){
$sender->sendMessage(KnownTranslationFactory::commands_kill_successful($sender->getName()));
}else{
throw new InvalidCommandSyntaxException();
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_kill_successful($player->getName()));
}
return true;

View File

@@ -70,7 +70,7 @@ use function explode;
use function max;
use function microtime;
use function mt_rand;
use function strpos;
use function str_starts_with;
use function strtolower;
class ParticleCommand extends VanillaCommand{
@@ -208,17 +208,17 @@ class ParticleCommand extends VanillaCommand{
return new EntityFlameParticle();
}
if(strpos($name, "iconcrack_") === 0){
if(str_starts_with($name, "iconcrack_")){
$d = explode("_", $name);
if(count($d) === 3){
return new ItemBreakParticle(ItemFactory::getInstance()->get((int) $d[1], (int) $d[2]));
}
}elseif(strpos($name, "blockcrack_") === 0){
}elseif(str_starts_with($name, "blockcrack_")){
$d = explode("_", $name);
if(count($d) === 2){
return new TerrainParticle(BlockFactory::getInstance()->get(((int) $d[1]) & 0xff, ((int) $d[1]) >> 12));
}
}elseif(strpos($name, "blockdust_") === 0){
}elseif(str_starts_with($name, "blockdust_")){
$d = explode("_", $name);
if(count($d) >= 4){
return new DustParticle(new Color(((int) $d[1]) & 0xff, ((int) $d[2]) & 0xff, ((int) $d[3]) & 0xff, isset($d[4]) ? ((int) $d[4]) & 0xff : 255));

View File

@@ -29,10 +29,10 @@ use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use pocketmine\world\Position;
use pocketmine\world\World;
use function count;
use function implode;
use function round;
class SpawnpointCommand extends VanillaCommand{
@@ -43,7 +43,10 @@ class SpawnpointCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_spawnpoint_description(),
KnownTranslationFactory::commands_spawnpoint_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_SPAWNPOINT);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_SPAWNPOINT_SELF,
DefaultPermissionNames::COMMAND_SPAWNPOINT_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -51,23 +54,9 @@ class SpawnpointCommand extends VanillaCommand{
return true;
}
$target = null;
if(count($args) === 0){
if($sender instanceof Player){
$target = $sender;
}else{
$sender->sendMessage(TextFormat::RED . "Please provide a player!");
return true;
}
}else{
$target = $sender->getServer()->getPlayerByPrefix($args[0]);
if($target === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}
$target = $this->fetchPermittedPlayerTarget($sender, $args[0] ?? null, DefaultPermissionNames::COMMAND_SPAWNPOINT_SELF, DefaultPermissionNames::COMMAND_SPAWNPOINT_OTHER);
if($target === null){
return true;
}
if(count($args) === 4){
@@ -81,19 +70,13 @@ class SpawnpointCommand extends VanillaCommand{
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_spawnpoint_success($target->getName(), (string) round($x, 2), (string) round($y, 2), (string) round($z, 2)));
return true;
}elseif(count($args) <= 1){
if($sender instanceof Player){
$cpos = $sender->getPosition();
$pos = Position::fromObject($cpos->floor(), $cpos->getWorld());
$target->setSpawn($pos);
}elseif(count($args) <= 1 && $sender instanceof Player){
$cpos = $sender->getPosition();
$pos = Position::fromObject($cpos->floor(), $cpos->getWorld());
$target->setSpawn($pos);
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_spawnpoint_success($target->getName(), (string) round($pos->x, 2), (string) round($pos->y, 2), (string) round($pos->z, 2)));
return true;
}else{
$sender->sendMessage(TextFormat::RED . "Please provide a player!");
return true;
}
Command::broadcastCommandMessage($sender, KnownTranslationFactory::commands_spawnpoint_success($target->getName(), (string) round($pos->x, 2), (string) round($pos->y, 2), (string) round($pos->z, 2)));
return true;
}
throw new InvalidCommandSyntaxException();

View File

@@ -35,6 +35,7 @@ use pocketmine\utils\TextFormat;
use pocketmine\world\World;
use function array_shift;
use function count;
use function implode;
use function round;
class TeleportCommand extends VanillaCommand{
@@ -46,7 +47,10 @@ class TeleportCommand extends VanillaCommand{
KnownTranslationFactory::commands_tp_usage(),
["teleport"]
);
$this->setPermission(DefaultPermissionNames::COMMAND_TELEPORT);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_TELEPORT_SELF,
DefaultPermissionNames::COMMAND_TELEPORT_OTHER
]));
}
private function findPlayer(CommandSender $sender, string $playerName) : ?Player{
@@ -67,31 +71,25 @@ class TeleportCommand extends VanillaCommand{
case 1: // /tp targetPlayer
case 3: // /tp x y z
case 5: // /tp x y z yaw pitch - TODO: 5 args could be target x y z yaw :(
if(!($sender instanceof Player)){
$sender->sendMessage(TextFormat::RED . "Please provide a player!");
return true;
}
$subject = $sender;
$targetArgs = $args;
$subjectName = null; //self
break;
case 2: // /tp player1 player2
case 4: // /tp player1 x y z - TODO: 4 args could be x y z yaw :(
case 6: // /tp player1 x y z yaw pitch
$subject = $this->findPlayer($sender, $args[0]);
if($subject === null){
return true;
}
$targetArgs = $args;
array_shift($targetArgs);
$subjectName = array_shift($args);
break;
default:
throw new InvalidCommandSyntaxException();
}
switch(count($targetArgs)){
$subject = $this->fetchPermittedPlayerTarget($sender, $subjectName, DefaultPermissionNames::COMMAND_TELEPORT_SELF, DefaultPermissionNames::COMMAND_TELEPORT_OTHER);
if($subject === null){
return true;
}
switch(count($args)){
case 1:
$targetPlayer = $this->findPlayer($sender, $targetArgs[0]);
$targetPlayer = $this->findPlayer($sender, $args[0]);
if($targetPlayer === null){
return true;
}
@@ -103,17 +101,17 @@ class TeleportCommand extends VanillaCommand{
case 3:
case 5:
$base = $subject->getLocation();
if(count($targetArgs) === 5){
$yaw = (float) $targetArgs[3];
$pitch = (float) $targetArgs[4];
if(count($args) === 5){
$yaw = (float) $args[3];
$pitch = (float) $args[4];
}else{
$yaw = $base->yaw;
$pitch = $base->pitch;
}
$x = $this->getRelativeDouble($base->x, $sender, $targetArgs[0]);
$y = $this->getRelativeDouble($base->y, $sender, $targetArgs[1], World::Y_MIN, World::Y_MAX);
$z = $this->getRelativeDouble($base->z, $sender, $targetArgs[2]);
$x = $this->getRelativeDouble($base->x, $sender, $args[0]);
$y = $this->getRelativeDouble($base->y, $sender, $args[1], World::Y_MIN, World::Y_MAX);
$z = $this->getRelativeDouble($base->z, $sender, $args[2]);
$targetLocation = new Location($x, $y, $z, $base->getWorld(), $yaw, $pitch);
$subject->teleport($targetLocation);

View File

@@ -27,7 +27,6 @@ use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\permission\DefaultPermissionNames;
use pocketmine\utils\TextFormat;
use function array_slice;
use function count;
use function implode;
@@ -40,7 +39,10 @@ class TitleCommand extends VanillaCommand{
KnownTranslationFactory::pocketmine_command_title_description(),
KnownTranslationFactory::commands_title_usage()
);
$this->setPermission(DefaultPermissionNames::COMMAND_TITLE);
$this->setPermission(implode(";", [
DefaultPermissionNames::COMMAND_TITLE_SELF,
DefaultPermissionNames::COMMAND_TITLE_OTHER
]));
}
public function execute(CommandSender $sender, string $commandLabel, array $args){
@@ -52,9 +54,8 @@ class TitleCommand extends VanillaCommand{
throw new InvalidCommandSyntaxException();
}
$player = $sender->getServer()->getPlayerByPrefix($args[0]);
$player = $this->fetchPermittedPlayerTarget($sender, $args[0], DefaultPermissionNames::COMMAND_TITLE_SELF, DefaultPermissionNames::COMMAND_TITLE_OTHER);
if($player === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return true;
}

View File

@@ -27,6 +27,7 @@ use pocketmine\command\Command;
use pocketmine\command\CommandSender;
use pocketmine\command\utils\InvalidCommandSyntaxException;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\player\Player;
use pocketmine\utils\TextFormat;
use function is_numeric;
use function substr;
@@ -35,6 +36,28 @@ abstract class VanillaCommand extends Command{
public const MAX_COORD = 30000000;
public const MIN_COORD = -30000000;
protected function fetchPermittedPlayerTarget(CommandSender $sender, ?string $target, string $selfPermission, string $otherPermission) : ?Player{
if($target !== null){
$player = $sender->getServer()->getPlayerByPrefix($target);
}elseif($sender instanceof Player){
$player = $sender;
}else{
throw new InvalidCommandSyntaxException();
}
if($player === null){
$sender->sendMessage(KnownTranslationFactory::commands_generic_player_notFound()->prefix(TextFormat::RED));
return null;
}
if(
($player === $sender && $this->testPermission($sender, $selfPermission)) ||
($player !== $sender && $this->testPermission($sender, $otherPermission))
){
return $player;
}
return null;
}
protected function getInteger(CommandSender $sender, string $value, int $min = self::MIN_COORD, int $max = self::MAX_COORD) : int{
$i = (int) $value;

View File

@@ -26,9 +26,8 @@ namespace pocketmine\crafting;
use pocketmine\item\Item;
use pocketmine\item\ItemFactory;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use pocketmine\utils\Filesystem;
use function array_map;
use function file_get_contents;
use function is_array;
use function json_decode;
@@ -52,7 +51,7 @@ final class CraftingManagerFromDataHelper{
}
public static function make(string $filePath) : CraftingManager{
$recipes = json_decode(Utils::assumeNotFalse(file_get_contents($filePath), "Missing required resource file"), true);
$recipes = json_decode(Filesystem::fileGetContents($filePath), true);
if(!is_array($recipes)){
throw new AssumptionFailedError("recipes.json root should contain a map of recipe types");
}

View File

@@ -29,8 +29,8 @@ use pocketmine\utils\Utils;
use function array_values;
use function count;
use function implode;
use function str_contains;
use function strlen;
use function strpos;
class ShapedRecipe implements CraftingRecipe{
/** @var string[] */
@@ -86,7 +86,7 @@ class ShapedRecipe implements CraftingRecipe{
$this->shape = $shape;
foreach($ingredients as $char => $i){
if(strpos(implode($this->shape), $char) === false){
if(!str_contains(implode($this->shape), $char)){
throw new \InvalidArgumentException("Symbol '$char' does not appear in the recipe shape");
}

View File

@@ -55,6 +55,7 @@ use function phpversion;
use function preg_replace;
use function sprintf;
use function str_split;
use function str_starts_with;
use function strpos;
use function substr;
use function zend_version;
@@ -237,7 +238,7 @@ class CrashDump{
private function determinePluginFromFile(string $filePath, bool $crashFrame) : bool{
$frameCleanPath = Filesystem::cleanPath($filePath);
if(strpos($frameCleanPath, Filesystem::CLEAN_PATH_SRC_PREFIX) !== 0){
if(!str_starts_with($frameCleanPath, Filesystem::CLEAN_PATH_SRC_PREFIX)){
if($crashFrame){
$this->data->plugin_involvement = self::PLUGIN_INVOLVEMENT_DIRECT;
}else{
@@ -250,7 +251,7 @@ class CrashDump{
$file->setAccessible(true);
foreach($this->server->getPluginManager()->getPlugins() as $plugin){
$filePath = Filesystem::cleanPath($file->getValue($plugin));
if(strpos($frameCleanPath, $filePath) === 0){
if(str_starts_with($frameCleanPath, $filePath)){
$this->data->plugin = $plugin->getName();
break;
}

View File

@@ -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\data\bedrock;
use const pocketmine\BEDROCK_DATA_PATH;
final class BedrockDataFiles{
private function __construct(){
//NOOP
}
public const BANNER_PATTERNS_JSON = BEDROCK_DATA_PATH . '/banner_patterns.json';
public const BIOME_DEFINITIONS_NBT = BEDROCK_DATA_PATH . '/biome_definitions.nbt';
public const BIOME_DEFINITIONS_FULL_NBT = BEDROCK_DATA_PATH . '/biome_definitions_full.nbt';
public const BIOME_ID_MAP_JSON = BEDROCK_DATA_PATH . '/biome_id_map.json';
public const BLOCK_ID_TO_ITEM_ID_MAP_JSON = BEDROCK_DATA_PATH . '/block_id_to_item_id_map.json';
public const BLOCK_STATE_META_MAP_JSON = BEDROCK_DATA_PATH . '/block_state_meta_map.json';
public const CANONICAL_BLOCK_STATES_NBT = BEDROCK_DATA_PATH . '/canonical_block_states.nbt';
public const COMMAND_ARG_TYPES_JSON = BEDROCK_DATA_PATH . '/command_arg_types.json';
public const CREATIVEITEMS_JSON = BEDROCK_DATA_PATH . '/creativeitems.json';
public const ENTITY_ID_MAP_JSON = BEDROCK_DATA_PATH . '/entity_id_map.json';
public const ENTITY_IDENTIFIERS_NBT = BEDROCK_DATA_PATH . '/entity_identifiers.nbt';
public const ITEM_TAGS_JSON = BEDROCK_DATA_PATH . '/item_tags.json';
public const LEVEL_SOUND_ID_MAP_JSON = BEDROCK_DATA_PATH . '/level_sound_id_map.json';
public const PARTICLE_ID_MAP_JSON = BEDROCK_DATA_PATH . '/particle_id_map.json';
public const R12_TO_CURRENT_BLOCK_MAP_BIN = BEDROCK_DATA_PATH . '/r12_to_current_block_map.bin';
public const R16_TO_CURRENT_ITEM_MAP_JSON = BEDROCK_DATA_PATH . '/r16_to_current_item_map.json';
public const REQUIRED_ITEM_LIST_JSON = BEDROCK_DATA_PATH . '/required_item_list.json';
}

View File

@@ -73,6 +73,8 @@ final class EnchantmentIdMap{
$this->register(EnchantmentIds::MENDING, VanillaEnchantments::MENDING());
$this->register(EnchantmentIds::VANISHING, VanillaEnchantments::VANISHING());
$this->register(EnchantmentIds::SWIFT_SNEAK, VanillaEnchantments::SWIFT_SNEAK());
}
public function register(int $mcpeId, Enchantment $enchantment) : void{

View File

@@ -66,4 +66,5 @@ final class EnchantmentIds{
public const PIERCING = 34;
public const QUICK_CHARGE = 35;
public const SOUL_SPEED = 36;
public const SWIFT_SNEAK = 37;
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Symfony\Component\Filesystem\Path;
final class LegacyBiomeIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;
public function __construct(){
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'biome_id_map.json'));
parent::__construct(BedrockDataFiles::BIOME_ID_MAP_JSON);
}
}

View File

@@ -30,6 +30,6 @@ final class LegacyBlockIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;
public function __construct(){
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'block_id_map.json'));
parent::__construct(Path::join(\pocketmine\BEDROCK_BLOCK_UPGRADE_SCHEMA_PATH, 'block_legacy_id_map.json'));
}
}

View File

@@ -24,12 +24,11 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\SingletonTrait;
use Symfony\Component\Filesystem\Path;
final class LegacyEntityIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;
public function __construct(){
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'entity_id_map.json'));
parent::__construct(BedrockDataFiles::ENTITY_ID_MAP_JSON);
}
}

View File

@@ -30,6 +30,6 @@ final class LegacyItemIdToStringIdMap extends LegacyToStringBidirectionalIdMap{
use SingletonTrait;
public function __construct(){
parent::__construct(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'item_id_map.json'));
parent::__construct(Path::join(\pocketmine\BEDROCK_ITEM_UPGRADE_SCHEMA_PATH, 'item_legacy_id_map.json'));
}
}

View File

@@ -24,8 +24,7 @@ declare(strict_types=1);
namespace pocketmine\data\bedrock;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
use function file_get_contents;
use pocketmine\utils\Filesystem;
use function is_array;
use function is_int;
use function is_string;
@@ -45,7 +44,7 @@ abstract class LegacyToStringBidirectionalIdMap{
private array $stringToLegacy = [];
public function __construct(string $file){
$stringToLegacyId = json_decode(Utils::assumeNotFalse(file_get_contents($file), "Missing required resource file"), true);
$stringToLegacyId = json_decode(Filesystem::fileGetContents($file), true);
if(!is_array($stringToLegacyId)){
throw new AssumptionFailedError("Invalid format of ID map");
}

View File

@@ -81,6 +81,15 @@ abstract class Entity{
public const MOTION_THRESHOLD = 0.00001;
protected const STEP_CLIP_MULTIPLIER = 0.4;
private const TAG_FIRE = "Fire"; //TAG_Short
private const TAG_ON_GROUND = "OnGround"; //TAG_Byte
private const TAG_FALL_DISTANCE = "FallDistance"; //TAG_Float
private const TAG_CUSTOM_NAME = "CustomName"; //TAG_String
private const TAG_CUSTOM_NAME_VISIBLE = "CustomNameVisible"; //TAG_Byte
public const TAG_POS = "Pos"; //TAG_List<TAG_Double>|TAG_List<TAG_Float>
public const TAG_MOTION = "Motion"; //TAG_List<TAG_Double>|TAG_List<TAG_Float>
public const TAG_ROTATION = "Rotation"; //TAG_List<TAG_Float>
private static int $entityCount = 1;
/**
@@ -233,7 +242,7 @@ abstract class Entity{
$this->recalculateBoundingBox();
if($nbt !== null){
$this->motion = EntityDataHelper::parseVec3($nbt, "Motion", true);
$this->motion = EntityDataHelper::parseVec3($nbt, self::TAG_MOTION, true);
}else{
$this->motion = new Vector3(0, 0, 0);
}
@@ -466,17 +475,17 @@ abstract class Entity{
public function saveNBT() : CompoundTag{
$nbt = CompoundTag::create()
->setTag("Pos", new ListTag([
->setTag(self::TAG_POS, new ListTag([
new DoubleTag($this->location->x),
new DoubleTag($this->location->y),
new DoubleTag($this->location->z)
]))
->setTag("Motion", new ListTag([
->setTag(self::TAG_MOTION, new ListTag([
new DoubleTag($this->motion->x),
new DoubleTag($this->motion->y),
new DoubleTag($this->motion->z)
]))
->setTag("Rotation", new ListTag([
->setTag(self::TAG_ROTATION, new ListTag([
new FloatTag($this->location->yaw),
new FloatTag($this->location->pitch)
]));
@@ -485,33 +494,33 @@ abstract class Entity{
EntityFactory::getInstance()->injectSaveId(get_class($this), $nbt);
if($this->getNameTag() !== ""){
$nbt->setString("CustomName", $this->getNameTag());
$nbt->setByte("CustomNameVisible", $this->isNameTagVisible() ? 1 : 0);
$nbt->setString(self::TAG_CUSTOM_NAME, $this->getNameTag());
$nbt->setByte(self::TAG_CUSTOM_NAME_VISIBLE, $this->isNameTagVisible() ? 1 : 0);
}
}
$nbt->setFloat("FallDistance", $this->fallDistance);
$nbt->setShort("Fire", $this->fireTicks);
$nbt->setByte("OnGround", $this->onGround ? 1 : 0);
$nbt->setFloat(self::TAG_FALL_DISTANCE, $this->fallDistance);
$nbt->setShort(self::TAG_FIRE, $this->fireTicks);
$nbt->setByte(self::TAG_ON_GROUND, $this->onGround ? 1 : 0);
return $nbt;
}
protected function initEntity(CompoundTag $nbt) : void{
$this->fireTicks = $nbt->getShort("Fire", 0);
$this->fireTicks = $nbt->getShort(self::TAG_FIRE, 0);
$this->onGround = $nbt->getByte("OnGround", 0) !== 0;
$this->onGround = $nbt->getByte(self::TAG_ON_GROUND, 0) !== 0;
$this->fallDistance = $nbt->getFloat("FallDistance", 0.0);
$this->fallDistance = $nbt->getFloat(self::TAG_FALL_DISTANCE, 0.0);
if(($customNameTag = $nbt->getTag("CustomName")) instanceof StringTag){
if(($customNameTag = $nbt->getTag(self::TAG_CUSTOM_NAME)) instanceof StringTag){
$this->setNameTag($customNameTag->getValue());
if(($customNameVisibleTag = $nbt->getTag("CustomNameVisible")) instanceof StringTag){
if(($customNameVisibleTag = $nbt->getTag(self::TAG_CUSTOM_NAME_VISIBLE)) instanceof StringTag){
//Older versions incorrectly saved this as a string (see 890f72dbf23a77f294169b79590770470041adc4)
$this->setNameTagVisible($customNameVisibleTag->getValue() !== "");
}else{
$this->setNameTagVisible($nbt->getByte("CustomNameVisible", 1) !== 0);
$this->setNameTagVisible($nbt->getByte(self::TAG_CUSTOM_NAME_VISIBLE, 1) !== 0);
}
}
}
@@ -794,6 +803,15 @@ abstract class Entity{
$this->server->broadcastPackets($this->hasSpawned, [SetActorMotionPacket::create($this->id, $this->getMotion())]);
}
public function getGravity() : float{
return $this->gravity;
}
public function setGravity(float $gravity) : void{
Utils::checkFloatNotInfOrNaN("gravity", $gravity);
$this->gravity = $gravity;
}
public function hasGravity() : bool{
return $this->gravityEnabled;
}

View File

@@ -57,19 +57,19 @@ final class EntityDataHelper{
* @throws SavedDataLoadingException
*/
public static function parseLocation(CompoundTag $nbt, World $world) : Location{
$pos = self::parseVec3($nbt, "Pos", false);
$pos = self::parseVec3($nbt, Entity::TAG_POS, false);
$yawPitch = $nbt->getTag("Rotation");
$yawPitch = $nbt->getTag(Entity::TAG_ROTATION);
if(!($yawPitch instanceof ListTag) || $yawPitch->getTagType() !== NBT::TAG_Float){
throw new SavedDataLoadingException("'Rotation' should be a List<Float>");
throw new SavedDataLoadingException("'" . Entity::TAG_ROTATION . "' should be a List<Float>");
}
/** @var FloatTag[] $values */
$values = $yawPitch->getValue();
if(count($values) !== 2){
throw new SavedDataLoadingException("Expected exactly 2 entries for 'Rotation'");
}
self::validateFloat("Rotation", "yaw", $values[0]->getValue());
self::validateFloat("Rotation", "pitch", $values[1]->getValue());
self::validateFloat(Entity::TAG_ROTATION, "yaw", $values[0]->getValue());
self::validateFloat(Entity::TAG_ROTATION, "pitch", $values[1]->getValue());
return Location::fromObject($pos, $world, $values[0]->getValue(), $values[1]->getValue());
}

View File

@@ -66,6 +66,9 @@ use function reset;
final class EntityFactory{
use SingletonTrait;
public const TAG_IDENTIFIER = "identifier"; //TAG_String
public const TAG_LEGACY_ID = "id"; //TAG_Int
/**
* @var \Closure[] save ID => creator function
* @phpstan-var array<int|string, \Closure(World, CompoundTag) : Entity>
@@ -113,9 +116,9 @@ final class EntityFactory{
}, ['FallingSand', 'minecraft:falling_block'], LegacyIds::FALLING_BLOCK);
$this->register(ItemEntity::class, function(World $world, CompoundTag $nbt) : ItemEntity{
$itemTag = $nbt->getCompoundTag("Item");
$itemTag = $nbt->getCompoundTag(ItemEntity::TAG_ITEM);
if($itemTag === null){
throw new SavedDataLoadingException("Expected \"Item\" NBT tag not found");
throw new SavedDataLoadingException("Expected \"" . ItemEntity::TAG_ITEM . "\" NBT tag not found");
}
$item = Item::nbtDeserialize($itemTag);
@@ -126,14 +129,14 @@ final class EntityFactory{
}, ['Item', 'minecraft:item'], LegacyIds::ITEM);
$this->register(Painting::class, function(World $world, CompoundTag $nbt) : Painting{
$motive = PaintingMotive::getMotiveByName($nbt->getString("Motive"));
$motive = PaintingMotive::getMotiveByName($nbt->getString(Painting::TAG_MOTIVE));
if($motive === null){
throw new SavedDataLoadingException("Unknown painting motive");
}
$blockIn = new Vector3($nbt->getInt("TileX"), $nbt->getInt("TileY"), $nbt->getInt("TileZ"));
if(($directionTag = $nbt->getTag("Direction")) instanceof ByteTag){
$blockIn = new Vector3($nbt->getInt(Painting::TAG_TILE_X), $nbt->getInt(Painting::TAG_TILE_Y), $nbt->getInt(Painting::TAG_TILE_Z));
if(($directionTag = $nbt->getTag(Painting::TAG_DIRECTION_BE)) instanceof ByteTag){
$facing = Painting::DATA_TO_FACING[$directionTag->getValue()] ?? Facing::NORTH;
}elseif(($facingTag = $nbt->getTag("Facing")) instanceof ByteTag){
}elseif(($facingTag = $nbt->getTag(Painting::TAG_FACING_JE)) instanceof ByteTag){
$facing = Painting::DATA_TO_FACING[$facingTag->getValue()] ?? Facing::NORTH;
}else{
throw new SavedDataLoadingException("Missing facing info");
@@ -151,7 +154,7 @@ final class EntityFactory{
}, ['Snowball', 'minecraft:snowball'], LegacyIds::SNOWBALL);
$this->register(SplashPotion::class, function(World $world, CompoundTag $nbt) : SplashPotion{
$potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort("PotionId", PotionTypeIds::WATER));
$potionType = PotionTypeIdMap::getInstance()->fromId($nbt->getShort(SplashPotion::TAG_POTION_ID, PotionTypeIds::WATER));
if($potionType === null){
throw new SavedDataLoadingException("No such potion type");
}
@@ -217,7 +220,7 @@ final class EntityFactory{
*/
public function createFromData(World $world, CompoundTag $nbt) : ?Entity{
try{
$saveId = $nbt->getTag("identifier") ?? $nbt->getTag("id");
$saveId = $nbt->getTag(self::TAG_IDENTIFIER) ?? $nbt->getTag(self::TAG_LEGACY_ID);
$func = null;
if($saveId instanceof StringTag){
$func = $this->creationFuncs[$saveId->getValue()] ?? null;
@@ -238,7 +241,7 @@ final class EntityFactory{
public function injectSaveId(string $class, CompoundTag $saveData) : void{
if(isset($this->saveNames[$class])){
$saveData->setTag("identifier", new StringTag($this->saveNames[$class]));
$saveData->setTag(self::TAG_IDENTIFIER, new StringTag($this->saveNames[$class]));
}else{
throw new \InvalidArgumentException("Entity $class is not registered");
}

View File

@@ -50,6 +50,8 @@ use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\protocol\AddPlayerPacket;
use pocketmine\network\mcpe\protocol\PlayerListPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
use pocketmine\network\mcpe\protocol\types\AbilitiesData;
use pocketmine\network\mcpe\protocol\types\AbilitiesLayer;
use pocketmine\network\mcpe\protocol\types\command\CommandPermissions;
use pocketmine\network\mcpe\protocol\types\DeviceOS;
use pocketmine\network\mcpe\protocol\types\entity\EntityIds;
@@ -60,7 +62,6 @@ use pocketmine\network\mcpe\protocol\types\GameMode;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\network\mcpe\protocol\types\PlayerListEntry;
use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
use pocketmine\network\mcpe\protocol\types\UpdateAbilitiesPacketLayer;
use pocketmine\network\mcpe\protocol\UpdateAbilitiesPacket;
use pocketmine\player\Player;
use pocketmine\utils\Limits;
@@ -77,6 +78,26 @@ use function random_int;
class Human extends Living implements ProjectileSource, InventoryHolder{
private const TAG_INVENTORY = "Inventory"; //TAG_List<TAG_Compound>
private const TAG_OFF_HAND_ITEM = "OffHandItem"; //TAG_Compound
private const TAG_ENDER_CHEST_INVENTORY = "EnderChestInventory"; //TAG_List<TAG_Compound>
private const TAG_SELECTED_INVENTORY_SLOT = "SelectedInventorySlot"; //TAG_Int
private const TAG_FOOD_LEVEL = "foodLevel"; //TAG_Int
private const TAG_FOOD_EXHAUSTION_LEVEL = "foodExhaustionLevel"; //TAG_Float
private const TAG_FOOD_SATURATION_LEVEL = "foodSaturationLevel"; //TAG_Float
private const TAG_FOOD_TICK_TIMER = "foodTickTimer"; //TAG_Int
private const TAG_XP_LEVEL = "XpLevel"; //TAG_Int
private const TAG_XP_PROGRESS = "XpP"; //TAG_Float
private const TAG_LIFETIME_XP_TOTAL = "XpTotal"; //TAG_Int
private const TAG_XP_SEED = "XpSeed"; //TAG_Int
private const TAG_NAME_TAG = "NameTag"; //TAG_String
private const TAG_SKIN = "Skin"; //TAG_Compound
private const TAG_SKIN_NAME = "Name"; //TAG_String
private const TAG_SKIN_DATA = "Data"; //TAG_ByteArray
private const TAG_SKIN_CAPE_DATA = "CapeData"; //TAG_ByteArray
private const TAG_SKIN_GEOMETRY_NAME = "GeometryName"; //TAG_String
private const TAG_SKIN_GEOMETRY_DATA = "GeometryData"; //TAG_ByteArray
public static function getNetworkTypeId() : string{ return EntityIds::PLAYER; }
/** @var PlayerInventory */
@@ -114,16 +135,16 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
* @throws SavedDataLoadingException
*/
public static function parseSkinNBT(CompoundTag $nbt) : Skin{
$skinTag = $nbt->getCompoundTag("Skin");
$skinTag = $nbt->getCompoundTag(self::TAG_SKIN);
if($skinTag === null){
throw new SavedDataLoadingException("Missing skin data");
}
return new Skin( //this throws if the skin is invalid
$skinTag->getString("Name"),
($skinDataTag = $skinTag->getTag("Data")) instanceof StringTag ? $skinDataTag->getValue() : $skinTag->getByteArray("Data"), //old data (this used to be saved as a StringTag in older versions of PM)
$skinTag->getByteArray("CapeData", ""),
$skinTag->getString("GeometryName", ""),
$skinTag->getByteArray("GeometryData", "")
$skinTag->getString(self::TAG_SKIN_NAME),
($skinDataTag = $skinTag->getTag(self::TAG_SKIN_DATA)) instanceof StringTag ? $skinDataTag->getValue() : $skinTag->getByteArray(self::TAG_SKIN_DATA), //old data (this used to be saved as a StringTag in older versions of PM)
$skinTag->getByteArray(self::TAG_SKIN_CAPE_DATA, ""),
$skinTag->getString(self::TAG_SKIN_GEOMETRY_NAME, ""),
$skinTag->getByteArray(self::TAG_SKIN_GEOMETRY_DATA, "")
);
}
@@ -221,7 +242,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
* For Human entities which are not players, sets their properties such as nametag, skin and UUID from NBT.
*/
protected function initHumanData(CompoundTag $nbt) : void{
if(($nameTagTag = $nbt->getTag("NameTag")) instanceof StringTag){
if(($nameTagTag = $nbt->getTag(self::TAG_NAME_TAG)) instanceof StringTag){
$this->setNameTag($nameTagTag->getValue());
}
@@ -270,14 +291,14 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->enderInventory = new PlayerEnderInventory($this);
$this->initHumanData($nbt);
$inventoryTag = $nbt->getListTag("Inventory");
$inventoryTag = $nbt->getListTag(self::TAG_INVENTORY);
if($inventoryTag !== null){
$inventoryItems = [];
$armorInventoryItems = [];
/** @var CompoundTag $item */
foreach($inventoryTag as $i => $item){
$slot = $item->getByte("Slot");
$slot = $item->getByte(Item::TAG_SLOT);
if($slot >= 0 && $slot < 9){ //Hotbar
//Old hotbar saving stuff, ignore it
}elseif($slot >= 100 && $slot < 104){ //Armor
@@ -290,7 +311,7 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
self::populateInventoryFromListTag($this->inventory, $inventoryItems);
self::populateInventoryFromListTag($this->armorInventory, $armorInventoryItems);
}
$offHand = $nbt->getCompoundTag("OffHandItem");
$offHand = $nbt->getCompoundTag(self::TAG_OFF_HAND_ITEM);
if($offHand !== null){
$this->offHandInventory->setItem(0, Item::nbtDeserialize($offHand));
}
@@ -300,35 +321,35 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
}));
$enderChestInventoryTag = $nbt->getListTag("EnderChestInventory");
$enderChestInventoryTag = $nbt->getListTag(self::TAG_ENDER_CHEST_INVENTORY);
if($enderChestInventoryTag !== null){
$enderChestInventoryItems = [];
/** @var CompoundTag $item */
foreach($enderChestInventoryTag as $i => $item){
$enderChestInventoryItems[$item->getByte("Slot")] = Item::nbtDeserialize($item);
$enderChestInventoryItems[$item->getByte(Item::TAG_SLOT)] = Item::nbtDeserialize($item);
}
self::populateInventoryFromListTag($this->enderInventory, $enderChestInventoryItems);
}
$this->inventory->setHeldItemIndex($nbt->getInt("SelectedInventorySlot", 0));
$this->inventory->setHeldItemIndex($nbt->getInt(self::TAG_SELECTED_INVENTORY_SLOT, 0));
$this->inventory->getHeldItemIndexChangeListeners()->add(function(int $oldIndex) : void{
foreach($this->getViewers() as $viewer){
$viewer->getNetworkSession()->onMobMainHandItemChange($this);
}
});
$this->hungerManager->setFood((float) $nbt->getInt("foodLevel", (int) $this->hungerManager->getFood()));
$this->hungerManager->setExhaustion($nbt->getFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion()));
$this->hungerManager->setSaturation($nbt->getFloat("foodSaturationLevel", $this->hungerManager->getSaturation()));
$this->hungerManager->setFoodTickTimer($nbt->getInt("foodTickTimer", $this->hungerManager->getFoodTickTimer()));
$this->hungerManager->setFood((float) $nbt->getInt(self::TAG_FOOD_LEVEL, (int) $this->hungerManager->getFood()));
$this->hungerManager->setExhaustion($nbt->getFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion()));
$this->hungerManager->setSaturation($nbt->getFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation()));
$this->hungerManager->setFoodTickTimer($nbt->getInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer()));
$this->xpManager->setXpAndProgressNoEvent(
$nbt->getInt("XpLevel", 0),
$nbt->getFloat("XpP", 0.0));
$this->xpManager->setLifetimeTotalXp($nbt->getInt("XpTotal", 0));
$nbt->getInt(self::TAG_XP_LEVEL, 0),
$nbt->getFloat(self::TAG_XP_PROGRESS, 0.0));
$this->xpManager->setLifetimeTotalXp($nbt->getInt(self::TAG_LIFETIME_XP_TOTAL, 0));
if(($xpSeedTag = $nbt->getTag("XpSeed")) instanceof IntTag){
if(($xpSeedTag = $nbt->getTag(self::TAG_XP_SEED)) instanceof IntTag){
$this->xpSeed = $xpSeedTag->getValue();
}else{
$this->xpSeed = random_int(Limits::INT32_MIN, Limits::INT32_MAX);
@@ -391,24 +412,24 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
$this->inventory !== null ? array_values($this->inventory->getContents()) : [],
$this->armorInventory !== null ? array_values($this->armorInventory->getContents()) : [],
$this->offHandInventory !== null ? array_values($this->offHandInventory->getContents()) : [],
), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()); });
), function(Item $item) : bool{ return !$item->hasEnchantment(VanillaEnchantments::VANISHING()) && !$item->keepOnDeath(); });
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setInt("foodLevel", (int) $this->hungerManager->getFood());
$nbt->setFloat("foodExhaustionLevel", $this->hungerManager->getExhaustion());
$nbt->setFloat("foodSaturationLevel", $this->hungerManager->getSaturation());
$nbt->setInt("foodTickTimer", $this->hungerManager->getFoodTickTimer());
$nbt->setInt(self::TAG_FOOD_LEVEL, (int) $this->hungerManager->getFood());
$nbt->setFloat(self::TAG_FOOD_EXHAUSTION_LEVEL, $this->hungerManager->getExhaustion());
$nbt->setFloat(self::TAG_FOOD_SATURATION_LEVEL, $this->hungerManager->getSaturation());
$nbt->setInt(self::TAG_FOOD_TICK_TIMER, $this->hungerManager->getFoodTickTimer());
$nbt->setInt("XpLevel", $this->xpManager->getXpLevel());
$nbt->setFloat("XpP", $this->xpManager->getXpProgress());
$nbt->setInt("XpTotal", $this->xpManager->getLifetimeTotalXp());
$nbt->setInt("XpSeed", $this->xpSeed);
$nbt->setInt(self::TAG_XP_LEVEL, $this->xpManager->getXpLevel());
$nbt->setFloat(self::TAG_XP_PROGRESS, $this->xpManager->getXpProgress());
$nbt->setInt(self::TAG_LIFETIME_XP_TOTAL, $this->xpManager->getLifetimeTotalXp());
$nbt->setInt(self::TAG_XP_SEED, $this->xpSeed);
$inventoryTag = new ListTag([], NBT::TAG_Compound);
$nbt->setTag("Inventory", $inventoryTag);
$nbt->setTag(self::TAG_INVENTORY, $inventoryTag);
if($this->inventory !== null){
//Normal inventory
$slotCount = $this->inventory->getSize() + $this->inventory->getHotbarSize();
@@ -427,11 +448,11 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
}
$nbt->setInt("SelectedInventorySlot", $this->inventory->getHeldItemIndex());
$nbt->setInt(self::TAG_SELECTED_INVENTORY_SLOT, $this->inventory->getHeldItemIndex());
}
$offHandItem = $this->offHandInventory->getItem(0);
if(!$offHandItem->isNull()){
$nbt->setTag("OffHandItem", $offHandItem->nbtSerialize());
$nbt->setTag(self::TAG_OFF_HAND_ITEM, $offHandItem->nbtSerialize());
}
if($this->enderInventory !== null){
@@ -446,16 +467,16 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
}
}
$nbt->setTag("EnderChestInventory", new ListTag($items, NBT::TAG_Compound));
$nbt->setTag(self::TAG_ENDER_CHEST_INVENTORY, new ListTag($items, NBT::TAG_Compound));
}
if($this->skin !== null){
$nbt->setTag("Skin", CompoundTag::create()
->setString("Name", $this->skin->getSkinId())
->setByteArray("Data", $this->skin->getSkinData())
->setByteArray("CapeData", $this->skin->getCapeData())
->setString("GeometryName", $this->skin->getGeometryName())
->setByteArray("GeometryData", $this->skin->getGeometryData())
$nbt->setTag(self::TAG_SKIN, CompoundTag::create()
->setString(self::TAG_SKIN_NAME, $this->skin->getSkinId())
->setByteArray(self::TAG_SKIN_DATA, $this->skin->getSkinData())
->setByteArray(self::TAG_SKIN_CAPE_DATA, $this->skin->getCapeData())
->setString(self::TAG_SKIN_GEOMETRY_NAME, $this->skin->getGeometryName())
->setByteArray(self::TAG_SKIN_GEOMETRY_DATA, $this->skin->getGeometryData())
);
}
@@ -487,14 +508,14 @@ class Human extends Living implements ProjectileSource, InventoryHolder{
GameMode::SURVIVAL,
$this->getAllNetworkData(),
new PropertySyncData([], []),
UpdateAbilitiesPacket::create(CommandPermissions::NORMAL, PlayerPermissions::VISITOR, $this->getId() /* TODO: this should be unique ID */, [
new UpdateAbilitiesPacketLayer(
UpdateAbilitiesPacketLayer::LAYER_BASE,
array_fill(0, UpdateAbilitiesPacketLayer::NUMBER_OF_ABILITIES, false),
UpdateAbilitiesPacket::create(new AbilitiesData(CommandPermissions::NORMAL, PlayerPermissions::VISITOR, $this->getId() /* TODO: this should be unique ID */, [
new AbilitiesLayer(
AbilitiesLayer::LAYER_BASE,
array_fill(0, AbilitiesLayer::NUMBER_OF_ABILITIES, false),
0.0,
0.0
)
]),
])),
[], //TODO: entity links
"", //device ID (we intentionally don't send this - secvuln)
DeviceOS::UNKNOWN //we intentionally don't send this (secvuln)

View File

@@ -77,6 +77,16 @@ use const M_PI;
abstract class Living extends Entity{
protected const DEFAULT_BREATH_TICKS = 300;
private const TAG_LEGACY_HEALTH = "HealF"; //TAG_Float
private const TAG_HEALTH = "Health"; //TAG_Float
private const TAG_BREATH_TICKS = "Air"; //TAG_Short
private const TAG_ACTIVE_EFFECTS = "ActiveEffects"; //TAG_List<TAG_Compound>
private const TAG_EFFECT_ID = "Id"; //TAG_Byte
private const TAG_EFFECT_DURATION = "Duration"; //TAG_Int
private const TAG_EFFECT_AMPLIFIER = "Amplifier"; //TAG_Byte
private const TAG_EFFECT_SHOW_PARTICLES = "ShowParticles"; //TAG_Byte
private const TAG_EFFECT_AMBIENT = "Ambient"; //TAG_Byte
protected $gravity = 0.08;
protected $drag = 0.02;
@@ -143,9 +153,9 @@ abstract class Living extends Entity{
$health = $this->getMaxHealth();
if(($healFTag = $nbt->getTag("HealF")) instanceof FloatTag){
if(($healFTag = $nbt->getTag(self::TAG_LEGACY_HEALTH)) instanceof FloatTag){
$health = $healFTag->getValue();
}elseif(($healthTag = $nbt->getTag("Health")) instanceof ShortTag){
}elseif(($healthTag = $nbt->getTag(self::TAG_HEALTH)) instanceof ShortTag){
$health = $healthTag->getValue(); //Older versions of PocketMine-MP incorrectly saved this as a short instead of a float
}elseif($healthTag instanceof FloatTag){
$health = $healthTag->getValue();
@@ -153,23 +163,23 @@ abstract class Living extends Entity{
$this->setHealth($health);
$this->setAirSupplyTicks($nbt->getShort("Air", self::DEFAULT_BREATH_TICKS));
$this->setAirSupplyTicks($nbt->getShort(self::TAG_BREATH_TICKS, self::DEFAULT_BREATH_TICKS));
/** @var CompoundTag[]|ListTag|null $activeEffectsTag */
$activeEffectsTag = $nbt->getListTag("ActiveEffects");
$activeEffectsTag = $nbt->getListTag(self::TAG_ACTIVE_EFFECTS);
if($activeEffectsTag !== null){
foreach($activeEffectsTag as $e){
$effect = EffectIdMap::getInstance()->fromId($e->getByte("Id"));
$effect = EffectIdMap::getInstance()->fromId($e->getByte(self::TAG_EFFECT_ID));
if($effect === null){
continue;
}
$this->effectManager->add(new EffectInstance(
$effect,
$e->getInt("Duration"),
Binary::unsignByte($e->getByte("Amplifier")),
$e->getByte("ShowParticles", 1) !== 0,
$e->getByte("Ambient", 0) !== 0
$e->getInt(self::TAG_EFFECT_DURATION),
Binary::unsignByte($e->getByte(self::TAG_EFFECT_AMPLIFIER)),
$e->getByte(self::TAG_EFFECT_SHOW_PARTICLES, 1) !== 0,
$e->getByte(self::TAG_EFFECT_AMBIENT, 0) !== 0
));
}
}
@@ -184,6 +194,13 @@ abstract class Living extends Entity{
$this->attributeMap->add($this->absorptionAttr = AttributeFactory::getInstance()->mustGet(Attribute::ABSORPTION));
}
/**
* Returns the name used to describe this entity in chat and command outputs.
*/
public function getDisplayName() : string{
return $this->nameTag !== "" ? $this->nameTag : $this->getName();
}
public function setHealth(float $amount) : void{
$wasAlive = $this->isAlive();
parent::setHealth($amount);
@@ -272,22 +289,22 @@ abstract class Living extends Entity{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setFloat("Health", $this->getHealth());
$nbt->setFloat(self::TAG_HEALTH, $this->getHealth());
$nbt->setShort("Air", $this->getAirSupplyTicks());
$nbt->setShort(self::TAG_BREATH_TICKS, $this->getAirSupplyTicks());
if(count($this->effectManager->all()) > 0){
$effects = [];
foreach($this->effectManager->all() as $effect){
$effects[] = CompoundTag::create()
->setByte("Id", EffectIdMap::getInstance()->toId($effect->getType()))
->setByte("Amplifier", Binary::signByte($effect->getAmplifier()))
->setInt("Duration", $effect->getDuration())
->setByte("Ambient", $effect->isAmbient() ? 1 : 0)
->setByte("ShowParticles", $effect->isVisible() ? 1 : 0);
->setByte(self::TAG_EFFECT_ID, EffectIdMap::getInstance()->toId($effect->getType()))
->setByte(self::TAG_EFFECT_AMPLIFIER, Binary::signByte($effect->getAmplifier()))
->setInt(self::TAG_EFFECT_DURATION, $effect->getDuration())
->setByte(self::TAG_EFFECT_AMBIENT, $effect->isAmbient() ? 1 : 0)
->setByte(self::TAG_EFFECT_SHOW_PARTICLES, $effect->isVisible() ? 1 : 0);
}
$nbt->setTag("ActiveEffects", new ListTag($effects));
$nbt->setTag(self::TAG_ACTIVE_EFFECTS, new ListTag($effects));
}
return $nbt;
@@ -448,7 +465,9 @@ abstract class Living extends Entity{
*/
protected function applyPostDamageEffects(EntityDamageEvent $source) : void{
$this->setAbsorption(max(0, $this->getAbsorption() + $source->getModifier(EntityDamageEvent::MODIFIER_ABSORPTION)));
$this->damageArmor($source->getBaseDamage());
if($source->canBeReducedByArmor()){
$this->damageArmor($source->getBaseDamage());
}
if($source instanceof EntityDamageByEntityEvent && ($attacker = $source->getDamager()) !== null){
$damage = 0;

View File

@@ -36,6 +36,8 @@ class Villager extends Living implements Ageable{
public const PROFESSION_BLACKSMITH = 3;
public const PROFESSION_BUTCHER = 4;
private const TAG_PROFESSION = "Profession"; //TAG_Int
public static function getNetworkTypeId() : string{ return EntityIds::VILLAGER; }
private bool $baby = false;
@@ -53,7 +55,7 @@ class Villager extends Living implements Ageable{
parent::initEntity($nbt);
/** @var int $profession */
$profession = $nbt->getInt("Profession", self::PROFESSION_FARMER);
$profession = $nbt->getInt(self::TAG_PROFESSION, self::PROFESSION_FARMER);
if($profession > 4 || $profession < 0){
$profession = self::PROFESSION_FARMER;
@@ -64,7 +66,7 @@ class Villager extends Living implements Ageable{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setInt("Profession", $this->getProfession());
$nbt->setInt(self::TAG_PROFESSION, $this->getProfession());
return $nbt;
}

View File

@@ -40,6 +40,7 @@ class ExperienceOrb extends Entity{
public const TAG_VALUE_PC = "Value"; //short
public const TAG_VALUE_PE = "experience value"; //int (WTF?)
private const TAG_AGE = "Age"; //TAG_Short
/** Max distance an orb will follow a player across. */
public const MAX_TARGET_DISTANCE = 8.0;
@@ -109,13 +110,13 @@ class ExperienceOrb extends Entity{
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
$this->age = $nbt->getShort("Age", 0);
$this->age = $nbt->getShort(self::TAG_AGE, 0);
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setShort("Age", $this->age);
$nbt->setShort(self::TAG_AGE, $this->age);
$nbt->setShort(self::TAG_VALUE_PC, $this->getXpValue());
$nbt->setInt(self::TAG_VALUE_PE, $this->getXpValue());

View File

@@ -44,6 +44,10 @@ use function abs;
class FallingBlock extends Entity{
private const TAG_TILE_ID = "TileID"; //TAG_Int
private const TAG_TILE = "Tile"; //TAG_Byte
private const TAG_DATA = "Data"; //TAG_Byte
public static function getNetworkTypeId() : string{ return EntityIds::FALLING_BLOCK; }
protected $gravity = 0.04;
@@ -65,9 +69,9 @@ class FallingBlock extends Entity{
$blockId = 0;
//TODO: 1.8+ save format
if(($tileIdTag = $nbt->getTag("TileID")) instanceof IntTag){
if(($tileIdTag = $nbt->getTag(self::TAG_TILE_ID)) instanceof IntTag){
$blockId = $tileIdTag->getValue();
}elseif(($tileTag = $nbt->getTag("Tile")) instanceof ByteTag){
}elseif(($tileTag = $nbt->getTag(self::TAG_TILE)) instanceof ByteTag){
$blockId = $tileTag->getValue();
}
@@ -75,7 +79,7 @@ class FallingBlock extends Entity{
throw new SavedDataLoadingException("Missing block info from NBT");
}
$damage = $nbt->getByte("Data", 0);
$damage = $nbt->getByte(self::TAG_DATA, 0);
return $factory->get($blockId, $damage);
}
@@ -138,8 +142,8 @@ class FallingBlock extends Entity{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setInt("TileID", $this->block->getId());
$nbt->setByte("Data", $this->block->getMeta());
$nbt->setInt(self::TAG_TILE_ID, $this->block->getId());
$nbt->setByte(self::TAG_DATA, $this->block->getMeta());
return $nbt;
}

View File

@@ -43,6 +43,13 @@ use function max;
class ItemEntity extends Entity{
private const TAG_HEALTH = "Health"; //TAG_Short
private const TAG_AGE = "Age"; //TAG_Short
private const TAG_PICKUP_DELAY = "PickupDelay"; //TAG_Short
private const TAG_OWNER = "Owner"; //TAG_String
private const TAG_THROWER = "Thrower"; //TAG_String
public const TAG_ITEM = "Item"; //TAG_Compound
public static function getNetworkTypeId() : string{ return EntityIds::ITEM; }
public const MERGE_CHECK_PERIOD = 2; //0.1 seconds
@@ -81,17 +88,17 @@ class ItemEntity extends Entity{
parent::initEntity($nbt);
$this->setMaxHealth(5);
$this->setHealth($nbt->getShort("Health", (int) $this->getHealth()));
$this->setHealth($nbt->getShort(self::TAG_HEALTH, (int) $this->getHealth()));
$age = $nbt->getShort("Age", 0);
$age = $nbt->getShort(self::TAG_AGE, 0);
if($age === -32768){
$this->despawnDelay = self::NEVER_DESPAWN;
}else{
$this->despawnDelay = max(0, self::DEFAULT_DESPAWN_DELAY - $age);
}
$this->pickupDelay = $nbt->getShort("PickupDelay", $this->pickupDelay);
$this->owner = $nbt->getString("Owner", $this->owner);
$this->thrower = $nbt->getString("Thrower", $this->thrower);
$this->pickupDelay = $nbt->getShort(self::TAG_PICKUP_DELAY, $this->pickupDelay);
$this->owner = $nbt->getString(self::TAG_OWNER, $this->owner);
$this->thrower = $nbt->getString(self::TAG_THROWER, $this->thrower);
}
protected function onFirstUpdate(int $currentTick) : void{
@@ -106,33 +113,42 @@ class ItemEntity extends Entity{
$hasUpdate = parent::entityBaseTick($tickDiff);
if(!$this->isFlaggedForDespawn() && $this->pickupDelay !== self::NEVER_DESPAWN){ //Infinite delay
if($this->isFlaggedForDespawn()){
return $hasUpdate;
}
if($this->pickupDelay !== self::NEVER_DESPAWN && $this->pickupDelay > 0){ //Infinite delay
$hasUpdate = true;
$this->pickupDelay -= $tickDiff;
if($this->pickupDelay < 0){
$this->pickupDelay = 0;
}
if($this->hasMovementUpdate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
$mergeable = [$this]; //in case the merge target ends up not being this
$mergeTarget = $this;
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
continue;
}
}
if($entity->isMergeable($this)){
$mergeable[] = $entity;
if($entity->item->getCount() > $mergeTarget->item->getCount()){
$mergeTarget = $entity;
}
}
if($this->hasMovementUpdate() && $this->despawnDelay % self::MERGE_CHECK_PERIOD === 0){
$mergeable = [$this]; //in case the merge target ends up not being this
$mergeTarget = $this;
foreach($this->getWorld()->getNearbyEntities($this->boundingBox->expandedCopy(0.5, 0.5, 0.5), $this) as $entity){
if(!$entity instanceof ItemEntity || $entity->isFlaggedForDespawn()){
continue;
}
foreach($mergeable as $itemEntity){
if($itemEntity !== $mergeTarget){
$itemEntity->tryMergeInto($mergeTarget);
if($entity->isMergeable($this)){
$mergeable[] = $entity;
if($entity->item->getCount() > $mergeTarget->item->getCount()){
$mergeTarget = $entity;
}
}
}
foreach($mergeable as $itemEntity){
if($itemEntity !== $mergeTarget){
$itemEntity->tryMergeInto($mergeTarget);
}
}
}
if(!$this->isFlaggedForDespawn() && $this->despawnDelay !== self::NEVER_DESPAWN){
$hasUpdate = true;
$this->despawnDelay -= $tickDiff;
if($this->despawnDelay <= 0){
$ev = new ItemDespawnEvent($this);
@@ -141,7 +157,6 @@ class ItemEntity extends Entity{
$this->despawnDelay = self::DEFAULT_DESPAWN_DELAY;
}else{
$this->flagForDespawn();
$hasUpdate = true;
}
}
}
@@ -195,20 +210,20 @@ class ItemEntity extends Entity{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setTag("Item", $this->item->nbtSerialize());
$nbt->setShort("Health", (int) $this->getHealth());
$nbt->setTag(self::TAG_ITEM, $this->item->nbtSerialize());
$nbt->setShort(self::TAG_HEALTH, (int) $this->getHealth());
if($this->despawnDelay === self::NEVER_DESPAWN){
$age = -32768;
}else{
$age = self::DEFAULT_DESPAWN_DELAY - $this->despawnDelay;
}
$nbt->setShort("Age", $age);
$nbt->setShort("PickupDelay", $this->pickupDelay);
$nbt->setShort(self::TAG_AGE, $age);
$nbt->setShort(self::TAG_PICKUP_DELAY, $this->pickupDelay);
if($this->owner !== null){
$nbt->setString("Owner", $this->owner);
$nbt->setString(self::TAG_OWNER, $this->owner);
}
if($this->thrower !== null){
$nbt->setString("Thrower", $this->thrower);
$nbt->setString(self::TAG_THROWER, $this->thrower);
}
return $nbt;

View File

@@ -41,6 +41,13 @@ use pocketmine\world\World;
use function ceil;
class Painting extends Entity{
public const TAG_TILE_X = "TileX"; //TAG_Int
public const TAG_TILE_Y = "TileY"; //TAG_Int
public const TAG_TILE_Z = "TileZ"; //TAG_Int
public const TAG_FACING_JE = "Facing"; //TAG_Byte
public const TAG_DIRECTION_BE = "Direction"; //TAG_Byte
public const TAG_MOTIVE = "Motive"; //TAG_String
public static function getNetworkTypeId() : string{ return EntityIds::PAINTING; }
public const DATA_TO_FACING = [
@@ -88,14 +95,14 @@ class Painting extends Entity{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setInt("TileX", (int) $this->blockIn->x);
$nbt->setInt("TileY", (int) $this->blockIn->y);
$nbt->setInt("TileZ", (int) $this->blockIn->z);
$nbt->setInt(self::TAG_TILE_X, (int) $this->blockIn->x);
$nbt->setInt(self::TAG_TILE_Y, (int) $this->blockIn->y);
$nbt->setInt(self::TAG_TILE_Z, (int) $this->blockIn->z);
$nbt->setByte("Facing", self::FACING_TO_DATA[$this->facing]);
$nbt->setByte("Direction", self::FACING_TO_DATA[$this->facing]); //Save both for full compatibility
$nbt->setByte(self::TAG_FACING_JE, self::FACING_TO_DATA[$this->facing]);
$nbt->setByte(self::TAG_DIRECTION_BE, self::FACING_TO_DATA[$this->facing]); //Save both for full compatibility
$nbt->setString("Motive", $this->motive->getName());
$nbt->setString(self::TAG_MOTIVE, $this->motive->getName());
return $nbt;
}

View File

@@ -39,6 +39,8 @@ use pocketmine\world\Position;
class PrimedTNT extends Entity implements Explosive{
private const TAG_FUSE = "Fuse"; //TAG_Short
public static function getNetworkTypeId() : string{ return EntityIds::TNT; }
protected $gravity = 0.04;
@@ -81,7 +83,7 @@ class PrimedTNT extends Entity implements Explosive{
protected function initEntity(CompoundTag $nbt) : void{
parent::initEntity($nbt);
$this->fuse = $nbt->getShort("Fuse", 80);
$this->fuse = $nbt->getShort(self::TAG_FUSE, 80);
}
public function canCollideWith(Entity $entity) : bool{
@@ -90,7 +92,7 @@ class PrimedTNT extends Entity implements Explosive{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setShort("Fuse", $this->fuse);
$nbt->setShort(self::TAG_FUSE, $this->fuse);
return $nbt;
}

View File

@@ -52,6 +52,7 @@ class Arrow extends Projectile{
private const TAG_PICKUP = "pickup"; //TAG_Byte
public const TAG_CRIT = "crit"; //TAG_Byte
private const TAG_LIFE = "life"; //TAG_Short
protected $gravity = 0.05;
protected $drag = 0.01;
@@ -83,14 +84,14 @@ class Arrow extends Projectile{
$this->pickupMode = $nbt->getByte(self::TAG_PICKUP, self::PICKUP_ANY);
$this->critical = $nbt->getByte(self::TAG_CRIT, 0) === 1;
$this->collideTicks = $nbt->getShort("life", $this->collideTicks);
$this->collideTicks = $nbt->getShort(self::TAG_LIFE, $this->collideTicks);
}
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setByte(self::TAG_PICKUP, $this->pickupMode);
$nbt->setByte(self::TAG_CRIT, $this->critical ? 1 : 0);
$nbt->setShort("life", $this->collideTicks);
$nbt->setShort(self::TAG_LIFE, $this->collideTicks);
return $nbt;
}

View File

@@ -50,6 +50,12 @@ use const M_PI;
use const PHP_INT_MAX;
abstract class Projectile extends Entity{
private const TAG_DAMAGE = "damage"; //TAG_Double
private const TAG_TILE_X = "tileX"; //TAG_Int
private const TAG_TILE_Y = "tileY"; //TAG_Int
private const TAG_TILE_Z = "tileZ"; //TAG_Int
private const TAG_BLOCK_ID = "blockId"; //TAG_Int
private const TAG_BLOCK_DATA = "blockData"; //TAG_Byte
/** @var float */
protected $damage = 0.0;
@@ -75,22 +81,22 @@ abstract class Projectile extends Entity{
$this->setMaxHealth(1);
$this->setHealth(1);
$this->damage = $nbt->getDouble("damage", $this->damage);
$this->damage = $nbt->getDouble(self::TAG_DAMAGE, $this->damage);
(function() use ($nbt) : void{
if(($tileXTag = $nbt->getTag("tileX")) instanceof IntTag && ($tileYTag = $nbt->getTag("tileY")) instanceof IntTag && ($tileZTag = $nbt->getTag("tileZ")) instanceof IntTag){
if(($tileXTag = $nbt->getTag(self::TAG_TILE_X)) instanceof IntTag && ($tileYTag = $nbt->getTag(self::TAG_TILE_Y)) instanceof IntTag && ($tileZTag = $nbt->getTag(self::TAG_TILE_Z)) instanceof IntTag){
$blockPos = new Vector3($tileXTag->getValue(), $tileYTag->getValue(), $tileZTag->getValue());
}else{
return;
}
if(($blockIdTag = $nbt->getTag("blockId")) instanceof IntTag){
if(($blockIdTag = $nbt->getTag(self::TAG_BLOCK_ID)) instanceof IntTag){
$blockId = $blockIdTag->getValue();
}else{
return;
}
if(($blockDataTag = $nbt->getTag("blockData")) instanceof ByteTag){
if(($blockDataTag = $nbt->getTag(self::TAG_BLOCK_DATA)) instanceof ByteTag){
$blockData = $blockDataTag->getValue();
}else{
return;
@@ -134,17 +140,17 @@ abstract class Projectile extends Entity{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setDouble("damage", $this->damage);
$nbt->setDouble(self::TAG_DAMAGE, $this->damage);
if($this->blockHit !== null){
$pos = $this->blockHit->getPosition();
$nbt->setInt("tileX", $pos->x);
$nbt->setInt("tileY", $pos->y);
$nbt->setInt("tileZ", $pos->z);
$nbt->setInt(self::TAG_TILE_X, $pos->x);
$nbt->setInt(self::TAG_TILE_Y, $pos->y);
$nbt->setInt(self::TAG_TILE_Z, $pos->z);
//we intentionally use different ones to PC because we don't have stringy IDs
$nbt->setInt("blockId", $this->blockHit->getId());
$nbt->setByte("blockData", $this->blockHit->getMeta());
$nbt->setInt(self::TAG_BLOCK_ID, $this->blockHit->getId());
$nbt->setByte(self::TAG_BLOCK_DATA, $this->blockHit->getMeta());
}
return $nbt;

View File

@@ -50,6 +50,8 @@ use function sqrt;
class SplashPotion extends Throwable{
public const TAG_POTION_ID = "PotionId"; //TAG_Short
public static function getNetworkTypeId() : string{ return EntityIds::SPLASH_POTION; }
protected $gravity = 0.05;
@@ -66,7 +68,7 @@ class SplashPotion extends Throwable{
public function saveNBT() : CompoundTag{
$nbt = parent::saveNBT();
$nbt->setShort("PotionId", PotionTypeIdMap::getInstance()->toId($this->getPotionType()));
$nbt->setShort(self::TAG_POTION_ID, PotionTypeIdMap::getInstance()->toId($this->getPotionType()));
return $nbt;
}

View File

@@ -98,17 +98,15 @@ class PlayerDeathEvent extends EntityDeathEvent{
if($e instanceof Player){
return KnownTranslationFactory::death_attack_player($name, $e->getDisplayName());
}elseif($e instanceof Living){
return KnownTranslationFactory::death_attack_mob($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName());
return KnownTranslationFactory::death_attack_mob($name, $e->getDisplayName());
}
}
break;
case EntityDamageEvent::CAUSE_PROJECTILE:
if($deathCause instanceof EntityDamageByEntityEvent){
$e = $deathCause->getDamager();
if($e instanceof Player){
if($e instanceof Living){
return KnownTranslationFactory::death_attack_arrow($name, $e->getDisplayName());
}elseif($e instanceof Living){
return KnownTranslationFactory::death_attack_arrow($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName());
}
}
break;
@@ -149,10 +147,8 @@ class PlayerDeathEvent extends EntityDeathEvent{
case EntityDamageEvent::CAUSE_ENTITY_EXPLOSION:
if($deathCause instanceof EntityDamageByEntityEvent){
$e = $deathCause->getDamager();
if($e instanceof Player){
if($e instanceof Living){
return KnownTranslationFactory::death_attack_explosion_player($name, $e->getDisplayName());
}elseif($e instanceof Living){
return KnownTranslationFactory::death_attack_explosion_player($name, $e->getNameTag() !== "" ? $e->getNameTag() : $e->getName());
}
}
return KnownTranslationFactory::death_attack_explosion($name);

View File

@@ -69,8 +69,8 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
/**
* Returns an object containing self-proclaimed information about the connecting player.
* WARNING: THE PLAYER IS NOT VERIFIED DURING THIS EVENT. At this point, it's unknown if the player is real or a
* hacker.
* WARNING: THE PLAYER IS NOT VERIFIED DURING THIS EVENT. At this point, this could be a hacker posing as another
* player.
*/
public function getPlayerInfo() : PlayerInfo{
return $this->playerInfo;
@@ -109,7 +109,7 @@ class PlayerPreLoginEvent extends Event implements Cancellable{
}
/**
* Sets a reason to disallow the player to continue continue authenticating, with a message.
* Sets a reason to disallow the player to continue authenticating, with a message.
* This can also be used to change kick messages for already-set flags.
*/
public function setKickReason(int $flag, string $message) : void{

View File

@@ -0,0 +1,73 @@
<?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\world;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\particle\Particle;
use pocketmine\world\World;
class WorldParticleEvent extends WorldEvent implements Cancellable{
use CancellableTrait;
/**
* @param Player[] $recipients
*/
public function __construct(
World $world,
private Particle $particle,
private Vector3 $position,
private array $recipients
){
parent::__construct($world);
}
public function getParticle() : Particle{
return $this->particle;
}
public function setParticle(Particle $particle) : void{
$this->particle = $particle;
}
public function getPosition() : Vector3{
return $this->position;
}
/**
* @return Player[]
*/
public function getRecipients() : array{
return $this->recipients;
}
/**
* @param Player[] $recipients
*/
public function setRecipients(array $recipients) : void{
$this->recipients = $recipients;
}
}

View File

@@ -0,0 +1,77 @@
<?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\world;
use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\sound\Sound;
use pocketmine\world\World;
/**
* Called when a sound is played in a world
* @see World::addSound()
*/
class WorldSoundEvent extends WorldEvent implements Cancellable{
use CancellableTrait;
/**
* @param Player[] $recipients
*/
public function __construct(
World $world,
private Sound $sound,
private Vector3 $position,
private array $recipients
){
parent::__construct($world);
}
public function getSound() : Sound{
return $this->sound;
}
public function setSound(Sound $sound) : void{
$this->sound = $sound;
}
public function getPosition() : Vector3{
return $this->position;
}
/**
* @return Player[]
*/
public function getRecipients() : array{
return $this->recipients;
}
/**
* @param Player[] $recipients
*/
public function setRecipients(array $recipients) : void{
$this->recipients = $recipients;
}
}

View File

@@ -25,9 +25,9 @@ namespace pocketmine\inventory;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
use function json_decode;
final class CreativeInventory{
@@ -37,7 +37,7 @@ final class CreativeInventory{
private array $creative = [];
private function __construct(){
$creativeItems = json_decode(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, "creativeitems.json")), true);
$creativeItems = json_decode(Filesystem::fileGetContents(Path::join(\pocketmine\RESOURCE_PATH, "legacy_creativeitems.json")), true);
foreach($creativeItems as $data){
$item = Item::jsonDeserialize($data);

View File

@@ -59,12 +59,27 @@ class Item implements \JsonSerializable{
use ItemEnchantmentHandlingTrait;
public const TAG_ENCH = "ench";
private const TAG_ENCH_ID = "id"; //TAG_Short
private const TAG_ENCH_LVL = "lvl"; //TAG_Short
public const TAG_DISPLAY = "display";
public const TAG_BLOCK_ENTITY_TAG = "BlockEntityTag";
public const TAG_DISPLAY_NAME = "Name";
public const TAG_DISPLAY_LORE = "Lore";
public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death";
private const TAG_ID = "id"; //TAG_Short
private const TAG_COUNT = "Count"; //TAG_Byte
private const TAG_DAMAGE = "Damage"; //TAG_Short
private const TAG_TAG = "tag"; //TAG_Compound
public const TAG_SLOT = "Slot"; //TAG_Byte
private const TAG_CAN_PLACE_ON = "CanPlaceOn"; //TAG_List<TAG_String>
private const TAG_CAN_DESTROY = "CanDestroy"; //TAG_List<TAG_String>
private ItemIdentifier $identifier;
private CompoundTag $nbt;
@@ -96,6 +111,8 @@ class Item implements \JsonSerializable{
*/
protected $canDestroy;
protected bool $keepOnDeath = false;
/**
* Constructs a new Item type. This constructor should ONLY be used when constructing a new item TYPE to register
* into the index.
@@ -222,6 +239,17 @@ class Item implements \JsonSerializable{
}
}
/**
* Returns whether players will retain this item on death. If a non-player dies it will be excluded from the drops.
*/
public function keepOnDeath() : bool{
return $this->keepOnDeath;
}
public function setKeepOnDeath(bool $keepOnDeath) : void{
$this->keepOnDeath = $keepOnDeath;
}
/**
* Returns whether this Item has a non-empty NBT.
*/
@@ -290,8 +318,8 @@ class Item implements \JsonSerializable{
if($enchantments !== null && $enchantments->getTagType() === NBT::TAG_Compound){
/** @var CompoundTag $enchantment */
foreach($enchantments as $enchantment){
$magicNumber = $enchantment->getShort("id", -1);
$level = $enchantment->getShort("lvl", 0);
$magicNumber = $enchantment->getShort(self::TAG_ENCH_ID, -1);
$level = $enchantment->getShort(self::TAG_ENCH_LVL, 0);
if($level <= 0){
continue;
}
@@ -305,7 +333,7 @@ class Item implements \JsonSerializable{
$this->blockEntityTag = $tag->getCompoundTag(self::TAG_BLOCK_ENTITY_TAG);
$this->canPlaceOn = [];
$canPlaceOn = $tag->getListTag("CanPlaceOn");
$canPlaceOn = $tag->getListTag(self::TAG_CAN_PLACE_ON);
if($canPlaceOn !== null && $canPlaceOn->getTagType() === NBT::TAG_String){
/** @var StringTag $entry */
foreach($canPlaceOn as $entry){
@@ -313,13 +341,15 @@ class Item implements \JsonSerializable{
}
}
$this->canDestroy = [];
$canDestroy = $tag->getListTag("CanDestroy");
$canDestroy = $tag->getListTag(self::TAG_CAN_DESTROY);
if($canDestroy !== null && $canDestroy->getTagType() === NBT::TAG_String){
/** @var StringTag $entry */
foreach($canDestroy as $entry){
$this->canDestroy[$entry->getValue()] = $entry->getValue();
}
}
$this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0;
}
protected function serializeCompoundTag(CompoundTag $tag) : void{
@@ -346,8 +376,8 @@ class Item implements \JsonSerializable{
$ench = new ListTag();
foreach($this->getEnchantments() as $enchantmentInstance){
$ench->push(CompoundTag::create()
->setShort("id", EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType()))
->setShort("lvl", $enchantmentInstance->getLevel())
->setShort(self::TAG_ENCH_ID, EnchantmentIdMap::getInstance()->toId($enchantmentInstance->getType()))
->setShort(self::TAG_ENCH_LVL, $enchantmentInstance->getLevel())
);
}
$tag->setTag(self::TAG_ENCH, $ench);
@@ -364,18 +394,24 @@ class Item implements \JsonSerializable{
foreach($this->canPlaceOn as $item){
$canPlaceOn->push(new StringTag($item));
}
$tag->setTag("CanPlaceOn", $canPlaceOn);
$tag->setTag(self::TAG_CAN_PLACE_ON, $canPlaceOn);
}else{
$tag->removeTag("CanPlaceOn");
$tag->removeTag(self::TAG_CAN_PLACE_ON);
}
if(count($this->canDestroy) > 0){
$canDestroy = new ListTag();
foreach($this->canDestroy as $item){
$canDestroy->push(new StringTag($item));
}
$tag->setTag("CanDestroy", $canDestroy);
$tag->setTag(self::TAG_CAN_DESTROY, $canDestroy);
}else{
$tag->removeTag("CanDestroy");
$tag->removeTag(self::TAG_CAN_DESTROY);
}
if($this->keepOnDeath){
$tag->setByte(self::TAG_KEEP_ON_DEATH, 1);
}else{
$tag->removeTag(self::TAG_KEEP_ON_DEATH);
}
}
@@ -554,6 +590,16 @@ class Item implements \JsonSerializable{
return false;
}
/**
* Called when a player uses the item to interact with entity, for example by using a name tag.
*
* @param Vector3 $clickVector The exact position of the click (absolute coordinates)
* @return bool whether some action took place
*/
public function onInteractEntity(Player $player, Entity $entity, Vector3 $clickVector) : bool{
return false;
}
/**
* Returns the number of ticks a player must wait before activating this item again.
*/
@@ -655,17 +701,17 @@ class Item implements \JsonSerializable{
*/
public function nbtSerialize(int $slot = -1) : CompoundTag{
$result = CompoundTag::create()
->setShort("id", $this->getId())
->setByte("Count", Binary::signByte($this->count))
->setShort("Damage", $this->getMeta());
->setShort(self::TAG_ID, $this->getId())
->setByte(self::TAG_COUNT, Binary::signByte($this->count))
->setShort(self::TAG_DAMAGE, $this->getMeta());
$tag = $this->getNamedTag();
if($tag->count() > 0){
$result->setTag("tag", $tag);
$result->setTag(self::TAG_TAG, $tag);
}
if($slot !== -1){
$result->setByte("Slot", $slot);
$result->setByte(self::TAG_SLOT, $slot);
}
return $result;
@@ -677,14 +723,14 @@ class Item implements \JsonSerializable{
* @throws SavedDataLoadingException
*/
public static function nbtDeserialize(CompoundTag $tag) : Item{
if($tag->getTag("id") === null || $tag->getTag("Count") === null){
if($tag->getTag(self::TAG_ID) === null || $tag->getTag(self::TAG_COUNT) === null){
return VanillaItems::AIR();
}
$count = Binary::unsignByte($tag->getByte("Count"));
$meta = $tag->getShort("Damage", 0);
$count = Binary::unsignByte($tag->getByte(self::TAG_COUNT));
$meta = $tag->getShort(self::TAG_DAMAGE, 0);
$idTag = $tag->getTag("id");
$idTag = $tag->getTag(self::TAG_ID);
if($idTag instanceof ShortTag){
$item = ItemFactory::getInstance()->get($idTag->getValue(), $meta, $count);
}elseif($idTag instanceof StringTag){ //PC item save format
@@ -699,7 +745,7 @@ class Item implements \JsonSerializable{
throw new SavedDataLoadingException("Item CompoundTag ID must be an instance of StringTag or ShortTag, " . get_class($idTag) . " given");
}
$itemNBT = $tag->getCompoundTag("tag");
$itemNBT = $tag->getCompoundTag(self::TAG_TAG);
if($itemNBT !== null){
$item->setNamedTag(clone $itemNBT);
}

View File

@@ -24,11 +24,10 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function explode;
use function file_get_contents;
use function is_array;
use function is_int;
use function is_numeric;
@@ -53,7 +52,7 @@ final class LegacyStringToItemParser{
private static function make() : self{
$result = new self(ItemFactory::getInstance());
$mappingsRaw = Utils::assumeNotFalse(@file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, 'item_from_string_bc_map.json')), "Missing required resource file");
$mappingsRaw = Filesystem::fileGetContents(Path::join(\pocketmine\RESOURCE_PATH, 'item_from_string_bc_map.json'));
$mappings = json_decode($mappingsRaw, true);
if(!is_array($mappings)) throw new AssumptionFailedError("Invalid mappings format, expected array");

View File

@@ -53,6 +53,7 @@ final class StringToEnchantmentParser extends StringToTParser{
$result->register("respiration", fn() => VanillaEnchantments::RESPIRATION());
$result->register("sharpness", fn() => VanillaEnchantments::SHARPNESS());
$result->register("silk_touch", fn() => VanillaEnchantments::SILK_TOUCH());
$result->register("swift_sneak", fn() => VanillaEnchantments::SWIFT_SNEAK());
$result->register("thorns", fn() => VanillaEnchantments::THORNS());
$result->register("unbreaking", fn() => VanillaEnchantments::UNBREAKING());
$result->register("vanishing", fn() => VanillaEnchantments::VANISHING());

View File

@@ -49,6 +49,7 @@ use pocketmine\utils\RegistryTrait;
* @method static Enchantment RESPIRATION()
* @method static SharpnessEnchantment SHARPNESS()
* @method static Enchantment SILK_TOUCH()
* @method static Enchantment SWIFT_SNEAK()
* @method static Enchantment THORNS()
* @method static Enchantment UNBREAKING()
* @method static Enchantment VANISHING()
@@ -95,6 +96,8 @@ final class VanillaEnchantments{
self::register("MENDING", new Enchantment(KnownTranslationFactory::enchantment_mending(), Rarity::RARE, ItemFlags::NONE, ItemFlags::ALL, 1));
self::register("VANISHING", new Enchantment(KnownTranslationFactory::enchantment_curse_vanishing(), Rarity::MYTHIC, ItemFlags::NONE, ItemFlags::ALL, 1));
self::register("SWIFT_SNEAK", new Enchantment(KnownTranslationFactory::enchantment_swift_sneak(), Rarity::MYTHIC, ItemFlags::NONE, ItemFlags::LEGS, 3));
}
protected static function register(string $name, Enchantment $member) : void{

View File

@@ -661,6 +661,12 @@ final class KnownTranslationFactory{
]);
}
public static function death_attack_fireworks(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::DEATH_ATTACK_FIREWORKS, [
0 => $param0,
]);
}
public static function death_attack_generic(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::DEATH_ATTACK_GENERIC, [
0 => $param0,
@@ -760,6 +766,10 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_INVALIDSKIN, []);
}
public static function disconnectionScreen_loggedinOtherLocation() : Translatable{
return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION, []);
}
public static function disconnectionScreen_noReason() : Translatable{
return new Translatable(KnownTranslationKeys::DISCONNECTIONSCREEN_NOREASON, []);
}
@@ -1166,6 +1176,10 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DIFFICULTY_DESCRIPTION, []);
}
public static function pocketmine_command_dumpmemory_description() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_DUMPMEMORY_DESCRIPTION, []);
}
public static function pocketmine_command_effect_description() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_COMMAND_EFFECT_DESCRIPTION, []);
}
@@ -1574,6 +1588,51 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_DEBUG_ENABLE, []);
}
public static function pocketmine_disconnect_ban(Translatable|string $reason) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_BAN, [
"reason" => $reason,
]);
}
public static function pocketmine_disconnect_ban_hardcore() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_BAN_HARDCORE, []);
}
public static function pocketmine_disconnect_ban_ip() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_BAN_IP, []);
}
public static function pocketmine_disconnect_ban_noReason() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_BAN_NOREASON, []);
}
public static function pocketmine_disconnect_error(Translatable|string $error, Translatable|string $errorId) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR, [
"error" => $error,
"errorId" => $errorId,
]);
}
public static function pocketmine_disconnect_error_authentication() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_AUTHENTICATION, []);
}
public static function pocketmine_disconnect_error_badPacket() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_BADPACKET, []);
}
public static function pocketmine_disconnect_error_internal() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_INTERNAL, []);
}
public static function pocketmine_disconnect_error_loginTimeout() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_LOGINTIMEOUT, []);
}
public static function pocketmine_disconnect_error_respawn() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_ERROR_RESPAWN, []);
}
public static function pocketmine_disconnect_incompatibleProtocol(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INCOMPATIBLEPROTOCOL, [
0 => $param0,
@@ -1602,6 +1661,28 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE, []);
}
public static function pocketmine_disconnect_kick(Translatable|string $reason) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_KICK, [
"reason" => $reason,
]);
}
public static function pocketmine_disconnect_kick_noReason() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_KICK_NOREASON, []);
}
public static function pocketmine_disconnect_transfer() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_TRANSFER, []);
}
public static function pocketmine_disconnect_whitelisted() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_WHITELISTED, []);
}
public static function pocketmine_disconnect_xblImpersonation() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_DISCONNECT_XBLIMPERSONATION, []);
}
public static function pocketmine_level_ambiguousFormat(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_LEVEL_AMBIGUOUSFORMAT, [
0 => $param0,
@@ -1707,6 +1788,274 @@ final class KnownTranslationFactory{
]);
}
public static function pocketmine_network_session_close(Translatable|string $reason) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_NETWORK_SESSION_CLOSE, [
"reason" => $reason,
]);
}
public static function pocketmine_network_session_open() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_NETWORK_SESSION_OPEN, []);
}
public static function pocketmine_network_session_playerName(Translatable|string $playerName) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_NETWORK_SESSION_PLAYERNAME, [
"playerName" => $playerName,
]);
}
public static function pocketmine_permission_broadcast_admin() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_BROADCAST_ADMIN, []);
}
public static function pocketmine_permission_broadcast_user() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_BROADCAST_USER, []);
}
public static function pocketmine_permission_command_ban_ip() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_BAN_IP, []);
}
public static function pocketmine_permission_command_ban_list() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_BAN_LIST, []);
}
public static function pocketmine_permission_command_ban_player() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_BAN_PLAYER, []);
}
public static function pocketmine_permission_command_clear_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CLEAR_OTHER, []);
}
public static function pocketmine_permission_command_clear_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_CLEAR_SELF, []);
}
public static function pocketmine_permission_command_defaultgamemode() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_DEFAULTGAMEMODE, []);
}
public static function pocketmine_permission_command_difficulty() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_DIFFICULTY, []);
}
public static function pocketmine_permission_command_dumpmemory() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_DUMPMEMORY, []);
}
public static function pocketmine_permission_command_effect_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_EFFECT_OTHER, []);
}
public static function pocketmine_permission_command_effect_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_EFFECT_SELF, []);
}
public static function pocketmine_permission_command_enchant_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_ENCHANT_OTHER, []);
}
public static function pocketmine_permission_command_enchant_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_ENCHANT_SELF, []);
}
public static function pocketmine_permission_command_gamemode_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_GAMEMODE_OTHER, []);
}
public static function pocketmine_permission_command_gamemode_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_GAMEMODE_SELF, []);
}
public static function pocketmine_permission_command_gc() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_GC, []);
}
public static function pocketmine_permission_command_give_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_GIVE_OTHER, []);
}
public static function pocketmine_permission_command_give_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_GIVE_SELF, []);
}
public static function pocketmine_permission_command_help() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_HELP, []);
}
public static function pocketmine_permission_command_kick() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_KICK, []);
}
public static function pocketmine_permission_command_kill_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_KILL_OTHER, []);
}
public static function pocketmine_permission_command_kill_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_KILL_SELF, []);
}
public static function pocketmine_permission_command_list() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_LIST, []);
}
public static function pocketmine_permission_command_me() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_ME, []);
}
public static function pocketmine_permission_command_op_give() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_OP_GIVE, []);
}
public static function pocketmine_permission_command_op_take() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_OP_TAKE, []);
}
public static function pocketmine_permission_command_particle() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_PARTICLE, []);
}
public static function pocketmine_permission_command_plugins() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_PLUGINS, []);
}
public static function pocketmine_permission_command_save_disable() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SAVE_DISABLE, []);
}
public static function pocketmine_permission_command_save_enable() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SAVE_ENABLE, []);
}
public static function pocketmine_permission_command_save_perform() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SAVE_PERFORM, []);
}
public static function pocketmine_permission_command_say() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SAY, []);
}
public static function pocketmine_permission_command_seed() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SEED, []);
}
public static function pocketmine_permission_command_setworldspawn() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SETWORLDSPAWN, []);
}
public static function pocketmine_permission_command_spawnpoint_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SPAWNPOINT_OTHER, []);
}
public static function pocketmine_permission_command_spawnpoint_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_SPAWNPOINT_SELF, []);
}
public static function pocketmine_permission_command_status() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_STATUS, []);
}
public static function pocketmine_permission_command_stop() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_STOP, []);
}
public static function pocketmine_permission_command_teleport_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TELEPORT_OTHER, []);
}
public static function pocketmine_permission_command_teleport_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TELEPORT_SELF, []);
}
public static function pocketmine_permission_command_tell() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TELL, []);
}
public static function pocketmine_permission_command_time_add() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIME_ADD, []);
}
public static function pocketmine_permission_command_time_query() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIME_QUERY, []);
}
public static function pocketmine_permission_command_time_set() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIME_SET, []);
}
public static function pocketmine_permission_command_time_start() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIME_START, []);
}
public static function pocketmine_permission_command_time_stop() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIME_STOP, []);
}
public static function pocketmine_permission_command_timings() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TIMINGS, []);
}
public static function pocketmine_permission_command_title_other() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TITLE_OTHER, []);
}
public static function pocketmine_permission_command_title_self() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TITLE_SELF, []);
}
public static function pocketmine_permission_command_transferserver() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_TRANSFERSERVER, []);
}
public static function pocketmine_permission_command_unban_ip() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_UNBAN_IP, []);
}
public static function pocketmine_permission_command_unban_player() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_UNBAN_PLAYER, []);
}
public static function pocketmine_permission_command_version() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_VERSION, []);
}
public static function pocketmine_permission_command_whitelist_add() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_ADD, []);
}
public static function pocketmine_permission_command_whitelist_disable() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_DISABLE, []);
}
public static function pocketmine_permission_command_whitelist_enable() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_ENABLE, []);
}
public static function pocketmine_permission_command_whitelist_list() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_LIST, []);
}
public static function pocketmine_permission_command_whitelist_reload() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_RELOAD, []);
}
public static function pocketmine_permission_command_whitelist_remove() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_COMMAND_WHITELIST_REMOVE, []);
}
public static function pocketmine_permission_group_console() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_GROUP_CONSOLE, []);
}
public static function pocketmine_permission_group_operator() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_GROUP_OPERATOR, []);
}
public static function pocketmine_permission_group_user() : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PERMISSION_GROUP_USER, []);
}
public static function pocketmine_player_invalidEntity(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::POCKETMINE_PLAYER_INVALIDENTITY, [
0 => $param0,
@@ -2241,6 +2590,12 @@ final class KnownTranslationFactory{
return new Translatable(KnownTranslationKeys::QUERY_WARNING2, []);
}
public static function record_nowPlaying(Translatable|string $param0) : Translatable{
return new Translatable(KnownTranslationKeys::RECORD_NOWPLAYING, [
0 => $param0,
]);
}
public static function server_port() : Translatable{
return new Translatable(KnownTranslationKeys::SERVER_PORT, []);
}

View File

@@ -146,6 +146,7 @@ final class KnownTranslationKeys{
public const DEATH_ATTACK_EXPLOSION_PLAYER = "death.attack.explosion.player";
public const DEATH_ATTACK_FALL = "death.attack.fall";
public const DEATH_ATTACK_FALLINGBLOCK = "death.attack.fallingBlock";
public const DEATH_ATTACK_FIREWORKS = "death.attack.fireworks";
public const DEATH_ATTACK_GENERIC = "death.attack.generic";
public const DEATH_ATTACK_INFIRE = "death.attack.inFire";
public const DEATH_ATTACK_INWALL = "death.attack.inWall";
@@ -163,6 +164,7 @@ final class KnownTranslationKeys{
public const DEFAULT_VALUES_INFO = "default_values_info";
public const DISCONNECTIONSCREEN_INVALIDNAME = "disconnectionScreen.invalidName";
public const DISCONNECTIONSCREEN_INVALIDSKIN = "disconnectionScreen.invalidSkin";
public const DISCONNECTIONSCREEN_LOGGEDINOTHERLOCATION = "disconnectionScreen.loggedinOtherLocation";
public const DISCONNECTIONSCREEN_NOREASON = "disconnectionScreen.noReason";
public const DISCONNECTIONSCREEN_NOTAUTHENTICATED = "disconnectionScreen.notAuthenticated";
public const DISCONNECTIONSCREEN_OUTDATEDCLIENT = "disconnectionScreen.outdatedClient";
@@ -258,6 +260,7 @@ final class KnownTranslationKeys{
public const POCKETMINE_COMMAND_DEFAULTGAMEMODE_DESCRIPTION = "pocketmine.command.defaultgamemode.description";
public const POCKETMINE_COMMAND_DEOP_DESCRIPTION = "pocketmine.command.deop.description";
public const POCKETMINE_COMMAND_DIFFICULTY_DESCRIPTION = "pocketmine.command.difficulty.description";
public const POCKETMINE_COMMAND_DUMPMEMORY_DESCRIPTION = "pocketmine.command.dumpmemory.description";
public const POCKETMINE_COMMAND_EFFECT_DESCRIPTION = "pocketmine.command.effect.description";
public const POCKETMINE_COMMAND_ENCHANT_DESCRIPTION = "pocketmine.command.enchant.description";
public const POCKETMINE_COMMAND_ERROR_PERMISSION = "pocketmine.command.error.permission";
@@ -342,12 +345,27 @@ final class KnownTranslationKeys{
public const POCKETMINE_DATA_PLAYEROLD = "pocketmine.data.playerOld";
public const POCKETMINE_DATA_SAVEERROR = "pocketmine.data.saveError";
public const POCKETMINE_DEBUG_ENABLE = "pocketmine.debug.enable";
public const POCKETMINE_DISCONNECT_BAN = "pocketmine.disconnect.ban";
public const POCKETMINE_DISCONNECT_BAN_HARDCORE = "pocketmine.disconnect.ban.hardcore";
public const POCKETMINE_DISCONNECT_BAN_IP = "pocketmine.disconnect.ban.ip";
public const POCKETMINE_DISCONNECT_BAN_NOREASON = "pocketmine.disconnect.ban.noReason";
public const POCKETMINE_DISCONNECT_ERROR = "pocketmine.disconnect.error";
public const POCKETMINE_DISCONNECT_ERROR_AUTHENTICATION = "pocketmine.disconnect.error.authentication";
public const POCKETMINE_DISCONNECT_ERROR_BADPACKET = "pocketmine.disconnect.error.badPacket";
public const POCKETMINE_DISCONNECT_ERROR_INTERNAL = "pocketmine.disconnect.error.internal";
public const POCKETMINE_DISCONNECT_ERROR_LOGINTIMEOUT = "pocketmine.disconnect.error.loginTimeout";
public const POCKETMINE_DISCONNECT_ERROR_RESPAWN = "pocketmine.disconnect.error.respawn";
public const POCKETMINE_DISCONNECT_INCOMPATIBLEPROTOCOL = "pocketmine.disconnect.incompatibleProtocol";
public const POCKETMINE_DISCONNECT_INVALIDSESSION = "pocketmine.disconnect.invalidSession";
public const POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE = "pocketmine.disconnect.invalidSession.badSignature";
public const POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY = "pocketmine.disconnect.invalidSession.missingKey";
public const POCKETMINE_DISCONNECT_INVALIDSESSION_TOOEARLY = "pocketmine.disconnect.invalidSession.tooEarly";
public const POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE = "pocketmine.disconnect.invalidSession.tooLate";
public const POCKETMINE_DISCONNECT_KICK = "pocketmine.disconnect.kick";
public const POCKETMINE_DISCONNECT_KICK_NOREASON = "pocketmine.disconnect.kick.noReason";
public const POCKETMINE_DISCONNECT_TRANSFER = "pocketmine.disconnect.transfer";
public const POCKETMINE_DISCONNECT_WHITELISTED = "pocketmine.disconnect.whitelisted";
public const POCKETMINE_DISCONNECT_XBLIMPERSONATION = "pocketmine.disconnect.xblImpersonation";
public const POCKETMINE_LEVEL_AMBIGUOUSFORMAT = "pocketmine.level.ambiguousFormat";
public const POCKETMINE_LEVEL_BACKGROUNDGENERATION = "pocketmine.level.backgroundGeneration";
public const POCKETMINE_LEVEL_BADDEFAULTFORMAT = "pocketmine.level.badDefaultFormat";
@@ -365,6 +383,72 @@ final class KnownTranslationKeys{
public const POCKETMINE_LEVEL_UNKNOWNGENERATOR = "pocketmine.level.unknownGenerator";
public const POCKETMINE_LEVEL_UNLOADING = "pocketmine.level.unloading";
public const POCKETMINE_LEVEL_UNSUPPORTEDFORMAT = "pocketmine.level.unsupportedFormat";
public const POCKETMINE_NETWORK_SESSION_CLOSE = "pocketmine.network.session.close";
public const POCKETMINE_NETWORK_SESSION_OPEN = "pocketmine.network.session.open";
public const POCKETMINE_NETWORK_SESSION_PLAYERNAME = "pocketmine.network.session.playerName";
public const POCKETMINE_PERMISSION_BROADCAST_ADMIN = "pocketmine.permission.broadcast.admin";
public const POCKETMINE_PERMISSION_BROADCAST_USER = "pocketmine.permission.broadcast.user";
public const POCKETMINE_PERMISSION_COMMAND_BAN_IP = "pocketmine.permission.command.ban.ip";
public const POCKETMINE_PERMISSION_COMMAND_BAN_LIST = "pocketmine.permission.command.ban.list";
public const POCKETMINE_PERMISSION_COMMAND_BAN_PLAYER = "pocketmine.permission.command.ban.player";
public const POCKETMINE_PERMISSION_COMMAND_CLEAR_OTHER = "pocketmine.permission.command.clear.other";
public const POCKETMINE_PERMISSION_COMMAND_CLEAR_SELF = "pocketmine.permission.command.clear.self";
public const POCKETMINE_PERMISSION_COMMAND_DEFAULTGAMEMODE = "pocketmine.permission.command.defaultgamemode";
public const POCKETMINE_PERMISSION_COMMAND_DIFFICULTY = "pocketmine.permission.command.difficulty";
public const POCKETMINE_PERMISSION_COMMAND_DUMPMEMORY = "pocketmine.permission.command.dumpmemory";
public const POCKETMINE_PERMISSION_COMMAND_EFFECT_OTHER = "pocketmine.permission.command.effect.other";
public const POCKETMINE_PERMISSION_COMMAND_EFFECT_SELF = "pocketmine.permission.command.effect.self";
public const POCKETMINE_PERMISSION_COMMAND_ENCHANT_OTHER = "pocketmine.permission.command.enchant.other";
public const POCKETMINE_PERMISSION_COMMAND_ENCHANT_SELF = "pocketmine.permission.command.enchant.self";
public const POCKETMINE_PERMISSION_COMMAND_GAMEMODE_OTHER = "pocketmine.permission.command.gamemode.other";
public const POCKETMINE_PERMISSION_COMMAND_GAMEMODE_SELF = "pocketmine.permission.command.gamemode.self";
public const POCKETMINE_PERMISSION_COMMAND_GC = "pocketmine.permission.command.gc";
public const POCKETMINE_PERMISSION_COMMAND_GIVE_OTHER = "pocketmine.permission.command.give.other";
public const POCKETMINE_PERMISSION_COMMAND_GIVE_SELF = "pocketmine.permission.command.give.self";
public const POCKETMINE_PERMISSION_COMMAND_HELP = "pocketmine.permission.command.help";
public const POCKETMINE_PERMISSION_COMMAND_KICK = "pocketmine.permission.command.kick";
public const POCKETMINE_PERMISSION_COMMAND_KILL_OTHER = "pocketmine.permission.command.kill.other";
public const POCKETMINE_PERMISSION_COMMAND_KILL_SELF = "pocketmine.permission.command.kill.self";
public const POCKETMINE_PERMISSION_COMMAND_LIST = "pocketmine.permission.command.list";
public const POCKETMINE_PERMISSION_COMMAND_ME = "pocketmine.permission.command.me";
public const POCKETMINE_PERMISSION_COMMAND_OP_GIVE = "pocketmine.permission.command.op.give";
public const POCKETMINE_PERMISSION_COMMAND_OP_TAKE = "pocketmine.permission.command.op.take";
public const POCKETMINE_PERMISSION_COMMAND_PARTICLE = "pocketmine.permission.command.particle";
public const POCKETMINE_PERMISSION_COMMAND_PLUGINS = "pocketmine.permission.command.plugins";
public const POCKETMINE_PERMISSION_COMMAND_SAVE_DISABLE = "pocketmine.permission.command.save.disable";
public const POCKETMINE_PERMISSION_COMMAND_SAVE_ENABLE = "pocketmine.permission.command.save.enable";
public const POCKETMINE_PERMISSION_COMMAND_SAVE_PERFORM = "pocketmine.permission.command.save.perform";
public const POCKETMINE_PERMISSION_COMMAND_SAY = "pocketmine.permission.command.say";
public const POCKETMINE_PERMISSION_COMMAND_SEED = "pocketmine.permission.command.seed";
public const POCKETMINE_PERMISSION_COMMAND_SETWORLDSPAWN = "pocketmine.permission.command.setworldspawn";
public const POCKETMINE_PERMISSION_COMMAND_SPAWNPOINT_OTHER = "pocketmine.permission.command.spawnpoint.other";
public const POCKETMINE_PERMISSION_COMMAND_SPAWNPOINT_SELF = "pocketmine.permission.command.spawnpoint.self";
public const POCKETMINE_PERMISSION_COMMAND_STATUS = "pocketmine.permission.command.status";
public const POCKETMINE_PERMISSION_COMMAND_STOP = "pocketmine.permission.command.stop";
public const POCKETMINE_PERMISSION_COMMAND_TELEPORT_OTHER = "pocketmine.permission.command.teleport.other";
public const POCKETMINE_PERMISSION_COMMAND_TELEPORT_SELF = "pocketmine.permission.command.teleport.self";
public const POCKETMINE_PERMISSION_COMMAND_TELL = "pocketmine.permission.command.tell";
public const POCKETMINE_PERMISSION_COMMAND_TIME_ADD = "pocketmine.permission.command.time.add";
public const POCKETMINE_PERMISSION_COMMAND_TIME_QUERY = "pocketmine.permission.command.time.query";
public const POCKETMINE_PERMISSION_COMMAND_TIME_SET = "pocketmine.permission.command.time.set";
public const POCKETMINE_PERMISSION_COMMAND_TIME_START = "pocketmine.permission.command.time.start";
public const POCKETMINE_PERMISSION_COMMAND_TIME_STOP = "pocketmine.permission.command.time.stop";
public const POCKETMINE_PERMISSION_COMMAND_TIMINGS = "pocketmine.permission.command.timings";
public const POCKETMINE_PERMISSION_COMMAND_TITLE_OTHER = "pocketmine.permission.command.title.other";
public const POCKETMINE_PERMISSION_COMMAND_TITLE_SELF = "pocketmine.permission.command.title.self";
public const POCKETMINE_PERMISSION_COMMAND_TRANSFERSERVER = "pocketmine.permission.command.transferserver";
public const POCKETMINE_PERMISSION_COMMAND_UNBAN_IP = "pocketmine.permission.command.unban.ip";
public const POCKETMINE_PERMISSION_COMMAND_UNBAN_PLAYER = "pocketmine.permission.command.unban.player";
public const POCKETMINE_PERMISSION_COMMAND_VERSION = "pocketmine.permission.command.version";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_ADD = "pocketmine.permission.command.whitelist.add";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_DISABLE = "pocketmine.permission.command.whitelist.disable";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_ENABLE = "pocketmine.permission.command.whitelist.enable";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_LIST = "pocketmine.permission.command.whitelist.list";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_RELOAD = "pocketmine.permission.command.whitelist.reload";
public const POCKETMINE_PERMISSION_COMMAND_WHITELIST_REMOVE = "pocketmine.permission.command.whitelist.remove";
public const POCKETMINE_PERMISSION_GROUP_CONSOLE = "pocketmine.permission.group.console";
public const POCKETMINE_PERMISSION_GROUP_OPERATOR = "pocketmine.permission.group.operator";
public const POCKETMINE_PERMISSION_GROUP_USER = "pocketmine.permission.group.user";
public const POCKETMINE_PLAYER_INVALIDENTITY = "pocketmine.player.invalidEntity";
public const POCKETMINE_PLAYER_INVALIDMOVE = "pocketmine.player.invalidMove";
public const POCKETMINE_PLAYER_LOGIN = "pocketmine.player.logIn";
@@ -466,6 +550,7 @@ final class KnownTranslationKeys{
public const QUERY_DISABLE = "query_disable";
public const QUERY_WARNING1 = "query_warning1";
public const QUERY_WARNING2 = "query_warning2";
public const RECORD_NOWPLAYING = "record.nowPlaying";
public const SERVER_PORT = "server_port";
public const SERVER_PORT_V4 = "server_port_v4";
public const SERVER_PORT_V6 = "server_port_v6";

View File

@@ -34,7 +34,9 @@ use function is_dir;
use function ord;
use function parse_ini_file;
use function scandir;
use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strpos;
use function strtolower;
@@ -62,7 +64,7 @@ class Language{
if($allFiles !== false){
$files = array_filter($allFiles, function(string $filename) : bool{
return substr($filename, -4) === ".ini";
return str_ends_with($filename, ".ini");
});
$result = [];
@@ -71,8 +73,8 @@ class Language{
try{
$code = explode(".", $file)[0];
$strings = self::loadLang($path, $code);
if(isset($strings["language.name"])){
$result[$code] = $strings["language.name"];
if(isset($strings[KnownTranslationKeys::LANGUAGE_NAME])){
$result[$code] = $strings[KnownTranslationKeys::LANGUAGE_NAME];
}
}catch(LanguageNotFoundException $e){
// no-op
@@ -142,8 +144,10 @@ class Language{
* @param (float|int|string|Translatable)[] $params
*/
public function translateString(string $str, array $params = [], ?string $onlyPrefix = null) : string{
$baseText = $this->get($str);
$baseText = $this->parseTranslation(($onlyPrefix === null || strpos($str, $onlyPrefix) === 0) ? $baseText : $str, $onlyPrefix);
$baseText = ($onlyPrefix === null || str_starts_with($str, $onlyPrefix)) ? $this->internalGet($str) : null;
if($baseText === null){ //key not found, embedded inside format string, or doesn't match prefix
$baseText = $this->parseTranslation($str, $onlyPrefix);
}
foreach($params as $i => $p){
$replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p;
@@ -155,7 +159,9 @@ class Language{
public function translate(Translatable $c) : string{
$baseText = $this->internalGet($c->getText());
$baseText = $this->parseTranslation($baseText ?? $c->getText());
if($baseText === null){ //key not found or embedded inside format string
$baseText = $this->parseTranslation($c->getText());
}
foreach($c->getParameters() as $i => $p){
$replacement = $p instanceof Translatable ? $this->translate($p) : $p;
@@ -181,6 +187,19 @@ class Language{
return $this->lang;
}
/**
* Replaces translation keys embedded inside a string with their raw values.
* Embedded translation keys must be prefixed by a "%" character.
*
* This is used to allow the "text" field of a Translatable to contain formatting (e.g. colour codes) and
* multiple embedded translation keys.
*
* Normal translations whose "text" is just a single translation key don't need to use this method, and can be
* processed via get() directly.
*
* @param string|null $onlyPrefix If non-null, only translation keys with this prefix will be replaced. This is
* used to allow a client to do its own translating of vanilla strings.
*/
protected function parseTranslation(string $text, ?string $onlyPrefix = null) : string{
$newString = "";

View File

@@ -33,6 +33,7 @@ use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\network\mcpe\protocol\types\ChunkPosition;
use pocketmine\network\mcpe\serializer\ChunkSerializer;
use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\BinaryStream;
use pocketmine\world\format\Chunk;
use pocketmine\world\format\io\FastChunkSerializer;
@@ -72,7 +73,10 @@ class ChunkRequestTask extends AsyncTask{
$subCount = ChunkSerializer::getSubChunkCount($chunk) + ChunkSerializer::LOWER_PADDING_SIZE;
$encoderContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
$payload = ChunkSerializer::serializeFullChunk($chunk, RuntimeBlockMapping::getInstance(), $encoderContext, $this->tiles);
$this->setResult($this->compressor->compress(PacketBatch::fromPackets($encoderContext, LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $subCount, false, null, $payload))->getBuffer()));
$stream = new BinaryStream();
PacketBatch::encodePackets($stream, $encoderContext, [LevelChunkPacket::create(new ChunkPosition($this->chunkX, $this->chunkZ), $subCount, false, null, $payload)]);
$this->setResult($this->compressor->compress($stream->getBuffer()));
}
public function onError() : void{

View File

@@ -43,7 +43,6 @@ use pocketmine\network\mcpe\cache\ChunkCache;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\Compressor;
use pocketmine\network\mcpe\compression\DecompressionException;
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\encryption\DecryptionException;
@@ -90,6 +89,8 @@ use pocketmine\network\mcpe\protocol\TakeItemActorPacket;
use pocketmine\network\mcpe\protocol\TextPacket;
use pocketmine\network\mcpe\protocol\ToastRequestPacket;
use pocketmine\network\mcpe\protocol\TransferPacket;
use pocketmine\network\mcpe\protocol\types\AbilitiesData;
use pocketmine\network\mcpe\protocol\types\AbilitiesLayer;
use pocketmine\network\mcpe\protocol\types\BlockPosition;
use pocketmine\network\mcpe\protocol\types\command\CommandData;
use pocketmine\network\mcpe\protocol\types\command\CommandEnum;
@@ -103,7 +104,6 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper;
use pocketmine\network\mcpe\protocol\types\PlayerListEntry;
use pocketmine\network\mcpe\protocol\types\PlayerPermissions;
use pocketmine\network\mcpe\protocol\types\UpdateAbilitiesPacketLayer;
use pocketmine\network\mcpe\protocol\UpdateAbilitiesPacket;
use pocketmine\network\mcpe\protocol\UpdateAdventureSettingsPacket;
use pocketmine\network\mcpe\protocol\UpdateAttributesPacket;
@@ -119,6 +119,7 @@ use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\ObjectSet;
use pocketmine\utils\TextFormat;
use pocketmine\utils\Utils;
@@ -129,9 +130,12 @@ use function base64_encode;
use function bin2hex;
use function count;
use function get_class;
use function hrtime;
use function in_array;
use function intdiv;
use function json_encode;
use function ksort;
use function min;
use function strcasecmp;
use function strlen;
use function strtolower;
@@ -142,6 +146,20 @@ use const JSON_THROW_ON_ERROR;
use const SORT_NUMERIC;
class NetworkSession{
private const INCOMING_PACKET_BATCH_PER_TICK = 2; //usually max 1 per tick, but transactions may arrive separately
private const INCOMING_PACKET_BATCH_MAX_BUDGET = 100 * self::INCOMING_PACKET_BATCH_PER_TICK; //enough to account for a 5-second lag spike
/**
* At most this many more packets can be received. If this reaches zero, any additional packets received will cause
* the player to be kicked from the server.
* This number is increased every tick up to a maximum limit.
*
* @see self::INCOMING_PACKET_BATCH_PER_TICK
* @see self::INCOMING_PACKET_BATCH_MAX_BUDGET
*/
private int $incomingPacketBatchBudget = self::INCOMING_PACKET_BATCH_MAX_BUDGET;
private int $lastPacketBudgetUpdateTimeNs;
private \PrefixedLogger $logger;
private ?Player $player = null;
private ?PlayerInfo $info = null;
@@ -158,7 +176,7 @@ class NetworkSession{
private ?EncryptionContext $cipher = null;
/** @var Packet[] */
/** @var string[] */
private array $sendBuffer = [];
/**
@@ -169,8 +187,6 @@ class NetworkSession{
private bool $forceAsyncCompression = true;
private bool $enableCompression = false; //disabled until handshake completed
private PacketSerializerContext $packetSerializerContext;
private ?InventoryManager $invManager = null;
/**
@@ -183,6 +199,7 @@ class NetworkSession{
private Server $server,
private NetworkSessionManager $manager,
private PacketPool $packetPool,
private PacketSerializerContext $packetSerializerContext,
private PacketSender $sender,
private PacketBroadcaster $broadcaster,
private Compressor $compressor,
@@ -193,12 +210,10 @@ class NetworkSession{
$this->compressedQueue = new \SplQueue();
//TODO: allow this to be injected
$this->packetSerializerContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
$this->disposeHooks = new ObjectSet();
$this->connectTime = time();
$this->lastPacketBudgetUpdateTimeNs = hrtime(true);
$this->setHandler(new SessionStartPacketHandler(
$this->server,
@@ -339,48 +354,67 @@ class NetworkSession{
return;
}
if($this->cipher !== null){
Timings::$playerNetworkReceiveDecrypt->startTiming();
try{
$payload = $this->cipher->decrypt($payload);
}catch(DecryptionException $e){
$this->logger->debug("Encrypted packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Packet decryption error");
}finally{
Timings::$playerNetworkReceiveDecrypt->stopTiming();
}
}
if($this->enableCompression){
Timings::$playerNetworkReceiveDecompress->startTiming();
try{
$decompressed = $this->compressor->decompress($payload);
}catch(DecompressionException $e){
$this->logger->debug("Failed to decompress packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Compressed packet batch decode error");
}finally{
Timings::$playerNetworkReceiveDecompress->stopTiming();
}
}else{
$decompressed = $payload;
}
Timings::$playerNetworkReceive->startTiming();
try{
foreach((new PacketBatch($decompressed))->getPackets($this->packetPool, $this->packetSerializerContext, 1300) as [$packet, $buffer]){
if($packet === null){
$this->logger->debug("Unknown packet: " . base64_encode($buffer));
throw new PacketHandlingException("Unknown packet received");
}
try{
$this->handleDataPacket($packet, $buffer);
}catch(PacketHandlingException $e){
$this->logger->debug($packet->getName() . ": " . base64_encode($buffer));
throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName());
if($this->incomingPacketBatchBudget <= 0){
$this->updatePacketBudget();
if($this->incomingPacketBatchBudget <= 0){
throw new PacketHandlingException("Receiving packets too fast");
}
}
}catch(PacketDecodeException $e){
$this->logger->logException($e);
throw PacketHandlingException::wrap($e, "Packet batch decode error");
$this->incomingPacketBatchBudget--;
if($this->cipher !== null){
Timings::$playerNetworkReceiveDecrypt->startTiming();
try{
$payload = $this->cipher->decrypt($payload);
}catch(DecryptionException $e){
$this->logger->debug("Encrypted packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Packet decryption error");
}finally{
Timings::$playerNetworkReceiveDecrypt->stopTiming();
}
}
if($this->enableCompression){
Timings::$playerNetworkReceiveDecompress->startTiming();
try{
$decompressed = $this->compressor->decompress($payload);
}catch(DecompressionException $e){
$this->logger->debug("Failed to decompress packet: " . base64_encode($payload));
throw PacketHandlingException::wrap($e, "Compressed packet batch decode error");
}finally{
Timings::$playerNetworkReceiveDecompress->stopTiming();
}
}else{
$decompressed = $payload;
}
try{
$stream = new BinaryStream($decompressed);
$count = 0;
foreach(PacketBatch::decodeRaw($stream) as $buffer){
if(++$count > 1300){
throw new PacketHandlingException("Too many packets in batch");
}
$packet = $this->packetPool->getPacket($buffer);
if($packet === null){
$this->logger->debug("Unknown packet: " . base64_encode($buffer));
throw new PacketHandlingException("Unknown packet received");
}
try{
$this->handleDataPacket($packet, $buffer);
}catch(PacketHandlingException $e){
$this->logger->debug($packet->getName() . ": " . base64_encode($buffer));
throw PacketHandlingException::wrap($e, "Error processing " . $packet->getName());
}
}
}catch(PacketDecodeException $e){
$this->logger->logException($e);
throw PacketHandlingException::wrap($e, "Packet batch decode error");
}
}finally{
Timings::$playerNetworkReceive->stopTiming();
}
}
@@ -392,32 +426,39 @@ class NetworkSession{
throw new PacketHandlingException("Unexpected non-serverbound packet");
}
$timings = Timings::getDecodeDataPacketTimings($packet);
$timings = Timings::getReceiveDataPacketTimings($packet);
$timings->startTiming();
try{
$stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext);
try{
$packet->decode($stream);
}catch(PacketDecodeException $e){
throw PacketHandlingException::wrap($e);
}
if(!$stream->feof()){
$remains = substr($stream->getBuffer(), $stream->getOffset());
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
}
}finally{
$timings->stopTiming();
}
$timings = Timings::getHandleDataPacketTimings($packet);
$timings->startTiming();
try{
//TODO: I'm not sure DataPacketReceiveEvent should be included in the handler timings, but it needs to be
//included for now to ensure the receivePacket timings are counted the way they were before
$decodeTimings = Timings::getDecodeDataPacketTimings($packet);
$decodeTimings->startTiming();
try{
$stream = PacketSerializer::decoder($buffer, 0, $this->packetSerializerContext);
try{
$packet->decode($stream);
}catch(PacketDecodeException $e){
throw PacketHandlingException::wrap($e);
}
if(!$stream->feof()){
$remains = substr($stream->getBuffer(), $stream->getOffset());
$this->logger->debug("Still " . strlen($remains) . " bytes unread in " . $packet->getName() . ": " . bin2hex($remains));
}
}finally{
$decodeTimings->stopTiming();
}
$ev = new DataPacketReceiveEvent($this, $packet);
$ev->call();
if(!$ev->isCancelled() && ($this->handler === null || !$packet->handle($this->handler))){
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
if(!$ev->isCancelled()){
$handlerTimings = Timings::getHandleDataPacketTimings($packet);
$handlerTimings->startTiming();
try{
if($this->handler === null || !$packet->handle($this->handler)){
$this->logger->debug("Unhandled " . $packet->getName() . ": " . base64_encode($stream->getBuffer()));
}
}finally{
$handlerTimings->stopTiming();
}
}
}finally{
$timings->stopTiming();
@@ -425,6 +466,9 @@ class NetworkSession{
}
public function sendDataPacket(ClientboundPacket $packet, bool $immediate = false) : bool{
if(!$this->connected){
return false;
}
//Basic safety restriction. TODO: improve this
if(!$this->loggedIn && !$packet->canBeSentBeforeLogin()){
throw new \InvalidArgumentException("Attempted to send " . get_class($packet) . " to " . $this->getDisplayName() . " too early");
@@ -439,7 +483,7 @@ class NetworkSession{
return false;
}
$this->addToSendBuffer($packet);
$this->addToSendBuffer(self::encodePacketTimed(PacketSerializer::encoder($this->packetSerializerContext), $packet));
if($immediate){
$this->flushSendBuffer(true);
}
@@ -453,34 +497,49 @@ class NetworkSession{
/**
* @internal
*/
public function addToSendBuffer(ClientboundPacket $packet) : void{
$timings = Timings::getSendDataPacketTimings($packet);
public static function encodePacketTimed(PacketSerializer $serializer, ClientboundPacket $packet) : string{
$timings = Timings::getEncodeDataPacketTimings($packet);
$timings->startTiming();
try{
$this->sendBuffer[] = $packet;
$packet->encode($serializer);
return $serializer->getBuffer();
}finally{
$timings->stopTiming();
}
}
/**
* @internal
*/
public function addToSendBuffer(string $buffer) : void{
$this->sendBuffer[] = $buffer;
}
private function flushSendBuffer(bool $immediate = false) : void{
if(count($this->sendBuffer) > 0){
$syncMode = null; //automatic
if($immediate){
$syncMode = true;
}elseif($this->forceAsyncCompression){
$syncMode = false;
}
Timings::$playerNetworkSend->startTiming();
try{
$syncMode = null; //automatic
if($immediate){
$syncMode = true;
}elseif($this->forceAsyncCompression){
$syncMode = false;
}
$batch = PacketBatch::fromPackets($this->packetSerializerContext, ...$this->sendBuffer);
if($this->enableCompression){
$promise = $this->server->prepareBatch($batch, $this->compressor, $syncMode);
}else{
$promise = new CompressBatchPromise();
$promise->resolve($batch->getBuffer());
$stream = new BinaryStream();
PacketBatch::encodeRaw($stream, $this->sendBuffer);
if($this->enableCompression){
$promise = $this->server->prepareBatch(new PacketBatch($stream->getBuffer()), $this->compressor, $syncMode, Timings::$playerNetworkSendCompressSessionBuffer);
}else{
$promise = new CompressBatchPromise();
$promise->resolve($stream->getBuffer());
}
$this->sendBuffer = [];
$this->queueCompressedNoBufferFlush($promise, $immediate);
}finally{
Timings::$playerNetworkSend->stopTiming();
}
$this->sendBuffer = [];
$this->queueCompressedNoBufferFlush($promise, $immediate);
}
}
@@ -493,35 +552,45 @@ class NetworkSession{
}
public function queueCompressed(CompressBatchPromise $payload, bool $immediate = false) : void{
$this->flushSendBuffer($immediate); //Maintain ordering if possible
$this->queueCompressedNoBufferFlush($payload, $immediate);
Timings::$playerNetworkSend->startTiming();
try{
$this->flushSendBuffer($immediate); //Maintain ordering if possible
$this->queueCompressedNoBufferFlush($payload, $immediate);
}finally{
Timings::$playerNetworkSend->stopTiming();
}
}
private function queueCompressedNoBufferFlush(CompressBatchPromise $payload, bool $immediate = false) : void{
if($immediate){
//Skips all queues
$this->sendEncoded($payload->getResult(), true);
}else{
$this->compressedQueue->enqueue($payload);
$payload->onResolve(function(CompressBatchPromise $payload) : void{
if($this->connected && $this->compressedQueue->bottom() === $payload){
$this->compressedQueue->dequeue(); //result unused
$this->sendEncoded($payload->getResult());
Timings::$playerNetworkSend->startTiming();
try{
if($immediate){
//Skips all queues
$this->sendEncoded($payload->getResult(), true);
}else{
$this->compressedQueue->enqueue($payload);
$payload->onResolve(function(CompressBatchPromise $payload) : void{
if($this->connected && $this->compressedQueue->bottom() === $payload){
$this->compressedQueue->dequeue(); //result unused
$this->sendEncoded($payload->getResult());
while(!$this->compressedQueue->isEmpty()){
/** @var CompressBatchPromise $current */
$current = $this->compressedQueue->bottom();
if($current->hasResult()){
$this->compressedQueue->dequeue();
while(!$this->compressedQueue->isEmpty()){
/** @var CompressBatchPromise $current */
$current = $this->compressedQueue->bottom();
if($current->hasResult()){
$this->compressedQueue->dequeue();
$this->sendEncoded($current->getResult());
}else{
//can't send any more queued until this one is ready
break;
$this->sendEncoded($current->getResult());
}else{
//can't send any more queued until this one is ready
break;
}
}
}
}
});
});
}
}finally{
Timings::$playerNetworkSend->stopTiming();
}
}
@@ -542,28 +611,37 @@ class NetworkSession{
$this->disconnectGuard = true;
$func();
$this->disconnectGuard = false;
$this->flushSendBuffer(true);
$this->sender->close("");
foreach($this->disposeHooks as $callback){
$callback();
}
$this->disposeHooks->clear();
$this->setHandler(null);
$this->connected = false;
$this->manager->remove($this);
$this->logger->info("Session closed due to $reason");
$this->invManager = null; //break cycles - TODO: this really ought to be deferred until it's safe
}
}
/**
* Performs actions after the session has been disconnected. By this point, nothing should be interacting with the
* session, so it's safe to destroy any cycles and perform destructive cleanup.
*/
private function dispose() : void{
$this->invManager = null;
}
/**
* Disconnects the session, destroying the associated player (if it exists).
*/
public function disconnect(string $reason, bool $notify = true) : void{
$this->tryDisconnect(function() use ($reason, $notify) : void{
if($notify){
$this->sendDataPacket(DisconnectPacket::create($reason));
}
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
}
$this->doServerDisconnect($reason, $notify);
}, $reason);
}
@@ -576,7 +654,6 @@ class NetworkSession{
if($this->player !== null){
$this->player->onPostDisconnect($reason, null);
}
$this->doServerDisconnect($reason, false);
}, $reason);
}
@@ -585,21 +662,10 @@ class NetworkSession{
*/
public function onPlayerDestroyed(string $reason) : void{
$this->tryDisconnect(function() use ($reason) : void{
$this->doServerDisconnect($reason, true);
$this->sendDataPacket(DisconnectPacket::create($reason));
}, $reason);
}
/**
* Internal helper function used to handle server disconnections.
*/
private function doServerDisconnect(string $reason, bool $notify = true) : void{
if($notify){
$this->sendDataPacket(DisconnectPacket::create($reason !== "" ? $reason : null), true);
}
$this->sender->close($notify ? $reason : "");
}
/**
* Called by the network interface to close the session when the client disconnects without server input, for
* example in a timeout condition or voluntary client disconnect.
@@ -683,7 +749,7 @@ class NetworkSession{
//TODO: we shouldn't be loading player data here at all, but right now we don't have any choice :(
$this->cachedOfflinePlayerData = $this->server->getOfflinePlayerData($this->info->getUsername());
if($checkXUID){
$recordedXUID = $this->cachedOfflinePlayerData !== null ? $this->cachedOfflinePlayerData->getTag("LastKnownXUID") : null;
$recordedXUID = $this->cachedOfflinePlayerData !== null ? $this->cachedOfflinePlayerData->getTag(Player::TAG_LAST_KNOWN_XUID) : null;
if(!($recordedXUID instanceof StringTag)){
$this->logger->debug("No previous XUID recorded, no choice but to trust this player");
}elseif(!$kickForXUIDMismatch($recordedXUID->getValue())){
@@ -744,9 +810,9 @@ class NetworkSession{
$this->setHandler(new InGamePacketHandler($this->player, $this, $this->invManager));
}
public function onServerDeath() : void{
public function onServerDeath(Translatable|string $deathMessage) : void{
if($this->handler instanceof InGamePacketHandler){ //TODO: this is a bad fix for pre-spawn death, this shouldn't be reachable at all at this stage :(
$this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError()));
$this->setHandler(new DeathPacketHandler($this->player, $this, $this->invManager ?? throw new AssumptionFailedError(), $deathMessage));
}
}
@@ -817,33 +883,33 @@ class NetworkSession{
//ALL of these need to be set for the base layer, otherwise the client will cry
$boolAbilities = [
UpdateAbilitiesPacketLayer::ABILITY_ALLOW_FLIGHT => $for->getAllowFlight(),
UpdateAbilitiesPacketLayer::ABILITY_FLYING => $for->isFlying(),
UpdateAbilitiesPacketLayer::ABILITY_NO_CLIP => !$for->hasBlockCollision(),
UpdateAbilitiesPacketLayer::ABILITY_OPERATOR => $isOp,
UpdateAbilitiesPacketLayer::ABILITY_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT),
UpdateAbilitiesPacketLayer::ABILITY_INVULNERABLE => $for->isCreative(),
UpdateAbilitiesPacketLayer::ABILITY_MUTED => false,
UpdateAbilitiesPacketLayer::ABILITY_WORLD_BUILDER => false,
UpdateAbilitiesPacketLayer::ABILITY_INFINITE_RESOURCES => !$for->hasFiniteResources(),
UpdateAbilitiesPacketLayer::ABILITY_LIGHTNING => false,
UpdateAbilitiesPacketLayer::ABILITY_BUILD => !$for->isSpectator(),
UpdateAbilitiesPacketLayer::ABILITY_MINE => !$for->isSpectator(),
UpdateAbilitiesPacketLayer::ABILITY_DOORS_AND_SWITCHES => !$for->isSpectator(),
UpdateAbilitiesPacketLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
UpdateAbilitiesPacketLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
UpdateAbilitiesPacketLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
AbilitiesLayer::ABILITY_ALLOW_FLIGHT => $for->getAllowFlight(),
AbilitiesLayer::ABILITY_FLYING => $for->isFlying(),
AbilitiesLayer::ABILITY_NO_CLIP => !$for->hasBlockCollision(),
AbilitiesLayer::ABILITY_OPERATOR => $isOp,
AbilitiesLayer::ABILITY_TELEPORT => $for->hasPermission(DefaultPermissionNames::COMMAND_TELEPORT_SELF),
AbilitiesLayer::ABILITY_INVULNERABLE => $for->isCreative(),
AbilitiesLayer::ABILITY_MUTED => false,
AbilitiesLayer::ABILITY_WORLD_BUILDER => false,
AbilitiesLayer::ABILITY_INFINITE_RESOURCES => !$for->hasFiniteResources(),
AbilitiesLayer::ABILITY_LIGHTNING => false,
AbilitiesLayer::ABILITY_BUILD => !$for->isSpectator(),
AbilitiesLayer::ABILITY_MINE => !$for->isSpectator(),
AbilitiesLayer::ABILITY_DOORS_AND_SWITCHES => !$for->isSpectator(),
AbilitiesLayer::ABILITY_OPEN_CONTAINERS => !$for->isSpectator(),
AbilitiesLayer::ABILITY_ATTACK_PLAYERS => !$for->isSpectator(),
AbilitiesLayer::ABILITY_ATTACK_MOBS => !$for->isSpectator(),
];
$this->sendDataPacket(UpdateAbilitiesPacket::create(
$this->sendDataPacket(UpdateAbilitiesPacket::create(new AbilitiesData(
$isOp ? CommandPermissions::OPERATOR : CommandPermissions::NORMAL,
$isOp ? PlayerPermissions::OPERATOR : PlayerPermissions::MEMBER,
$for->getId(),
[
//TODO: dynamic flying speed! FINALLY!!!!!!!!!!!!!!!!!
new UpdateAbilitiesPacketLayer(UpdateAbilitiesPacketLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
new AbilitiesLayer(AbilitiesLayer::LAYER_BASE, $boolAbilities, 0.05, 0.1),
]
));
)));
}
public function syncAdventureSettings() : void{
@@ -931,15 +997,19 @@ class NetworkSession{
$this->sendDataPacket(AvailableCommandsPacket::create($commandData, [], [], []));
}
public function onRawChatMessage(string $message) : void{
$this->sendDataPacket(TextPacket::raw($message));
}
/**
* @param string[] $parameters
*/
public function onTranslatedChatMessage(string $key, array $parameters) : void{
$this->sendDataPacket(TextPacket::translation($key, $parameters));
public function onChatMessage(Translatable|string $message) : void{
if($message instanceof Translatable){
$language = $this->player->getLanguage();
if(!$this->server->isLanguageForced()){
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $message->getParameters());
$this->sendDataPacket(TextPacket::translation($language->translateString($message->getText(), $parameters, "pocketmine."), $parameters));
}else{
$this->sendDataPacket(TextPacket::raw($language->translate($message)));
}
}else{
$this->sendDataPacket(TextPacket::raw($message));
}
}
/**
@@ -1107,7 +1177,29 @@ class NetworkSession{
$this->sendDataPacket(ToastRequestPacket::create($title, $body));
}
private function updatePacketBudget() : void{
$nowNs = hrtime(true);
$timeSinceLastUpdateNs = $nowNs - $this->lastPacketBudgetUpdateTimeNs;
if($timeSinceLastUpdateNs > 50_000_000){
$ticksSinceLastUpdate = intdiv($timeSinceLastUpdateNs, 50_000_000);
/*
* If the server takes an abnormally long time to process a tick, add the budget for time difference to
* compensate. This extra budget may be very large, but it will disappear the next time a normal update
* occurs. This ensures that backlogs during a large lag spike don't cause everyone to get kicked.
* As long as all the backlogged packets are processed before the next tick, everything should be OK for
* clients behaving normally.
*/
$this->incomingPacketBatchBudget = min($this->incomingPacketBatchBudget, self::INCOMING_PACKET_BATCH_MAX_BUDGET) + (self::INCOMING_PACKET_BATCH_PER_TICK * 2 * $ticksSinceLastUpdate);
$this->lastPacketBudgetUpdateTimeNs = $nowNs;
}
}
public function tick() : void{
if(!$this->isConnected()){
$this->dispose();
return;
}
if($this->info === null){
if(time() >= $this->connectTime + 10){
$this->disconnect("Login timeout");

View File

@@ -24,44 +24,64 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\Server;
use pocketmine\timings\Timings;
use pocketmine\utils\BinaryStream;
use function count;
use function spl_object_id;
use function strlen;
final class StandardPacketBroadcaster implements PacketBroadcaster{
public function __construct(private Server $server){}
public function __construct(
private Server $server,
private PacketSerializerContext $protocolContext
){}
public function broadcastPackets(array $recipients, array $packets) : void{
$buffers = [];
$compressors = [];
$targetMap = [];
/** @var NetworkSession[][] $targetsByCompressor */
$targetsByCompressor = [];
foreach($recipients as $recipient){
$serializerContext = $recipient->getPacketSerializerContext();
$bufferId = spl_object_id($serializerContext);
if(!isset($buffers[$bufferId])){
$buffers[$bufferId] = PacketBatch::fromPackets($serializerContext, ...$packets);
if($recipient->getPacketSerializerContext() !== $this->protocolContext){
throw new \InvalidArgumentException("Only recipients with the same protocol context as the broadcaster can be broadcast to by this broadcaster");
}
//TODO: different compressors might be compatible, it might not be necessary to split them up by object
$compressor = $recipient->getCompressor();
$compressors[spl_object_id($compressor)] = $compressor;
$targetMap[$bufferId][spl_object_id($compressor)][] = $recipient;
$targetsByCompressor[spl_object_id($compressor)][] = $recipient;
}
foreach($targetMap as $bufferId => $compressorMap){
$buffer = $buffers[$bufferId];
foreach($compressorMap as $compressorId => $compressorTargets){
$compressor = $compressors[$compressorId];
if(!$compressor->willCompress($buffer->getBuffer())){
foreach($compressorTargets as $target){
foreach($packets as $pk){
$target->addToSendBuffer($pk);
}
}
}else{
$promise = $this->server->prepareBatch($buffer, $compressor);
foreach($compressorTargets as $target){
$target->queueCompressed($promise);
$totalLength = 0;
$packetBuffers = [];
foreach($packets as $packet){
$buffer = NetworkSession::encodePacketTimed(PacketSerializer::encoder($this->protocolContext), $packet);
$totalLength += strlen($buffer);
$packetBuffers[] = $buffer;
}
foreach($targetsByCompressor as $compressorId => $compressorTargets){
$compressor = $compressors[$compressorId];
$threshold = $compressor->getCompressionThreshold();
if(count($compressorTargets) > 1 && $threshold !== null && $totalLength >= $threshold){
//do not prepare shared batch unless we're sure it will be compressed
$stream = new BinaryStream();
PacketBatch::encodeRaw($stream, $packetBuffers);
$batchBuffer = $stream->getBuffer();
$promise = $this->server->prepareBatch(new PacketBatch($batchBuffer), $compressor, timings: Timings::$playerNetworkSendCompressBroadcast);
foreach($compressorTargets as $target){
$target->queueCompressed($promise);
}
}else{
foreach($compressorTargets as $target){
foreach($packetBuffers as $packetBuffer){
$target->addToSendBuffer($packetBuffer);
}
}
}

View File

@@ -23,13 +23,13 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\cache;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
class StaticPacketCache{
use SingletonTrait;
@@ -38,15 +38,13 @@ class StaticPacketCache{
* @phpstan-return CacheableNbt<\pocketmine\nbt\tag\CompoundTag>
*/
private static function loadCompoundFromFile(string $filePath) : CacheableNbt{
$rawNbt = @file_get_contents($filePath);
if($rawNbt === false) throw new \RuntimeException("Failed to read file");
return new CacheableNbt((new NetworkNbtSerializer())->read($rawNbt)->mustGetCompoundTag());
return new CacheableNbt((new NetworkNbtSerializer())->read(Filesystem::fileGetContents($filePath))->mustGetCompoundTag());
}
private static function make() : self{
return new self(
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'biome_definitions.nbt'))),
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'entity_identifiers.nbt')))
BiomeDefinitionListPacket::create(self::loadCompoundFromFile(BedrockDataFiles::BIOME_DEFINITIONS_NBT)),
AvailableActorIdentifiersPacket::create(self::loadCompoundFromFile(BedrockDataFiles::ENTITY_IDENTIFIERS_NBT))
);
}

View File

@@ -24,13 +24,20 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\compression;
interface Compressor{
public function willCompress(string $data) : bool;
/**
* @throws DecompressionException
*/
public function decompress(string $payload) : string;
public function compress(string $payload) : string;
/**
* Returns the minimum size of packet batch that the compressor will attempt to compress.
*
* The compressor's output **MUST** still be valid input for the decompressor even if the compressor input is
* below this threshold.
* However, it may choose to use a cheaper compression option (e.g. zlib level 0, which simply wraps the data and
* doesn't attempt to compress it) to avoid wasting CPU time.
*/
public function getCompressionThreshold() : ?int;
}

View File

@@ -48,12 +48,12 @@ final class ZlibCompressor implements Compressor{
public function __construct(
private int $level,
private int $minCompressionSize,
private ?int $minCompressionSize,
private int $maxDecompressionSize
){}
public function willCompress(string $data) : bool{
return $this->minCompressionSize > -1 && strlen($data) >= $this->minCompressionSize;
public function getCompressionThreshold() : ?int{
return $this->minCompressionSize;
}
/**
@@ -72,11 +72,12 @@ final class ZlibCompressor implements Compressor{
}
public function compress(string $payload) : string{
$compressible = $this->minCompressionSize !== null && strlen($payload) >= $this->minCompressionSize;
if(function_exists('libdeflate_deflate_compress')){
return $this->willCompress($payload) ?
return $compressible ?
libdeflate_deflate_compress($payload, $this->level) :
self::zlib_encode($payload, 0);
}
return self::zlib_encode($payload, $this->willCompress($payload) ? $this->level : 0);
return self::zlib_encode($payload, $compressible ? $this->level : 0);
}
}

View File

@@ -23,13 +23,12 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\network\mcpe\protocol\types\ItemTypeEntry;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
use function is_array;
use function is_bool;
use function is_int;
@@ -40,7 +39,7 @@ final class GlobalItemTypeDictionary{
use SingletonTrait;
private static function make() : self{
$data = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'required_item_list.json')), "Missing required resource file");
$data = Filesystem::fileGetContents(BedrockDataFiles::REQUIRED_ITEM_LIST_JSON);
$table = json_decode($data, true);
if(!is_array($table)){
throw new AssumptionFailedError("Invalid item list format");

View File

@@ -23,14 +23,14 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\convert;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\LegacyItemIdToStringIdMap;
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function array_key_exists;
use function file_get_contents;
use function is_array;
use function is_numeric;
use function is_string;
@@ -67,7 +67,7 @@ final class ItemTranslator{
private array $complexNetToCoreMapping = [];
private static function make() : self{
$data = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\BEDROCK_DATA_PATH, 'r16_to_current_item_map.json')), "Missing required resource file");
$data = Filesystem::fileGetContents(BedrockDataFiles::R16_TO_CURRENT_ITEM_MAP_JSON);
$json = json_decode($data, true);
if(!is_array($json) || !isset($json["simple"], $json["complex"]) || !is_array($json["simple"]) || !is_array($json["complex"])){
throw new AssumptionFailedError("Invalid item table format");

View File

@@ -25,15 +25,13 @@ namespace pocketmine\network\mcpe\convert;
use pocketmine\block\Block;
use pocketmine\block\BlockLegacyIds;
use pocketmine\data\bedrock\BedrockDataFiles;
use pocketmine\data\bedrock\LegacyBlockIdToStringIdMap;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\protocol\serializer\NetworkNbtSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Filesystem;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function file_get_contents;
/**
* @internal
@@ -50,20 +48,20 @@ final class RuntimeBlockMapping{
private static function make() : self{
return new self(
Path::join(\pocketmine\BEDROCK_DATA_PATH, "canonical_block_states.nbt"),
Path::join(\pocketmine\BEDROCK_DATA_PATH, "r12_to_current_block_map.bin")
BedrockDataFiles::CANONICAL_BLOCK_STATES_NBT,
BedrockDataFiles::R12_TO_CURRENT_BLOCK_MAP_BIN
);
}
public function __construct(string $canonicalBlockStatesFile, string $r12ToCurrentBlockMapFile){
$stream = PacketSerializer::decoder(
Utils::assumeNotFalse(file_get_contents($canonicalBlockStatesFile), "Missing required resource file"),
0,
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
);
$stream = new BinaryStream(Filesystem::fileGetContents($canonicalBlockStatesFile));
$list = [];
$nbtReader = new NetworkNbtSerializer();
while(!$stream->feof()){
$list[] = $stream->getNbtCompoundRoot();
$offset = $stream->getOffset();
$blockState = $nbtReader->read($stream->getBuffer(), $offset)->mustGetCompoundTag();
$stream->setOffset($offset);
$list[] = $blockState;
}
$this->bedrockKnownStates = $list;
@@ -74,14 +72,10 @@ final class RuntimeBlockMapping{
$legacyIdMap = LegacyBlockIdToStringIdMap::getInstance();
/** @var R12ToCurrentBlockMapEntry[] $legacyStateMap */
$legacyStateMap = [];
$legacyStateMapReader = PacketSerializer::decoder(
Utils::assumeNotFalse(file_get_contents($r12ToCurrentBlockMapFile), "Missing required resource file"),
0,
new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary())
);
$legacyStateMapReader = new BinaryStream(Filesystem::fileGetContents($r12ToCurrentBlockMapFile));
$nbtReader = new NetworkNbtSerializer();
while(!$legacyStateMapReader->feof()){
$id = $legacyStateMapReader->getString();
$id = $legacyStateMapReader->get($legacyStateMapReader->getUnsignedVarInt());
$meta = $legacyStateMapReader->getLShort();
$offset = $legacyStateMapReader->getOffset();

View File

@@ -23,19 +23,23 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\InventoryManager;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\ContainerClosePacket;
use pocketmine\network\mcpe\protocol\DeathInfoPacket;
use pocketmine\network\mcpe\protocol\PlayerActionPacket;
use pocketmine\network\mcpe\protocol\RespawnPacket;
use pocketmine\network\mcpe\protocol\types\PlayerAction;
use pocketmine\player\Player;
use function array_map;
class DeathPacketHandler extends PacketHandler{
public function __construct(
private Player $player,
private NetworkSession $session,
private InventoryManager $inventoryManager
private InventoryManager $inventoryManager,
private Translatable|string $deathMessage
){}
public function setUp() : void{
@@ -44,6 +48,22 @@ class DeathPacketHandler extends PacketHandler{
RespawnPacket::SEARCHING_FOR_SPAWN,
$this->player->getId()
));
/** @var string[] $parameters */
$parameters = [];
if($this->deathMessage instanceof Translatable){
$language = $this->player->getLanguage();
if(!$this->player->getServer()->isLanguageForced()){
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $language->translate($p) : $p, $this->deathMessage->getParameters());
$message = $language->translateString($this->deathMessage->getText(), $parameters, "pocketmine.");
}else{
$message = $language->translate($this->deathMessage);
}
}else{
$message = $this->deathMessage;
}
$this->session->sendDataPacket(DeathInfoPacket::create($message, $parameters));
}
public function handlePlayerAction(PlayerActionPacket $packet) : bool{

View File

@@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\block\BaseSign;
use pocketmine\block\ItemFrame;
use pocketmine\block\Lectern;
use pocketmine\block\tile\Sign;
use pocketmine\block\utils\SignText;
use pocketmine\entity\animation\ConsumingItemAnimation;
use pocketmine\entity\Attribute;
@@ -124,8 +125,8 @@ use function max;
use function mb_strlen;
use function microtime;
use function sprintf;
use function str_starts_with;
use function strlen;
use function strpos;
use const JSON_THROW_ON_ERROR;
/**
@@ -150,6 +151,8 @@ class InGamePacketHandler extends PacketHandler{
/** @var bool */
public $forceMoveSync = false;
protected ?string $lastRequestedFullSkinId = null;
public function __construct(
private Player $player,
private NetworkSession $session,
@@ -250,6 +253,26 @@ class InGamePacketHandler extends PacketHandler{
$packetHandled = true;
$blockActions = $packet->getBlockActions();
if($blockActions !== null){
if(count($blockActions) > 100){
throw new PacketHandlingException("Too many block actions in PlayerAuthInputPacket");
}
foreach($blockActions as $k => $blockAction){
$actionHandled = false;
if($blockAction instanceof PlayerBlockActionStopBreak){
$actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), new BlockPosition(0, 0, 0), Facing::DOWN);
}elseif($blockAction instanceof PlayerBlockActionWithBlockInfo){
$actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), $blockAction->getBlockPosition(), $blockAction->getFace());
}
if(!$actionHandled){
$packetHandled = false;
$this->session->getLogger()->debug("Unhandled player block action at offset $k in PlayerAuthInputPacket");
}
}
}
$useItemTransaction = $packet->getItemInteractionData();
if($useItemTransaction !== null){
if(count($useItemTransaction->getTransactionData()->getActions()) > 100){
@@ -273,26 +296,6 @@ class InGamePacketHandler extends PacketHandler{
$this->session->sendDataPacket(ItemStackResponsePacket::create([$result]));
}
$blockActions = $packet->getBlockActions();
if($blockActions !== null){
if(count($blockActions) > 100){
throw new PacketHandlingException("Too many block actions in PlayerAuthInputPacket");
}
foreach($blockActions as $k => $blockAction){
$actionHandled = false;
if($blockAction instanceof PlayerBlockActionStopBreak){
$actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), new BlockPosition(0, 0, 0), Facing::DOWN);
}elseif($blockAction instanceof PlayerBlockActionWithBlockInfo){
$actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), $blockAction->getBlockPosition(), $blockAction->getFace());
}
if(!$actionHandled){
$packetHandled = false;
$this->session->getLogger()->debug("Unhandled player block action at offset $k in PlayerAuthInputPacket");
}
}
}
return $packetHandled;
}
@@ -676,7 +679,7 @@ class InGamePacketHandler extends PacketHandler{
if(!($nbt instanceof CompoundTag)) throw new AssumptionFailedError("PHPStan should ensure this is a CompoundTag"); //for phpstorm's benefit
if($block instanceof BaseSign){
if(($textBlobTag = $nbt->getTag("Text")) instanceof StringTag){
if(($textBlobTag = $nbt->getTag(Sign::TAG_TEXT_BLOB)) instanceof StringTag){
try{
$text = SignText::fromBlob($textBlobTag->getValue());
}catch(\InvalidArgumentException $e){
@@ -747,7 +750,7 @@ class InGamePacketHandler extends PacketHandler{
}
public function handleCommandRequest(CommandRequestPacket $packet) : bool{
if(strpos($packet->command, '/') === 0){
if(str_starts_with($packet->command, '/')){
$this->player->chat($packet->command);
return true;
}
@@ -759,6 +762,15 @@ class InGamePacketHandler extends PacketHandler{
}
public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{
if($packet->skin->getFullSkinId() === $this->lastRequestedFullSkinId){
//TODO: HACK! In 1.19.60, the client sends its skin back to us if we sent it a skin different from the one
//it's using. We need to prevent this from causing a feedback loop.
$this->session->getLogger()->debug("Refused duplicate skin change request");
return true;
}
$this->lastRequestedFullSkinId = $packet->skin->getFullSkinId();
$this->session->getLogger()->debug("Processing skin change request");
try{
$skin = SkinAdapterSingleton::get()->fromSkinData($packet->skin);
}catch(InvalidSkinException $e){

View File

@@ -25,7 +25,6 @@ namespace pocketmine\network\mcpe\handler;
use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\KnownTranslationKeys;
use pocketmine\network\mcpe\auth\ProcessLoginTask;
use pocketmine\network\mcpe\convert\SkinAdapterSingleton;
@@ -33,8 +32,6 @@ use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\PlayStatusPacket;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
@@ -63,18 +60,6 @@ class LoginPacketHandler extends PacketHandler{
){}
public function handleLogin(LoginPacket $packet) : bool{
if(!$this->isCompatibleProtocol($packet->protocol)){
$this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);
//This pocketmine disconnect message will only be seen by the console (PlayStatusPacket causes the messages to be shown for the client)
$this->session->disconnect(
$this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_disconnect_incompatibleProtocol((string) $packet->protocol)),
false
);
return true;
}
$extraData = $this->fetchAuthData($packet->chainDataJwt);
if(!Player::isValidUserName($extraData->displayName)){
@@ -84,6 +69,7 @@ class LoginPacketHandler extends PacketHandler{
}
$clientData = $this->parseClientData($packet->clientDataJwt);
try{
$skin = SkinAdapterSingleton::get()->fromSkinData(ClientDataToSkinDataHelper::fromClientData($clientData));
}catch(\InvalidArgumentException | InvalidSkinException $e){
@@ -215,8 +201,4 @@ class LoginPacketHandler extends PacketHandler{
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($packet->chainDataJwt->chain, $packet->clientDataJwt, $authRequired, $this->authCallback));
$this->session->setHandler(null); //drop packets received during login verification
}
protected function isCompatibleProtocol(int $protocolVersion) : bool{
return $protocolVersion === ProtocolInfo::CURRENT_PROTOCOL;
}
}

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\network\mcpe\protocol\PlayerAuthInputPacket;
use pocketmine\network\mcpe\protocol\PlayerSkinPacket;
use pocketmine\network\mcpe\protocol\SetLocalPlayerAsInitializedPacket;
final class SpawnResponsePacketHandler extends PacketHandler{
@@ -37,6 +38,13 @@ final class SpawnResponsePacketHandler extends PacketHandler{
return true;
}
public function handlePlayerSkin(PlayerSkinPacket $packet) : bool{
//TODO: REMOVE THIS
//As of 1.19.60, we receive this packet during pre-spawn for no obvious reason. The skin is still sent in the
//login packet, so we can ignore this one. If unhandled, this packet makes a huge debug spam in the log.
return true;
}
public function handlePlayerAuthInput(PlayerAuthInputPacket $packet) : bool{
//the client will send this every tick once we start sending chunks, but we don't handle it in this stage
//this is very spammy so we filter it out

View File

@@ -25,17 +25,20 @@ namespace pocketmine\network\mcpe\raklib;
use pocketmine\network\AdvancedNetworkInterface;
use pocketmine\network\mcpe\compression\ZlibCompressor;
use pocketmine\network\mcpe\convert\GlobalItemTypeDictionary;
use pocketmine\network\mcpe\convert\TypeConverter;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\PacketBroadcaster;
use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializerContext;
use pocketmine\network\mcpe\StandardPacketBroadcaster;
use pocketmine\network\Network;
use pocketmine\network\NetworkInterfaceStartException;
use pocketmine\network\PacketHandlingException;
use pocketmine\Server;
use pocketmine\snooze\SleeperNotifier;
use pocketmine\timings\Timings;
use pocketmine\utils\Utils;
use raklib\generic\SocketException;
use raklib\protocol\EncapsulatedPacket;
@@ -78,6 +81,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
private SleeperNotifier $sleeper;
private PacketBroadcaster $broadcaster;
private PacketSerializerContext $packetSerializerContext;
public function __construct(Server $server, string $ip, int $port, bool $ipV6){
$this->server = $server;
@@ -105,12 +109,18 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
new PthreadsChannelWriter($mainToThreadBuffer)
);
$this->broadcaster = new StandardPacketBroadcaster($this->server);
$this->packetSerializerContext = new PacketSerializerContext(GlobalItemTypeDictionary::getInstance()->getDictionary());
$this->broadcaster = new StandardPacketBroadcaster($this->server, $this->packetSerializerContext);
}
public function start() : void{
$this->server->getTickSleeper()->addNotifier($this->sleeper, function() : void{
while($this->eventReceiver->handle($this));
Timings::$connection->startTiming();
try{
while($this->eventReceiver->handle($this));
}finally{
Timings::$connection->stopTiming();
}
});
$this->server->getLogger()->debug("Waiting for RakLib to start...");
try{
@@ -160,6 +170,7 @@ class RakLibInterface implements ServerEventListener, AdvancedNetworkInterface{
$this->server,
$this->network->getSessionManager(),
PacketPool::getInstance(),
$this->packetSerializerContext,
new RakLibPacketSender($sessionId, $this),
$this->broadcaster,
ZlibCompressor::getInstance(), //TODO: this shouldn't be hardcoded, but we might need the RakNet protocol version to select it

View File

@@ -35,10 +35,18 @@ final class DefaultPermissionNames{
public const COMMAND_DIFFICULTY = "pocketmine.command.difficulty";
public const COMMAND_DUMPMEMORY = "pocketmine.command.dumpmemory";
public const COMMAND_EFFECT = "pocketmine.command.effect";
public const COMMAND_EFFECT_OTHER = "pocketmine.command.effect.other";
public const COMMAND_EFFECT_SELF = "pocketmine.command.effect.self";
public const COMMAND_ENCHANT = "pocketmine.command.enchant";
public const COMMAND_ENCHANT_OTHER = "pocketmine.command.enchant.other";
public const COMMAND_ENCHANT_SELF = "pocketmine.command.enchant.self";
public const COMMAND_GAMEMODE = "pocketmine.command.gamemode";
public const COMMAND_GAMEMODE_OTHER = "pocketmine.command.gamemode.other";
public const COMMAND_GAMEMODE_SELF = "pocketmine.command.gamemode.self";
public const COMMAND_GC = "pocketmine.command.gc";
public const COMMAND_GIVE = "pocketmine.command.give";
public const COMMAND_GIVE_OTHER = "pocketmine.command.give.other";
public const COMMAND_GIVE_SELF = "pocketmine.command.give.self";
public const COMMAND_HELP = "pocketmine.command.help";
public const COMMAND_KICK = "pocketmine.command.kick";
public const COMMAND_KILL_OTHER = "pocketmine.command.kill.other";
@@ -56,9 +64,13 @@ final class DefaultPermissionNames{
public const COMMAND_SEED = "pocketmine.command.seed";
public const COMMAND_SETWORLDSPAWN = "pocketmine.command.setworldspawn";
public const COMMAND_SPAWNPOINT = "pocketmine.command.spawnpoint";
public const COMMAND_SPAWNPOINT_OTHER = "pocketmine.command.spawnpoint.other";
public const COMMAND_SPAWNPOINT_SELF = "pocketmine.command.spawnpoint.self";
public const COMMAND_STATUS = "pocketmine.command.status";
public const COMMAND_STOP = "pocketmine.command.stop";
public const COMMAND_TELEPORT = "pocketmine.command.teleport";
public const COMMAND_TELEPORT_OTHER = "pocketmine.command.teleport.other";
public const COMMAND_TELEPORT_SELF = "pocketmine.command.teleport.self";
public const COMMAND_TELL = "pocketmine.command.tell";
public const COMMAND_TIME_ADD = "pocketmine.command.time.add";
public const COMMAND_TIME_QUERY = "pocketmine.command.time.query";
@@ -67,6 +79,8 @@ final class DefaultPermissionNames{
public const COMMAND_TIME_STOP = "pocketmine.command.time.stop";
public const COMMAND_TIMINGS = "pocketmine.command.timings";
public const COMMAND_TITLE = "pocketmine.command.title";
public const COMMAND_TITLE_OTHER = "pocketmine.command.title.other";
public const COMMAND_TITLE_SELF = "pocketmine.command.title.self";
public const COMMAND_TRANSFERSERVER = "pocketmine.command.transferserver";
public const COMMAND_UNBAN_IP = "pocketmine.command.unban.ip";
public const COMMAND_UNBAN_PLAYER = "pocketmine.command.unban.player";

View File

@@ -23,10 +23,12 @@ declare(strict_types=1);
namespace pocketmine\permission;
use pocketmine\permission\DefaultPermissionNames as Names;
abstract class DefaultPermissions{
public const ROOT_CONSOLE = DefaultPermissionNames::GROUP_CONSOLE;
public const ROOT_OPERATOR = DefaultPermissionNames::GROUP_OPERATOR;
public const ROOT_USER = DefaultPermissionNames::GROUP_USER;
public const ROOT_CONSOLE = Names::GROUP_CONSOLE;
public const ROOT_OPERATOR = Names::GROUP_OPERATOR;
public const ROOT_USER = Names::GROUP_USER;
/**
* @param Permission[] $grantedBy
@@ -44,63 +46,95 @@ abstract class DefaultPermissions{
return PermissionManager::getInstance()->getPermission($candidate->getName());
}
private static function registerDeprecatedPermission(string $name) : Permission{
$permission = new Permission($name, "Deprecated, kept for backwards compatibility only");
PermissionManager::getInstance()->addPermission($permission);
return $permission;
}
public static function registerCorePermissions() : void{
$consoleRoot = self::registerPermission(new Permission(self::ROOT_CONSOLE, "Grants all console permissions"));
$operatorRoot = self::registerPermission(new Permission(self::ROOT_OPERATOR, "Grants all operator permissions"), [$consoleRoot]);
$everyoneRoot = self::registerPermission(new Permission(self::ROOT_USER, "Grants all non-sensitive permissions that everyone gets by default"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::BROADCAST_ADMIN, "Allows the user to receive administrative broadcasts"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::BROADCAST_USER, "Allows the user to receive user broadcasts"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_IP, "Allows the user to ban IP addresses"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_LIST, "Allows the user to list banned players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_BAN_PLAYER, "Allows the user to ban players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_OTHER, "Allows the user to clear inventory of other players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_CLEAR_SELF, "Allows the user to clear their own inventory"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DEFAULTGAMEMODE, "Allows the user to change the default gamemode"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DIFFICULTY, "Allows the user to change the game difficulty"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_DUMPMEMORY, "Allows the user to dump memory contents"), [$consoleRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_EFFECT, "Allows the user to give/take potion effects"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ENCHANT, "Allows the user to enchant items"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GAMEMODE, "Allows the user to change the gamemode of players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GC, "Allows the user to fire garbage collection tasks"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_GIVE, "Allows the user to give items to players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_HELP, "Allows the user to view the help menu"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KICK, "Allows the user to kick players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_OTHER, "Allows the user to kill other players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_KILL_SELF, "Allows the user to commit suicide"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_LIST, "Allows the user to list all online players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_ME, "Allows the user to perform a chat action"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_OP_GIVE, "Allows the user to give a player operator status"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_OP_TAKE, "Allows the user to take a player's operator status"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PARTICLE, "Allows the user to create particle effects"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_PLUGINS, "Allows the user to view the list of plugins"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_DISABLE, "Allows the user to disable automatic saving"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_ENABLE, "Allows the user to enable automatic saving"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAVE_PERFORM, "Allows the user to perform a manual save"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SAY, "Allows the user to talk as the console"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SEED, "Allows the user to view the seed of the world"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SETWORLDSPAWN, "Allows the user to change the world spawn"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_SPAWNPOINT, "Allows the user to change player's spawnpoint"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STATUS, "Allows the user to view the server performance"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_STOP, "Allows the user to stop the server"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELEPORT, "Allows the user to teleport players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TELL, "Allows the user to privately message another player"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_ADD, "Allows the user to fast-forward time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_QUERY, "Allows the user query the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_SET, "Allows the user to change the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_START, "Allows the user to restart the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIME_STOP, "Allows the user to stop the time"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TIMINGS, "Allows the user to record timings to analyse server performance"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TITLE, "Allows the user to send a title to the specified player"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_TRANSFERSERVER, "Allows the user to transfer self to another server"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_IP, "Allows the user to unban IP addresses"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_UNBAN_PLAYER, "Allows the user to unban players"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_VERSION, "Allows the user to view the version of the server"), [$everyoneRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ADD, "Allows the user to add a player to the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_DISABLE, "Allows the user to disable the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_ENABLE, "Allows the user to enable the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_LIST, "Allows the user to list all players on the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_RELOAD, "Allows the user to reload the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(DefaultPermissionNames::COMMAND_WHITELIST_REMOVE, "Allows the user to remove a player from the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::BROADCAST_ADMIN, "Allows the user to receive administrative broadcasts"), [$operatorRoot]);
self::registerPermission(new Permission(Names::BROADCAST_USER, "Allows the user to receive user broadcasts"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_BAN_IP, "Allows the user to ban IP addresses"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_BAN_LIST, "Allows the user to list banned players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_BAN_PLAYER, "Allows the user to ban players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_CLEAR_OTHER, "Allows the user to clear inventory of other players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_CLEAR_SELF, "Allows the user to clear their own inventory"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_DEFAULTGAMEMODE, "Allows the user to change the default gamemode"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_DIFFICULTY, "Allows the user to change the game difficulty"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_DUMPMEMORY, "Allows the user to dump memory contents"), [$consoleRoot]);
$effectRoot = self::registerDeprecatedPermission(Names::COMMAND_EFFECT);
self::registerPermission(new Permission(Names::COMMAND_EFFECT_OTHER, "Allows the user to modify effects of other players"), [$operatorRoot, $effectRoot]);
self::registerPermission(new Permission(Names::COMMAND_EFFECT_SELF, "Allows the user to modify their own effects"), [$operatorRoot, $effectRoot]);
$enchantRoot = self::registerDeprecatedPermission(Names::COMMAND_ENCHANT);
self::registerPermission(new Permission(Names::COMMAND_ENCHANT_OTHER, "Allows the user to enchant the held items of other players"), [$operatorRoot, $enchantRoot]);
self::registerPermission(new Permission(Names::COMMAND_ENCHANT_SELF, "Allows the user to enchant their own held item"), [$operatorRoot, $enchantRoot]);
$gameModeRoot = self::registerDeprecatedPermission(Names::COMMAND_GAMEMODE);
self::registerPermission(new Permission(Names::COMMAND_GAMEMODE_OTHER, "Allows the user to change the game mode of other players"), [$operatorRoot, $gameModeRoot]);
self::registerPermission(new Permission(Names::COMMAND_GAMEMODE_SELF, "Allows the user to change their own game mode"), [$operatorRoot, $gameModeRoot]);
self::registerPermission(new Permission(Names::COMMAND_GC, "Allows the user to fire garbage collection tasks"), [$operatorRoot]);
$giveRoot = self::registerDeprecatedPermission(Names::COMMAND_GIVE);
self::registerPermission(new Permission(Names::COMMAND_GIVE_OTHER, "Allows the user to give items to other players"), [$operatorRoot, $giveRoot]);
self::registerPermission(new Permission(Names::COMMAND_GIVE_SELF, "Allows the user to give items to themselves"), [$operatorRoot, $giveRoot]);
self::registerPermission(new Permission(Names::COMMAND_HELP, "Allows the user to view the help menu"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_KICK, "Allows the user to kick players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_KILL_OTHER, "Allows the user to kill other players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_KILL_SELF, "Allows the user to commit suicide"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_LIST, "Allows the user to list all online players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_ME, "Allows the user to perform a chat action"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_OP_GIVE, "Allows the user to give a player operator status"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_OP_TAKE, "Allows the user to take a player's operator status"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_PARTICLE, "Allows the user to create particle effects"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_PLUGINS, "Allows the user to view the list of plugins"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SAVE_DISABLE, "Allows the user to disable automatic saving"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SAVE_ENABLE, "Allows the user to enable automatic saving"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SAVE_PERFORM, "Allows the user to perform a manual save"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SAY, "Allows the user to talk as the console"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SEED, "Allows the user to view the seed of the world"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_SETWORLDSPAWN, "Allows the user to change the world spawn"), [$operatorRoot]);
$spawnpointRoot = self::registerDeprecatedPermission(Names::COMMAND_SPAWNPOINT);
self::registerPermission(new Permission(Names::COMMAND_SPAWNPOINT_OTHER, "Allows the user to change the respawn point of other players"), [$operatorRoot, $spawnpointRoot]);
self::registerPermission(new Permission(Names::COMMAND_SPAWNPOINT_SELF, "Allows the user to change their own respawn point"), [$operatorRoot, $spawnpointRoot]);
self::registerPermission(new Permission(Names::COMMAND_STATUS, "Allows the user to view the server performance"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_STOP, "Allows the user to stop the server"), [$operatorRoot]);
$teleportRoot = self::registerDeprecatedPermission(Names::COMMAND_TELEPORT);
self::registerPermission(new Permission(Names::COMMAND_TELEPORT_OTHER, "Allows the user to teleport other players"), [$operatorRoot, $teleportRoot]);
self::registerPermission(new Permission(Names::COMMAND_TELEPORT_SELF, "Allows the user to teleport themselves"), [$operatorRoot, $teleportRoot]);
self::registerPermission(new Permission(Names::COMMAND_TELL, "Allows the user to privately message another player"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIME_ADD, "Allows the user to fast-forward time"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIME_QUERY, "Allows the user query the time"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIME_SET, "Allows the user to change the time"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIME_START, "Allows the user to restart the time"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIME_STOP, "Allows the user to stop the time"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_TIMINGS, "Allows the user to record timings to analyse server performance"), [$operatorRoot]);
$titleRoot = self::registerDeprecatedPermission(Names::COMMAND_TITLE);
self::registerPermission(new Permission(Names::COMMAND_TITLE_OTHER, "Allows the user to send a title to the specified player"), [$operatorRoot, $titleRoot]);
self::registerPermission(new Permission(Names::COMMAND_TITLE_SELF, "Allows the user to send a title to themselves"), [$operatorRoot, $titleRoot]);
self::registerPermission(new Permission(Names::COMMAND_TRANSFERSERVER, "Allows the user to transfer self to another server"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_UNBAN_IP, "Allows the user to unban IP addresses"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_UNBAN_PLAYER, "Allows the user to unban players"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_VERSION, "Allows the user to view the version of the server"), [$everyoneRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_ADD, "Allows the user to add a player to the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_DISABLE, "Allows the user to disable the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_ENABLE, "Allows the user to enable the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_LIST, "Allows the user to list all players on the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_RELOAD, "Allows the user to reload the server whitelist"), [$operatorRoot]);
self::registerPermission(new Permission(Names::COMMAND_WHITELIST_REMOVE, "Allows the user to remove a player from the server whitelist"), [$operatorRoot]);
}
}

View File

@@ -53,6 +53,10 @@ class PermissionParser{
"false" => self::DEFAULT_FALSE,
];
private const KEY_DEFAULT = "default";
private const KEY_CHILDREN = "children";
private const KEY_DESCRIPTION = "description";
/**
* @param bool|string $value
*
@@ -60,11 +64,7 @@ class PermissionParser{
*/
public static function defaultFromString($value) : string{
if(is_bool($value)){
if($value){
return "true";
}else{
return "false";
}
return $value ? self::DEFAULT_TRUE : self::DEFAULT_FALSE;
}
$lower = strtolower($value);
if(isset(self::DEFAULT_STRING_MAP[$lower])){
@@ -86,16 +86,16 @@ class PermissionParser{
$result = [];
foreach(Utils::stringifyKeys($data) as $name => $entry){
$desc = null;
if(isset($entry["default"])){
$default = PermissionParser::defaultFromString($entry["default"]);
if(isset($entry[self::KEY_DEFAULT])){
$default = PermissionParser::defaultFromString($entry[self::KEY_DEFAULT]);
}
if(isset($entry["children"])){
if(isset($entry[self::KEY_CHILDREN])){
throw new PermissionParserException("Nested permission declarations are no longer supported. Declare each permission separately.");
}
if(isset($entry["description"])){
$desc = $entry["description"];
if(isset($entry[self::KEY_DESCRIPTION])){
$desc = $entry[self::KEY_DESCRIPTION];
}
$result[$default][] = new Permission($name, $desc);

View File

@@ -0,0 +1,100 @@
<?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\player;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\nbt\BigEndianNbtSerializer;
use pocketmine\nbt\NbtDataException;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\nbt\TreeRoot;
use pocketmine\utils\Filesystem;
use pocketmine\utils\Utils;
use Symfony\Component\Filesystem\Path;
use function file_exists;
use function rename;
use function strtolower;
use function zlib_decode;
use function zlib_encode;
use const ZLIB_ENCODING_GZIP;
/**
* Stores player data in a single .dat file per player. Each file is gzipped big-endian NBT.
*/
final class DatFilePlayerDataProvider implements PlayerDataProvider{
public function __construct(
private string $path
){}
private function getPlayerDataPath(string $username) : string{
return Path::join($this->path, strtolower($username) . '.dat');
}
private function handleCorruptedPlayerData(string $name) : void{
$path = $this->getPlayerDataPath($name);
rename($path, $path . '.bak');
}
public function hasData(string $name) : bool{
return file_exists($this->getPlayerDataPath($name));
}
public function loadData(string $name) : ?CompoundTag{
$name = strtolower($name);
$path = $this->getPlayerDataPath($name);
if(!file_exists($path)){
return null;
}
try{
$contents = Filesystem::fileGetContents($path);
}catch(\RuntimeException $e){
throw new PlayerDataLoadException("Failed to read player data file \"$path\": " . $e->getMessage(), 0, $e);
}
try{
$decompressed = ErrorToExceptionHandler::trapAndRemoveFalse(fn() => zlib_decode($contents));
}catch(\ErrorException $e){
$this->handleCorruptedPlayerData($name);
throw new PlayerDataLoadException("Failed to decompress raw player data for \"$name\": " . $e->getMessage(), 0, $e);
}
try{
return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag();
}catch(NbtDataException $e){ //corrupt data
$this->handleCorruptedPlayerData($name);
throw new PlayerDataLoadException("Failed to decode NBT data for \"$name\": " . $e->getMessage(), 0, $e);
}
}
public function saveData(string $name, CompoundTag $data) : void{
$nbt = new BigEndianNbtSerializer();
$contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($data)), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly");
try{
Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents);
}catch(\RuntimeException $e){
throw new PlayerDataSaveException("Failed to write player data file: " . $e->getMessage(), 0, $e);
}
}
}

View File

@@ -37,11 +37,11 @@ class OfflinePlayer implements IPlayer{
}
public function getFirstPlayed() : ?int{
return ($this->namedtag !== null && ($firstPlayedTag = $this->namedtag->getTag("firstPlayed")) instanceof LongTag) ? $firstPlayedTag->getValue() : null;
return ($this->namedtag !== null && ($firstPlayedTag = $this->namedtag->getTag(Player::TAG_FIRST_PLAYED)) instanceof LongTag) ? $firstPlayedTag->getValue() : null;
}
public function getLastPlayed() : ?int{
return ($this->namedtag !== null && ($lastPlayedTag = $this->namedtag->getTag("lastPlayed")) instanceof LongTag) ? $lastPlayedTag->getValue() : null;
return ($this->namedtag !== null && ($lastPlayedTag = $this->namedtag->getTag(Player::TAG_LAST_PLAYED)) instanceof LongTag) ? $lastPlayedTag->getValue() : null;
}
public function hasPlayedBefore() : bool{

View File

@@ -131,7 +131,7 @@ use pocketmine\world\sound\Sound;
use pocketmine\world\World;
use Ramsey\Uuid\UuidInterface;
use function abs;
use function array_map;
use function array_filter;
use function assert;
use function count;
use function explode;
@@ -145,8 +145,8 @@ use function min;
use function preg_match;
use function spl_object_id;
use function sqrt;
use function str_starts_with;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
use function trim;
@@ -175,6 +175,16 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
private const MAX_REACH_DISTANCE_SURVIVAL = 7;
private const MAX_REACH_DISTANCE_ENTITY_INTERACTION = 8;
public const TAG_FIRST_PLAYED = "firstPlayed"; //TAG_Long
public const TAG_LAST_PLAYED = "lastPlayed"; //TAG_Long
private const TAG_GAME_MODE = "playerGameType"; //TAG_Int
private const TAG_SPAWN_WORLD = "SpawnLevel"; //TAG_String
private const TAG_SPAWN_X = "SpawnX"; //TAG_Int
private const TAG_SPAWN_Y = "SpawnY"; //TAG_Int
private const TAG_SPAWN_Z = "SpawnZ"; //TAG_Int
public const TAG_LEVEL = "Level"; //TAG_String
public const TAG_LAST_KNOWN_XUID = "LastKnownXUID"; //TAG_String
/**
* Validates the given username.
*/
@@ -343,10 +353,10 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
));
$this->firstPlayed = $nbt->getLong("firstPlayed", $now = (int) (microtime(true) * 1000));
$this->lastPlayed = $nbt->getLong("lastPlayed", $now);
$this->firstPlayed = $nbt->getLong(self::TAG_FIRST_PLAYED, $now = (int) (microtime(true) * 1000));
$this->lastPlayed = $nbt->getLong(self::TAG_LAST_PLAYED, $now);
if(!$this->server->getForceGamemode() && ($gameModeTag = $nbt->getTag("playerGameType")) instanceof IntTag){
if(!$this->server->getForceGamemode() && ($gameModeTag = $nbt->getTag(self::TAG_GAME_MODE)) instanceof IntTag){
$this->internalSetGameMode(GameModeIdMap::getInstance()->fromId($gameModeTag->getValue()) ?? GameMode::SURVIVAL()); //TODO: bad hack here to avoid crashes on corrupted data
}else{
$this->internalSetGameMode($this->server->getGamemode());
@@ -358,8 +368,8 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->setNameTagAlwaysVisible();
$this->setCanClimb();
if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString("SpawnLevel", ""))) instanceof World){
$this->spawnPosition = new Position($nbt->getInt("SpawnX"), $nbt->getInt("SpawnY"), $nbt->getInt("SpawnZ"), $world);
if(($world = $this->server->getWorldManager()->getWorldByName($nbt->getString(self::TAG_SPAWN_WORLD, ""))) instanceof World){
$this->spawnPosition = new Position($nbt->getInt(self::TAG_SPAWN_X), $nbt->getInt(self::TAG_SPAWN_Y), $nbt->getInt(self::TAG_SPAWN_Z), $world);
}
}
@@ -1425,7 +1435,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$message = TextFormat::clean($message, false);
foreach(explode("\n", $message, $this->messageCounter + 1) as $messagePart){
if(trim($messagePart) !== "" && strlen($messagePart) <= self::MAX_CHAT_BYTE_LENGTH && mb_strlen($messagePart, 'UTF-8') <= self::MAX_CHAT_CHAR_LENGTH && $this->messageCounter-- > 0){
if(strpos($messagePart, './') === 0){
if(str_starts_with($messagePart, './')){
$messagePart = substr($messagePart, 1);
}
@@ -1436,7 +1446,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
break;
}
if(strpos($ev->getMessage(), "/") === 0){
if(str_starts_with($ev->getMessage(), "/")){
Timings::$playerCommand->startTiming();
$this->server->dispatchCommand($ev->getPlayer(), substr($ev->getMessage(), 1));
Timings::$playerCommand->stopTiming();
@@ -1818,7 +1828,17 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$ev->call();
$item = $this->inventory->getItemInHand();
$oldItem = clone $item;
if(!$ev->isCancelled()){
if($item->onInteractEntity($this, $entity, $clickPos)){
if($this->hasFiniteResources() && !$item->equalsExact($oldItem) && $oldItem->equalsExact($this->inventory->getItemInHand())){
if($item instanceof Durable && $item->isBroken()){
$this->broadcastSound(new ItemBreakSound());
}
$this->inventory->setItemInHand($item);
}
}
return $entity->onInteract($this, $clickPos);
}
return false;
@@ -1973,28 +1993,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
* Sends a direct chat message to a player
*/
public function sendMessage(Translatable|string $message) : void{
if($message instanceof Translatable){
$this->sendTranslation($message->getText(), $message->getParameters());
return;
}
$this->getNetworkSession()->onRawChatMessage($message);
$this->getNetworkSession()->onChatMessage($message);
}
/**
* @deprecated Use {@link Player::sendMessage()} with a Translatable instead.
* @param string[]|Translatable[] $parameters
*/
public function sendTranslation(string $message, array $parameters = []) : void{
//we can't send nested translations to the client, so make sure they are always pre-translated by the server
$parameters = array_map(fn(string|Translatable $p) => $p instanceof Translatable ? $this->getLanguage()->translate($p) : $p, $parameters);
if(!$this->server->isLanguageForced()){
foreach($parameters as $i => $p){
$parameters[$i] = $this->getLanguage()->translateString($p, [], "pocketmine.");
}
$this->getNetworkSession()->onTranslatedChatMessage($this->getLanguage()->translateString($message, $parameters, "pocketmine."), $parameters);
}else{
$this->sendMessage($this->getLanguage()->translateString($message, $parameters));
}
$this->sendMessage(new Translatable($message, $parameters));
}
/**
@@ -2214,23 +2221,23 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
public function getSaveData() : CompoundTag{
$nbt = $this->saveNBT();
$nbt->setString("LastKnownXUID", $this->xuid);
$nbt->setString(self::TAG_LAST_KNOWN_XUID, $this->xuid);
if($this->location->isValid()){
$nbt->setString("Level", $this->getWorld()->getFolderName());
$nbt->setString(self::TAG_LEVEL, $this->getWorld()->getFolderName());
}
if($this->hasValidCustomSpawn()){
$spawn = $this->getSpawn();
$nbt->setString("SpawnLevel", $spawn->getWorld()->getFolderName());
$nbt->setInt("SpawnX", $spawn->getFloorX());
$nbt->setInt("SpawnY", $spawn->getFloorY());
$nbt->setInt("SpawnZ", $spawn->getFloorZ());
$nbt->setString(self::TAG_SPAWN_WORLD, $spawn->getWorld()->getFolderName());
$nbt->setInt(self::TAG_SPAWN_X, $spawn->getFloorX());
$nbt->setInt(self::TAG_SPAWN_Y, $spawn->getFloorY());
$nbt->setInt(self::TAG_SPAWN_Z, $spawn->getFloorZ());
}
$nbt->setInt("playerGameType", GameModeIdMap::getInstance()->toId($this->gamemode));
$nbt->setLong("firstPlayed", $this->firstPlayed);
$nbt->setLong("lastPlayed", (int) floor(microtime(true) * 1000));
$nbt->setInt(self::TAG_GAME_MODE, GameModeIdMap::getInstance()->toId($this->gamemode));
$nbt->setLong(self::TAG_FIRST_PLAYED, $this->firstPlayed);
$nbt->setLong(self::TAG_LAST_PLAYED, (int) floor(microtime(true) * 1000));
return $nbt;
}
@@ -2255,15 +2262,16 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->getWorld()->dropItem($this->location, $item);
}
$clearInventory = fn(Inventory $inventory) => $inventory->setContents(array_filter($inventory->getContents(), fn(Item $item) => $item->keepOnDeath()));
if($this->inventory !== null){
$this->inventory->setHeldItemIndex(0);
$this->inventory->clearAll();
$clearInventory($this->inventory);
}
if($this->armorInventory !== null){
$this->armorInventory->clearAll();
$clearInventory($this->armorInventory);
}
if($this->offHandInventory !== null){
$this->offHandInventory->clearAll();
$clearInventory($this->offHandInventory);
}
}
@@ -2278,7 +2286,7 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
$this->startDeathAnimation();
$this->getNetworkSession()->onServerDeath();
$this->getNetworkSession()->onServerDeath($ev->getDeathMessage());
}
protected function onDeathUpdate(int $tickDiff) : bool{
@@ -2303,16 +2311,15 @@ class Player extends Human implements CommandSender, ChunkListener, IPlayer{
}
$this->respawnLocked = true;
$this->logger->debug("Waiting for spawn terrain generation for respawn");
$this->logger->debug("Waiting for safe respawn position to be located");
$spawn = $this->getSpawn();
$spawn->getWorld()->orderChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
function() use ($spawn) : void{
$spawn->getWorld()->requestSafeSpawn($spawn)->onCompletion(
function(Position $safeSpawn) : void{
if(!$this->isConnected()){
return;
}
$this->logger->debug("Spawn terrain generation done, completing respawn");
$spawn = $spawn->getWorld()->getSafeSpawn($spawn);
$ev = new PlayerRespawnEvent($this, $spawn);
$this->logger->debug("Respawn position located, completing respawn");
$ev = new PlayerRespawnEvent($this, $safeSpawn);
$ev->call();
$realSpawn = Position::fromObject($ev->getRespawnPosition()->add(0.5, 0, 0.5), $ev->getRespawnPosition()->getWorld());

View File

@@ -0,0 +1,28 @@
<?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\player;
final class PlayerDataLoadException extends \RuntimeException{
}

View File

@@ -0,0 +1,52 @@
<?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\player;
use pocketmine\nbt\tag\CompoundTag;
/**
* Handles storage of player data. Implementations must treat player names in a case-insensitive manner.
*/
interface PlayerDataProvider{
/**
* Returns whether there are any data associated with the given player name.
*/
public function hasData(string $name) : bool;
/**
* Returns the data associated with the given player name, or null if there is no data.
* TODO: we need an async version of this
*
* @throws PlayerDataLoadException
*/
public function loadData(string $name) : ?CompoundTag;
/**
* Saves data for the give player name.
*
* @throws PlayerDataSaveException
*/
public function saveData(string $name, CompoundTag $data) : void;
}

View File

@@ -0,0 +1,28 @@
<?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\player;
final class PlayerDataSaveException extends \RuntimeException{
}

View File

@@ -24,8 +24,7 @@ declare(strict_types=1);
namespace pocketmine\plugin;
use function is_file;
use function strlen;
use function substr;
use function str_ends_with;
/**
* Handles different types of plugins
@@ -36,8 +35,7 @@ class PharPluginLoader implements PluginLoader{
){}
public function canLoadPlugin(string $path) : bool{
$ext = ".phar";
return is_file($path) && substr($path, -strlen($ext)) === $ext;
return is_file($path) && str_ends_with($path, ".phar");
}
/**

View File

@@ -41,8 +41,8 @@ use function file_exists;
use function fopen;
use function mkdir;
use function rtrim;
use function str_contains;
use function stream_copy_to_stream;
use function strpos;
use function strtolower;
use function trim;
use const DIRECTORY_SEPARATOR;
@@ -145,7 +145,7 @@ abstract class PluginBase implements Plugin, CommandExecutor{
$pluginCmds = [];
foreach(Utils::stringifyKeys($this->getDescription()->getCommands()) as $key => $data){
if(strpos($key, ":") !== false){
if(str_contains($key, ":")){
$this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_commandError($key, $this->getDescription()->getFullName(), ":")));
continue;
}
@@ -161,7 +161,7 @@ abstract class PluginBase implements Plugin, CommandExecutor{
$aliasList = [];
foreach($data->getAliases() as $alias){
if(strpos($alias, ":") !== false){
if(str_contains($alias, ":")){
$this->logger->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_aliasError($alias, $this->getDescription()->getFullName(), ":")));
continue;
}

View File

@@ -37,6 +37,32 @@ use function stripos;
use function yaml_parse;
class PluginDescription{
private const KEY_NAME = "name";
private const KEY_VERSION = "version";
private const KEY_MAIN = "main";
private const KEY_SRC_NAMESPACE_PREFIX = "src-namespace-prefix";
private const KEY_API = "api";
private const KEY_MCPE_PROTOCOL = "mcpe-protocol";
private const KEY_OS = "os";
private const KEY_DEPEND = "depend";
private const KEY_SOFTDEPEND = "softdepend";
private const KEY_LOADBEFORE = "loadbefore";
private const KEY_EXTENSIONS = "extensions";
private const KEY_WEBSITE = "website";
private const KEY_DESCRIPTION = "description";
private const KEY_LOGGER_PREFIX = "prefix";
private const KEY_LOAD = "load";
private const KEY_AUTHOR = "author";
private const KEY_AUTHORS = "authors";
private const KEY_PERMISSIONS = "permissions";
private const KEY_COMMANDS = "commands";
private const KEY_COMMAND_PERMISSION = "permission";
private const KEY_COMMAND_DESCRIPTION = self::KEY_DESCRIPTION;
private const KEY_COMMAND_USAGE = "usage";
private const KEY_COMMAND_ALIASES = "aliases";
private const KEY_COMMAND_PERMISSION_MESSAGE = "permission-message";
/**
* @var mixed[]
* @phpstan-var array<string, mixed>
@@ -107,49 +133,49 @@ class PluginDescription{
private function loadMap(array $plugin) : void{
$this->map = $plugin;
$this->name = $plugin["name"];
$this->name = $plugin[self::KEY_NAME];
if(preg_match('/^[A-Za-z0-9 _.-]+$/', $this->name) === 0){
throw new PluginDescriptionParseException("Invalid Plugin name");
}
$this->name = str_replace(" ", "_", $this->name);
$this->version = (string) $plugin["version"];
$this->main = $plugin["main"];
$this->version = (string) $plugin[self::KEY_VERSION];
$this->main = $plugin[self::KEY_MAIN];
if(stripos($this->main, "pocketmine\\") === 0){
throw new PluginDescriptionParseException("Invalid Plugin main, cannot start within the PocketMine namespace");
}
$this->srcNamespacePrefix = $plugin["src-namespace-prefix"] ?? "";
$this->srcNamespacePrefix = $plugin[self::KEY_SRC_NAMESPACE_PREFIX] ?? "";
$this->api = array_map("\strval", (array) ($plugin["api"] ?? []));
$this->compatibleMcpeProtocols = array_map("\intval", (array) ($plugin["mcpe-protocol"] ?? []));
$this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin["os"] ?? []));
$this->api = array_map("\strval", (array) ($plugin[self::KEY_API] ?? []));
$this->compatibleMcpeProtocols = array_map("\intval", (array) ($plugin[self::KEY_MCPE_PROTOCOL] ?? []));
$this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin[self::KEY_OS] ?? []));
if(isset($plugin["commands"]) && is_array($plugin["commands"])){
foreach($plugin["commands"] as $commandName => $commandData){
if(isset($plugin[self::KEY_COMMANDS]) && is_array($plugin[self::KEY_COMMANDS])){
foreach($plugin[self::KEY_COMMANDS] as $commandName => $commandData){
if(!is_string($commandName)){
throw new PluginDescriptionParseException("Invalid Plugin commands, key must be the name of the command");
}
if(!is_array($commandData)){
throw new PluginDescriptionParseException("Command $commandName has invalid properties");
}
if(!isset($commandData["permission"]) || !is_string($commandData["permission"])){
if(!isset($commandData[self::KEY_COMMAND_PERMISSION]) || !is_string($commandData[self::KEY_COMMAND_PERMISSION])){
throw new PluginDescriptionParseException("Command $commandName does not have a valid permission set");
}
$this->commands[$commandName] = new PluginDescriptionCommandEntry(
$commandData["description"] ?? null,
$commandData["usage"] ?? null,
$commandData["aliases"] ?? [],
$commandData["permission"],
$commandData["permission-message"] ?? null
$commandData[self::KEY_COMMAND_DESCRIPTION] ?? null,
$commandData[self::KEY_COMMAND_USAGE] ?? null,
$commandData[self::KEY_COMMAND_ALIASES] ?? [],
$commandData[self::KEY_COMMAND_PERMISSION],
$commandData[self::KEY_COMMAND_PERMISSION_MESSAGE] ?? null
);
}
}
if(isset($plugin["depend"])){
$this->depend = (array) $plugin["depend"];
if(isset($plugin[self::KEY_DEPEND])){
$this->depend = (array) $plugin[self::KEY_DEPEND];
}
if(isset($plugin["extensions"])){
$extensions = (array) $plugin["extensions"];
if(isset($plugin[self::KEY_EXTENSIONS])){
$extensions = (array) $plugin[self::KEY_EXTENSIONS];
$isLinear = $extensions === array_values($extensions);
foreach($extensions as $k => $v){
if($isLinear){
@@ -160,20 +186,20 @@ class PluginDescription{
}
}
$this->softDepend = (array) ($plugin["softdepend"] ?? $this->softDepend);
$this->softDepend = (array) ($plugin[self::KEY_SOFTDEPEND] ?? $this->softDepend);
$this->loadBefore = (array) ($plugin["loadbefore"] ?? $this->loadBefore);
$this->loadBefore = (array) ($plugin[self::KEY_LOADBEFORE] ?? $this->loadBefore);
$this->website = (string) ($plugin["website"] ?? $this->website);
$this->website = (string) ($plugin[self::KEY_WEBSITE] ?? $this->website);
$this->description = (string) ($plugin["description"] ?? $this->description);
$this->description = (string) ($plugin[self::KEY_DESCRIPTION] ?? $this->description);
$this->prefix = (string) ($plugin["prefix"] ?? $this->prefix);
$this->prefix = (string) ($plugin[self::KEY_LOGGER_PREFIX] ?? $this->prefix);
if(isset($plugin["load"])){
$order = PluginEnableOrder::fromString($plugin["load"]);
if(isset($plugin[self::KEY_LOAD])){
$order = PluginEnableOrder::fromString($plugin[self::KEY_LOAD]);
if($order === null){
throw new PluginDescriptionParseException("Invalid Plugin \"load\"");
throw new PluginDescriptionParseException("Invalid Plugin \"" . self::KEY_LOAD . "\"");
}
$this->order = $order;
}else{
@@ -181,24 +207,24 @@ class PluginDescription{
}
$this->authors = [];
if(isset($plugin["author"])){
if(is_array($plugin["author"])){
$this->authors = $plugin["author"];
if(isset($plugin[self::KEY_AUTHOR])){
if(is_array($plugin[self::KEY_AUTHOR])){
$this->authors = $plugin[self::KEY_AUTHOR];
}else{
$this->authors[] = $plugin["author"];
$this->authors[] = $plugin[self::KEY_AUTHOR];
}
}
if(isset($plugin["authors"])){
foreach($plugin["authors"] as $author){
if(isset($plugin[self::KEY_AUTHORS])){
foreach($plugin[self::KEY_AUTHORS] as $author){
$this->authors[] = $author;
}
}
if(isset($plugin["permissions"])){
if(isset($plugin[self::KEY_PERMISSIONS])){
try{
$this->permissions = PermissionParser::loadPermissions($plugin["permissions"]);
$this->permissions = PermissionParser::loadPermissions($plugin[self::KEY_PERMISSIONS]);
}catch(PermissionParserException $e){
throw new PluginDescriptionParseException("Invalid Plugin \"permissions\": " . $e->getMessage(), 0, $e);
throw new PluginDescriptionParseException("Invalid Plugin \"" . self::KEY_PERMISSIONS . "\": " . $e->getMessage(), 0, $e);
}
}
}

View File

@@ -34,6 +34,7 @@ use function extension_loaded;
use function implode;
use function in_array;
use function phpversion;
use function str_starts_with;
use function stripos;
use function strlen;
use function substr;
@@ -96,7 +97,7 @@ final class PluginLoadabilityChecker{
}
foreach(["<=", "le", "<>", "!=", "ne", "<", "lt", "==", "=", "eq", ">=", "ge", ">", "gt"] as $comparator){
// warning: the > character should be quoted in YAML
if(substr($constr, 0, strlen($comparator)) === $comparator){
if(str_starts_with($constr, $comparator)){
$version = substr($constr, strlen($comparator));
if(!version_compare($gotVersion, $version, $comparator)){
return KnownTranslationFactory::pocketmine_plugin_incompatibleExtensionVersion(extensionName: $extensionName, extensionVersion: $gotVersion, pluginRequirement: $constr);

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